diverse bugfixes
All checks were successful
release-tag / release-image (push) Successful in 1m32s

This commit is contained in:
2025-09-27 17:39:39 +02:00
parent d125e3dd54
commit 43f1d01a8a
2 changed files with 60 additions and 50 deletions

View File

@@ -193,7 +193,7 @@ func writeJSON(w http.ResponseWriter, code int, v any) {
/*** API-Routen ***/ /*** API-Routen ***/
func fileRoutes(mux *http.ServeMux, store filesvc.MeshStore) { func fileRoutes(mux *http.ServeMux, store filesvc.MeshStore, blobs blobfs.Store) {
// Health // Health
mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) {
writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) writeJSON(w, http.StatusOK, map[string]string{"status": "ok"})
@@ -275,6 +275,7 @@ func fileRoutes(mux *http.ServeMux, store filesvc.MeshStore) {
return return
} }
it, err := store.Delete(r.Context(), in.ID) it, err := store.Delete(r.Context(), in.ID)
_ = blobs.Delete(r.Context(), int64(in.ID))
if err != nil { if err != nil {
status := http.StatusBadRequest status := http.StatusBadRequest
if errors.Is(err, filesvc.ErrNotFound) { if errors.Is(err, filesvc.ErrNotFound) {
@@ -330,53 +331,51 @@ func apiFiles(mux *http.ServeMux, store filesvc.MeshStore, blobs blobfs.Store, m
// Download // Download
mux.HandleFunc("/api/v1/files/", func(w http.ResponseWriter, r *http.Request) { mux.HandleFunc("/api/v1/files/", func(w http.ResponseWriter, r *http.Request) {
// /api/v1/files/{id}/download
parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/files/"), "/") parts := strings.Split(strings.TrimPrefix(r.URL.Path, "/api/v1/files/"), "/")
if len(parts) != 2 || parts[1] != "download" { if len(parts) != 2 || parts[1] != "download" {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
id, err := strconv.ParseInt(parts[0], 10, 64) id, err := strconv.ParseInt(parts[0], 10, 64)
if err != nil { if err != nil || id <= 0 {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
// 1) lokal // 1) Metadaten prüfen
if rc, meta, err := blobs.Open(r.Context(), id); err == nil { it, err := store.Get(r.Context(), filesvc.ID(id))
defer rc.Close() if err != nil || it.Deleted {
w.Header().Set("Content-Type", meta.ContentType) http.NotFound(w, r)
w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10))
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, meta.Name))
_, _ = io.Copy(w, rc)
return return
} }
// 2) remote holen // 2) lokal
rrc, name, _, _, err := meshNode.FetchBlobAny(r.Context(), id) if rc, meta, err := blobs.Open(r.Context(), id); err == nil {
defer rc.Close()
serveBlob(w, r, rc, meta, it.Name)
return
}
// 3) remote holen & cachen
rrc, _, _, _, err := meshNode.FetchBlobAny(r.Context(), id)
if err != nil { if err != nil {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
defer rrc.Close() defer rrc.Close()
if _, err := blobs.Save(r.Context(), id, it.Name, rrc); err != nil {
// 3) lokal cachen http.Error(w, "cache failed", http.StatusInternalServerError)
if _, err := blobs.Save(r.Context(), id, name, rrc); err != nil {
http.Error(w, "cache failed: "+err.Error(), http.StatusInternalServerError)
return return
} }
// 4) erneut lokal öffnen und streamen // 4) lokal streamen
lrc, meta, err := blobs.Open(r.Context(), id) lrc, meta, err := blobs.Open(r.Context(), id)
if err != nil { if err != nil {
http.Error(w, "open failed", http.StatusInternalServerError) http.Error(w, "open failed", http.StatusInternalServerError)
return return
} }
defer lrc.Close() defer lrc.Close()
w.Header().Set("Content-Type", meta.ContentType) serveBlob(w, r, lrc, meta, it.Name)
w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10))
w.Header().Set("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, meta.Name))
_, _ = io.Copy(w, lrc)
}) })
} }
@@ -417,7 +416,7 @@ func main() {
st := filesvc.NewMemStore() st := filesvc.NewMemStore()
// Mesh starten // Mesh starten
mcfg := mesh.FromEnv() //mcfg := mesh.FromEnv()
blobs := blobfs.New(getenvDefault("DATA_DIR", "./data")) blobs := blobfs.New(getenvDefault("DATA_DIR", "./data"))
mnode, err := mesh.New(cfg.Mesh, mesh.Callbacks{ mnode, err := mesh.New(cfg.Mesh, mesh.Callbacks{
@@ -432,11 +431,15 @@ func main() {
return st.ApplyRemote(ctx, fromMeshSnapshot(s)) return st.ApplyRemote(ctx, fromMeshSnapshot(s))
}, },
BlobOpen: func(ctx context.Context, id int64) (io.ReadCloser, string, string, int64, error) { BlobOpen: func(ctx context.Context, id int64) (io.ReadCloser, string, string, int64, error) {
it, err := st.Get(ctx, filesvc.ID(id))
if err != nil || it.Deleted {
return nil, "", "", 0, fmt.Errorf("not found")
}
rc, meta, err := blobs.Open(ctx, id) rc, meta, err := blobs.Open(ctx, id)
if err != nil { if err != nil {
return nil, "", "", 0, err return nil, "", "", 0, err
} }
return rc, meta.Name, meta.ContentType, meta.Size, nil return rc, it.Name, meta.ContentType, meta.Size, nil
}, },
}) })
if err != nil { if err != nil {
@@ -444,7 +447,7 @@ func main() {
} }
go func() { go func() {
log.Printf("[mesh] listening on %s advertise %s seeds=%v discovery=%v", log.Printf("[mesh] listening on %s advertise %s seeds=%v discovery=%v",
mcfg.BindAddr, mcfg.AdvertURL, mcfg.Seeds, mcfg.EnableDiscovery) cfg.Mesh.BindAddr, cfg.Mesh.AdvertURL, cfg.Mesh.Seeds, cfg.Mesh.EnableDiscovery)
if err := mnode.Serve(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := mnode.Serve(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("mesh serve: %v", err) log.Fatalf("mesh serve: %v", err)
} }
@@ -456,12 +459,12 @@ func main() {
// API (Bearer-Auth) // API (Bearer-Auth)
//blobs := blobfs.New(getenvDefault("DATA_DIR", "./data")) //blobs := blobfs.New(getenvDefault("DATA_DIR", "./data"))
apiMux := http.NewServeMux() apiMux := http.NewServeMux()
fileRoutes(apiMux, st) fileRoutes(apiMux, st, blobs)
apiFiles(apiMux, st, blobs, mnode) apiFiles(apiMux, st, blobs, mnode)
root.Handle("/api/", authMiddleware(cfg.APIKey, apiMux)) root.Handle("/api/", authMiddleware(cfg.APIKey, apiMux))
if cfg.PublicDownloads { if cfg.PublicDownloads {
registerPublicDownloads(root, blobs, mnode, cfg.PublicPath) registerPublicDownloads(root, st, blobs, mnode, cfg.PublicPath)
} }
// Admin-UI (optional BasicAuth via ADMIN_USER/ADMIN_PASS) // Admin-UI (optional BasicAuth via ADMIN_USER/ADMIN_PASS)
@@ -494,7 +497,8 @@ func main() {
// Graceful shutdown // Graceful shutdown
go func() { go func() {
log.Printf("http listening on %s (api=/api/v1, admin=/admin)", cfg.Mesh.BindAddr) log.Printf("http listening on %s (api=/api/v1, admin=/admin)", cfg.HTTPAddr)
log.Printf("mesh listening on %s", cfg.Mesh.BindAddr)
if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) { if err := srv.ListenAndServe(); err != nil && !errors.Is(err, http.ErrServerClosed) {
log.Fatalf("http server: %v", err) log.Fatalf("http server: %v", err)
} }
@@ -514,7 +518,7 @@ func main() {
// Public: GET {base}/{id} // Public: GET {base}/{id}
// Beispiel: /dl/1 // Beispiel: /dl/1
func registerPublicDownloads(mux *http.ServeMux, blobs blobfs.Store, meshNode *mesh.Node, base string) { func registerPublicDownloads(mux *http.ServeMux, store filesvc.MeshStore, blobs blobfs.Store, meshNode *mesh.Node, base string) {
if !strings.HasPrefix(base, "/") { if !strings.HasPrefix(base, "/") {
base = "/" + base base = "/" + base
} }
@@ -530,59 +534,64 @@ func registerPublicDownloads(mux *http.ServeMux, blobs blobfs.Store, meshNode *m
return return
} }
// 1) lokal versuchen // 1) Metadaten prüfen (nicht vorhanden oder gelöscht → 404)
if rc, meta, err := blobs.Open(r.Context(), id); err == nil { it, err := store.Get(r.Context(), filesvc.ID(id))
defer rc.Close() if err != nil || it.Deleted {
serveBlob(w, r, rc, meta) http.NotFound(w, r)
return return
} }
// 2) aus dem Mesh ziehen (signiert) und lokal cachen // 2) lokal versuchen
rrc, name, _, _, err := meshNode.FetchBlobAny(r.Context(), id) if rc, meta, err := blobs.Open(r.Context(), id); err == nil {
defer rc.Close()
serveBlob(w, r, rc, meta, it.Name) // Download-Name aus Store!
return
}
// 3) aus Mesh holen (signiert) und cachen
rrc, _, _, _, err := meshNode.FetchBlobAny(r.Context(), id)
if err != nil { if err != nil {
http.NotFound(w, r) http.NotFound(w, r)
return return
} }
defer rrc.Close() defer rrc.Close()
if _, err := blobs.Save(r.Context(), id, name, rrc); err != nil { if _, err := blobs.Save(r.Context(), id, it.Name, rrc); err != nil {
http.Error(w, "cache failed", http.StatusInternalServerError) http.Error(w, "cache failed", http.StatusInternalServerError)
return return
} }
// 3) erneut lokal öffnen (saubere Meta/Größe/CT) // 4) erneut lokal öffnen und streamen
lrc, meta, err := blobs.Open(r.Context(), id) lrc, meta, err := blobs.Open(r.Context(), id)
if err != nil { if err != nil {
http.Error(w, "open failed", http.StatusInternalServerError) http.Error(w, "open failed", http.StatusInternalServerError)
return return
} }
defer lrc.Close() defer lrc.Close()
serveBlob(w, r, lrc, meta) serveBlob(w, r, lrc, meta, it.Name)
}) })
} }
// Hilfsfunktion: setzt sinnvolle Header und streamt die Datei // Hilfsfunktion: setzt sinnvolle Header und streamt die Datei
func serveBlob(w http.ResponseWriter, r *http.Request, rc io.ReadSeeker, meta blobfs.Meta) { func serveBlob(w http.ResponseWriter, r *http.Request, rc io.ReadSeeker, meta blobfs.Meta, downloadName string) {
// Caching (einfach): ETag = SHA256
if meta.SHA256 != "" { if meta.SHA256 != "" {
etag := `W/"` + meta.SHA256 + `"` etag := `W/"` + meta.SHA256 + `"`
if inm := r.Header.Get("If-None-Match"); inm == etag { if r.Header.Get("If-None-Match") == etag {
w.WriteHeader(http.StatusNotModified) w.WriteHeader(http.StatusNotModified)
return return
} }
w.Header().Set("ETag", etag) w.Header().Set("ETag", etag)
} }
// Standard-Header
if meta.ContentType == "" { if meta.ContentType == "" {
meta.ContentType = "application/octet-stream" meta.ContentType = "application/octet-stream"
} }
if downloadName == "" {
downloadName = meta.Name
}
w.Header().Set("Content-Type", meta.ContentType) w.Header().Set("Content-Type", meta.ContentType)
w.Header().Set("Content-Length", strconv.FormatInt(meta.Size, 10)) 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("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, downloadName))
w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") // für Browser-JS w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition")
w.Header().Set("X-Robots-Tag", "noindex") w.Header().Set("X-Robots-Tag", "noindex")
// (Optional) einfache Range-Unterstützung weglassen, ums schlank zu halten
// Stream
_, _ = io.Copy(w, rc) _, _ = io.Copy(w, rc)
} }

View File

@@ -221,9 +221,10 @@ func Register(mux *http.ServeMux, d Deps) {
http.Error(w, "method not allowed", http.StatusMethodNotAllowed) http.Error(w, "method not allowed", http.StatusMethodNotAllowed)
return return
} }
if id, err := strconv.ParseInt(r.FormValue("id"), 10, 64); err == nil {
_, _ = d.Store.Delete(r.Context(), filesvc.ID(id)) if id64, err := strconv.ParseInt(r.FormValue("id"), 10, 64); err == nil {
_ = d.Blob.Delete(r.Context(), int64(id)) _, _ = d.Store.Delete(r.Context(), filesvc.ID(id64))
_ = d.Blob.Delete(r.Context(), id64) // <— Blob löschen
_ = d.Mesh.SyncNow(r.Context()) _ = d.Mesh.SyncNow(r.Context())
} }
http.Redirect(w, r, "/admin", http.StatusSeeOther) http.Redirect(w, r, "/admin", http.StatusSeeOther)