package webui import ( "embed" "errors" "html/template" "io" "path" "strings" "time" ) //go:embed templates/*.gohtml static/admin.css static/admin.js var embeddedFiles embed.FS type Renderer struct { templates *template.Template } func NewRenderer() (*Renderer, error) { templates, err := template.New("admin"). Option("missingkey=error"). Funcs(template.FuncMap{ "displayTime": displayTime, "machineTime": machineTime, "pathPart": pathPart, }). ParseFS(embeddedFiles, "templates/*.gohtml") if err != nil { return nil, errors.New("parse admin templates") } return &Renderer{templates: templates}, nil } func (r *Renderer) Execute( writer io.Writer, name string, data any, ) error { if r == nil || r.templates == nil { return errors.New("admin renderer is not configured") } if err := r.templates.ExecuteTemplate(writer, name, data); err != nil { return errors.New("render admin template") } return nil } func staticFile(name string) ([]byte, error) { if name != "admin.css" && name != "admin.js" { return nil, errors.New("unknown admin static file") } content, err := embeddedFiles.ReadFile("static/" + name) if err != nil { return nil, errors.New("read admin static file") } return content, nil } func displayTime(value time.Time) string { if value.IsZero() { return "尚未记录" } return value.Local().Format("2006-01-02 15:04") } func machineTime(value time.Time) string { if value.IsZero() { return "" } return value.UTC().Format(time.RFC3339) } func pathPart(value string) string { return pathEscape(strings.TrimSpace(value)) } func pathEscape(value string) string { // Keep IDs as one opaque path component. if value == "" || value == "." || value == ".." || path.Base(value) != value || strings.ContainsAny(value, `/\`) { return "invalid" } return value }