package main import ( "fmt" "html/template" "io/fs" "mime" "net/http" "os" "path" "slices" "strconv" "strings" "time" ) type ( Directory struct { Name string Root bool Files []*File } 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: filesDir = os.Args[i] } i++ } ignoredFiles := []string{ ".", ".DS_Store", } dirTemplate, err := template.ParseGlob("./templates/dir.html") 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 filepath := path.Join(filesDir, strings.TrimPrefix(r.URL.Path, root)) info, err := os.Stat(filepath) 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(filepath) 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", filepath, 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", filepath, err) } return } if !strings.HasSuffix(r.URL.Path, "/") { http.Redirect(w, r, r.URL.Path + "/", http.StatusFound) return } data := Directory{ Root: isRoot, Name: r.URL.Path, Files: []*File{}, } fsDir := os.DirFS(filepath) 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) }