diff --git a/README.md b/README.md index b1dc9bc..de37ec7 100644 --- a/README.md +++ b/README.md @@ -2,7 +2,7 @@ a nifty static file indexer -indir provides a nice, simple frontend for browsing your web directories! +indir provides a nice, simple web frontend for browsing your file directories! heavily inspired by traditional file indexers provided by the likes of nginx and apache, with some modern amenities on top :) @@ -18,23 +18,9 @@ indir [--host address] [--port port] [--root http_root] directory ## features -### 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: -- [x] use templates instead of hard-coded HTML (i was lazy) -- [x] directory header from readme file -- [x] directory stylesheet overrides -- [x] index.html serving support -- [x] fix mime-types for browser file view +- **index files:** if available, indir will prefer serving an `index.html` file +instead of a directory listing. +- **custom CSS:** indir will append the contents of a local `index.css` file to +the default stylesheet, if applicable. +- **readme files:** adding a `readme.md` file (case insensitive) will append +its contents beneath the directory tree in rendered markdown. diff --git a/log/main.go b/log/main.go new file mode 100644 index 0000000..a58ee28 --- /dev/null +++ b/log/main.go @@ -0,0 +1,57 @@ +package log + +import ( + "fmt" + "net/http" + "strconv" + "time" +) + +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]) + }) +} diff --git a/main.go b/main.go index cc2247e..4a09b47 100755 --- a/main.go +++ b/main.go @@ -2,43 +2,13 @@ package main 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 - CSS template.CSS - } - - File struct { - Name string - URI string - IsDir bool - Size string - ModifiedDate string - } + "forge.arimelody.space/ari/indir/log" + "forge.arimelody.space/ari/indir/web" ) func main() { @@ -108,230 +78,30 @@ func main() { ".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 + app := web.AppState{ + Root: root, + FilesDir: filesDir, + IgnoredFiles: ignoredFiles, + } - 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 { - if m := mime.TypeByExtension(info.Name()[extPos:]); m != "" { - mimeType = m - } - } - - 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 indexInfo, err := os.Stat(indexPath); err == nil && !indexInfo.IsDir() { - http.ServeFile(w, r, indexPath) - 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) - 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, - CSS: customCSS, - } - - 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]) - }) + http.ListenAndServe( + fmt.Sprintf("%s:%d", host, port), + log.HTTPLog(web.Handler(&app)), + ) } func printHelp() { fmt.Printf( - `%s [--host address] [--port port] [--root http_root] directory + `indir [--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/web/dir.html similarity index 97% rename from templates/dir.html rename to web/dir.html index bd812e3..d5f4d74 100644 --- a/templates/dir.html +++ b/web/dir.html @@ -179,9 +179,9 @@