This commit is contained in:
@@ -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)
|
||||
}
|
||||
|
||||
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user