update auf public download
All checks were successful
release-tag / release-image (push) Successful in 1m40s

This commit is contained in:
2025-09-27 15:55:16 +02:00
parent 7d0f4befe1
commit 8c05ad6ffe
2 changed files with 92 additions and 8 deletions

View File

@@ -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, ums schlank zu halten
// Stream
_, _ = io.Copy(w, rc)
}