storage.go raw
1 package blossom
2
3 import (
4 "fmt"
5 "os"
6 "path/filepath"
7
8 "next.orly.dev/pkg/nostr/encoders/hex"
9 "next.orly.dev/pkg/lol/log"
10
11 "next.orly.dev/pkg/database"
12 )
13
14 // Storage provides blob storage operations using the database interface.
15 // This is a thin wrapper that delegates to the database's blob methods.
16 type Storage struct {
17 db database.Database
18 blobDir string // Directory for storing blob files (for backward compatibility info)
19 }
20
21 // NewStorage creates a new storage instance using the database interface.
22 func NewStorage(db database.Database) *Storage {
23 // Derive blob directory from database path (for informational purposes)
24 blobDir := filepath.Join(db.Path(), "blossom")
25
26 // Ensure blob directory exists (the database implementation handles this,
27 // but we keep this for backward compatibility with code that checks the directory)
28 if err := os.MkdirAll(blobDir, 0755); err != nil {
29 log.E.F("failed to create blob directory %s: %v", blobDir, err)
30 }
31
32 return &Storage{
33 db: db,
34 blobDir: blobDir,
35 }
36 }
37
38 // BlobDir returns the directory where blob files are stored.
39 // Used by the streaming upload handler to create temp files on the same
40 // filesystem, enabling atomic rename to the final content-addressed path.
41 func (s *Storage) BlobDir() string {
42 return s.blobDir
43 }
44
45 // SaveBlob stores a blob with its metadata.
46 // Delegates to the database interface's SaveBlob method.
47 func (s *Storage) SaveBlob(
48 sha256Hash []byte, data []byte, pubkey []byte, mimeType string, extension string,
49 ) error {
50 return s.db.SaveBlob(sha256Hash, data, pubkey, mimeType, extension)
51 }
52
53 // SaveBlobFromFile moves a completed temp file to its final content-addressed
54 // path and saves metadata. The temp file must already be on the same filesystem
55 // as blobDir (created via os.CreateTemp(s.BlobDir(), ...)) so that os.Rename is
56 // an atomic operation. This is used by the streaming upload path where the hash
57 // is computed during the write, eliminating the need to buffer the entire blob
58 // in memory.
59 func (s *Storage) SaveBlobFromFile(
60 sha256Hash []byte, tempPath string, size int64, pubkey []byte, mimeType string, extension string,
61 ) error {
62 sha256Hex := hex.Enc(sha256Hash)
63
64 // Build final path
65 finalPath := s.GetBlobPath(sha256Hex, extension)
66
67 // Rename temp file to final content-addressed path (atomic on same fs)
68 if err := os.Rename(tempPath, finalPath); err != nil {
69 return fmt.Errorf("failed to rename temp file to blob path: %w", err)
70 }
71
72 // Save metadata to database (no re-hash, no file I/O)
73 return s.db.SaveBlobMetadata(sha256Hash, size, pubkey, mimeType, extension)
74 }
75
76 // GetBlob retrieves blob data by SHA256 hash.
77 // Returns the data and metadata from the database.
78 func (s *Storage) GetBlob(sha256Hash []byte) (data []byte, metadata *BlobMetadata, err error) {
79 data, dbMeta, err := s.db.GetBlob(sha256Hash)
80 if err != nil {
81 return nil, nil, err
82 }
83 // Convert database.BlobMetadata to blossom.BlobMetadata
84 metadata = &BlobMetadata{
85 Pubkey: dbMeta.Pubkey,
86 MimeType: dbMeta.MimeType,
87 Uploaded: dbMeta.Uploaded,
88 Size: dbMeta.Size,
89 Extension: dbMeta.Extension,
90 }
91 return data, metadata, nil
92 }
93
94 // HasBlob checks if a blob exists.
95 func (s *Storage) HasBlob(sha256Hash []byte) (exists bool, err error) {
96 return s.db.HasBlob(sha256Hash)
97 }
98
99 // DeleteBlob deletes a blob and its metadata.
100 func (s *Storage) DeleteBlob(sha256Hash []byte, pubkey []byte) error {
101 return s.db.DeleteBlob(sha256Hash, pubkey)
102 }
103
104 // ListBlobs lists all blobs for a given pubkey.
105 // Returns blob descriptors with time filtering.
106 func (s *Storage) ListBlobs(pubkey []byte, since, until int64) ([]*BlobDescriptor, error) {
107 dbDescriptors, err := s.db.ListBlobs(pubkey, since, until)
108 if err != nil {
109 return nil, err
110 }
111 // Convert database.BlobDescriptor to blossom.BlobDescriptor
112 descriptors := make([]*BlobDescriptor, 0, len(dbDescriptors))
113 for _, d := range dbDescriptors {
114 descriptors = append(descriptors, &BlobDescriptor{
115 URL: d.URL,
116 SHA256: d.SHA256,
117 Size: d.Size,
118 Type: d.Type,
119 Uploaded: d.Uploaded,
120 NIP94: d.NIP94,
121 })
122 }
123 return descriptors, nil
124 }
125
126 // GetBlobPath returns the filesystem path for a blob given its hash and extension.
127 // This is used by handlers that need to serve files directly via http.ServeFile.
128 func (s *Storage) GetBlobPath(sha256Hex string, extension string) string {
129 filename := sha256Hex + extension
130 return filepath.Join(s.blobDir, filename)
131 }
132
133 // GetBlobMetadata retrieves only metadata for a blob.
134 func (s *Storage) GetBlobMetadata(sha256Hash []byte) (*BlobMetadata, error) {
135 dbMeta, err := s.db.GetBlobMetadata(sha256Hash)
136 if err != nil {
137 return nil, err
138 }
139 // Convert database.BlobMetadata to blossom.BlobMetadata
140 return &BlobMetadata{
141 Pubkey: dbMeta.Pubkey,
142 MimeType: dbMeta.MimeType,
143 Uploaded: dbMeta.Uploaded,
144 Size: dbMeta.Size,
145 Extension: dbMeta.Extension,
146 }, nil
147 }
148
149 // GetTotalStorageUsed calculates total storage used by a pubkey in MB.
150 func (s *Storage) GetTotalStorageUsed(pubkey []byte) (totalMB int64, err error) {
151 return s.db.GetTotalBlobStorageUsed(pubkey)
152 }
153
154 // SaveReport stores a report for a blob (BUD-09).
155 func (s *Storage) SaveReport(sha256Hash []byte, reportData []byte) error {
156 return s.db.SaveBlobReport(sha256Hash, reportData)
157 }
158
159 // ListAllUserStats returns storage statistics for all users who have uploaded blobs.
160 func (s *Storage) ListAllUserStats() ([]*UserBlobStats, error) {
161 dbStats, err := s.db.ListAllBlobUserStats()
162 if err != nil {
163 return nil, err
164 }
165 // Convert database.UserBlobStats to blossom.UserBlobStats
166 stats := make([]*UserBlobStats, 0, len(dbStats))
167 for _, s := range dbStats {
168 stats = append(stats, &UserBlobStats{
169 PubkeyHex: s.PubkeyHex,
170 BlobCount: s.BlobCount,
171 TotalSizeBytes: s.TotalSizeBytes,
172 })
173 }
174 return stats, nil
175 }
176
177 // UserBlobStats represents storage statistics for a single user.
178 // This mirrors database.UserBlobStats for API compatibility.
179 type UserBlobStats struct {
180 PubkeyHex string `json:"pubkey"`
181 BlobCount int64 `json:"blob_count"`
182 TotalSizeBytes int64 `json:"total_size_bytes"`
183 }
184
185 // GetThumbnail retrieves a cached thumbnail by key.
186 func (s *Storage) GetThumbnail(key string) ([]byte, error) {
187 return s.db.GetThumbnail(key)
188 }
189
190 // SaveThumbnail caches a thumbnail with the given key.
191 func (s *Storage) SaveThumbnail(key string, data []byte) error {
192 return s.db.SaveThumbnail(key, data)
193 }
194
195 // ListImageBlobs returns all image blobs that could have thumbnails generated.
196 func (s *Storage) ListImageBlobs() ([]*BlobDescriptor, error) {
197 // Get all blobs
198 dbDescriptors, err := s.db.ListAllBlobs()
199 if err != nil {
200 return nil, err
201 }
202
203 // Filter to images only
204 var images []*BlobDescriptor
205 for _, d := range dbDescriptors {
206 if IsImageMimeType(d.Type) {
207 images = append(images, &BlobDescriptor{
208 URL: d.URL,
209 SHA256: d.SHA256,
210 Size: d.Size,
211 Type: d.Type,
212 Uploaded: d.Uploaded,
213 })
214 }
215 }
216 return images, nil
217 }
218