blob.go raw
1 //go:build !(js && wasm)
2
3 package database
4
5 import (
6 "encoding/json"
7 "os"
8 "path/filepath"
9 "sort"
10 "strings"
11 "time"
12
13 "github.com/dgraph-io/badger/v4"
14 "github.com/minio/sha256-simd"
15 "next.orly.dev/pkg/lol/chk"
16 "next.orly.dev/pkg/lol/errorf"
17 "next.orly.dev/pkg/lol/log"
18
19 "next.orly.dev/pkg/nostr/encoders/hex"
20 )
21
22 const (
23 // Database key prefixes for blob storage (metadata and indexes only, blob data stored as files)
24 prefixBlobMeta = "blob:meta:"
25 prefixBlobIndex = "blob:index:"
26 prefixBlobReport = "blob:report:"
27 )
28
29 // getBlobDir returns the directory for storing blob files
30 func (d *D) getBlobDir() string {
31 return filepath.Join(d.dataDir, "blossom")
32 }
33
34 // getBlobPath returns the filesystem path for a blob given its hash and extension
35 func (d *D) getBlobPath(sha256Hex string, ext string) string {
36 filename := sha256Hex + ext
37 return filepath.Join(d.getBlobDir(), filename)
38 }
39
40 // ensureBlobDir ensures the blob directory exists
41 func (d *D) ensureBlobDir() error {
42 return os.MkdirAll(d.getBlobDir(), 0755)
43 }
44
45 // SaveBlob stores a blob with its metadata
46 func (d *D) SaveBlob(
47 sha256Hash []byte, data []byte, pubkey []byte, mimeType string, extension string,
48 ) (err error) {
49 sha256Hex := hex.Enc(sha256Hash)
50
51 // Verify SHA256 matches
52 calculatedHash := sha256.Sum256(data)
53 if !bytesEqual(calculatedHash[:], sha256Hash) {
54 err = errorf.E(
55 "SHA256 mismatch: calculated %x, provided %x",
56 calculatedHash[:], sha256Hash,
57 )
58 return
59 }
60
61 // If extension not provided, infer from MIME type
62 if extension == "" {
63 extension = getExtensionFromMimeType(mimeType)
64 }
65
66 // Create metadata with extension
67 metadata := &BlobMetadata{
68 Pubkey: pubkey,
69 MimeType: mimeType,
70 Uploaded: time.Now().Unix(),
71 Size: int64(len(data)),
72 Extension: extension,
73 }
74 if mimeType == "" {
75 metadata.MimeType = "application/octet-stream"
76 }
77
78 var metaData []byte
79 if metaData, err = json.Marshal(metadata); chk.E(err) {
80 return
81 }
82
83 // Ensure blob directory exists
84 if err = d.ensureBlobDir(); err != nil {
85 return errorf.E("failed to create blob directory: %w", err)
86 }
87
88 // Get blob file path
89 blobPath := d.getBlobPath(sha256Hex, extension)
90
91 // Check if blob file already exists (deduplication)
92 if _, err = os.Stat(blobPath); err == nil {
93 // File exists, just update metadata and index
94 log.D.F("blob file already exists: %s", blobPath)
95 } else if !os.IsNotExist(err) {
96 return errorf.E("error checking blob file: %w", err)
97 } else {
98 // Write blob data to file
99 if err = os.WriteFile(blobPath, data, 0644); chk.E(err) {
100 return errorf.E("failed to write blob file: %w", err)
101 }
102 log.D.F("wrote blob file: %s (%d bytes)", blobPath, len(data))
103 }
104
105 // Store metadata and index in database
106 if err = d.Update(func(txn *badger.Txn) error {
107 // Store metadata
108 metaKey := prefixBlobMeta + sha256Hex
109 if err := txn.Set([]byte(metaKey), metaData); err != nil {
110 return err
111 }
112
113 // Index by pubkey
114 indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex
115 if err := txn.Set([]byte(indexKey), []byte{1}); err != nil {
116 return err
117 }
118
119 return nil
120 }); chk.E(err) {
121 return
122 }
123
124 log.D.F("saved blob %s (%d bytes) for pubkey %s", sha256Hex, len(data), hex.Enc(pubkey))
125 return
126 }
127
128 // SaveBlobMetadata stores only the metadata and index for a blob whose file
129 // already exists on disk. This is used by the streaming upload path where the
130 // file is written during hashing and then renamed into place before this call.
131 func (d *D) SaveBlobMetadata(
132 sha256Hash []byte, size int64, pubkey []byte, mimeType string, extension string,
133 ) (err error) {
134 sha256Hex := hex.Enc(sha256Hash)
135
136 if extension == "" {
137 extension = getExtensionFromMimeType(mimeType)
138 }
139
140 metadata := &BlobMetadata{
141 Pubkey: pubkey,
142 MimeType: mimeType,
143 Uploaded: time.Now().Unix(),
144 Size: size,
145 Extension: extension,
146 }
147 if mimeType == "" {
148 metadata.MimeType = "application/octet-stream"
149 }
150
151 var metaData []byte
152 if metaData, err = json.Marshal(metadata); chk.E(err) {
153 return
154 }
155
156 if err = d.Update(func(txn *badger.Txn) error {
157 metaKey := prefixBlobMeta + sha256Hex
158 if err := txn.Set([]byte(metaKey), metaData); err != nil {
159 return err
160 }
161
162 indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex
163 if err := txn.Set([]byte(indexKey), []byte{1}); err != nil {
164 return err
165 }
166
167 return nil
168 }); chk.E(err) {
169 return
170 }
171
172 log.D.F("saved blob metadata %s (%d bytes) for pubkey %s", sha256Hex, size, hex.Enc(pubkey))
173 return
174 }
175
176 // GetBlob retrieves blob data by SHA256 hash
177 func (d *D) GetBlob(sha256Hash []byte) (data []byte, metadata *BlobMetadata, err error) {
178 sha256Hex := hex.Enc(sha256Hash)
179
180 // Get metadata first to get extension
181 metaKey := prefixBlobMeta + sha256Hex
182 if err = d.View(func(txn *badger.Txn) error {
183 item, err := txn.Get([]byte(metaKey))
184 if err != nil {
185 return err
186 }
187
188 return item.Value(func(val []byte) error {
189 metadata = &BlobMetadata{}
190 if err = json.Unmarshal(val, metadata); err != nil {
191 return err
192 }
193 return nil
194 })
195 }); chk.E(err) {
196 return
197 }
198
199 // Read blob data from file
200 blobPath := d.getBlobPath(sha256Hex, metadata.Extension)
201 data, err = os.ReadFile(blobPath)
202 if err != nil {
203 if os.IsNotExist(err) {
204 err = badger.ErrKeyNotFound
205 }
206 return
207 }
208
209 return
210 }
211
212 // HasBlob checks if a blob exists
213 func (d *D) HasBlob(sha256Hash []byte) (exists bool, err error) {
214 sha256Hex := hex.Enc(sha256Hash)
215
216 // Get metadata to find extension
217 metaKey := prefixBlobMeta + sha256Hex
218 var metadata *BlobMetadata
219 if err = d.View(func(txn *badger.Txn) error {
220 item, err := txn.Get([]byte(metaKey))
221 if err == badger.ErrKeyNotFound {
222 return badger.ErrKeyNotFound
223 }
224 if err != nil {
225 return err
226 }
227
228 return item.Value(func(val []byte) error {
229 metadata = &BlobMetadata{}
230 if err = json.Unmarshal(val, metadata); err != nil {
231 return err
232 }
233 return nil
234 })
235 }); err == badger.ErrKeyNotFound {
236 exists = false
237 return false, nil
238 }
239 if err != nil {
240 return
241 }
242
243 // Check if file exists
244 blobPath := d.getBlobPath(sha256Hex, metadata.Extension)
245 if _, err = os.Stat(blobPath); err == nil {
246 exists = true
247 return
248 }
249 if os.IsNotExist(err) {
250 exists = false
251 err = nil
252 return
253 }
254 return
255 }
256
257 // DeleteBlob deletes a blob and its metadata
258 func (d *D) DeleteBlob(sha256Hash []byte, pubkey []byte) (err error) {
259 sha256Hex := hex.Enc(sha256Hash)
260
261 // Get metadata to find extension
262 metaKey := prefixBlobMeta + sha256Hex
263 var metadata *BlobMetadata
264 if err = d.View(func(txn *badger.Txn) error {
265 item, err := txn.Get([]byte(metaKey))
266 if err == badger.ErrKeyNotFound {
267 return badger.ErrKeyNotFound
268 }
269 if err != nil {
270 return err
271 }
272
273 return item.Value(func(val []byte) error {
274 metadata = &BlobMetadata{}
275 if err = json.Unmarshal(val, metadata); err != nil {
276 return err
277 }
278 return nil
279 })
280 }); err == badger.ErrKeyNotFound {
281 return errorf.E("blob %s not found", sha256Hex)
282 }
283 if err != nil {
284 return
285 }
286
287 blobPath := d.getBlobPath(sha256Hex, metadata.Extension)
288 indexKey := prefixBlobIndex + hex.Enc(pubkey) + ":" + sha256Hex
289
290 if err = d.Update(func(txn *badger.Txn) error {
291 // Delete metadata
292 if err := txn.Delete([]byte(metaKey)); err != nil {
293 return err
294 }
295
296 // Delete index entry
297 if err := txn.Delete([]byte(indexKey)); err != nil {
298 return err
299 }
300
301 return nil
302 }); chk.E(err) {
303 return
304 }
305
306 // Delete blob file
307 if err = os.Remove(blobPath); err != nil && !os.IsNotExist(err) {
308 log.E.F("failed to delete blob file %s: %v", blobPath, err)
309 // Don't fail if file doesn't exist
310 }
311
312 log.D.F("deleted blob %s for pubkey %s", sha256Hex, hex.Enc(pubkey))
313 return
314 }
315
316 // ListBlobs lists all blobs for a given pubkey
317 func (d *D) ListBlobs(
318 pubkey []byte, since, until int64,
319 ) (descriptors []*BlobDescriptor, err error) {
320 pubkeyHex := hex.Enc(pubkey)
321 prefix := prefixBlobIndex + pubkeyHex + ":"
322
323 descriptors = make([]*BlobDescriptor, 0)
324
325 if err = d.View(func(txn *badger.Txn) error {
326 opts := badger.DefaultIteratorOptions
327 opts.Prefix = []byte(prefix)
328 it := txn.NewIterator(opts)
329 defer it.Close()
330
331 for it.Rewind(); it.Valid(); it.Next() {
332 item := it.Item()
333 key := item.Key()
334
335 // Extract SHA256 from key: prefixBlobIndex + pubkeyHex + ":" + sha256Hex
336 sha256Hex := string(key[len(prefix):])
337
338 // Get blob metadata
339 metaKey := prefixBlobMeta + sha256Hex
340 metaItem, err := txn.Get([]byte(metaKey))
341 if err != nil {
342 continue
343 }
344
345 var metadata *BlobMetadata
346 if err = metaItem.Value(func(val []byte) error {
347 metadata = &BlobMetadata{}
348 if err = json.Unmarshal(val, metadata); err != nil {
349 return err
350 }
351 return nil
352 }); err != nil {
353 continue
354 }
355
356 // Filter by time range
357 if since > 0 && metadata.Uploaded < since {
358 continue
359 }
360 if until > 0 && metadata.Uploaded > until {
361 continue
362 }
363
364 // Verify blob file exists
365 blobPath := d.getBlobPath(sha256Hex, metadata.Extension)
366 if _, errGet := os.Stat(blobPath); errGet != nil {
367 continue
368 }
369
370 // Create descriptor (URL will be set by handler)
371 mimeType := metadata.MimeType
372 if mimeType == "" {
373 mimeType = "application/octet-stream"
374 }
375 descriptor := &BlobDescriptor{
376 URL: "", // URL will be set by handler
377 SHA256: sha256Hex,
378 Size: metadata.Size,
379 Type: mimeType,
380 Uploaded: metadata.Uploaded,
381 }
382
383 descriptors = append(descriptors, descriptor)
384 }
385
386 return nil
387 }); chk.E(err) {
388 return
389 }
390
391 return
392 }
393
394 // GetBlobMetadata retrieves only metadata for a blob
395 func (d *D) GetBlobMetadata(sha256Hash []byte) (metadata *BlobMetadata, err error) {
396 sha256Hex := hex.Enc(sha256Hash)
397 metaKey := prefixBlobMeta + sha256Hex
398
399 if err = d.View(func(txn *badger.Txn) error {
400 item, err := txn.Get([]byte(metaKey))
401 if err != nil {
402 return err
403 }
404
405 return item.Value(func(val []byte) error {
406 metadata = &BlobMetadata{}
407 if err = json.Unmarshal(val, metadata); err != nil {
408 return err
409 }
410 return nil
411 })
412 }); chk.E(err) {
413 return
414 }
415
416 return
417 }
418
419 // GetTotalBlobStorageUsed calculates total storage used by a pubkey in MB
420 func (d *D) GetTotalBlobStorageUsed(pubkey []byte) (totalMB int64, err error) {
421 pubkeyHex := hex.Enc(pubkey)
422 prefix := prefixBlobIndex + pubkeyHex + ":"
423
424 totalBytes := int64(0)
425
426 if err = d.View(func(txn *badger.Txn) error {
427 opts := badger.DefaultIteratorOptions
428 opts.Prefix = []byte(prefix)
429 it := txn.NewIterator(opts)
430 defer it.Close()
431
432 for it.Rewind(); it.Valid(); it.Next() {
433 item := it.Item()
434 key := item.Key()
435
436 // Extract SHA256 from key: prefixBlobIndex + pubkeyHex + ":" + sha256Hex
437 sha256Hex := string(key[len(prefix):])
438
439 // Get blob metadata
440 metaKey := prefixBlobMeta + sha256Hex
441 metaItem, err := txn.Get([]byte(metaKey))
442 if err != nil {
443 continue
444 }
445
446 var metadata *BlobMetadata
447 if err = metaItem.Value(func(val []byte) error {
448 metadata = &BlobMetadata{}
449 if err = json.Unmarshal(val, metadata); err != nil {
450 return err
451 }
452 return nil
453 }); err != nil {
454 continue
455 }
456
457 // Verify blob file exists
458 blobPath := d.getBlobPath(sha256Hex, metadata.Extension)
459 if _, errGet := os.Stat(blobPath); errGet != nil {
460 continue
461 }
462
463 totalBytes += metadata.Size
464 }
465
466 return nil
467 }); chk.E(err) {
468 return
469 }
470
471 // Convert bytes to MB (rounding up)
472 totalMB = (totalBytes + 1024*1024 - 1) / (1024 * 1024)
473 return
474 }
475
476 // SaveBlobReport stores a report for a blob (BUD-09)
477 func (d *D) SaveBlobReport(sha256Hash []byte, reportData []byte) (err error) {
478 sha256Hex := hex.Enc(sha256Hash)
479 reportKey := prefixBlobReport + sha256Hex
480
481 // Get existing reports
482 var existingReports [][]byte
483 if err = d.View(func(txn *badger.Txn) error {
484 item, err := txn.Get([]byte(reportKey))
485 if err == badger.ErrKeyNotFound {
486 return nil
487 }
488 if err != nil {
489 return err
490 }
491
492 return item.Value(func(val []byte) error {
493 if err = json.Unmarshal(val, &existingReports); err != nil {
494 return err
495 }
496 return nil
497 })
498 }); chk.E(err) {
499 return
500 }
501
502 // Append new report
503 existingReports = append(existingReports, reportData)
504
505 // Store updated reports
506 var reportsData []byte
507 if reportsData, err = json.Marshal(existingReports); chk.E(err) {
508 return
509 }
510
511 if err = d.Update(func(txn *badger.Txn) error {
512 return txn.Set([]byte(reportKey), reportsData)
513 }); chk.E(err) {
514 return
515 }
516
517 log.D.F("saved report for blob %s", sha256Hex)
518 return
519 }
520
521 // ListAllBlobUserStats returns storage statistics for all users who have uploaded blobs
522 func (d *D) ListAllBlobUserStats() (stats []*UserBlobStats, err error) {
523 statsMap := make(map[string]*UserBlobStats)
524
525 if err = d.View(func(txn *badger.Txn) error {
526 opts := badger.DefaultIteratorOptions
527 opts.Prefix = []byte(prefixBlobIndex)
528 opts.PrefetchValues = false
529 it := txn.NewIterator(opts)
530 defer it.Close()
531
532 for it.Rewind(); it.Valid(); it.Next() {
533 key := string(it.Item().Key())
534 // Key format: blob:index:<pubkey-hex>:<sha256-hex>
535 remainder := key[len(prefixBlobIndex):]
536 parts := strings.SplitN(remainder, ":", 2)
537 if len(parts) != 2 {
538 continue
539 }
540 pubkeyHex := parts[0]
541 sha256Hex := parts[1]
542
543 // Get or create stats entry
544 stat, ok := statsMap[pubkeyHex]
545 if !ok {
546 stat = &UserBlobStats{PubkeyHex: pubkeyHex}
547 statsMap[pubkeyHex] = stat
548 }
549 stat.BlobCount++
550
551 // Get blob size from metadata
552 metaKey := prefixBlobMeta + sha256Hex
553 metaItem, errGet := txn.Get([]byte(metaKey))
554 if errGet != nil {
555 continue
556 }
557 metaItem.Value(func(val []byte) error {
558 metadata := &BlobMetadata{}
559 if errDeser := json.Unmarshal(val, metadata); errDeser == nil {
560 stat.TotalSizeBytes += metadata.Size
561 }
562 return nil
563 })
564 }
565 return nil
566 }); chk.E(err) {
567 return
568 }
569
570 // Convert map to slice
571 stats = make([]*UserBlobStats, 0, len(statsMap))
572 for _, stat := range statsMap {
573 stats = append(stats, stat)
574 }
575
576 // Sort by total size descending
577 sort.Slice(stats, func(i, j int) bool {
578 return stats[i].TotalSizeBytes > stats[j].TotalSizeBytes
579 })
580
581 return
582 }
583
584 // getExtensionFromMimeType returns a file extension for a MIME type
585 func getExtensionFromMimeType(mimeType string) string {
586 // Common MIME type to extension mapping
587 mimeToExt := map[string]string{
588 "image/png": ".png",
589 "image/jpeg": ".jpg",
590 "image/gif": ".gif",
591 "image/webp": ".webp",
592 "image/svg+xml": ".svg",
593 "image/bmp": ".bmp",
594 "image/tiff": ".tiff",
595 "video/mp4": ".mp4",
596 "video/webm": ".webm",
597 "video/ogg": ".ogv",
598 "video/quicktime": ".mov",
599 "audio/mpeg": ".mp3",
600 "audio/ogg": ".ogg",
601 "audio/wav": ".wav",
602 "audio/webm": ".weba",
603 "audio/flac": ".flac",
604 "application/pdf": ".pdf",
605 "application/zip": ".zip",
606 "text/plain": ".txt",
607 "text/html": ".html",
608 "text/css": ".css",
609 "text/javascript": ".js",
610 "application/json": ".json",
611 }
612
613 if ext, ok := mimeToExt[mimeType]; ok {
614 return ext
615 }
616 return "" // No extension for unknown types
617 }
618
619 // getMimeTypeFromExtension returns a MIME type for a file extension
620 func getMimeTypeFromExtension(ext string) string {
621 extToMime := map[string]string{
622 ".png": "image/png",
623 ".jpg": "image/jpeg",
624 ".jpeg": "image/jpeg",
625 ".gif": "image/gif",
626 ".webp": "image/webp",
627 ".svg": "image/svg+xml",
628 ".bmp": "image/bmp",
629 ".tiff": "image/tiff",
630 ".mp4": "video/mp4",
631 ".webm": "video/webm",
632 ".ogv": "video/ogg",
633 ".mov": "video/quicktime",
634 ".avi": "video/x-msvideo",
635 ".mp3": "audio/mpeg",
636 ".wav": "audio/wav",
637 ".ogg": "audio/ogg",
638 ".flac": "audio/flac",
639 ".pdf": "application/pdf",
640 ".txt": "text/plain",
641 ".html": "text/html",
642 ".css": "text/css",
643 ".js": "text/javascript",
644 ".json": "application/json",
645 }
646
647 if mime, ok := extToMime[ext]; ok {
648 return mime
649 }
650 return "application/octet-stream"
651 }
652
653 // ReconcileBlobMetadata scans the blossom directory for blob files that
654 // don't have corresponding metadata in the database and creates entries for them.
655 // This is useful for recovering from situations where blob files exist but
656 // their metadata was lost or never created.
657 func (d *D) ReconcileBlobMetadata() (reconciled int, err error) {
658 blobDir := d.getBlobDir()
659
660 // Scan directory for blob files
661 entries, err := os.ReadDir(blobDir)
662 if err != nil {
663 if os.IsNotExist(err) {
664 log.I.F("blossom directory does not exist, nothing to reconcile")
665 return 0, nil
666 }
667 return 0, errorf.E("failed to read blossom directory: %w", err)
668 }
669
670 log.I.F("scanning %d files in blossom directory for reconciliation", len(entries))
671
672 for _, entry := range entries {
673 if entry.IsDir() {
674 continue
675 }
676
677 filename := entry.Name()
678
679 // Parse filename: sha256hex.extension
680 ext := filepath.Ext(filename)
681 sha256Hex := strings.TrimSuffix(filename, ext)
682
683 // Validate it looks like a SHA256 hex (64 characters)
684 if len(sha256Hex) != 64 {
685 continue
686 }
687
688 _, decErr := hex.Dec(sha256Hex)
689 if decErr != nil {
690 continue
691 }
692
693 // Check if metadata already exists
694 metaKey := prefixBlobMeta + sha256Hex
695 var exists bool
696 if viewErr := d.View(func(txn *badger.Txn) error {
697 _, err := txn.Get([]byte(metaKey))
698 if err == badger.ErrKeyNotFound {
699 exists = false
700 return nil
701 }
702 if err != nil {
703 return err
704 }
705 exists = true
706 return nil
707 }); viewErr != nil {
708 log.W.F("error checking metadata for %s: %v", sha256Hex, viewErr)
709 continue
710 }
711
712 if exists {
713 // Metadata already exists, skip
714 continue
715 }
716
717 // Get file info for size
718 info, infoErr := entry.Info()
719 if infoErr != nil {
720 log.W.F("error getting file info for %s: %v", filename, infoErr)
721 continue
722 }
723
724 // Create metadata entry
725 mimeType := getMimeTypeFromExtension(ext)
726 metadata := &BlobMetadata{
727 Pubkey: nil, // Unknown owner - will be nil/empty
728 MimeType: mimeType,
729 Uploaded: info.ModTime().Unix(), // Use file modification time
730 Size: info.Size(),
731 Extension: ext,
732 }
733
734 metaData, marshalErr := json.Marshal(metadata)
735 if marshalErr != nil {
736 log.W.F("error marshaling metadata for %s: %v", sha256Hex, marshalErr)
737 continue
738 }
739
740 // Store metadata in database
741 if updateErr := d.Update(func(txn *badger.Txn) error {
742 return txn.Set([]byte(metaKey), metaData)
743 }); updateErr != nil {
744 log.W.F("error saving metadata for %s: %v", sha256Hex, updateErr)
745 continue
746 }
747
748 log.I.F("reconciled blob metadata: %s (%s, %d bytes)", sha256Hex, mimeType, info.Size())
749 reconciled++
750
751 // Also create an index entry with empty pubkey for anonymous ownership
752 indexKey := prefixBlobIndex + "anonymous:" + sha256Hex
753 if indexErr := d.Update(func(txn *badger.Txn) error {
754 // Check if any index exists for this blob already
755 opts := badger.DefaultIteratorOptions
756 opts.Prefix = []byte(prefixBlobIndex)
757 opts.PrefetchValues = false
758 it := txn.NewIterator(opts)
759 defer it.Close()
760
761 suffix := ":" + sha256Hex
762 for it.Rewind(); it.Valid(); it.Next() {
763 key := string(it.Item().Key())
764 if strings.HasSuffix(key, suffix) {
765 // Found an existing index, don't create anonymous one
766 return nil
767 }
768 }
769
770 // No index found, create anonymous one
771 return txn.Set([]byte(indexKey), []byte{1})
772 }); indexErr != nil {
773 log.W.F("error creating index for %s: %v", sha256Hex, indexErr)
774 }
775 }
776
777 log.I.F("blob metadata reconciliation complete: %d files reconciled", reconciled)
778 return reconciled, nil
779 }
780
781 // ListAllBlobs returns all blob descriptors in the database
782 func (d *D) ListAllBlobs() (descriptors []*BlobDescriptor, err error) {
783 descriptors = make([]*BlobDescriptor, 0)
784
785 if err = d.View(func(txn *badger.Txn) error {
786 opts := badger.DefaultIteratorOptions
787 opts.Prefix = []byte(prefixBlobMeta)
788 it := txn.NewIterator(opts)
789 defer it.Close()
790
791 for it.Rewind(); it.Valid(); it.Next() {
792 item := it.Item()
793 key := item.Key()
794
795 // Extract SHA256 from key: prefixBlobMeta + sha256Hex
796 sha256Hex := string(key[len(prefixBlobMeta):])
797
798 var metadata *BlobMetadata
799 if errVal := item.Value(func(val []byte) error {
800 metadata = &BlobMetadata{}
801 return json.Unmarshal(val, metadata)
802 }); errVal != nil {
803 continue
804 }
805
806 // Verify blob file exists
807 blobPath := d.getBlobPath(sha256Hex, metadata.Extension)
808 if _, errStat := os.Stat(blobPath); errStat != nil {
809 continue
810 }
811
812 mimeType := metadata.MimeType
813 if mimeType == "" {
814 mimeType = "application/octet-stream"
815 }
816
817 descriptor := &BlobDescriptor{
818 SHA256: sha256Hex,
819 Size: metadata.Size,
820 Type: mimeType,
821 Uploaded: metadata.Uploaded,
822 }
823
824 descriptors = append(descriptors, descriptor)
825 }
826
827 return nil
828 }); chk.E(err) {
829 return
830 }
831
832 return
833 }
834
835 const prefixThumbnail = "blob:thumb:"
836
837 // GetThumbnail retrieves a cached thumbnail by key
838 func (d *D) GetThumbnail(key string) (data []byte, err error) {
839 thumbKey := prefixThumbnail + key
840
841 err = d.View(func(txn *badger.Txn) error {
842 item, err := txn.Get([]byte(thumbKey))
843 if err != nil {
844 return err
845 }
846 return item.Value(func(val []byte) error {
847 data = make([]byte, len(val))
848 copy(data, val)
849 return nil
850 })
851 })
852
853 return
854 }
855
856 // SaveThumbnail caches a thumbnail with the given key
857 func (d *D) SaveThumbnail(key string, data []byte) error {
858 thumbKey := prefixThumbnail + key
859
860 return d.Update(func(txn *badger.Txn) error {
861 return txn.Set([]byte(thumbKey), data)
862 })
863 }
864