From 0df6c8f2aeff572bbe2457d1e0dd62f3bf7dbd3e Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Thu, 7 Aug 2025 00:05:48 +0100 Subject: [PATCH 1/2] feat(blossom)!: add file extension parameter to Blossom hooks This enhancement allows hooks to make extension-aware decisions for storage, loading, and access control operations. The extension is intelligently derived from MIME type detection, file magic bytes, or URL parsing. Additionally improves MIME type handling by providing fallback to "application/octet-stream" when extension-based detection fails as per BUD-01. BREAKING CHANGE: All Blossom hook functions now require an additional `ext` parameter: - StoreBlob hooks now accept (ctx, sha256, ext, body) instead of (ctx, sha256, body) - LoadBlob hooks now accept (ctx, sha256, ext) instead of (ctx, sha256) - DeleteBlob hooks now accept (ctx, sha256, ext) instead of (ctx, sha256) - RejectGet hooks now accept (ctx, auth, sha256, ext) instead of (ctx, auth, sha256) - RejectDelete hooks now accept (ctx, auth, sha256, ext) instead of (ctx, auth, sha256) --- blossom/handlers.go | 56 +++++++++++++++++++++++++++------------- blossom/server.go | 12 ++++----- docs/core/blossom.md | 6 ++--- examples/blossom/main.go | 6 ++--- 4 files changed, 50 insertions(+), 30 deletions(-) diff --git a/blossom/handlers.go b/blossom/handlers.go index f747082..66e56fc 100644 --- a/blossom/handlers.go +++ b/blossom/handlers.go @@ -125,13 +125,17 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) { hash := sha256.Sum256(b) hhash := hex.EncodeToString(hash[:]) + mimeType := mime.TypeByExtension(ext) + if mimeType == "" { + mimeType = "application/octet-stream" + } // keep track of the blob descriptor bd := BlobDescriptor{ URL: bs.ServiceURL + "/" + hhash + ext, SHA256: hhash, Size: len(b), - Type: mime.TypeByExtension(ext), + Type: mimeType, Uploaded: nostr.Now(), } if err := bs.Store.Keep(r.Context(), bd, auth.PubKey); err != nil { @@ -141,7 +145,7 @@ func (bs BlossomServer) handleUpload(w http.ResponseWriter, r *http.Request) { // save actual blob for _, sb := range bs.StoreBlob { - if err := sb(r.Context(), hhash, b); err != nil { + if err := sb(r.Context(), hhash, ext, b); err != nil { blossomError(w, "failed to save: "+err.Error(), 500) return } @@ -182,19 +186,25 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { } } + var ext string + bd, err := bs.Store.Get(r.Context(), hhash) + if err != nil { + // can't find the blob, try to get the extension from the URL + if len(spl) == 2 { + ext = spl[1] + } + } else { + ext = getExtension(bd.Type) + } + for _, rg := range bs.RejectGet { - reject, reason, code := rg(r.Context(), auth, hhash) + reject, reason, code := rg(r.Context(), auth, hhash, ext) if reject { blossomError(w, reason, code) return } } - var ext string - if len(spl) == 2 { - ext = spl[1] - } - if len(bs.RedirectGet) > 0 { for _, redirect := range bs.RedirectGet { redirectURL, code, err := redirect(r.Context(), hhash, ext) @@ -215,7 +225,7 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { } for _, lb := range bs.LoadBlob { - reader, _ := lb(r.Context(), hhash) + reader, _ := lb(r.Context(), hhash, ext) if reader != nil { // use unix epoch as the time if we can't find the descriptor // as described in the http.ServeContent documentation @@ -335,9 +345,20 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) { return } + var ext string + bd, err := bs.Store.Get(r.Context(), hhash) + if err != nil { + // can't find the blob, try to get the extension from the URL + if len(spl) == 2 { + ext = spl[1] + } + } else { + ext = getExtension(bd.Type) + } + // should we accept this delete? for _, rd := range bs.RejectDelete { - reject, reason, code := rd(r.Context(), auth, hhash) + reject, reason, code := rd(r.Context(), auth, hhash, ext) if reject { blossomError(w, reason, code) return @@ -353,7 +374,7 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) { // we will actually only delete the file if no one else owns it if bd, err := bs.Store.Get(r.Context(), hhash); err == nil && bd == nil { for _, del := range bs.DeleteBlob { - if err := del(r.Context(), hhash); err != nil { + if err := del(r.Context(), hhash, ext); err != nil { blossomError(w, "failed to delete blob: "+err.Error(), 500) return } @@ -442,13 +463,12 @@ func (bs BlossomServer) handleMirror(w http.ResponseWriter, r *http.Request) { // get content type and extension var ext string contentType := resp.Header.Get("Content-Type") - if contentType != "" { + if contentType != "" { // First try to get the extension from the Content-Type header ext = getExtension(contentType) - } else { - // Try to detect from URL extension - if idx := strings.LastIndex(body.URL, "."); idx >= 0 { - ext = body.URL[idx:] - } + } else if ft, _ := magic.Lookup(b); ft != nil { // Else try to infer extension from the file content + ext = "." + ft.Extension + } else if idx := strings.LastIndex(body.URL, "."); idx >= 0 { // Else, try to get the extension from the URL + ext = body.URL[idx:] } // run reject hooks @@ -477,7 +497,7 @@ func (bs BlossomServer) handleMirror(w http.ResponseWriter, r *http.Request) { // store actual blob for _, sb := range bs.StoreBlob { - if err := sb(r.Context(), hhash, b); err != nil { + if err := sb(r.Context(), hhash, ext, b); err != nil { blossomError(w, "failed to save blob: "+err.Error(), 500) return } diff --git a/blossom/server.go b/blossom/server.go index 28ee8c4..25351fb 100644 --- a/blossom/server.go +++ b/blossom/server.go @@ -14,16 +14,16 @@ type BlossomServer struct { ServiceURL string Store BlobIndex - StoreBlob []func(ctx context.Context, sha256 string, body []byte) error - LoadBlob []func(ctx context.Context, sha256 string) (io.ReadSeeker, error) - DeleteBlob []func(ctx context.Context, sha256 string) error + StoreBlob []func(ctx context.Context, sha256 string, ext string, body []byte) error + LoadBlob []func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, error) + DeleteBlob []func(ctx context.Context, sha256 string, ext string) error ReceiveReport []func(ctx context.Context, reportEvt *nostr.Event) error - RedirectGet []func(ctx context.Context, sha256 string, fileExtension string) (url string, code int, err error) + RedirectGet []func(ctx context.Context, sha256 string, ext string) (url string, code int, err error) RejectUpload []func(ctx context.Context, auth *nostr.Event, size int, ext string) (bool, string, int) - RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int) + RejectGet []func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) RejectList []func(ctx context.Context, auth *nostr.Event, pubkey string) (bool, string, int) - RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string) (bool, string, int) + RejectDelete []func(ctx context.Context, auth *nostr.Event, sha256 string, ext string) (bool, string, int) } // ServerOption represents a functional option for configuring a BlossomServer diff --git a/docs/core/blossom.md b/docs/core/blossom.md index c4db949..70f81a8 100644 --- a/docs/core/blossom.md +++ b/docs/core/blossom.md @@ -22,15 +22,15 @@ func main() { bl.Store = blossom.EventStoreBlobIndexWrapper{Store: blobdb, ServiceURL: bl.ServiceURL} // implement the required storage functions - bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error { + bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, ext string, body []byte) error { // store the blob data somewhere return nil }) - bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) { + bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, error) { // load and return the blob data return nil, nil }) - bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string) error { + bl.DeleteBlob = append(bl.DeleteBlob, func(ctx context.Context, sha256 string, ext string) error { // delete the blob data return nil }) diff --git a/examples/blossom/main.go b/examples/blossom/main.go index c5cf4ef..feb96bc 100644 --- a/examples/blossom/main.go +++ b/examples/blossom/main.go @@ -31,11 +31,11 @@ func main() { } bl := blossom.New(relay, "http://localhost:3334") bl.Store = blossom.EventStoreBlobIndexWrapper{Store: bdb, ServiceURL: bl.ServiceURL} - bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, body []byte) error { - fmt.Println("storing", sha256, len(body)) + bl.StoreBlob = append(bl.StoreBlob, func(ctx context.Context, sha256 string, ext string, body []byte) error { + fmt.Println("storing", sha256, ext, len(body)) return nil }) - bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string) (io.ReadSeeker, error) { + bl.LoadBlob = append(bl.LoadBlob, func(ctx context.Context, sha256 string, ext string) (io.ReadSeeker, error) { fmt.Println("loading", sha256) blob := strings.NewReader("aaaaa") return blob, nil From 7a49aeaf83ac70470b819cdaae910bd29bff73ef Mon Sep 17 00:00:00 2001 From: Anthony Accioly <1591739+aaccioly@users.noreply.github.com> Date: Thu, 7 Aug 2025 02:56:56 +0100 Subject: [PATCH 2/2] fix(blossom): handle nil BlobDescriptor in Get and Delete Refine extension derivation logic by ensuring `bd` is not nil before accessing its type. --- blossom/handlers.go | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/blossom/handlers.go b/blossom/handlers.go index 66e56fc..add1c17 100644 --- a/blossom/handlers.go +++ b/blossom/handlers.go @@ -189,11 +189,11 @@ func (bs BlossomServer) handleGetBlob(w http.ResponseWriter, r *http.Request) { var ext string bd, err := bs.Store.Get(r.Context(), hhash) if err != nil { - // can't find the blob, try to get the extension from the URL + // can't find the BlobDescriptor, try to get the extension from the URL if len(spl) == 2 { ext = spl[1] } - } else { + } else if bd != nil { ext = getExtension(bd.Type) } @@ -348,11 +348,11 @@ func (bs BlossomServer) handleDelete(w http.ResponseWriter, r *http.Request) { var ext string bd, err := bs.Store.Get(r.Context(), hhash) if err != nil { - // can't find the blob, try to get the extension from the URL + // can't find the BlobDescriptor, try to get the extension from the URL if len(spl) == 2 { ext = spl[1] } - } else { + } else if bd != nil { ext = getExtension(bd.Type) }