main pico / pkg / apps / pgs / gen_dir_listing.go
Antonio Mika  ·  2026-10-09
  1package pgs
  2
  3import (
  4	"bytes"
  5	"embed"
  6	"fmt"
  7	"html/template"
  8	"os"
  9	"sort"
 10	"strings"
 11
 12	"github.com/picosh/pico/pkg/storage"
 13)
 14
 15//go:embed html/*
 16var dirListingFS embed.FS
 17
 18var dirListingTmpl = template.Must(
 19	template.New("base").ParseFS(
 20		dirListingFS,
 21		"html/base.layout.tmpl",
 22		"html/marketing-footer.partial.tmpl",
 23		"html/directory_listing.page.tmpl",
 24	),
 25)
 26
 27type dirEntryDisplay struct {
 28	Href    string
 29	Display string
 30	Size    string
 31	ModTime string
 32}
 33
 34type DirectoryListingData struct {
 35	Path       string
 36	ShowParent bool
 37	Entries    []dirEntryDisplay
 38}
 39
 40func formatFileSize(size int64) string {
 41	const (
 42		KB = 1024
 43		MB = KB * 1024
 44		GB = MB * 1024
 45	)
 46
 47	switch {
 48	case size >= GB:
 49		return fmt.Sprintf("%.1f GB", float64(size)/float64(GB))
 50	case size >= MB:
 51		return fmt.Sprintf("%.1f MB", float64(size)/float64(MB))
 52	case size >= KB:
 53		return fmt.Sprintf("%.1f KB", float64(size)/float64(KB))
 54	default:
 55		return fmt.Sprintf("%d B", size)
 56	}
 57}
 58
 59func sortEntries(entries []os.FileInfo) {
 60	sort.Slice(entries, func(i, j int) bool {
 61		if entries[i].IsDir() != entries[j].IsDir() {
 62			return entries[i].IsDir()
 63		}
 64		return entries[i].Name() < entries[j].Name()
 65	})
 66}
 67
 68func toDisplayEntries(entries []os.FileInfo) []dirEntryDisplay {
 69	sortEntries(entries)
 70	displayEntries := make([]dirEntryDisplay, 0, len(entries))
 71
 72	for _, entry := range entries {
 73		if strings.HasPrefix(entry.Name(), ".") {
 74			continue
 75		}
 76		display := dirEntryDisplay{
 77			Href:    entry.Name(),
 78			Display: entry.Name(),
 79			Size:    formatFileSize(entry.Size()),
 80			ModTime: entry.ModTime().Format("2006-01-02 15:04"),
 81		}
 82
 83		if entry.IsDir() {
 84			display.Href += "/"
 85			display.Display += "/"
 86			display.Size = "-"
 87		}
 88
 89		displayEntries = append(displayEntries, display)
 90	}
 91
 92	return displayEntries
 93}
 94
 95// dirListing returns the entries to list for a path, and false when the path
 96// is not a directory or has an index.html to serve instead.
 97func dirListing(st storage.StorageServe, bucket storage.Bucket, projectDir string, path string) ([]os.FileInfo, bool) {
 98	dirPath := projectDir + path
 99
100	entries, err := st.ListObjects(bucket, dirPath, false)
101	if err != nil || len(entries) == 0 {
102		return nil, false
103	}
104
105	if _, err := st.StatObject(bucket, dirPath+"index.html"); err == nil {
106		return nil, false
107	}
108	return entries, true
109}
110
111func generateDirectoryHTML(path string, entries []os.FileInfo) string {
112	data := DirectoryListingData{
113		Path:       path,
114		ShowParent: path != "/",
115		Entries:    toDisplayEntries(entries),
116	}
117
118	var buf bytes.Buffer
119	if err := dirListingTmpl.Execute(&buf, data); err != nil {
120		return fmt.Sprintf("Error rendering directory listing: %s", err)
121	}
122
123	return buf.String()
124}