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