diff options
| author | Leonardo Bishop <me@leonardobishop.net> | 2025-07-17 21:52:26 +0100 |
|---|---|---|
| committer | Leonardo Bishop <me@leonardobishop.net> | 2025-07-17 21:52:26 +0100 |
| commit | 4b58544300847e5faf19be5baa4eb177a86b2b0f (patch) | |
| tree | 60c43bfe7ec7c1cd46a12db73946aad03ebff01f | |
| parent | d6a028feb7e7c3657f846889a1c0edf9f22e8dd2 (diff) | |
Add automatic index
| -rw-r--r-- | Makefile | 1 | ||||
| -rw-r--r-- | pkg/html/autoindex.go | 68 | ||||
| -rw-r--r-- | pkg/html/notfound.go | 25 | ||||
| -rw-r--r-- | pkg/server/serve.go (renamed from pkg/server/handle.go) | 4 | ||||
| -rw-r--r-- | pkg/site/fs.go | 84 | ||||
| -rw-r--r-- | pkg/site/site.go | 2 | ||||
| -rw-r--r-- | web/command/html/authenticate.go | 5 | ||||
| -rw-r--r-- | web/command/html/create.go | 11 | ||||
| -rw-r--r-- | web/command/html/delete.go | 11 | ||||
| -rw-r--r-- | web/command/html/error.go | 7 | ||||
| -rw-r--r-- | web/command/html/flags.go | 12 | ||||
| -rw-r--r-- | web/command/html/home.go | 11 | ||||
| -rw-r--r-- | web/command/html/host.go | 11 | ||||
| -rw-r--r-- | web/command/html/site.go | 25 | ||||
| -rw-r--r-- | web/command/html/upload.go | 11 | ||||
| -rw-r--r-- | web/skeleton/skeleton.go (renamed from web/command/html/skeleton.go) | 17 | ||||
| -rw-r--r-- | web/skeleton/style.css (renamed from web/command/html/style.css) | 40 | ||||
| -rw-r--r-- | web/status/404.html | 11 |
18 files changed, 255 insertions, 101 deletions
@@ -8,6 +8,7 @@ all: build build: go build -ldflags "-X 'github.com/LMBishop/scrapbook/pkg/constants.SysConfDir=${SYS_CONF_DIR}' -X 'github.com/LMBishop/scrapbook/pkg/constants.SysDataDir=${SYS_DATA_DIR}'" -o ${BINARY_NAME} main.go +.PHONY: runlocal runlocal: PWD=$(shell pwd) mkdir -p runlocal diff --git a/pkg/html/autoindex.go b/pkg/html/autoindex.go new file mode 100644 index 0000000..4c31134 --- /dev/null +++ b/pkg/html/autoindex.go @@ -0,0 +1,68 @@ +package html + +import ( + "fmt" + "strconv" + + . "github.com/LMBishop/scrapbook/web/skeleton" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +type File struct { + Name string + IsDir bool + Size int64 + Mtime int +} + +func IndexPage(dir string, err bool, files []File) Node { + return Page("Index of "+dir, + H1(Text("Index of "+dir)), + + Div( + Class("table files-table"), + Group{ + Span( + Class("header name"), + Text("Name"), + ), + Span( + Class("header size"), + Text("Size"), + ), + Span( + Class("header last-modified"), + Text("Last modified"), + ), + }, + + If(files != nil, Map(files, func(file File) Node { + var fileName string + if file.IsDir { + fileName = file.Name + "/" + } else { + fileName = file.Name + } + return Group{ + A( + Class("name"), + Href(fileName), + Text(fileName), + ), + Span( + Class("size"), + If(file.IsDir, Text("--")), + If(!file.IsDir, Text(strconv.FormatInt(file.Size, 10)+" bytes")), + ), + Span( + Class("last-modified"), + Text("--"), + ), + } + })), + ), + + If(err, AlertError(fmt.Sprintf("Failed to list directory"))), + ) +} diff --git a/pkg/html/notfound.go b/pkg/html/notfound.go new file mode 100644 index 0000000..ceb521f --- /dev/null +++ b/pkg/html/notfound.go @@ -0,0 +1,25 @@ +package html + +import ( + "fmt" + + . "github.com/LMBishop/scrapbook/web/skeleton" + . "maragu.dev/gomponents" + . "maragu.dev/gomponents/html" +) + +func NotFoundUrlPage(url, host string) Node { + return Page("Page not found", + H1(Text("Page not found")), + + P(Text(fmt.Sprintf("The URL %s could not be found on site %s", url, host))), + ) +} + +func NotFoundSitePage(host string) Node { + return Page("Site not found", + H1(Text("Site not found")), + + P(Text(fmt.Sprintf("The site %s is unknown", host))), + ) +} diff --git a/pkg/server/handle.go b/pkg/server/serve.go index afe65ce..9e7b722 100644 --- a/pkg/server/handle.go +++ b/pkg/server/serve.go @@ -1,9 +1,9 @@ package server import ( - "fmt" "net/http" + "github.com/LMBishop/scrapbook/pkg/html" "github.com/LMBishop/scrapbook/pkg/index" ) @@ -12,7 +12,7 @@ func ServeSite(siteIndex *index.SiteIndex) func(w http.ResponseWriter, r *http.R site := siteIndex.GetSiteByHost(r.Host) if site == nil { w.WriteHeader(http.StatusNotFound) - fmt.Fprintf(w, "unknown host %s", r.Host) + html.NotFoundSitePage(r.Host) return } diff --git a/pkg/site/fs.go b/pkg/site/fs.go index e8ee7fc..c9bbe21 100644 --- a/pkg/site/fs.go +++ b/pkg/site/fs.go @@ -1,45 +1,95 @@ package site import ( + "log/slog" "net/http" + "os" "path/filepath" "strings" + + "github.com/LMBishop/scrapbook/pkg/config" + "github.com/LMBishop/scrapbook/pkg/html" ) -type siteFS struct { - fs http.FileSystem +type SiteFileServer struct { + root http.FileSystem + siteConfig *config.SiteConfig +} + +func NewSiteFileServer(root http.FileSystem, siteConfig *config.SiteConfig) *SiteFileServer { + return &SiteFileServer{root: root, siteConfig: siteConfig} } -func (sfs siteFS) Open(path string) (http.File, error) { - f, err := sfs.fs.Open(path) +func (fs *SiteFileServer) ServeHTTP(w http.ResponseWriter, r *http.Request) { + path := filepath.Clean(r.URL.Path) + + file, err := fs.root.Open(path) if err != nil { if strings.HasSuffix(path, ".html") { - return nil, err + html.NotFoundUrlPage(path, fs.siteConfig.Host).Render(w) + return } htmlPath := path + ".html" - f, err = sfs.fs.Open(htmlPath) + file, err = fs.root.Open(htmlPath) if err != nil { - return nil, err + html.NotFoundUrlPage(path, fs.siteConfig.Host).Render(w) + return } } + defer file.Close() - s, err := f.Stat() + info, err := file.Stat() if err != nil { - return nil, err + html.NotFoundUrlPage(path, fs.siteConfig.Host).Render(w) + return } - if s.IsDir() { - index := filepath.Join(path, "index.html") - if _, err := sfs.fs.Open(index); err != nil { - closeErr := f.Close() - if closeErr != nil { - return nil, closeErr + if info.IsDir() { + indexPath := filepath.Join(path, "index.html") + if _, err := fs.root.Open(indexPath); os.IsNotExist(err) { + if fs.siteConfig.Flags&config.FlagIndex == 0 { + html.NotFoundUrlPage(path, fs.siteConfig.Host).Render(w) + return } + files, err := fs.listFiles(path) + if path != "/" { + files = append([]html.File{{Name: "..", IsDir: true, Size: 0}}, files...) + } + if err != nil { + html.IndexPage(path, true, files).Render(w) + slog.Error("could not list directory for index page generation", "host", fs.siteConfig.Host, "path", path, "error", err) + } else { + html.IndexPage(path, false, files).Render(w) + } + return + } + http.ServeFile(w, r, indexPath) + } else { + http.ServeFile(w, r, path) + } +} + +func (fs *SiteFileServer) listFiles(dir string) ([]html.File, error) { + file, err := fs.root.Open(dir) + if err != nil { + return nil, err + } + defer file.Close() + + entries, err := file.Readdir(-1) + if err != nil { + return nil, err + } - return nil, err + var files []html.File + for _, entry := range entries { + if !entry.IsDir() { + files = append(files, html.File{Name: entry.Name(), IsDir: false, Size: entry.Size()}) + } else { + files = append(files, html.File{Name: entry.Name(), IsDir: true, Size: 0}) } } - return f, nil + return files, nil } diff --git a/pkg/site/site.go b/pkg/site/site.go index 82f25ad..8b7af41 100644 --- a/pkg/site/site.go +++ b/pkg/site/site.go @@ -30,7 +30,7 @@ func NewSite(name string, dir string, config *config.SiteConfig) *Site { site.Name = name site.Path = dir site.SiteConfig = config - site.Handler = http.FileServer(siteFS{http.Dir(path.Join(dir, "default"))}) + site.Handler = NewSiteFileServer(http.Dir(path.Join(dir, "default")), config) return &site } diff --git a/web/command/html/authenticate.go b/web/command/html/authenticate.go index c077529..3d85325 100644 --- a/web/command/html/authenticate.go +++ b/web/command/html/authenticate.go @@ -1,15 +1,16 @@ package html import ( + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func AuthenticatePage(err string) Node { - return page("Authenticate", + return Page("Authenticate", H1(Text("Welcome to scrapbook")), - If(err != "", alertError(err)), + If(err != "", AlertError(err)), Form( Action("/authenticate"), diff --git a/web/command/html/create.go b/web/command/html/create.go index 8b76776..47de92f 100644 --- a/web/command/html/create.go +++ b/web/command/html/create.go @@ -1,6 +1,7 @@ package html import ( + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) @@ -11,19 +12,19 @@ type CreatePageForm struct { } func CreatePage(success, err string, formValues CreatePageForm) Node { - return page("Create site", + return Page("Create site", H1(Text("Create site")), If(success != "", Group{ - alertSuccess(success), + AlertSuccess(success), Div( Class("control-group group-right"), - navButton("OK", "/"), + NavButton("OK", "/"), ), }), If(success == "", Group{ - If(err != "", alertError(err)), + If(err != "", AlertError(err)), Form( Action("/create"), @@ -62,7 +63,7 @@ func CreatePage(success, err string, formValues CreatePageForm) Node { Div( Class("control-group group-right"), - navButton("Go back", "/"), + NavButton("Go back", "/"), Input( Type("submit"), Value("Submit"), diff --git a/web/command/html/delete.go b/web/command/html/delete.go index a2059f5..54764c3 100644 --- a/web/command/html/delete.go +++ b/web/command/html/delete.go @@ -3,24 +3,25 @@ package html import ( "fmt" + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func DeletePage(success, err string, siteName string) Node { - return page("Delete "+siteName, + return Page("Delete "+siteName, H1(Text("Delete "+siteName)), If(success != "", Group{ - alertSuccess(success), + AlertSuccess(success), Div( Class("control-group group-right"), - navButton("OK", "/"), + NavButton("OK", "/"), ), }), If(success == "", Group{ - If(err != "", alertError(err)), + If(err != "", AlertError(err)), Form( Method("post"), @@ -46,7 +47,7 @@ func DeletePage(success, err string, siteName string) Node { Div( Class("control-group group-right"), - navButton("Go back", fmt.Sprintf("/site/%s/", siteName)), + NavButton("Go back", fmt.Sprintf("/site/%s/", siteName)), Input( Type("submit"), Value("Submit"), diff --git a/web/command/html/error.go b/web/command/html/error.go index b39cc19..20b49f3 100644 --- a/web/command/html/error.go +++ b/web/command/html/error.go @@ -1,13 +1,14 @@ package html import ( + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" ) func ErrorPage(err string) Node { - return page("Error", - alertError(err), + return Page("Error", + AlertError(err), - navButton("Home", "/"), + NavButton("Home", "/"), ) } diff --git a/web/command/html/flags.go b/web/command/html/flags.go index 728b415..61a1f50 100644 --- a/web/command/html/flags.go +++ b/web/command/html/flags.go @@ -4,24 +4,25 @@ import ( "fmt" "github.com/LMBishop/scrapbook/pkg/config" + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func FlagsPage(success, err string, siteName string, flags config.SiteFlag) Node { - return page("Set flags for "+siteName, + return Page("Set flags for "+siteName, H1(Text("Set flags for "+siteName)), If(success != "", Group{ - alertSuccess(success), + AlertSuccess(success), Div( Class("control-group group-right"), - navButton("OK", fmt.Sprintf("/site/%s/", siteName)), + NavButton("OK", fmt.Sprintf("/site/%s/", siteName)), ), }), If(success == "", Group{ - If(err != "", alertError(err)), + If(err != "", AlertError(err)), Form( Method("post"), @@ -70,7 +71,6 @@ func FlagsPage(success, err string, siteName string, flags config.SiteFlag) Node ID("index"), Name("index"), Type("checkbox"), - Disabled(), If(flags&config.FlagIndex != 0, Checked()), ), Label( @@ -121,7 +121,7 @@ func FlagsPage(success, err string, siteName string, flags config.SiteFlag) Node Div( Class("control-group group-right"), - navButton("Go back", fmt.Sprintf("/site/%s/", siteName)), + NavButton("Go back", fmt.Sprintf("/site/%s/", siteName)), Input( Type("submit"), Value("Submit"), diff --git a/web/command/html/home.go b/web/command/html/home.go index 5dc236a..6de30bd 100644 --- a/web/command/html/home.go +++ b/web/command/html/home.go @@ -5,16 +5,17 @@ import ( "github.com/LMBishop/scrapbook/pkg/index" "github.com/LMBishop/scrapbook/pkg/site" + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func HomePage(siteIndex *index.SiteIndex) Node { - return page("All sites", + return Page("All sites", H1(Text("All sites")), Div( - Class("sites-table"), + Class("table sites-table"), Group{ Span( Class("header name"), @@ -58,18 +59,18 @@ func HomePage(siteIndex *index.SiteIndex) Node { ), Span( Class("actions"), - navButton("Details", fmt.Sprintf("/site/%s/", site.Name)), + NavButton("Details", fmt.Sprintf("/site/%s/", site.Name)), ), } }), ), - If(len(siteIndex.GetSites()) == 0, alert("There are no sites to display", "")), + If(len(siteIndex.GetSites()) == 0, Alert("There are no sites to display", "")), Div( Class("control-group group-right"), - navButton("Create new", "/create"), + NavButton("Create new", "/create"), ), ) } diff --git a/web/command/html/host.go b/web/command/html/host.go index 36f0e6b..cd1c75a 100644 --- a/web/command/html/host.go +++ b/web/command/html/host.go @@ -3,24 +3,25 @@ package html import ( "fmt" + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func HostPage(success, err, siteName, hostValue string) Node { - return page("Change host for "+siteName, + return Page("Change host for "+siteName, H1(Text("Change host for "+siteName)), If(success != "", Group{ - alertSuccess(success), + AlertSuccess(success), Div( Class("control-group group-right"), - navButton("OK", fmt.Sprintf("/site/%s/", siteName)), + NavButton("OK", fmt.Sprintf("/site/%s/", siteName)), ), }), If(success == "", Group{ - If(err != "", alertError(err)), + If(err != "", AlertError(err)), Form( Method("post"), @@ -40,7 +41,7 @@ func HostPage(success, err, siteName, hostValue string) Node { Div( Class("control-group group-right"), - navButton("Go back", fmt.Sprintf("/site/%s/", siteName)), + NavButton("Go back", fmt.Sprintf("/site/%s/", siteName)), Input( Type("submit"), Value("Submit"), diff --git a/web/command/html/site.go b/web/command/html/site.go index cb1fa31..e9bfbac 100644 --- a/web/command/html/site.go +++ b/web/command/html/site.go @@ -5,6 +5,7 @@ import ( "github.com/LMBishop/scrapbook/pkg/config" "github.com/LMBishop/scrapbook/pkg/site" + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) @@ -13,10 +14,10 @@ func SitePage(mainConfig *config.MainConfig, site *site.Site) Node { versions, err := site.GetAllVersions() currentVersion, _ := site.GetCurrentVersion() - return page("Site "+site.Name, + return Page("Site "+site.Name, H1(Text("Site "+site.Name)), - If(site.EvaluateSiteStatus() != "live", alertError(site.EvaluateSiteStatusReason())), + If(site.EvaluateSiteStatus() != "live", AlertError(site.EvaluateSiteStatusReason())), FieldSet( Legend(Text("Site actions")), @@ -24,19 +25,19 @@ func SitePage(mainConfig *config.MainConfig, site *site.Site) Node { Div( Class("control-group"), - navButton("Change host", "host"), - navButton("Set flags", "flags"), - navButton("Delete site", "delete"), + NavButton("Change host", "host"), + NavButton("Set flags", "flags"), + NavButton("Delete site", "delete"), ), ), H2(Text("Version history")), - If(len(versions) == 0, Span(Class("span"), alert("There are no versions to display", ""))), - If(err != nil, Span(Class("span"), alertError(fmt.Errorf("Cannot show site versions: %w", err).Error()))), + If(len(versions) == 0, Span(Class("span"), Alert("There are no versions to display", ""))), + If(err != nil, Span(Class("span"), AlertError(fmt.Errorf("Cannot show site versions: %w", err).Error()))), If(len(versions) > 0 && err == nil, Group{ Div( - Class("versions-table"), + Class("table versions-table"), Group{ Span( Class("header date"), @@ -57,8 +58,8 @@ func SitePage(mainConfig *config.MainConfig, site *site.Site) Node { ), Span( Class("actions"), - If(currentVersion != version, navButton("Set current", fmt.Sprintf("/site/%s/", site.Name))), - navButton("Details", fmt.Sprintf("version/%s/", version)), + If(currentVersion != version, NavButton("Set current", fmt.Sprintf("/site/%s/", site.Name))), + NavButton("Details", fmt.Sprintf("version/%s/", version)), ), } }), @@ -67,7 +68,7 @@ func SitePage(mainConfig *config.MainConfig, site *site.Site) Node { Div( Class("control-group group-right"), - navButton("Upload new version", "upload"), + NavButton("Upload new version", "upload"), ), H2(Text("Information")), @@ -78,6 +79,6 @@ func SitePage(mainConfig *config.MainConfig, site *site.Site) Node { Br(), - navButton("Go back", "/"), + NavButton("Go back", "/"), ) } diff --git a/web/command/html/upload.go b/web/command/html/upload.go index 1225d0f..5810b19 100644 --- a/web/command/html/upload.go +++ b/web/command/html/upload.go @@ -3,24 +3,25 @@ package html import ( "fmt" + . "github.com/LMBishop/scrapbook/web/skeleton" . "maragu.dev/gomponents" . "maragu.dev/gomponents/html" ) func UploadPage(success, err string, siteName string) Node { - return page("Upload new version to "+siteName, + return Page("Upload new version to "+siteName, H1(Text("Upload new version to "+siteName)), If(success != "", Group{ - alertSuccess(success), + AlertSuccess(success), Div( Class("control-group group-right"), - navButton("OK", fmt.Sprintf("/site/%s/", siteName)), + NavButton("OK", fmt.Sprintf("/site/%s/", siteName)), ), }), If(success == "", Group{ - If(err != "", alertError(err)), + If(err != "", AlertError(err)), Form( Method("post"), @@ -41,7 +42,7 @@ func UploadPage(success, err string, siteName string) Node { Div( Class("control-group group-right"), - navButton("Go back", fmt.Sprintf("/site/%s/", siteName)), + NavButton("Go back", fmt.Sprintf("/site/%s/", siteName)), Input( Type("submit"), Value("Submit"), diff --git a/web/command/html/skeleton.go b/web/skeleton/skeleton.go index 9e17475..a3ba73e 100644 --- a/web/command/html/skeleton.go +++ b/web/skeleton/skeleton.go @@ -1,4 +1,4 @@ -package html +package skeleton import ( _ "embed" @@ -11,7 +11,7 @@ import ( //go:embed style.css var styles string -func page(title string, children ...Node) Node { +func Page(title string, children ...Node) Node { return HTML5(HTML5Props{ Title: title, Language: "en", @@ -34,24 +34,25 @@ func footer() Node { ) } -func navButton(label string, dest string) Node { +func NavButton(label string, dest string) Node { return A( + Class("button"), Href(dest), Text(label), ) } -func alert(label string, class string) Node { +func Alert(label string, class string) Node { return Div( Class("alert "+class), Text(label), ) } -func alertError(label string) Node { - return alert(label, "error") +func AlertError(label string) Node { + return Alert(label, "error") } -func alertSuccess(label string) Node { - return alert(label, "success") +func AlertSuccess(label string) Node { + return Alert(label, "success") } diff --git a/web/command/html/style.css b/web/skeleton/style.css index e8db90c..b4cd0eb 100644 --- a/web/command/html/style.css +++ b/web/skeleton/style.css @@ -16,7 +16,7 @@ th { background-color: #f2f2f2; } -a, input[type=submit] { +a.button, input[type=submit] { /* font-weight: bold; */ border: 1px solid #000!important; color: #000; @@ -26,8 +26,8 @@ a, input[type=submit] { text-decoration: none; } -a :visited{ - color: #fff; +a.button:visited{ + color: #000; } form { @@ -70,7 +70,7 @@ button, input[type=submit] { cursor: pointer; } -a:hover, button:hover, input[type=submit]:hover, button:active, a:active, input[type=submit]:active { +a.button:hover, button:hover, input[type=submit]:hover, button:active, a.button:active, input[type=submit]:active { background: #000!important; color: #fff!important; } @@ -94,24 +94,41 @@ a:hover, button:hover, input[type=submit]:hover, button:active, a:active, input[ justify-content: flex-end; } -.control-group a, .control-group button, .control-group input[type=submit] { +.control-group a.button, .control-group button, .control-group input[type=submit] { text-decoration: none; width: initial; } -.sites-table { +.table { width: 100%; - display: grid; - grid-template-columns: 40% 1fr 1fr 1fr; align-items: center; + display: grid; gap: 1rem; margin-bottom: 1rem; } -.sites-table > .header, .versions-table > .header { +.table > .header { font-weight: bold; } +.files-table { + grid-template-columns: 1fr 1fr 1fr; + gap: 0.5rem; +} + +.files-table > a.name { + width: 100%; + border: none !important; + color: #0074D9 !important; + padding: 0 !important; + margin: 0 !important; + text-decoration: none; +} + +.sites-table { + grid-template-columns: 40% 1fr 1fr 1fr; +} + .sites-table > .name { display: flex; flex-direction: column; @@ -132,12 +149,7 @@ a:hover, button:hover, input[type=submit]:hover, button:active, a:active, input[ } .versions-table { - width: 100%; - display: grid; grid-template-columns: 1fr 1fr; - align-items: center; - gap: 1rem; - margin-bottom: 1rem; } .versions-table > .span { diff --git a/web/status/404.html b/web/status/404.html deleted file mode 100644 index a094ccd..0000000 --- a/web/status/404.html +++ /dev/null @@ -1,11 +0,0 @@ -<!DOCTYPE html> -<html> - <head> - <title>404 Not Found</title> - </head> - <body> - <p>The request page could not be found.</p> - <hr> - <p>scrapbook</p> - </body> -</html> |
