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}