From 6708d9ff33aedb90708dadddcfc43d664491b762 Mon Sep 17 00:00:00 2001 From: ari melody Date: Sun, 8 Jun 2025 21:40:19 +0100 Subject: [PATCH 01/19] add makefile --- .gitignore | 1 + Makefile | 28 ++++++++++++++++++++++++++++ main.go | 6 +++++- 3 files changed, 34 insertions(+), 1 deletion(-) create mode 100644 Makefile diff --git a/.gitignore b/.gitignore index 7e016e8..7b56d29 100644 --- a/.gitignore +++ b/.gitignore @@ -1,3 +1,4 @@ .DS_Store tmp indir +indir-* diff --git a/Makefile b/Makefile new file mode 100644 index 0000000..7858ed9 --- /dev/null +++ b/Makefile @@ -0,0 +1,28 @@ +GOOS := $(shell uname -s | tr '[:upper:]' '[:lower:]') +GOARCH := $(shell uname -m) +TRIPLE := $(GOOS)-$(GOARCH) + +.PHONY: build + +build: + go build -o indir-$(TRIPLE) . + ln -sf ./indir-$(TRIPLE) ./indir + +build-multiplatform: indir-linux-amd64 indir-darwin-arm64 + +indir-linux-amd64: + GOOS=linux GOARCH=amd64 go build -o ./indir-linux-amd64 . + +indir-darwin-arm64: + GOOS=darwin GOARCH=arm64 go build -o ./indir-darwin-arm64 . + +clean: + rm -rf \ + ./indir \ + ./indir-$(TRIPLE) \ + ./indir-darwin-arm64 \ + ./indir-linux-amd64 + +install: build + cp ./indir-$(TRIPLE) /usr/local/bin/indir + chmod +x /usr/local/bin/indir diff --git a/main.go b/main.go index 60bb278..06dc368 100755 --- a/main.go +++ b/main.go @@ -12,8 +12,12 @@ import ( "strconv" "strings" "time" + _ "embed" ) +//go:embed templates/dir.html +var dirTemplateSrc string + type ( Directory struct { Name string @@ -89,7 +93,7 @@ func main() { ".DS_Store", } - dirTemplate, err := template.ParseGlob("./templates/dir.html") + dirTemplate, err := template.New("dir").Parse(dirTemplateSrc) if err != nil { fmt.Fprintf(os.Stderr, "fatal: failed to parse directory template: %v\n", err) os.Exit(1) From 7aef1a80124cda27ee46dab9ec769c45f7a274d2 Mon Sep 17 00:00:00 2001 From: ari melody Date: Mon, 16 Mar 2026 18:29:09 +0000 Subject: [PATCH 02/19] update old domain --- go.mod | 2 +- templates/dir.html | 2 +- 2 files changed, 2 insertions(+), 2 deletions(-) diff --git a/go.mod b/go.mod index c405b03..c1104b3 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,3 @@ -module git.arimelody.me/ari/indir +module forge.arimelody.space/ari/indir go 1.24.3 diff --git a/templates/dir.html b/templates/dir.html index 7d0ac2b..a0f5b53 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -85,7 +85,7 @@ footer {
From 245d6e0fa0b8caa29ae610f50404915bd2be8a23 Mon Sep 17 00:00:00 2001 From: zaire Date: Mon, 16 Mar 2026 20:41:16 +0100 Subject: [PATCH 03/19] fixed prior merge conflic causing double'd embed, added readme parser with full markdown, added readme render to HTML, renamed filepath to fpath to use FilePath.Join for case insensitivity, fixed ident in html src (inline css), added readme css with proper accents --- go.mod | 2 + go.sum | 2 + main.go | 185 ++++++++++++++++++++++++--------------- templates/dir.html | 214 ++++++++++++++++++++++++++++++++------------- 4 files changed, 270 insertions(+), 133 deletions(-) create mode 100644 go.sum diff --git a/go.mod b/go.mod index c1104b3..0b80eac 100644 --- a/go.mod +++ b/go.mod @@ -1,3 +1,5 @@ module forge.arimelody.space/ari/indir go 1.24.3 + +require github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab diff --git a/go.sum b/go.sum new file mode 100644 index 0000000..52d2bfd --- /dev/null +++ b/go.sum @@ -0,0 +1,2 @@ +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab h1:VYNivV7P8IRHUam2swVUNkhIdp0LRRFKe4hXNnoZKTc= +github.com/gomarkdown/markdown v0.0.0-20260217112301-37c66b85d6ab/go.mod h1:JDGcbDT52eL4fju3sZ4TeHGsQwhG9nbDV21aMyhwPoA= diff --git a/main.go b/main.go index a00ecd0..778ec1b 100755 --- a/main.go +++ b/main.go @@ -1,6 +1,7 @@ package main import ( + _ "embed" "fmt" "html/template" "io/fs" @@ -8,11 +9,15 @@ import ( "net/http" "os" "path" + "path/filepath" "slices" "strconv" "strings" "time" - _ "embed" + + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" ) //go:embed templates/dir.html @@ -20,25 +25,25 @@ var dirTemplateSrc string type ( Directory struct { - Name string - Root bool - Files []*File + Name string + Root bool + Files []*File + Readme template.HTML } File struct { - Name string - URI string - IsDir bool - Size string + Name string + URI string + IsDir bool + Size string ModifiedDate string } ) -//go:embed templates/dir.html -var dirHTML string - func main() { - if len(os.Args) < 2 { printHelp() } + if len(os.Args) < 2 { + printHelp() + } host := "127.0.0.1" port := 8080 @@ -47,7 +52,9 @@ func main() { filesDir := "" i := 1 for { - if i >= len(os.Args) { break } + if i >= len(os.Args) { + break + } switch os.Args[i] { case "-h": fallthrough @@ -55,7 +62,7 @@ func main() { printHelp() case "--host": - if i + 1 >= len(os.Args) { + if i+1 >= len(os.Args) { fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n") os.Exit(1) } @@ -63,7 +70,7 @@ func main() { host = os.Args[i] case "--port": - if i + 1 >= len(os.Args) { + if i+1 >= len(os.Args) { fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n") os.Exit(1) } @@ -76,14 +83,18 @@ func main() { } case "--root": - if i + 1 >= len(os.Args) { + if i+1 >= len(os.Args) { fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n") os.Exit(1) } i++ root = os.Args[i] - if !strings.HasPrefix(root, "/") { root = "/" + root } - if !strings.HasSuffix(root, "/") { root += "/" } + if !strings.HasPrefix(root, "/") { + root = "/" + root + } + if !strings.HasSuffix(root, "/") { + root += "/" + } default: if len(filesDir) > 0 { @@ -111,7 +122,9 @@ func main() { } fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) - if root != "/" { fmt.Printf("%s", root) } + if root != "/" { + fmt.Printf("%s", root) + } fmt.Println(".") http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -121,8 +134,8 @@ func main() { } isRoot := r.URL.Path == root - filepath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) - info, err := os.Stat(filepath) + fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) + info, err := os.Stat(fpath) if err != nil { http.NotFound(w, r) return @@ -135,7 +148,7 @@ func main() { return } - file, err := os.Open(filepath) + file, err := os.Open(fpath) if err != nil { http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return @@ -144,7 +157,7 @@ func main() { defer func() { err := file.Close() if err != nil { - fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", filepath, err) + fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err) } }() @@ -159,30 +172,54 @@ func main() { _, err = file.WriteTo(w) if err != nil { - fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", filepath, err) + fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err) } return } if !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path + "/", http.StatusFound) + http.Redirect(w, r, r.URL.Path+"/", http.StatusFound) return } - data := Directory{ - Root: isRoot, - Name: r.URL.Path, - Files: []*File{}, + // embeded readme + var readmeHTML template.HTML + entries, err := os.ReadDir(fpath) + if err == nil { + for _, entry := range entries { + if strings.EqualFold(entry.Name(), "readme.md") { + src, err := os.ReadFile(filepath.Join(fpath, entry.Name())) + if err == nil { + mdFlags := html.CommonFlags | html.HrefTargetBlank + mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags}) + mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) + md := mdParser.Parse(src) + readmeHTML = template.HTML(markdown.Render(md, mdRenderer)) + } + break + } + } } - fsDir := os.DirFS(filepath) + data := Directory{ + Root: isRoot, + Name: r.URL.Path, + Files: []*File{}, + Readme: readmeHTML, + } + + fsDir := os.DirFS(fpath) directories, err := fs.ReadDir(fsDir, ".") for _, dir := range directories { name := dir.Name() - if slices.Contains(ignoredFiles, name) { continue } + if slices.Contains(ignoredFiles, name) { + continue + } info, err := dir.Info() - if err != nil { continue } + if err != nil { + continue + } var uri string if isRoot { @@ -213,10 +250,10 @@ func main() { dateStr := info.ModTime().Format("02-Jan-2006 15:04") data.Files = append(data.Files, &File{ - Name: name, - URI: uri, - IsDir: info.IsDir(), - Size: sizeStr, + Name: name, + URI: uri, + IsDir: info.IsDir(), + Size: sizeStr, ModifiedDate: dateStr, }) } @@ -232,52 +269,60 @@ func main() { } type LoggingResponseWriter struct { - http.ResponseWriter - Status int + http.ResponseWriter + Status int } -var COL_Reset = "\033[0m" -var COL_Red = "\033[31m" -var COL_Green = "\033[32m" +var COL_Reset = "\033[0m" +var COL_Red = "\033[31m" +var COL_Green = "\033[32m" var COL_Yellow = "\033[33m" -var COL_Blue = "\033[34m" +var COL_Blue = "\033[34m" var COL_Purple = "\033[35m" -var COL_Cyan = "\033[36m" -var COL_Gray = "\033[37m" -var COL_White = "\033[97m" +var COL_Cyan = "\033[36m" +var COL_Gray = "\033[37m" +var COL_White = "\033[97m" func HTTPLog(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() - lrw := LoggingResponseWriter{w, http.StatusOK} + lrw := LoggingResponseWriter{w, http.StatusOK} - next.ServeHTTP(&lrw, r) + next.ServeHTTP(&lrw, r) - after := time.Now() - difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 - elapsed := "<1" - if difference >= 1 { - elapsed = strconv.Itoa(difference) - } + after := time.Now() + difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 + elapsed := "<1" + if difference >= 1 { + elapsed = strconv.Itoa(difference) + } - statusColour := COL_Reset + statusColour := COL_Reset - if lrw.Status - 600 <= 0 { statusColour = COL_Red } - if lrw.Status - 500 <= 0 { statusColour = COL_Yellow } - if lrw.Status - 400 <= 0 { statusColour = COL_White } - if lrw.Status - 300 <= 0 { statusColour = COL_Green } + if lrw.Status-600 <= 0 { + statusColour = COL_Red + } + if lrw.Status-500 <= 0 { + statusColour = COL_Yellow + } + if lrw.Status-400 <= 0 { + statusColour = COL_White + } + if lrw.Status-300 <= 0 { + statusColour = COL_Green + } - fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", - after.Format(time.UnixDate), - r.Method, - r.URL.Path, - statusColour, - lrw.Status, - COL_Reset, - elapsed, - r.Header["User-Agent"][0]) - }) + fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", + after.Format(time.UnixDate), + r.Method, + r.URL.Path, + statusColour, + lrw.Status, + COL_Reset, + elapsed, + r.Header["User-Agent"][0]) + }) } func printHelp() { diff --git a/templates/dir.html b/templates/dir.html index 562cc95..6d20006 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -1,82 +1,157 @@ - + - - + + Files in {{.Name}}

