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}}