// fileserver.go package main import ( "fmt" "log" "net/http" "os" "path" "path/filepath" "strings" "time" ) func main() { base := getenv("SERVE_DIR", ".") addr := getenv("SERVE_ADDR", ":8080") forceAtt := parseBoolEnv(getenv("FORCE_ATTACHMENT", "1")) // 1/true = Download erzwingen fsys := http.Dir(base) http.HandleFunc("/", func(w http.ResponseWriter, r *http.Request) { // Nur GET/HEAD if r.Method != http.MethodGet && r.Method != http.MethodHead { http.Error(w, "Method not allowed", http.StatusMethodNotAllowed) return } // Keine Startseite / kein Listing clean := path.Clean("/" + r.URL.Path) if clean == "/" { http.NotFound(w, r) return } // Sicher relativ zum SERVE_DIR öffnen (http.Dir verhindert "..") f, err := fsys.Open(clean) if err != nil { http.NotFound(w, r) return } defer f.Close() info, err := f.Stat() if err != nil || info.IsDir() { http.NotFound(w, r) return } if forceAtt { w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, filepath.Base(clean))) w.Header().Set("Content-Type", "application/octet-stream") w.Header().Set("X-Content-Type-Options", "nosniff") } // Unterstützt Range/If-Modified-Since/HEAD http.ServeContent(w, r, info.Name(), modTimeSafe(info.ModTime()), f) }) log.Printf("Serving files from %q on %s (attachment=%v)", base, addr, forceAtt) log.Fatal(http.ListenAndServe(addr, nil)) } func getenv(k, def string) string { if v, ok := os.LookupEnv(k); ok { return v } return def } func parseBoolEnv(v string) bool { v = strings.TrimSpace(strings.ToLower(v)) return v == "1" || v == "true" || v == "yes" || v == "on" } func modTimeSafe(t time.Time) time.Time { if t.Before(time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC)) { return time.Date(1980, 1, 1, 0, 0, 0, 0, time.UTC) } if t.After(time.Date(2107, 12, 31, 23, 59, 59, 0, time.UTC)) { return time.Date(2107, 12, 31, 23, 59, 59, 0, time.UTC) } return t }