This commit is contained in:
@@ -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, um’s schlank zu halten
|
|
||||||
// Stream
|
|
||||||
_, _ = io.Copy(w, rc)
|
_, _ = io.Copy(w, rc)
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -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)
|
||||||
|
|||||||
Reference in New Issue
Block a user