internal/storage/disk.go (view raw)
1package storage
2
3import (
4 "crypto/sha256"
5 "fmt"
6 "os"
7 "path/filepath"
8)
9
10// Storage handles reading/writing files on a hash-partitioned filesystem.
11//
12// File layout: {basePath}/ab/cd/ef/{hash[:12]}.{ext}
13// The first 6 hex chars of SHA-256 determine 3 directory levels (2 chars each).
14type Storage struct {
15 basePath string
16}
17
18// New creates a Storage rooted at basePath, ensuring the directory exists.
19func New(basePath string) (*Storage, error) {
20 abs, err := filepath.Abs(basePath)
21 if err != nil {
22 return nil, fmt.Errorf("storage path: %w", err)
23 }
24 if err := os.MkdirAll(abs, 0755); err != nil {
25 return nil, fmt.Errorf("create storage dir: %w", err)
26 }
27 return &Storage{basePath: abs}, nil
28}
29
30// Store writes data to a hash-partitioned path and returns the hex hash and
31// relative storage path (e.g. "ab/cd/ef/a1b2c3d4e5f6.jpg").
32func (s *Storage) Store(data []byte, ext string) (hash, storagePath string, err error) {
33 h := sha256.Sum256(data)
34 hexHash := fmt.Sprintf("%x", h)
35 rel := filepath.Join(hexHash[:2], hexHash[2:4], hexHash[4:6], hexHash[:12]+"."+ext)
36 abs := filepath.Join(s.basePath, rel)
37
38 if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
39 return "", "", fmt.Errorf("create partition dir: %w", err)
40 }
41 if err := os.WriteFile(abs, data, 0644); err != nil {
42 return "", "", fmt.Errorf("write file: %w", err)
43 }
44
45 return hexHash, rel, nil
46}
47
48// Open returns the file at the given relative storage path.
49func (s *Storage) Open(storagePath string) (*os.File, error) {
50 f, err := os.Open(filepath.Join(s.basePath, storagePath))
51 if err != nil {
52 return nil, fmt.Errorf("open %s: %w", storagePath, err)
53 }
54 return f, nil
55}
56
57// Delete removes the file at the given relative storage path and its parent
58// directories if they become empty.
59func (s *Storage) Delete(storagePath string) error {
60 abs := filepath.Join(s.basePath, storagePath)
61 if err := os.Remove(abs); err != nil {
62 return fmt.Errorf("delete %s: %w", storagePath, err)
63 }
64
65 // Clean up empty parent directories (3 levels: ab/ → cd/ → ef/).
66 dir := filepath.Dir(abs)
67 for range 3 {
68 entries, err := os.ReadDir(dir)
69 if err != nil || len(entries) > 0 {
70 break
71 }
72 os.Remove(dir)
73 dir = filepath.Dir(dir)
74 }
75
76 return nil
77}
78
79// StoreAt writes data to a specific relative path (used for thumbnails whose
80// path is derived from the original file's storage path).
81func (s *Storage) StoreAt(relPath string, data []byte) error {
82 abs := filepath.Join(s.basePath, relPath)
83 if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil {
84 return fmt.Errorf("create directory: %w", err)
85 }
86 return os.WriteFile(abs, data, 0644)
87}
88
89// AbsPath returns the absolute filesystem path for a relative storage path.
90func (s *Storage) AbsPath(storagePath string) string {
91 return filepath.Join(s.basePath, storagePath)
92}