Files in {{.Name}}

-
+
@@ -89,8 +164,7 @@ footer { - {{end}} - {{range .Files}} + {{end}} {{range .Files}} @@ -98,10 +172,24 @@ footer { {{end}}
Name
{{.Name}} {{if .IsDir}}—{{else}}{{.Size}}{{end}}
-
+
+ {{if .Readme}} +
+

README

+
+ {{.Readme}} +
+ {{end}}
From 73fdf8bb46fb0079522637899ef4d20764ccb8e5 Mon Sep 17 00:00:00 2001 From: supitszaire Date: Mon, 16 Mar 2026 21:00:51 +0100 Subject: [PATCH 04/19] format fix --- main.go | 146 ++++++++++++++++++++++++-------------------------------- 1 file changed, 62 insertions(+), 84 deletions(-) diff --git a/main.go b/main.go index 778ec1b..267f123 100755 --- a/main.go +++ b/main.go @@ -1,7 +1,6 @@ package main import ( - _ "embed" "fmt" "html/template" "io/fs" @@ -14,7 +13,8 @@ import ( "strconv" "strings" "time" - + _ "embed" + "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" @@ -25,25 +25,23 @@ var dirTemplateSrc string type ( Directory struct { - Name string - Root bool - Files []*File + Name string + Root bool + Files []*File Readme template.HTML } File struct { - Name string - URI string - IsDir bool - Size string + Name string + URI string + IsDir bool + Size string ModifiedDate string } ) func main() { - if len(os.Args) < 2 { - printHelp() - } + if len(os.Args) < 2 { printHelp() } host := "127.0.0.1" port := 8080 @@ -52,9 +50,7 @@ func main() { filesDir := "" i := 1 for { - if i >= len(os.Args) { - break - } + if i >= len(os.Args) { break } switch os.Args[i] { case "-h": fallthrough @@ -62,7 +58,7 @@ func main() { printHelp() case "--host": - if i+1 >= len(os.Args) { + if i + 1 >= len(os.Args) { fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n") os.Exit(1) } @@ -70,7 +66,7 @@ func main() { host = os.Args[i] case "--port": - if i+1 >= len(os.Args) { + if i + 1 >= len(os.Args) { fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n") os.Exit(1) } @@ -83,18 +79,14 @@ func main() { } case "--root": - if i+1 >= len(os.Args) { + if i + 1 >= len(os.Args) { fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n") os.Exit(1) } i++ root = os.Args[i] - if !strings.HasPrefix(root, "/") { - root = "/" + root - } - if !strings.HasSuffix(root, "/") { - root += "/" - } + if !strings.HasPrefix(root, "/") { root = "/" + root } + if !strings.HasSuffix(root, "/") { root += "/" } default: if len(filesDir) > 0 { @@ -122,9 +114,7 @@ func main() { } fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) - if root != "/" { - fmt.Printf("%s", root) - } + if root != "/" { fmt.Printf("%s", root) } fmt.Println(".") http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { @@ -178,7 +168,7 @@ func main() { } if !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path+"/", http.StatusFound) + http.Redirect(w, r, r.URL.Path + "/", http.StatusFound) return } @@ -200,11 +190,11 @@ func main() { } } } - + data := Directory{ - Root: isRoot, - Name: r.URL.Path, - Files: []*File{}, + Root: isRoot, + Name: r.URL.Path, + Files: []*File{}, Readme: readmeHTML, } @@ -212,14 +202,10 @@ func main() { directories, err := fs.ReadDir(fsDir, ".") for _, dir := range directories { name := dir.Name() - if slices.Contains(ignoredFiles, name) { - continue - } + if slices.Contains(ignoredFiles, name) { continue } info, err := dir.Info() - if err != nil { - continue - } + if err != nil { continue } var uri string if isRoot { @@ -250,10 +236,10 @@ func main() { dateStr := info.ModTime().Format("02-Jan-2006 15:04") data.Files = append(data.Files, &File{ - Name: name, - URI: uri, - IsDir: info.IsDir(), - Size: sizeStr, + Name: name, + URI: uri, + IsDir: info.IsDir(), + Size: sizeStr, ModifiedDate: dateStr, }) } @@ -269,60 +255,52 @@ func main() { } type LoggingResponseWriter struct { - http.ResponseWriter - Status int + http.ResponseWriter + Status int } -var COL_Reset = "\033[0m" -var COL_Red = "\033[31m" -var COL_Green = "\033[32m" +var COL_Reset = "\033[0m" +var COL_Red = "\033[31m" +var COL_Green = "\033[32m" var COL_Yellow = "\033[33m" -var COL_Blue = "\033[34m" +var COL_Blue = "\033[34m" var COL_Purple = "\033[35m" -var COL_Cyan = "\033[36m" -var COL_Gray = "\033[37m" -var COL_White = "\033[97m" +var COL_Cyan = "\033[36m" +var COL_Gray = "\033[37m" +var COL_White = "\033[97m" func HTTPLog(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() - lrw := LoggingResponseWriter{w, http.StatusOK} + lrw := LoggingResponseWriter{w, http.StatusOK} - next.ServeHTTP(&lrw, r) + next.ServeHTTP(&lrw, r) - after := time.Now() - difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 - elapsed := "<1" - if difference >= 1 { - elapsed = strconv.Itoa(difference) - } + after := time.Now() + difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 + elapsed := "<1" + if difference >= 1 { + elapsed = strconv.Itoa(difference) + } - statusColour := COL_Reset + statusColour := COL_Reset - if lrw.Status-600 <= 0 { - statusColour = COL_Red - } - if lrw.Status-500 <= 0 { - statusColour = COL_Yellow - } - if lrw.Status-400 <= 0 { - statusColour = COL_White - } - if lrw.Status-300 <= 0 { - statusColour = COL_Green - } + if lrw.Status - 600 <= 0 { statusColour = COL_Red } + if lrw.Status - 500 <= 0 { statusColour = COL_Yellow } + if lrw.Status - 400 <= 0 { statusColour = COL_White } + if lrw.Status - 300 <= 0 { statusColour = COL_Green } - fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", - after.Format(time.UnixDate), - r.Method, - r.URL.Path, - statusColour, - lrw.Status, - COL_Reset, - elapsed, - r.Header["User-Agent"][0]) - }) + fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", + after.Format(time.UnixDate), + r.Method, + r.URL.Path, + statusColour, + lrw.Status, + COL_Reset, + elapsed, + r.Header["User-Agent"][0]) + }) } func printHelp() { From ef3dc28e7ceab083559d1b9f80565ac4cc237703 Mon Sep 17 00:00:00 2001 From: supitszaire Date: Mon, 16 Mar 2026 21:01:53 +0100 Subject: [PATCH 05/19] format fix --- templates/dir.html | 43 +++++++++++++++++++------------------------ 1 file changed, 19 insertions(+), 24 deletions(-) diff --git a/templates/dir.html b/templates/dir.html index 6d20006..1936d24 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -56,42 +56,37 @@ .readme { margin-top: 2rem; } - - .readme h1, - .readme h2, - .readme h3, - .readme h4, - .readme h5, - .readme h6 { + + .readme h1, .readme h2, .readme h3, + .readme h4, .readme h5, .readme h6 { color: #f0f0f0; margin-top: 1.2rem; } - - .readme p, - .readme li { + + .readme p, .readme li { line-height: 1.7; color: #d0d0d0; } - + .readme a { color: #b7fd49; } - + .readme a:hover { color: white; text-decoration: underline; } - + .readme code { background: #1e1e1e; border: 1px solid #333; border-radius: 3px; - padding: 0.1em, 0.4em; - font-family: "Monaspace Argon", monospace; - font-size: 0.9em; + padding: .1em, .4em; + font-family: 'Monaspace Argon', monospace; + font-size: .9em; color: #b7fd79; } - + .readme pre { background: #1e1e1e; border: 3px solid #333; @@ -99,35 +94,35 @@ padding: 1em; overflow-x: auto; } - + .readme pre code { border: none; padding: 0; background: transparent; } - + .readme blockquote { border-left: 3px solid #b7fd49; margin: 0; padding-left: 1em; color: #a0a0a0; } - + .readme hr { border-color: #333; } - + .readme table { width: auto; } - + .readme table td, .readme table th { border: 1px solid #333; - padding: 0.3em 0.6em; + padding: .3em .6em; width: auto; } - + footer { padding: 1em 0; } From da76681c29388b9d3a67b5032fc3fac123238b17 Mon Sep 17 00:00:00 2001 From: supitszaire Date: Mon, 16 Mar 2026 21:08:06 +0100 Subject: [PATCH 06/19] removed README header in html --- templates/dir.html | 2 -- 1 file changed, 2 deletions(-) diff --git a/templates/dir.html b/templates/dir.html index 1936d24..b0b9ebf 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -170,8 +170,6 @@
{{if .Readme}}
-

README

-
{{.Readme}}
{{end}} From f567a2eb83a2dff3841fedf7bd9f87544c6ba4e1 Mon Sep 17 00:00:00 2001 From: zaire Date: Mon, 16 Mar 2026 22:05:29 +0100 Subject: [PATCH 07/19] serve index.html when present (case-sensitve) --- .gitignore | 2 + main.go | 657 ++++++++++++++++++++++----------------------- templates/dir.html | 21 +- 3 files changed, 330 insertions(+), 350 deletions(-) diff --git a/.gitignore b/.gitignore index 7b56d29..749bc60 100644 --- a/.gitignore +++ b/.gitignore @@ -2,3 +2,5 @@ tmp indir indir-* +.zed +index.html diff --git a/main.go b/main.go index 778ec1b..4a3517d 100755 --- a/main.go +++ b/main.go @@ -1,339 +1,324 @@ -package main + package main -import ( - _ "embed" - "fmt" - "html/template" - "io/fs" - "mime" - "net/http" - "os" - "path" - "path/filepath" - "slices" - "strconv" - "strings" - "time" + import ( + "fmt" + "html/template" + "io/fs" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "slices" + "strconv" + "strings" + "time" + _ "embed" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" -) - -//go:embed templates/dir.html -var dirTemplateSrc string - -type ( - Directory struct { - Name string - Root bool - Files []*File - Readme template.HTML - } - - File struct { - Name string - URI string - IsDir bool - Size string - ModifiedDate string - } -) - -func main() { - if len(os.Args) < 2 { - printHelp() - } - - host := "127.0.0.1" - port := 8080 - root := "/" - - filesDir := "" - i := 1 - for { - if i >= len(os.Args) { - break - } - switch os.Args[i] { - case "-h": - fallthrough - case "--help": - printHelp() - - case "--host": - if i+1 >= len(os.Args) { - fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n") - os.Exit(1) - } - i++ - host = os.Args[i] - - case "--port": - if i+1 >= len(os.Args) { - fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n") - os.Exit(1) - } - i++ - var err error - port, err = strconv.Atoi(os.Args[i]) - if err != nil { - fmt.Fprintf(os.Stderr, "fatal: failed to parse port %s: %v\n", os.Args[i], err) - os.Exit(1) - } - - case "--root": - if i+1 >= len(os.Args) { - fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n") - os.Exit(1) - } - i++ - root = os.Args[i] - if !strings.HasPrefix(root, "/") { - root = "/" + root - } - if !strings.HasSuffix(root, "/") { - root += "/" - } - - default: - if len(filesDir) > 0 { - fmt.Fprintf(os.Stderr, "unsupported argument: %s\n", os.Args[i]) - os.Exit(1) - } - filesDir = os.Args[i] - } - i++ - } - - if len(filesDir) == 0 { - filesDir = "." - } - - ignoredFiles := []string{ - ".", - ".DS_Store", - } - - dirTemplate, err := template.New("dir").Parse(dirTemplateSrc) - if err != nil { - fmt.Fprintf(os.Stderr, "fatal: failed to parse directory template: %v\n", err) - os.Exit(1) - } - - fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) - if root != "/" { - fmt.Printf("%s", root) - } - fmt.Println(".") - - http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.URL.Path, root) { - http.NotFound(w, r) - return - } - isRoot := r.URL.Path == root - - fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) - info, err := os.Stat(fpath) - if err != nil { - http.NotFound(w, r) - return - } - - // downloading file - if !info.IsDir() { - if strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/"), http.StatusFound) - return - } - - file, err := os.Open(fpath) - if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return - } - - defer func() { - err := file.Close() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err) - } - }() - - mimeType := "application/octet-stream" - extPos := strings.LastIndex(info.Name(), ".") - if extPos != -1 { - mimeType = mime.TypeByExtension(info.Name()[:extPos]) - } - - w.Header().Set("Content-Type", mimeType) - w.WriteHeader(http.StatusOK) - - _, err = file.WriteTo(w) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err) - } - return - } - - if !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path+"/", http.StatusFound) - return - } - - // embeded readme - var readmeHTML template.HTML - entries, err := os.ReadDir(fpath) - if err == nil { - for _, entry := range entries { - if strings.EqualFold(entry.Name(), "readme.md") { - src, err := os.ReadFile(filepath.Join(fpath, entry.Name())) - if err == nil { - mdFlags := html.CommonFlags | html.HrefTargetBlank - mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags}) - mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) - md := mdParser.Parse(src) - readmeHTML = template.HTML(markdown.Render(md, mdRenderer)) - } - break - } - } - } - - data := Directory{ - Root: isRoot, - Name: r.URL.Path, - Files: []*File{}, - Readme: readmeHTML, - } - - fsDir := os.DirFS(fpath) - directories, err := fs.ReadDir(fsDir, ".") - for _, dir := range directories { - name := dir.Name() - if slices.Contains(ignoredFiles, name) { - continue - } - - info, err := dir.Info() - if err != nil { - continue - } - - var uri string - if isRoot { - uri = root + name - } else { - uri = r.URL.Path + name - } - - sizeStr := "—" - if !info.IsDir() { - size := info.Size() - sizeDenom := "B" - if size > 1000 { - size /= 1000 - sizeDenom = "KB" - } - if size > 1000 { - size /= 1000 - sizeDenom = "MB" - } - if size > 1000 { - size /= 1000 - sizeDenom = "GB" - } - sizeStr = fmt.Sprintf("%d%s", size, sizeDenom) - } - - dateStr := info.ModTime().Format("02-Jan-2006 15:04") - - data.Files = append(data.Files, &File{ - Name: name, - URI: uri, - IsDir: info.IsDir(), - Size: sizeStr, - ModifiedDate: dateStr, - }) - } - if err != nil { - fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err) - } - - w.Header().Set("Content-Type", "text/html") - w.Header().Set("Server", "indir") - w.WriteHeader(http.StatusOK) - dirTemplate.Execute(w, data) - }))) -} - -type LoggingResponseWriter struct { - http.ResponseWriter - Status int -} - -var COL_Reset = "\033[0m" -var COL_Red = "\033[31m" -var COL_Green = "\033[32m" -var COL_Yellow = "\033[33m" -var COL_Blue = "\033[34m" -var COL_Purple = "\033[35m" -var COL_Cyan = "\033[36m" -var COL_Gray = "\033[37m" -var COL_White = "\033[97m" - -func HTTPLog(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() - - lrw := LoggingResponseWriter{w, http.StatusOK} - - next.ServeHTTP(&lrw, r) - - after := time.Now() - difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 - elapsed := "<1" - if difference >= 1 { - elapsed = strconv.Itoa(difference) - } - - statusColour := COL_Reset - - if lrw.Status-600 <= 0 { - statusColour = COL_Red - } - if lrw.Status-500 <= 0 { - statusColour = COL_Yellow - } - if lrw.Status-400 <= 0 { - statusColour = COL_White - } - if lrw.Status-300 <= 0 { - statusColour = COL_Green - } - - fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", - after.Format(time.UnixDate), - r.Method, - r.URL.Path, - statusColour, - lrw.Status, - COL_Reset, - elapsed, - r.Header["User-Agent"][0]) - }) -} - -func printHelp() { - fmt.Printf( - `%s [--host address] [--port port] [--root http_root] directory - - --help shows this help message - --host address hosts on the specified address - --port port hosts on the specified port - --root http_root hosts on the specified subdirectory, i.e. `+"`/files/`\n", - os.Args[0], + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" ) - os.Exit(0) -} + + //go:embed templates/dir.html + var dirTemplateSrc string + + type ( + Directory struct { + Name string + Root bool + Files []*File + Readme template.HTML + } + + File struct { + Name string + URI string + IsDir bool + Size string + ModifiedDate string + } + ) + + func main() { + if len(os.Args) < 2 { printHelp() } + + host := "127.0.0.1" + port := 8080 + root := "/" + + filesDir := "" + i := 1 + for { + if i >= len(os.Args) { break } + switch os.Args[i] { + case "-h": + fallthrough + case "--help": + printHelp() + + case "--host": + if i + 1 >= len(os.Args) { + fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n") + os.Exit(1) + } + i++ + host = os.Args[i] + + case "--port": + if i + 1 >= len(os.Args) { + fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n") + os.Exit(1) + } + i++ + var err error + port, err = strconv.Atoi(os.Args[i]) + if err != nil { + fmt.Fprintf(os.Stderr, "fatal: failed to parse port %s: %v\n", os.Args[i], err) + os.Exit(1) + } + + case "--root": + if i + 1 >= len(os.Args) { + fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n") + os.Exit(1) + } + i++ + root = os.Args[i] + if !strings.HasPrefix(root, "/") { root = "/" + root } + if !strings.HasSuffix(root, "/") { root += "/" } + + default: + if len(filesDir) > 0 { + fmt.Fprintf(os.Stderr, "unsupported argument: %s\n", os.Args[i]) + os.Exit(1) + } + filesDir = os.Args[i] + } + i++ + } + + if len(filesDir) == 0 { + filesDir = "." + } + + ignoredFiles := []string{ + ".", + ".DS_Store", + } + + dirTemplate, err := template.New("dir").Parse(dirTemplateSrc) + if err != nil { + fmt.Fprintf(os.Stderr, "fatal: failed to parse directory template: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) + if root != "/" { fmt.Printf("%s", root) } + fmt.Println(".") + + http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, root) { + http.NotFound(w, r) + return + } + isRoot := r.URL.Path == root + + fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) + info, err := os.Stat(fpath) + if err != nil { + http.NotFound(w, r) + return + } + + // downloading file + if !info.IsDir() { + if strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/"), http.StatusFound) + return + } + + file, err := os.Open(fpath) + if err != nil { + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) + return + } + + defer func() { + err := file.Close() + if err != nil { + fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err) + } + }() + + mimeType := "application/octet-stream" + extPos := strings.LastIndex(info.Name(), ".") + if extPos != -1 { + mimeType = mime.TypeByExtension(info.Name()[:extPos]) + } + + w.Header().Set("Content-Type", mimeType) + w.WriteHeader(http.StatusOK) + + _, err = file.WriteTo(w) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err) + } + return + } + + if !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, r.URL.Path + "/", http.StatusFound) + return + } + + // serve index.html if present (case-sensitive) + indexPath := filepath.Join(fpath, "index.html") + if _, err := os.Stat(indexPath); err == nil { + http.ServeFile(w, r, indexPath) + return + } + + // embeded readme + var readmeHTML template.HTML + entries, err := os.ReadDir(fpath) + if err == nil { + for _, entry := range entries { + if strings.EqualFold(entry.Name(), "readme.md") { + src, err := os.ReadFile(filepath.Join(fpath, entry.Name())) + if err == nil { + mdFlags := html.CommonFlags | html.HrefTargetBlank + mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags}) + mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) + md := mdParser.Parse(src) + readmeHTML = template.HTML(markdown.Render(md, mdRenderer)) + } + break + } + } + } + + data := Directory{ + Root: isRoot, + Name: r.URL.Path, + Files: []*File{}, + Readme: readmeHTML, + } + + fsDir := os.DirFS(fpath) + directories, err := fs.ReadDir(fsDir, ".") + for _, dir := range directories { + name := dir.Name() + if slices.Contains(ignoredFiles, name) { continue } + + info, err := dir.Info() + if err != nil { continue } + + var uri string + if isRoot { + uri = root + name + } else { + uri = r.URL.Path + name + } + + sizeStr := "—" + if !info.IsDir() { + size := info.Size() + sizeDenom := "B" + if size > 1000 { + size /= 1000 + sizeDenom = "KB" + } + if size > 1000 { + size /= 1000 + sizeDenom = "MB" + } + if size > 1000 { + size /= 1000 + sizeDenom = "GB" + } + sizeStr = fmt.Sprintf("%d%s", size, sizeDenom) + } + + dateStr := info.ModTime().Format("02-Jan-2006 15:04") + + data.Files = append(data.Files, &File{ + Name: name, + URI: uri, + IsDir: info.IsDir(), + Size: sizeStr, + ModifiedDate: dateStr, + }) + } + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err) + } + + w.Header().Set("Content-Type", "text/html") + w.Header().Set("Server", "indir") + w.WriteHeader(http.StatusOK) + dirTemplate.Execute(w, data) + }))) + } + + type LoggingResponseWriter struct { + http.ResponseWriter + Status int + } + + var COL_Reset = "\033[0m" + var COL_Red = "\033[31m" + var COL_Green = "\033[32m" + var COL_Yellow = "\033[33m" + var COL_Blue = "\033[34m" + var COL_Purple = "\033[35m" + var COL_Cyan = "\033[36m" + var COL_Gray = "\033[37m" + var COL_White = "\033[97m" + + func HTTPLog(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + lrw := LoggingResponseWriter{w, http.StatusOK} + + next.ServeHTTP(&lrw, r) + + after := time.Now() + difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 + elapsed := "<1" + if difference >= 1 { + elapsed = strconv.Itoa(difference) + } + + statusColour := COL_Reset + + if lrw.Status - 600 <= 0 { statusColour = COL_Red } + if lrw.Status - 500 <= 0 { statusColour = COL_Yellow } + if lrw.Status - 400 <= 0 { statusColour = COL_White } + if lrw.Status - 300 <= 0 { statusColour = COL_Green } + + fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", + after.Format(time.UnixDate), + r.Method, + r.URL.Path, + statusColour, + lrw.Status, + COL_Reset, + elapsed, + r.Header["User-Agent"][0]) + }) + } + + func printHelp() { + fmt.Printf( + `%s [--host address] [--port port] [--root http_root] directory + + --help shows this help message + --host address hosts on the specified address + --port port hosts on the specified port + --root http_root hosts on the specified subdirectory, i.e. `+"`/files/`\n", + os.Args[0], + ) + os.Exit(0) + } diff --git a/templates/dir.html b/templates/dir.html index 6d20006..a885117 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -57,18 +57,13 @@ margin-top: 2rem; } - .readme h1, - .readme h2, - .readme h3, - .readme h4, - .readme h5, - .readme h6 { + .readme h1, .readme h2, .readme h3, + .readme h4, .readme h5, .readme h6 { color: #f0f0f0; margin-top: 1.2rem; } - .readme p, - .readme li { + .readme p, .readme li { line-height: 1.7; color: #d0d0d0; } @@ -86,9 +81,9 @@ background: #1e1e1e; border: 1px solid #333; border-radius: 3px; - padding: 0.1em, 0.4em; - font-family: "Monaspace Argon", monospace; - font-size: 0.9em; + padding: .1em, .4em; + font-family: 'Monaspace Argon', monospace; + font-size: .9em; color: #b7fd79; } @@ -124,7 +119,7 @@ .readme table td, .readme table th { border: 1px solid #333; - padding: 0.3em 0.6em; + padding: .3em .6em; width: auto; } @@ -175,8 +170,6 @@
{{if .Readme}}
-

README

-
{{.Readme}}
{{end}} From 7014763271c0bc8c6b3af8ba0837a57bb6362a06 Mon Sep 17 00:00:00 2001 From: zaire Date: Mon, 16 Mar 2026 22:21:17 +0100 Subject: [PATCH 08/19] fixed formatting i hope :) --- main.go | 566 ++++++++++++++++++++++++++++---------------------------- 1 file changed, 283 insertions(+), 283 deletions(-) diff --git a/main.go b/main.go index 4a3517d..80002ec 100755 --- a/main.go +++ b/main.go @@ -1,324 +1,324 @@ - package main +package main - import ( - "fmt" - "html/template" - "io/fs" - "mime" - "net/http" - "os" - "path" - "path/filepath" - "slices" - "strconv" - "strings" - "time" - _ "embed" +import ( + "fmt" + "html/template" + "io/fs" + "mime" + "net/http" + "os" + "path" + "path/filepath" + "slices" + "strconv" + "strings" + "time" + _ "embed" - "github.com/gomarkdown/markdown" - "github.com/gomarkdown/markdown/html" - "github.com/gomarkdown/markdown/parser" - ) + "github.com/gomarkdown/markdown" + "github.com/gomarkdown/markdown/html" + "github.com/gomarkdown/markdown/parser" +) - //go:embed templates/dir.html - var dirTemplateSrc string +//go:embed templates/dir.html +var dirTemplateSrc string - type ( - Directory struct { - Name string - Root bool - Files []*File - Readme template.HTML - } +type ( + Directory struct { + Name string + Root bool + Files []*File + Readme template.HTML + } - File struct { - Name string - URI string - IsDir bool - Size string - ModifiedDate string - } - ) + File struct { + Name string + URI string + IsDir bool + Size string + ModifiedDate string + } +) - func main() { - if len(os.Args) < 2 { printHelp() } +func main() { + if len(os.Args) < 2 { printHelp() } - host := "127.0.0.1" - port := 8080 - root := "/" + host := "127.0.0.1" + port := 8080 + root := "/" - filesDir := "" - i := 1 - for { - if i >= len(os.Args) { break } - switch os.Args[i] { - case "-h": - fallthrough - case "--help": - printHelp() + filesDir := "" + i := 1 + for { + if i >= len(os.Args) { break } + switch os.Args[i] { + case "-h": + fallthrough + case "--help": + printHelp() - case "--host": - if i + 1 >= len(os.Args) { - fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n") - os.Exit(1) - } - i++ - host = os.Args[i] - - case "--port": - if i + 1 >= len(os.Args) { - fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n") - os.Exit(1) - } - i++ - var err error - port, err = strconv.Atoi(os.Args[i]) - if err != nil { - fmt.Fprintf(os.Stderr, "fatal: failed to parse port %s: %v\n", os.Args[i], err) - os.Exit(1) - } - - case "--root": - if i + 1 >= len(os.Args) { - fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n") - os.Exit(1) - } - i++ - root = os.Args[i] - if !strings.HasPrefix(root, "/") { root = "/" + root } - if !strings.HasSuffix(root, "/") { root += "/" } - - default: - if len(filesDir) > 0 { - fmt.Fprintf(os.Stderr, "unsupported argument: %s\n", os.Args[i]) - os.Exit(1) - } - filesDir = os.Args[i] + case "--host": + if i + 1 >= len(os.Args) { + fmt.Fprintf(os.Stderr, "fatal: --host argument cannot be empty\n") + os.Exit(1) } i++ - } + host = os.Args[i] - if len(filesDir) == 0 { - filesDir = "." - } + case "--port": + if i + 1 >= len(os.Args) { + fmt.Fprintf(os.Stderr, "fatal: --port argument cannot be empty\n") + os.Exit(1) + } + i++ + var err error + port, err = strconv.Atoi(os.Args[i]) + if err != nil { + fmt.Fprintf(os.Stderr, "fatal: failed to parse port %s: %v\n", os.Args[i], err) + os.Exit(1) + } - ignoredFiles := []string{ - ".", - ".DS_Store", - } + case "--root": + if i + 1 >= len(os.Args) { + fmt.Fprintf(os.Stderr, "fatal: --root argument cannot be empty\n") + os.Exit(1) + } + i++ + root = os.Args[i] + if !strings.HasPrefix(root, "/") { root = "/" + root } + if !strings.HasSuffix(root, "/") { root += "/" } - dirTemplate, err := template.New("dir").Parse(dirTemplateSrc) + default: + if len(filesDir) > 0 { + fmt.Fprintf(os.Stderr, "unsupported argument: %s\n", os.Args[i]) + os.Exit(1) + } + filesDir = os.Args[i] + } + i++ + } + + if len(filesDir) == 0 { + filesDir = "." + } + + ignoredFiles := []string{ + ".", + ".DS_Store", + } + + dirTemplate, err := template.New("dir").Parse(dirTemplateSrc) + if err != nil { + fmt.Fprintf(os.Stderr, "fatal: failed to parse directory template: %v\n", err) + os.Exit(1) + } + + fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) + if root != "/" { fmt.Printf("%s", root) } + fmt.Println(".") + + http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + if !strings.HasPrefix(r.URL.Path, root) { + http.NotFound(w, r) + return + } + isRoot := r.URL.Path == root + + fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) + info, err := os.Stat(fpath) if err != nil { - fmt.Fprintf(os.Stderr, "fatal: failed to parse directory template: %v\n", err) - os.Exit(1) + http.NotFound(w, r) + return } - fmt.Printf("Now hosting \"%s\" at http://%s:%d", filesDir, host, port) - if root != "/" { fmt.Printf("%s", root) } - fmt.Println(".") - - http.ListenAndServe(fmt.Sprintf("%s:%d", host, port), HTTPLog(http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - if !strings.HasPrefix(r.URL.Path, root) { - http.NotFound(w, r) + // downloading file + if !info.IsDir() { + if strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/"), http.StatusFound) return } - isRoot := r.URL.Path == root - fpath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) - info, err := os.Stat(fpath) + file, err := os.Open(fpath) if err != nil { - http.NotFound(w, r) + http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) return } - // downloading file - if !info.IsDir() { - if strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, strings.TrimSuffix(r.URL.Path, "/"), http.StatusFound) - return - } - - file, err := os.Open(fpath) + defer func() { + err := file.Close() if err != nil { - http.Error(w, http.StatusText(http.StatusInternalServerError), http.StatusInternalServerError) - return + fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err) } + }() - defer func() { - err := file.Close() - if err != nil { - fmt.Fprintf(os.Stderr, "failed to close file %s: %v\n", fpath, err) - } - }() - - mimeType := "application/octet-stream" - extPos := strings.LastIndex(info.Name(), ".") - if extPos != -1 { - mimeType = mime.TypeByExtension(info.Name()[:extPos]) - } - - w.Header().Set("Content-Type", mimeType) - w.WriteHeader(http.StatusOK) - - _, err = file.WriteTo(w) - if err != nil { - fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err) - } - return + mimeType := "application/octet-stream" + extPos := strings.LastIndex(info.Name(), ".") + if extPos != -1 { + mimeType = mime.TypeByExtension(info.Name()[:extPos]) } - if !strings.HasSuffix(r.URL.Path, "/") { - http.Redirect(w, r, r.URL.Path + "/", http.StatusFound) - return - } - - // serve index.html if present (case-sensitive) - indexPath := filepath.Join(fpath, "index.html") - if _, err := os.Stat(indexPath); err == nil { - http.ServeFile(w, r, indexPath) - return - } - - // embeded readme - var readmeHTML template.HTML - entries, err := os.ReadDir(fpath) - if err == nil { - for _, entry := range entries { - if strings.EqualFold(entry.Name(), "readme.md") { - src, err := os.ReadFile(filepath.Join(fpath, entry.Name())) - if err == nil { - mdFlags := html.CommonFlags | html.HrefTargetBlank - mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags}) - mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) - md := mdParser.Parse(src) - readmeHTML = template.HTML(markdown.Render(md, mdRenderer)) - } - break - } - } - } - - data := Directory{ - Root: isRoot, - Name: r.URL.Path, - Files: []*File{}, - Readme: readmeHTML, - } - - fsDir := os.DirFS(fpath) - directories, err := fs.ReadDir(fsDir, ".") - for _, dir := range directories { - name := dir.Name() - if slices.Contains(ignoredFiles, name) { continue } - - info, err := dir.Info() - if err != nil { continue } - - var uri string - if isRoot { - uri = root + name - } else { - uri = r.URL.Path + name - } - - sizeStr := "—" - if !info.IsDir() { - size := info.Size() - sizeDenom := "B" - if size > 1000 { - size /= 1000 - sizeDenom = "KB" - } - if size > 1000 { - size /= 1000 - sizeDenom = "MB" - } - if size > 1000 { - size /= 1000 - sizeDenom = "GB" - } - sizeStr = fmt.Sprintf("%d%s", size, sizeDenom) - } - - dateStr := info.ModTime().Format("02-Jan-2006 15:04") - - data.Files = append(data.Files, &File{ - Name: name, - URI: uri, - IsDir: info.IsDir(), - Size: sizeStr, - ModifiedDate: dateStr, - }) - } - if err != nil { - fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err) - } - - w.Header().Set("Content-Type", "text/html") - w.Header().Set("Server", "indir") + w.Header().Set("Content-Type", mimeType) w.WriteHeader(http.StatusOK) - dirTemplate.Execute(w, data) - }))) - } - type LoggingResponseWriter struct { - http.ResponseWriter - Status int - } + _, err = file.WriteTo(w) + if err != nil { + fmt.Fprintf(os.Stderr, "failed to send file %s: %v\n", fpath, err) + } + return + } - var COL_Reset = "\033[0m" - var COL_Red = "\033[31m" - var COL_Green = "\033[32m" - var COL_Yellow = "\033[33m" - var COL_Blue = "\033[34m" - var COL_Purple = "\033[35m" - var COL_Cyan = "\033[36m" - var COL_Gray = "\033[37m" - var COL_White = "\033[97m" + if !strings.HasSuffix(r.URL.Path, "/") { + http.Redirect(w, r, r.URL.Path + "/", http.StatusFound) + return + } - func HTTPLog(next http.Handler) http.Handler { - return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { - start := time.Now() + // serve index.html if present (case-sensitive) + indexPath := filepath.Join(fpath, "index.html") + if _, err := os.Stat(indexPath); err == nil { + http.ServeFile(w, r, indexPath) + return + } - lrw := LoggingResponseWriter{w, http.StatusOK} + // embeded readme + var readmeHTML template.HTML + entries, err := os.ReadDir(fpath) + if err == nil { + for _, entry := range entries { + if strings.EqualFold(entry.Name(), "readme.md") { + src, err := os.ReadFile(filepath.Join(fpath, entry.Name())) + if err == nil { + mdFlags := html.CommonFlags | html.HrefTargetBlank + mdRenderer := html.NewRenderer(html.RendererOptions{Flags: mdFlags}) + mdParser := parser.NewWithExtensions(parser.CommonExtensions | parser.AutoHeadingIDs) + md := mdParser.Parse(src) + readmeHTML = template.HTML(markdown.Render(md, mdRenderer)) + } + break + } + } + } - next.ServeHTTP(&lrw, r) + data := Directory{ + Root: isRoot, + Name: r.URL.Path, + Files: []*File{}, + Readme: readmeHTML, + } - after := time.Now() - difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 - elapsed := "<1" - if difference >= 1 { - elapsed = strconv.Itoa(difference) - } + fsDir := os.DirFS(fpath) + directories, err := fs.ReadDir(fsDir, ".") + for _, dir := range directories { + name := dir.Name() + if slices.Contains(ignoredFiles, name) { continue } - statusColour := COL_Reset + info, err := dir.Info() + if err != nil { continue } - if lrw.Status - 600 <= 0 { statusColour = COL_Red } - if lrw.Status - 500 <= 0 { statusColour = COL_Yellow } - if lrw.Status - 400 <= 0 { statusColour = COL_White } - if lrw.Status - 300 <= 0 { statusColour = COL_Green } + var uri string + if isRoot { + uri = root + name + } else { + uri = r.URL.Path + name + } - fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", - after.Format(time.UnixDate), - r.Method, - r.URL.Path, - statusColour, - lrw.Status, - COL_Reset, - elapsed, - r.Header["User-Agent"][0]) - }) - } + sizeStr := "—" + if !info.IsDir() { + size := info.Size() + sizeDenom := "B" + if size > 1000 { + size /= 1000 + sizeDenom = "KB" + } + if size > 1000 { + size /= 1000 + sizeDenom = "MB" + } + if size > 1000 { + size /= 1000 + sizeDenom = "GB" + } + sizeStr = fmt.Sprintf("%d%s", size, sizeDenom) + } - func printHelp() { - fmt.Printf( - `%s [--host address] [--port port] [--root http_root] directory + dateStr := info.ModTime().Format("02-Jan-2006 15:04") - --help shows this help message - --host address hosts on the specified address - --port port hosts on the specified port - --root http_root hosts on the specified subdirectory, i.e. `+"`/files/`\n", - os.Args[0], - ) - os.Exit(0) - } + data.Files = append(data.Files, &File{ + Name: name, + URI: uri, + IsDir: info.IsDir(), + Size: sizeStr, + ModifiedDate: dateStr, + }) + } + if err != nil { + fmt.Fprintf(os.Stderr, "failed to open directory: %v\n", err) + } + + w.Header().Set("Content-Type", "text/html") + w.Header().Set("Server", "indir") + w.WriteHeader(http.StatusOK) + dirTemplate.Execute(w, data) + }))) +} + +type LoggingResponseWriter struct { + http.ResponseWriter + Status int +} + +var COL_Reset = "\033[0m" +var COL_Red = "\033[31m" +var COL_Green = "\033[32m" +var COL_Yellow = "\033[33m" +var COL_Blue = "\033[34m" +var COL_Purple = "\033[35m" +var COL_Cyan = "\033[36m" +var COL_Gray = "\033[37m" +var COL_White = "\033[97m" + +func HTTPLog(next http.Handler) http.Handler { + return http.HandlerFunc(func(w http.ResponseWriter, r *http.Request) { + start := time.Now() + + lrw := LoggingResponseWriter{w, http.StatusOK} + + next.ServeHTTP(&lrw, r) + + after := time.Now() + difference := (after.Nanosecond() - start.Nanosecond()) / 1_000_000 + elapsed := "<1" + if difference >= 1 { + elapsed = strconv.Itoa(difference) + } + + statusColour := COL_Reset + + if lrw.Status - 600 <= 0 { statusColour = COL_Red } + if lrw.Status - 500 <= 0 { statusColour = COL_Yellow } + if lrw.Status - 400 <= 0 { statusColour = COL_White } + if lrw.Status - 300 <= 0 { statusColour = COL_Green } + + fmt.Printf("[%s] %s %s - %s%d%s (%sms) (%s)\n", + after.Format(time.UnixDate), + r.Method, + r.URL.Path, + statusColour, + lrw.Status, + COL_Reset, + elapsed, + r.Header["User-Agent"][0]) + }) +} + +func printHelp() { + fmt.Printf( + `%s [--host address] [--port port] [--root http_root] directory + + --help shows this help message + --host address hosts on the specified address + --port port hosts on the specified port + --root http_root hosts on the specified subdirectory, i.e. `+"`/files/`\n", + os.Args[0], + ) + os.Exit(0) +} From 0065861eabde6c29b48ee9719b8333b3cc90348a Mon Sep 17 00:00:00 2001 From: zaire Date: Mon, 16 Mar 2026 22:44:21 +0100 Subject: [PATCH 09/19] fixed serving of directory named 'index.html' --- main.go | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/main.go b/main.go index 80002ec..78c8271 100755 --- a/main.go +++ b/main.go @@ -173,8 +173,8 @@ func main() { } // serve index.html if present (case-sensitive) - indexPath := filepath.Join(fpath, "index.html") - if _, err := os.Stat(indexPath); err == nil { + indexPath := fpath + "/index.html" + if indexInfo, err := os.Stat(indexPath); err == nil && !indexInfo.IsDir() { http.ServeFile(w, r, indexPath) return } From eb7b1cf026ae91d3e22d4e9a0da7a9de3bc14b63 Mon Sep 17 00:00:00 2001 From: supitszaire Date: Mon, 16 Mar 2026 22:52:14 +0100 Subject: [PATCH 10/19] platform agnostic change --- main.go | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/main.go b/main.go index 78c8271..890efa7 100755 --- a/main.go +++ b/main.go @@ -173,7 +173,7 @@ func main() { } // serve index.html if present (case-sensitive) - indexPath := fpath + "/index.html" + indexPath := filepath.Join(fpath, "index.html") if indexInfo, err := os.Stat(indexPath); err == nil && !indexInfo.IsDir() { http.ServeFile(w, r, indexPath) return From 4ee46d8f5246e6449579f18499eb348ecf28cf9b Mon Sep 17 00:00:00 2001 From: supitszaire Date: Tue, 17 Mar 2026 00:57:20 +0100 Subject: [PATCH 11/19] updated readme to reflect additions added section: customization>readme added TODO & checked off things that are done --- README.md | 12 +++++++++++- 1 file changed, 11 insertions(+), 1 deletion(-) diff --git a/README.md b/README.md index 2f25adc..0f6d330 100644 --- a/README.md +++ b/README.md @@ -16,7 +16,17 @@ indir [--host address] [--port port] [--root http_root] directory --root http_root hosts on the specified subdirectory, i.e. `/files/` ``` +## customization + +### readme +drop a `README.md` into any directory and indir will render +it below the file listing with full markdown support! + +--- + ## to-do: - [x] use templates instead of hard-coded HTML (i was lazy) -- [ ] directory header from readme file +- [x] directory header from readme file - [ ] directory stylesheet overrides +- [x] index.html serving support +- [ ] fix mime-types for browser file view \ No newline at end of file From fccba73bacfdc08e9ac1b806974c7abcc684e140 Mon Sep 17 00:00:00 2001 From: zaire Date: Tue, 17 Mar 2026 10:49:09 +0100 Subject: [PATCH 12/19] FIX: mime-type returning only octet-stream --- main.go | 4 +++- 1 file changed, 3 insertions(+), 1 deletion(-) diff --git a/main.go b/main.go index 78c8271..a7a0b01 100755 --- a/main.go +++ b/main.go @@ -154,7 +154,9 @@ func main() { mimeType := "application/octet-stream" extPos := strings.LastIndex(info.Name(), ".") if extPos != -1 { - mimeType = mime.TypeByExtension(info.Name()[:extPos]) + if m := mime.TypeByExtension(info.Name()[extPos:]); m != "" { + mimeType = m + } } w.Header().Set("Content-Type", mimeType) From 523aa894d0cb8ebc0269c94a7cb50bdaba07cc2c Mon Sep 17 00:00:00 2001 From: supitszaire Date: Tue, 17 Mar 2026 10:52:04 +0100 Subject: [PATCH 13/19] update: reflect MIME-TYPE fix --- README.md | 2 +- 1 file changed, 1 insertion(+), 1 deletion(-) diff --git a/README.md b/README.md index 0f6d330..f6825ec 100644 --- a/README.md +++ b/README.md @@ -29,4 +29,4 @@ it below the file listing with full markdown support! - [x] directory header from readme file - [ ] directory stylesheet overrides - [x] index.html serving support -- [ ] fix mime-types for browser file view \ No newline at end of file +- [x] fix mime-types for browser file view \ No newline at end of file From eac93e08bb788f47f3765ba608650c4e79e9f2f1 Mon Sep 17 00:00:00 2001 From: zaire Date: Tue, 17 Mar 2026 20:16:28 +0100 Subject: [PATCH 14/19] FEAT: directory stylesheet overrides --- README.md | 4 ++-- main.go | 11 +++++++++++ templates/dir.html | 3 +++ 3 files changed, 16 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index f6825ec..c72e245 100644 --- a/README.md +++ b/README.md @@ -27,6 +27,6 @@ it below the file listing with full markdown support! ## to-do: - [x] use templates instead of hard-coded HTML (i was lazy) - [x] directory header from readme file -- [ ] directory stylesheet overrides +- [x] directory stylesheet overrides - [x] index.html serving support -- [x] fix mime-types for browser file view \ No newline at end of file +- [x] fix mime-types for browser file view diff --git a/main.go b/main.go index d01ac12..cc2247e 100755 --- a/main.go +++ b/main.go @@ -29,6 +29,7 @@ type ( Root bool Files []*File Readme template.HTML + CSS template.CSS } File struct { @@ -181,6 +182,15 @@ func main() { return } + // load index.css if present + var customCSS template.CSS + cssPath := filepath.Join(fpath, "index.css") + if cssInfo, err := os.Stat(cssPath); err == nil && !cssInfo.IsDir() { + if src, err := os.ReadFile(cssPath); err == nil { + customCSS = template.CSS(src) + } + } + // embeded readme var readmeHTML template.HTML entries, err := os.ReadDir(fpath) @@ -205,6 +215,7 @@ func main() { Name: r.URL.Path, Files: []*File{}, Readme: readmeHTML, + CSS: customCSS, } fsDir := os.DirFS(fpath) diff --git a/templates/dir.html b/templates/dir.html index a885117..bd812e3 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -142,6 +142,9 @@ } } + {{if .CSS}} + + {{end}}
From fe05bb8a8d4ce49266faa5b44780ddebabd8131f Mon Sep 17 00:00:00 2001 From: supitszaire Date: Tue, 17 Mar 2026 20:38:48 +0100 Subject: [PATCH 15/19] updated readme to reflect new features <3 --- README.md | 12 ++++++++++-- 1 file changed, 10 insertions(+), 2 deletions(-) diff --git a/README.md b/README.md index c72e245..b1dc9bc 100644 --- a/README.md +++ b/README.md @@ -16,12 +16,20 @@ indir [--host address] [--port port] [--root http_root] directory --root http_root hosts on the specified subdirectory, i.e. `/files/` ``` -## customization +## features -### readme +### file serving +indir has the possibility of automatically serving an `index.html` instead of displaying the directory view! + +### customization + +#### readme drop a `README.md` into any directory and indir will render it below the file listing with full markdown support! +#### custom css +using a 'index.css' file, you can customize the directory view! + --- ## to-do: From 3b64b2b97870e7b12ef1d5c5031ebc845f15f757 Mon Sep 17 00:00:00 2001 From: ari melody Date: Wed, 18 Mar 2026 09:18:02 +0000 Subject: [PATCH 16/19] credit contributors in footer --- templates/dir.html | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/templates/dir.html b/templates/dir.html index bd812e3..d5f4d74 100644 --- a/templates/dir.html +++ b/templates/dir.html @@ -179,9 +179,9 @@