package web import ( _ "embed" "fmt" "html/template" "io/fs" "mime" "net/http" "os" "path" "path/filepath" "slices" "strings" "github.com/gomarkdown/markdown" "github.com/gomarkdown/markdown/html" "github.com/gomarkdown/markdown/parser" ) type ( AppState struct { Root string FilesDir string IgnoredFiles []string } 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 } ) //go:embed dir.html var dirTemplateSrc string var dirTemplate = template.Must(template.New("dir").Parse(dirTemplateSrc)) func Handler(app *AppState) http.HandlerFunc { return func(w http.ResponseWriter, r *http.Request) { if !strings.HasPrefix(r.URL.Path, app.Root) { http.NotFound(w, r) return } isRoot := r.URL.Path == app.Root fpath := path.Join(app.FilesDir, strings.TrimPrefix(r.URL.Path, app.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(app.IgnoredFiles, name) { continue } info, err := dir.Info() if err != nil { continue } var uri string if isRoot { uri = app.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) } }