From 43f1d01a8a7a1bd7d7de7c7e07c2908d75983e9a Mon Sep 17 00:00:00 2001 From: jbergner Date: Sat, 27 Sep 2025 17:39:39 +0200 Subject: [PATCH] diverse bugfixes --- cmd/unified/main.go | 103 ++++++++++++++++++++++------------------ internal/admin/admin.go | 7 +-- 2 files changed, 60 insertions(+), 50 deletions(-) diff --git a/cmd/unified/main.go b/cmd/unified/main.go index d764624..d62ff49 100644 --- a/cmd/unified/main.go +++ b/cmd/unified/main.go @@ -193,7 +193,7 @@ func writeJSON(w http.ResponseWriter, code int, v any) { /*** API-Routen ***/ -func fileRoutes(mux *http.ServeMux, store filesvc.MeshStore) { +func fileRoutes(mux *http.ServeMux, store filesvc.MeshStore, blobs blobfs.Store) { // Health mux.HandleFunc("/api/v1/health", func(w http.ResponseWriter, r *http.Request) { writeJSON(w, http.StatusOK, map[string]string{"status": "ok"}) @@ -275,6 +275,7 @@ func fileRoutes(mux *http.ServeMux, store filesvc.MeshStore) { return } it, err := store.Delete(r.Context(), in.ID) + _ = blobs.Delete(r.Context(), int64(in.ID)) if err != nil { status := http.StatusBadRequest if errors.Is(err, filesvc.ErrNotFound) { @@ -330,53 +331,51 @@ func apiFiles(mux *http.ServeMux, store filesvc.MeshStore, blobs blobfs.Store, m // Download 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/"), "/") if len(parts) != 2 || parts[1] != "download" { http.NotFound(w, r) return } id, err := strconv.ParseInt(parts[0], 10, 64) - if err != nil { + if err != nil || id <= 0 { http.NotFound(w, r) return } - // 1) lokal - if rc, meta, err := blobs.Open(r.Context(), id); err == nil { - defer rc.Close() - 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)) - _, _ = io.Copy(w, rc) + // 1) Metadaten prüfen + it, err := store.Get(r.Context(), filesvc.ID(id)) + if err != nil || it.Deleted { + http.NotFound(w, r) return } - // 2) remote holen - rrc, name, _, _, err := meshNode.FetchBlobAny(r.Context(), id) + // 2) lokal + 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 { http.NotFound(w, r) return } defer rrc.Close() - - // 3) lokal cachen - if _, err := blobs.Save(r.Context(), id, name, rrc); err != nil { - http.Error(w, "cache failed: "+err.Error(), http.StatusInternalServerError) + if _, err := blobs.Save(r.Context(), id, it.Name, rrc); err != nil { + http.Error(w, "cache failed", http.StatusInternalServerError) return } - // 4) erneut lokal öffnen und streamen + // 4) lokal streamen lrc, meta, err := blobs.Open(r.Context(), id) if err != nil { http.Error(w, "open failed", http.StatusInternalServerError) return } defer lrc.Close() - 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)) - _, _ = io.Copy(w, lrc) + serveBlob(w, r, lrc, meta, it.Name) }) } @@ -417,7 +416,7 @@ func main() { st := filesvc.NewMemStore() // Mesh starten - mcfg := mesh.FromEnv() + //mcfg := mesh.FromEnv() blobs := blobfs.New(getenvDefault("DATA_DIR", "./data")) mnode, err := mesh.New(cfg.Mesh, mesh.Callbacks{ @@ -432,11 +431,15 @@ func main() { return st.ApplyRemote(ctx, fromMeshSnapshot(s)) }, 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) if err != nil { 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 { @@ -444,7 +447,7 @@ func main() { } go func() { 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) { log.Fatalf("mesh serve: %v", err) } @@ -456,12 +459,12 @@ func main() { // API (Bearer-Auth) //blobs := blobfs.New(getenvDefault("DATA_DIR", "./data")) apiMux := http.NewServeMux() - fileRoutes(apiMux, st) + fileRoutes(apiMux, st, blobs) apiFiles(apiMux, st, blobs, mnode) root.Handle("/api/", authMiddleware(cfg.APIKey, apiMux)) 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) @@ -494,7 +497,8 @@ func main() { // Graceful shutdown 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) { log.Fatalf("http server: %v", err) } @@ -514,7 +518,7 @@ func main() { // Public: GET {base}/{id} // 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, "/") { base = "/" + base } @@ -530,59 +534,64 @@ func registerPublicDownloads(mux *http.ServeMux, blobs blobfs.Store, meshNode *m return } - // 1) lokal versuchen - if rc, meta, err := blobs.Open(r.Context(), id); err == nil { - defer rc.Close() - serveBlob(w, r, rc, meta) + // 1) Metadaten prüfen (nicht vorhanden oder gelöscht → 404) + it, err := store.Get(r.Context(), filesvc.ID(id)) + if err != nil || it.Deleted { + http.NotFound(w, r) return } - // 2) aus dem Mesh ziehen (signiert) und lokal cachen - rrc, name, _, _, err := meshNode.FetchBlobAny(r.Context(), id) + // 2) lokal versuchen + 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 { http.NotFound(w, r) return } 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) return } - // 3) erneut lokal öffnen (saubere Meta/Größe/CT) + // 4) erneut lokal öffnen und streamen 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) + serveBlob(w, r, lrc, meta, it.Name) }) } // 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 +func serveBlob(w http.ResponseWriter, r *http.Request, rc io.ReadSeeker, meta blobfs.Meta, downloadName string) { if 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) return } w.Header().Set("ETag", etag) } - - // Standard-Header if meta.ContentType == "" { meta.ContentType = "application/octet-stream" } + if downloadName == "" { + downloadName = meta.Name + } + 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("Content-Disposition", fmt.Sprintf(`attachment; filename="%s"`, downloadName)) + w.Header().Set("Access-Control-Expose-Headers", "Content-Disposition") 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 2603a72..d22de0a 100644 --- a/internal/admin/admin.go +++ b/internal/admin/admin.go @@ -221,9 +221,10 @@ func Register(mux *http.ServeMux, d Deps) { http.Error(w, "method not allowed", http.StatusMethodNotAllowed) return } - if id, err := strconv.ParseInt(r.FormValue("id"), 10, 64); err == nil { - _, _ = d.Store.Delete(r.Context(), filesvc.ID(id)) - _ = d.Blob.Delete(r.Context(), int64(id)) + + if id64, err := strconv.ParseInt(r.FormValue("id"), 10, 64); err == nil { + _, _ = d.Store.Delete(r.Context(), filesvc.ID(id64)) + _ = d.Blob.Delete(r.Context(), id64) // <— Blob löschen _ = d.Mesh.SyncNow(r.Context()) } http.Redirect(w, r, "/admin", http.StatusSeeOther)