From 8c05ad6ffe065ec92caacb5629a258cc5d6a2d67 Mon Sep 17 00:00:00 2001 From: jbergner Date: Sat, 27 Sep 2025 15:55:16 +0200 Subject: [PATCH] update auf public download --- cmd/unified/main.go | 94 ++++++++++++++++++++++++++++++++++++++--- internal/admin/admin.go | 6 +-- 2 files changed, 92 insertions(+), 8 deletions(-) diff --git a/cmd/unified/main.go b/cmd/unified/main.go index 2a54c01..d764624 100644 --- a/cmd/unified/main.go +++ b/cmd/unified/main.go @@ -62,11 +62,13 @@ func loadConfig() AppConfig { } return AppConfig{ - HTTPAddr: httpAddr, - APIKey: apiKey, - AdminUser: adminUser, - AdminPass: adminPass, - Mesh: m, + HTTPAddr: httpAddr, + APIKey: apiKey, + AdminUser: adminUser, + AdminPass: adminPass, + Mesh: m, + PublicDownloads: parseBoolEnv("PUBLIC_DOWNLOADS", false), + PublicPath: getenvDefault("PUBLIC_DOWNLOAD_PATH", "/dl"), } } @@ -137,6 +139,9 @@ type AppConfig struct { AdminUser string AdminPass string Mesh mesh.Config + + PublicDownloads bool // ENV: PUBLIC_DOWNLOADS (default false) + PublicPath string // ENV: PUBLIC_DOWNLOAD_PATH (default "/dl") } /*** Middleware ***/ @@ -455,6 +460,10 @@ func main() { apiFiles(apiMux, st, blobs, mnode) root.Handle("/api/", authMiddleware(cfg.APIKey, apiMux)) + if cfg.PublicDownloads { + registerPublicDownloads(root, blobs, mnode, cfg.PublicPath) + } + // Admin-UI (optional BasicAuth via ADMIN_USER/ADMIN_PASS) adminRoot := http.NewServeMux() admin.Register(adminRoot, admin.Deps{Store: st, Mesh: mnode, Blob: blobs}) @@ -502,3 +511,78 @@ func main() { _ = mnode.Close(ctx) log.Println("shutdown complete") } + +// Public: GET {base}/{id} +// Beispiel: /dl/1 +func registerPublicDownloads(mux *http.ServeMux, blobs blobfs.Store, meshNode *mesh.Node, base string) { + if !strings.HasPrefix(base, "/") { + base = "/" + base + } + mux.HandleFunc(base+"/", func(w http.ResponseWriter, r *http.Request) { + idStr := strings.TrimPrefix(r.URL.Path, base+"/") + if idStr == "" { + http.NotFound(w, r) + return + } + id, err := strconv.ParseInt(idStr, 10, 64) + if err != nil || id <= 0 { + http.NotFound(w, r) + return + } + + // 1) lokal versuchen + if rc, meta, err := blobs.Open(r.Context(), id); err == nil { + defer rc.Close() + serveBlob(w, r, rc, meta) + return + } + + // 2) aus dem Mesh ziehen (signiert) und lokal cachen + rrc, name, _, _, err := meshNode.FetchBlobAny(r.Context(), id) + if err != nil { + http.NotFound(w, r) + return + } + defer rrc.Close() + if _, err := blobs.Save(r.Context(), id, name, rrc); err != nil { + http.Error(w, "cache failed", http.StatusInternalServerError) + return + } + + // 3) erneut lokal öffnen (saubere Meta/Größe/CT) + lrc, meta, err := blobs.Open(r.Context(), id) + if err != nil { + http.Error(w, "open failed", http.StatusInternalServerError) + return + } + defer lrc.Close() + serveBlob(w, r, lrc, meta) + }) +} + +// Hilfsfunktion: setzt sinnvolle Header und streamt die Datei +func serveBlob(w http.ResponseWriter, r *http.Request, rc io.ReadSeeker, meta blobfs.Meta) { + // Caching (einfach): ETag = SHA256 + if meta.SHA256 != "" { + etag := `W/"` + meta.SHA256 + `"` + if inm := r.Header.Get("If-None-Match"); inm == etag { + w.WriteHeader(http.StatusNotModified) + return + } + w.Header().Set("ETag", etag) + } + + // Standard-Header + if meta.ContentType == "" { + meta.ContentType = "application/octet-stream" + } + w.Header().Set("Content-Type", meta.ContentType) + w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10)) + w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, meta.Name)) + w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") // für Browser-JS + w.Header().Set("X-Robots-Tag", "noindex") + + // (Optional) einfache Range-Unterstützung weglassen, um’s schlank zu halten + // Stream + _, _ = io.Copy(w, rc) +} diff --git a/internal/admin/admin.go b/internal/admin/admin.go index a321cc5..2ddc687 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -199,7 +199,7 @@ func Register(mux *http.ServeMux, d Deps) { _ = d.Mesh.SyncNow(r.Context()) // prompt push (best effort) } // Nach Aktion Items partial zurückgeben (HTMX swap) - http.Redirect(w, r, "/admin/items", http.StatusSeeOther) + http.Redirect(w, r, "/admin", http.StatusSeeOther) }) mux.HandleFunc("/admin/items/rename", func(w http.ResponseWriter, r *http.Request) { @@ -213,7 +213,7 @@ func Register(mux *http.ServeMux, d Deps) { _, _ = d.Store.Rename(r.Context(), filesvc.ID(id), newName) _ = d.Mesh.SyncNow(r.Context()) } - http.Redirect(w, r, "/admin/items", http.StatusSeeOther) + http.Redirect(w, r, "/admin", http.StatusSeeOther) //hier test nicht mehr /admin/items }) mux.HandleFunc("/admin/items/delete", func(w http.ResponseWriter, r *http.Request) { @@ -226,7 +226,7 @@ func Register(mux *http.ServeMux, d Deps) { _ = d.Blob.Delete(r.Context(), int64(id)) _ = d.Mesh.SyncNow(r.Context()) } - http.Redirect(w, r, "/admin/items", http.StatusSeeOther) + http.Redirect(w, r, "/admin", http.StatusSeeOther) }) mux.HandleFunc("/admin/mesh/syncnow", func(w http.ResponseWriter, r *http.Request) {