all repos — weimar @ link-previews

Unnamed repository; edit this file 'description' to name the repository.

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}