- generator in Definition::addItemToSet order: 50000 URL cap, lastmod from the record or the updates manifest (regular file, 5 MiB cap, fail safe), BeforeAddItem (skip) and AddItem (extra XML) festival events, tier or item priority, per-alternate expansion of list type children - priority tiers from golem15.sitemap.priority_tiers, compiled at boot, defaults equal to the PHP getPriorityForUrl rules - frontend_url rewrite of loc, xhtml:link href and the stylesheet href; GET /sitemap.xsl serves the PHP stylesheet byte for byte - golem15_sitemap_settings singleton and Settings model - sitemap:import (PHP ImportCommand) and sitemap:import-winter with the read-only Winter export SQL - urlto and full goldens generated from PHP; TestSitemapImportSmoke
114 lines
3.0 KiB
Go
114 lines
3.0 KiB
Go
package classes
|
|
|
|
import (
|
|
"bytes"
|
|
"encoding/json"
|
|
"io"
|
|
"log/slog"
|
|
"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 {
|
|
slog.Default().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 {
|
|
slog.Default().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
|
|
}
|