Files
sm-sitemap-plugin/classes/manifest.go
Jakub Zych 96c790df39 feat: console help, examples and named loggers (SummerCMS v0.4.0)
Every command gets long help and examples, descriptions in imperative
sentence case, and a warn-only TestCommandsDocumented over bonfire.CheckDocs.
The plugin logs through a diary component logger instead of the process
default logger. Requires SummerCMS v0.4.0; command names and flags unchanged.
2026-10-21 17:35:43 +02:00

113 lines
3.0 KiB
Go

package classes
import (
"bytes"
"encoding/json"
"io"
"os"
"strings"
"time"
)
// ManifestMaxBytes is the largest updates manifest the sitemap reads.
const ManifestMaxBytes = 5 << 20
// Manifest maps a normalized URL path to its last-modified date text, the
// updates manifest a frontend build writes.
type Manifest map[string]string
// LoadManifest reads the updates manifest at path: a regular file of at
// most ManifestMaxBytes holding a JSON object of URL path to date. It fails
// safe like PHP: an empty path, a missing, unreadable, oversized or invalid
// file is no manifest (nil), logged without the file's content. Keys are
// normalized like PHP normalizeManifestPath; values that are not strings are
// ignored.
func LoadManifest(path string) Manifest {
path = strings.TrimSpace(path)
if path == "" {
return nil
}
info, err := os.Stat(path)
if err != nil || !info.Mode().IsRegular() {
return nil
}
if info.Size() > ManifestMaxBytes {
pluginLog.Warn("golem15.sitemap: updates manifest is too large; lastmod omitted", "path", path, "max_bytes", ManifestMaxBytes)
return nil
}
f, err := os.Open(path)
if err != nil {
return nil
}
defer f.Close()
raw, err := io.ReadAll(io.LimitReader(f, ManifestMaxBytes+1))
if err != nil || len(raw) > ManifestMaxBytes {
return nil
}
raw = bytes.TrimSpace(raw)
var decoded map[string]any
if len(raw) == 0 || raw[0] != '{' || json.Unmarshal(raw, &decoded) != nil {
pluginLog.Warn("golem15.sitemap: updates manifest is not a JSON object; lastmod omitted", "path", path)
return nil
}
m := Manifest{}
for key, value := range decoded {
if s, ok := value.(string); ok {
m[normalizeManifestPath(key)] = s
}
}
return m
}
// normalizeManifestPath is PHP normalizeManifestPath: a leading slash, no
// trailing slash except for the root.
func normalizeManifestPath(path string) string {
path = "/" + strings.TrimLeft(path, "/")
if path == "/" {
return path
}
return strings.TrimRight(path, "/")
}
// Lastmod is PHP lookupManifestLastmod: the date listed for the URL's path,
// parsed like the ISO 8601 forms strtotime reads (a date without a zone is
// UTC). ok is false when the path is not listed or its date does not parse.
func (m Manifest) Lastmod(url string) (time.Time, bool) {
if len(m) == 0 {
return time.Time{}, false
}
path := URLPath(url)
if path == "" {
path = "/"
}
text, ok := m[normalizeManifestPath(path)]
if !ok {
return time.Time{}, false
}
t, ok := parseManifestDate(text)
if !ok || t.Unix() == 0 {
return time.Time{}, false
}
return t, true
}
var manifestLayouts = []string{
time.RFC3339Nano,
"2006-01-02T15:04:05.999999999Z0700",
"2006-01-02T15:04:05",
"2006-01-02T15:04",
"2006-01-02 15:04:05Z07:00",
"2006-01-02 15:04:05",
"2006-01-02",
}
func parseManifestDate(s string) (time.Time, bool) {
s = strings.TrimSpace(s)
for _, layout := range manifestLayouts {
if t, err := time.ParseInLocation(layout, s, time.UTC); err == nil {
return t, true
}
}
return time.Time{}, false
}