package storage import ( "crypto/sha256" "fmt" "os" "path/filepath" ) // Storage handles reading/writing files on a hash-partitioned filesystem. // // File layout: {basePath}/ab/cd/ef/{hash[:12]}.{ext} // The first 6 hex chars of SHA-256 determine 3 directory levels (2 chars each). type Storage struct { basePath string } // New creates a Storage rooted at basePath, ensuring the directory exists. func New(basePath string) (*Storage, error) { abs, err := filepath.Abs(basePath) if err != nil { return nil, fmt.Errorf("storage path: %w", err) } if err := os.MkdirAll(abs, 0755); err != nil { return nil, fmt.Errorf("create storage dir: %w", err) } return &Storage{basePath: abs}, nil } // Store writes data to a hash-partitioned path and returns the hex hash and // relative storage path (e.g. "ab/cd/ef/a1b2c3d4e5f6.jpg"). func (s *Storage) Store(data []byte, ext string) (hash, storagePath string, err error) { h := sha256.Sum256(data) hexHash := fmt.Sprintf("%x", h) rel := filepath.Join(hexHash[:2], hexHash[2:4], hexHash[4:6], hexHash[:12]+"."+ext) abs := filepath.Join(s.basePath, rel) if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { return "", "", fmt.Errorf("create partition dir: %w", err) } if err := os.WriteFile(abs, data, 0644); err != nil { return "", "", fmt.Errorf("write file: %w", err) } return hexHash, rel, nil } // Open returns the file at the given relative storage path. func (s *Storage) Open(storagePath string) (*os.File, error) { f, err := os.Open(filepath.Join(s.basePath, storagePath)) if err != nil { return nil, fmt.Errorf("open %s: %w", storagePath, err) } return f, nil } // Delete removes the file at the given relative storage path and its parent // directories if they become empty. func (s *Storage) Delete(storagePath string) error { abs := filepath.Join(s.basePath, storagePath) if err := os.Remove(abs); err != nil { return fmt.Errorf("delete %s: %w", storagePath, err) } // Clean up empty parent directories (3 levels: ab/ → cd/ → ef/). dir := filepath.Dir(abs) for range 3 { entries, err := os.ReadDir(dir) if err != nil || len(entries) > 0 { break } os.Remove(dir) dir = filepath.Dir(dir) } return nil } // StoreAt writes data to a specific relative path (used for thumbnails whose // path is derived from the original file's storage path). func (s *Storage) StoreAt(relPath string, data []byte) error { abs := filepath.Join(s.basePath, relPath) if err := os.MkdirAll(filepath.Dir(abs), 0755); err != nil { return fmt.Errorf("create directory: %w", err) } return os.WriteFile(abs, data, 0644) } // AbsPath returns the absolute filesystem path for a relative storage path. func (s *Storage) AbsPath(storagePath string) string { return filepath.Join(s.basePath, storagePath) }