package html import ( "bytes" _ "embed" "html/template" "os" "path" "serveg/internal/disk_usage" "serveg/internal/http" sh "serveg/internal/string_helper" ) const DateTimeTZ = "2006-01-02 15:04:05 UTC-07" // The template file is embedded into the binary at build time. // //go:embed ListingPage.gohtml var listingPageSrc string // Parsed once at startup, Execute() is safe for concurrent use. // Must() panics on syntax error, so a broken template fails at start, not on request. var listingPageTemplate = template.Must(template.New("ListingPage").Parse(listingPageSrc)) // Data for ListingPage.gohtml. Fields must be exported to be visible in the template. // Values are not escaped: html/template does it. type listingPage struct { Title string Path string // directory url, ends with '/' PathUrl string // percent-encoded Path ParentUrl string // percent-encoded, ends with '/' Writable bool // show the upload form FreeSpace string // formatted with HumanBytes Entries []listingEntry } type listingEntry struct { Name string Url string // percent-encoded, without trailing '/' Size string // formatted with HumanBytes, empty for directories Date string // formatted with DateTimeTZ IsDir bool } // reqUrl is a clean url without trailing slash (except "/"), see sh.ResolveSafePath func GenDirListingHtml(dirPath, reqUrl, pageTitle string, writable bool) ([]byte, error) { dirEntries, err := os.ReadDir(dirPath) if err != nil { return nil, err } dirUrl := withTrailingSlash(reqUrl) page := listingPage{ Title: pageTitle + " " + dirUrl, Path: dirUrl, PathUrl: http.PercentEcodeUrl(dirUrl), ParentUrl: http.PercentEcodeUrl(withTrailingSlash(path.Dir(reqUrl))), Writable: writable, Entries: make([]listingEntry, 0, len(dirEntries)), } // listing is still useful without free space, so error is shown on the page if usage, err := disk_usage.DiskUsage(dirPath); err == nil { page.FreeSpace = sh.HumanBytes(usage.Free) } else { page.FreeSpace = "unknown (" + err.Error() + ")" } // directories first, then files, both sorted by name (os.ReadDir sorts) var files []listingEntry for _, e := range dirEntries { einf, err := e.Info() if err != nil { return nil, err } entry := listingEntry{ Name: e.Name(), Url: http.PercentEcodeUrl(dirUrl + e.Name()), Date: einf.ModTime().Format(DateTimeTZ), IsDir: e.IsDir(), } if entry.IsDir { page.Entries = append(page.Entries, entry) } else { entry.Size = sh.HumanBytes(einf.Size()) files = append(files, entry) } } page.Entries = append(page.Entries, files...) // render to buffer, not to socket: content-length must be known, // and an error in the middle must not send a half page var buf bytes.Buffer buf.Grow(len(listingPageSrc) + len(page.Entries)*200) if err := listingPageTemplate.Execute(&buf, &page); err != nil { return nil, err } return buf.Bytes(), nil } func withTrailingSlash(s string) string { if len(s) > 0 && s[len(s)-1] == '/' { return s } return s + "/" }