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 ***/
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, ums schlank zu halten
// Stream
_, _ = 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)
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)