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)
This commit is contained in:
Anthony Accioly
2025-08-07 00:05:48 +01:00
committed by fiatjaf_
parent e81416f41e
commit 3c802caff5
4 changed files with 50 additions and 30 deletions

View File

@@ -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
}

View File

@@ -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

View File

@@ -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
})

View File

@@ -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