You can not select more than 25 topics
Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.
334 lines
7.2 KiB
334 lines
7.2 KiB
package blossom |
|
|
|
import ( |
|
"encoding/json" |
|
|
|
"github.com/dgraph-io/badger/v4" |
|
"lol.mleku.dev/chk" |
|
"lol.mleku.dev/errorf" |
|
"lol.mleku.dev/log" |
|
"next.orly.dev/pkg/crypto/sha256" |
|
"next.orly.dev/pkg/database" |
|
"next.orly.dev/pkg/encoders/hex" |
|
"next.orly.dev/pkg/utils" |
|
) |
|
|
|
const ( |
|
// Database key prefixes |
|
prefixBlobData = "blob:data:" |
|
prefixBlobMeta = "blob:meta:" |
|
prefixBlobIndex = "blob:index:" |
|
prefixBlobReport = "blob:report:" |
|
) |
|
|
|
// Storage provides blob storage operations |
|
type Storage struct { |
|
db *database.D |
|
} |
|
|
|
// NewStorage creates a new storage instance |
|
func NewStorage(db *database.D) *Storage { |
|
return &Storage{db: db} |
|
} |
|
|
|
// SaveBlob stores a blob with its metadata |
|
func (s *Storage) SaveBlob( |
|
sha256Hash []byte, data []byte, pubkey []byte, mimeType string, |
|
) (err error) { |
|
sha256Hex := hex.Enc(sha256Hash) |
|
|
|
// Verify SHA256 matches |
|
calculatedHash := sha256.Sum256(data) |
|
if !utils.FastEqual(calculatedHash[:], sha256Hash) { |
|
err = errorf.E( |
|
"SHA256 mismatch: calculated %x, provided %x", |
|
calculatedHash[:], sha256Hash, |
|
) |
|
return |
|
} |
|
|
|
// Create metadata |
|
metadata := NewBlobMetadata(pubkey, mimeType, int64(len(data))) |
|
var metaData []byte |
|
if metaData, err = metadata.Serialize(); chk.E(err) { |
|
return |
|
} |
|
|
|
// Store blob data |
|
dataKey := prefixBlobData + sha256Hex |
|
if err = s.db.Update(func(txn *badger.Txn) error { |
|
if err := txn.Set([]byte(dataKey), data); err != nil { |
|
return err |
|
} |
|
|
|
// Store metadata |
|
metaKey := prefixBlobMeta + sha256Hex |
|
if err := txn.Set([]byte(metaKey), metaData); err != nil { |
|
return err |
|
} |
|
|
|
// Index by pubkey |
|
indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex |
|
if err := txn.Set([]byte(indexKey), []byte{1}); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
log.D.F("saved blob %s (%d bytes) for pubkey %s", sha256Hex, len(data), hex.Enc(pubkey)) |
|
return |
|
} |
|
|
|
// GetBlob retrieves blob data by SHA256 hash |
|
func (s *Storage) GetBlob(sha256Hash []byte) (data []byte, metadata *BlobMetadata, err error) { |
|
sha256Hex := hex.Enc(sha256Hash) |
|
dataKey := prefixBlobData + sha256Hex |
|
|
|
var blobData []byte |
|
if err = s.db.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(dataKey)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return item.Value(func(val []byte) error { |
|
blobData = make([]byte, len(val)) |
|
copy(blobData, val) |
|
return nil |
|
}) |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
// Get metadata |
|
metaKey := prefixBlobMeta + sha256Hex |
|
if err = s.db.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(metaKey)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return item.Value(func(val []byte) error { |
|
if metadata, err = DeserializeBlobMetadata(val); err != nil { |
|
return err |
|
} |
|
return nil |
|
}) |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
data = blobData |
|
return |
|
} |
|
|
|
// HasBlob checks if a blob exists |
|
func (s *Storage) HasBlob(sha256Hash []byte) (exists bool, err error) { |
|
sha256Hex := hex.Enc(sha256Hash) |
|
dataKey := prefixBlobData + sha256Hex |
|
|
|
if err = s.db.View(func(txn *badger.Txn) error { |
|
_, err := txn.Get([]byte(dataKey)) |
|
if err == badger.ErrKeyNotFound { |
|
exists = false |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
exists = true |
|
return nil |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// DeleteBlob deletes a blob and its metadata |
|
func (s *Storage) DeleteBlob(sha256Hash []byte, pubkey []byte) (err error) { |
|
sha256Hex := hex.Enc(sha256Hash) |
|
dataKey := prefixBlobData + sha256Hex |
|
metaKey := prefixBlobMeta + sha256Hex |
|
indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex |
|
|
|
if err = s.db.Update(func(txn *badger.Txn) error { |
|
// Verify blob exists |
|
_, err := txn.Get([]byte(dataKey)) |
|
if err == badger.ErrKeyNotFound { |
|
return errorf.E("blob %s not found", sha256Hex) |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
|
|
// Delete blob data |
|
if err := txn.Delete([]byte(dataKey)); err != nil { |
|
return err |
|
} |
|
|
|
// Delete metadata |
|
if err := txn.Delete([]byte(metaKey)); err != nil { |
|
return err |
|
} |
|
|
|
// Delete index entry |
|
if err := txn.Delete([]byte(indexKey)); err != nil { |
|
return err |
|
} |
|
|
|
return nil |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
log.D.F("deleted blob %s for pubkey %s", sha256Hex, hex.Enc(pubkey)) |
|
return |
|
} |
|
|
|
// ListBlobs lists all blobs for a given pubkey |
|
func (s *Storage) ListBlobs( |
|
pubkey []byte, since, until int64, |
|
) (descriptors []*BlobDescriptor, err error) { |
|
pubkeyHex := hex.Enc(pubkey) |
|
prefix := prefixBlobIndex + pubkeyHex + ":" |
|
|
|
descriptors = make([]*BlobDescriptor, 0) |
|
|
|
if err = s.db.View(func(txn *badger.Txn) error { |
|
opts := badger.DefaultIteratorOptions |
|
opts.Prefix = []byte(prefix) |
|
it := txn.NewIterator(opts) |
|
defer it.Close() |
|
|
|
for it.Rewind(); it.Valid(); it.Next() { |
|
item := it.Item() |
|
key := item.Key() |
|
|
|
// Extract SHA256 from key: prefixBlobIndex + pubkeyHex + ":" + sha256Hex |
|
sha256Hex := string(key[len(prefix):]) |
|
|
|
// Get blob metadata |
|
metaKey := prefixBlobMeta + sha256Hex |
|
metaItem, err := txn.Get([]byte(metaKey)) |
|
if err != nil { |
|
continue |
|
} |
|
|
|
var metadata *BlobMetadata |
|
if err = metaItem.Value(func(val []byte) error { |
|
if metadata, err = DeserializeBlobMetadata(val); err != nil { |
|
return err |
|
} |
|
return nil |
|
}); err != nil { |
|
continue |
|
} |
|
|
|
// Filter by time range |
|
if since > 0 && metadata.Uploaded < since { |
|
continue |
|
} |
|
if until > 0 && metadata.Uploaded > until { |
|
continue |
|
} |
|
|
|
// Verify blob exists |
|
dataKey := prefixBlobData + sha256Hex |
|
_, errGet := txn.Get([]byte(dataKey)) |
|
if errGet != nil { |
|
continue |
|
} |
|
|
|
// Create descriptor (URL will be set by handler) |
|
descriptor := NewBlobDescriptor( |
|
"", // URL will be set by handler |
|
sha256Hex, |
|
metadata.Size, |
|
metadata.MimeType, |
|
metadata.Uploaded, |
|
) |
|
|
|
descriptors = append(descriptors, descriptor) |
|
} |
|
|
|
return nil |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
// SaveReport stores a report for a blob (BUD-09) |
|
func (s *Storage) SaveReport(sha256Hash []byte, reportData []byte) (err error) { |
|
sha256Hex := hex.Enc(sha256Hash) |
|
reportKey := prefixBlobReport + sha256Hex |
|
|
|
// Get existing reports |
|
var existingReports [][]byte |
|
if err = s.db.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(reportKey)) |
|
if err == badger.ErrKeyNotFound { |
|
return nil |
|
} |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return item.Value(func(val []byte) error { |
|
if err = json.Unmarshal(val, &existingReports); err != nil { |
|
return err |
|
} |
|
return nil |
|
}) |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
// Append new report |
|
existingReports = append(existingReports, reportData) |
|
|
|
// Store updated reports |
|
var reportsData []byte |
|
if reportsData, err = json.Marshal(existingReports); chk.E(err) { |
|
return |
|
} |
|
|
|
if err = s.db.Update(func(txn *badger.Txn) error { |
|
return txn.Set([]byte(reportKey), reportsData) |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
log.D.F("saved report for blob %s", sha256Hex) |
|
return |
|
} |
|
|
|
// GetBlobMetadata retrieves only metadata for a blob |
|
func (s *Storage) GetBlobMetadata(sha256Hash []byte) (metadata *BlobMetadata, err error) { |
|
sha256Hex := hex.Enc(sha256Hash) |
|
metaKey := prefixBlobMeta + sha256Hex |
|
|
|
if err = s.db.View(func(txn *badger.Txn) error { |
|
item, err := txn.Get([]byte(metaKey)) |
|
if err != nil { |
|
return err |
|
} |
|
|
|
return item.Value(func(val []byte) error { |
|
if metadata, err = DeserializeBlobMetadata(val); err != nil { |
|
return err |
|
} |
|
return nil |
|
}) |
|
}); chk.E(err) { |
|
return |
|
} |
|
|
|
return |
|
} |
|
|
|
|