larc r18

200 lines ยท 4.3 KB Raw
1 package storage
2
3 import (
4 "errors"
5 "fmt"
6 "io"
7 "os"
8 "path/filepath"
9
10 "larc.wejust.rest/larc/internal/hash"
11 )
12
13 /* BlobStore manages content-addressable blob storage.
14 * Files are stored in objects/ directory with 2-char prefix subdirs.
15 * All blobs are zstd compressed.
16 *
17 * Structure: objects/ab/cd1234567890ef (where abcd1234567890ef is the hash) */
18
19 var (
20 ErrBlobNotFound = errors.New("blob not found")
21 ErrBlobExists = errors.New("blob already exists")
22 )
23
24 // BlobStore provides content-addressable storage for file contents
25 type BlobStore struct {
26 root string // path to objects/ directory
27 }
28
29 // NewBlobStore creates a new blob store at the given root directory
30 func NewBlobStore(root string) (*BlobStore, error) {
31 if err := os.MkdirAll(root, 0755); err != nil {
32 return nil, fmt.Errorf("create blob store dir: %w", err)
33 }
34 return &BlobStore{root: root}, nil
35 }
36
37 // blobPath returns the full path for a blob hash
38 func (s *BlobStore) blobPath(h string) string {
39 if len(h) < 2 {
40 return filepath.Join(s.root, "00", h)
41 }
42 return filepath.Join(s.root, h[:2], h[2:])
43 }
44
45 // Exists checks if a blob exists
46 func (s *BlobStore) Exists(h string) bool {
47 _, err := os.Stat(s.blobPath(h))
48 return err == nil
49 }
50
51 // Write stores data and returns its hash
52 func (s *BlobStore) Write(data []byte) (string, error) {
53 h := hash.Bytes(data)
54
55 /* fast path: already exists */
56 if s.Exists(h) {
57 return h, nil
58 }
59
60 /* compress data */
61 compressed, err := Compress(data)
62 if err != nil {
63 return "", fmt.Errorf("compress blob: %w", err)
64 }
65
66 /* ensure directory exists */
67 blobPath := s.blobPath(h)
68 dir := filepath.Dir(blobPath)
69 if err := os.MkdirAll(dir, 0755); err != nil {
70 return "", fmt.Errorf("create blob subdir: %w", err)
71 }
72
73 /* write atomically via temp file */
74 tmpPath := blobPath + ".tmp"
75 if err := os.WriteFile(tmpPath, compressed, 0644); err != nil {
76 return "", fmt.Errorf("write blob tmp: %w", err)
77 }
78
79 if err := os.Rename(tmpPath, blobPath); err != nil {
80 os.Remove(tmpPath)
81 return "", fmt.Errorf("rename blob: %w", err)
82 }
83
84 return h, nil
85 }
86
87 // WriteFile stores a file and returns its hash
88 func (s *BlobStore) WriteFile(path string) (string, int64, error) {
89 data, err := os.ReadFile(path)
90 if err != nil {
91 return "", 0, fmt.Errorf("read file for blob: %w", err)
92 }
93
94 h, err := s.Write(data)
95 if err != nil {
96 return "", 0, err
97 }
98
99 return h, int64(len(data)), nil
100 }
101
102 // Read retrieves blob content by hash
103 func (s *BlobStore) Read(h string) ([]byte, error) {
104 compressed, err := os.ReadFile(s.blobPath(h))
105 if err != nil {
106 if os.IsNotExist(err) {
107 return nil, ErrBlobNotFound
108 }
109 return nil, fmt.Errorf("read blob: %w", err)
110 }
111
112 data, err := Decompress(compressed)
113 if err != nil {
114 return nil, fmt.Errorf("decompress blob: %w", err)
115 }
116
117 return data, nil
118 }
119
120 // ReadTo reads blob content to writer
121 func (s *BlobStore) ReadTo(h string, w io.Writer) error {
122 data, err := s.Read(h)
123 if err != nil {
124 return err
125 }
126
127 if _, err := w.Write(data); err != nil {
128 return fmt.Errorf("write blob to dest: %w", err)
129 }
130
131 return nil
132 }
133
134 // Size returns the compressed size of a blob
135 func (s *BlobStore) Size(h string) (int64, error) {
136 info, err := os.Stat(s.blobPath(h))
137 if err != nil {
138 if os.IsNotExist(err) {
139 return 0, ErrBlobNotFound
140 }
141 return 0, fmt.Errorf("stat blob: %w", err)
142 }
143 return info.Size(), nil
144 }
145
146 // Delete removes a blob (use with caution!)
147 func (s *BlobStore) Delete(h string) error {
148 if err := os.Remove(s.blobPath(h)); err != nil {
149 if os.IsNotExist(err) {
150 return ErrBlobNotFound
151 }
152 return fmt.Errorf("delete blob: %w", err)
153 }
154 return nil
155 }
156
157 // Walk iterates over all blobs in the store
158 func (s *BlobStore) Walk(fn func(hash string) error) error {
159 entries, err := os.ReadDir(s.root)
160 if err != nil {
161 return fmt.Errorf("read blob root: %w", err)
162 }
163
164 for _, entry := range entries {
165 if !entry.IsDir() || len(entry.Name()) != 2 {
166 continue
167 }
168
169 prefix := entry.Name()
170 subdir := filepath.Join(s.root, prefix)
171
172 subEntries, err := os.ReadDir(subdir)
173 if err != nil {
174 continue
175 }
176
177 for _, subEntry := range subEntries {
178 if subEntry.IsDir() {
179 continue
180 }
181 hash := prefix + subEntry.Name()
182 if err := fn(hash); err != nil {
183 return err
184 }
185 }
186 }
187
188 return nil
189 }
190
191 // Count returns the total number of blobs
192 func (s *BlobStore) Count() (int, error) {
193 count := 0
194 err := s.Walk(func(_ string) error {
195 count++
196 return nil
197 })
198 return count, err
199 }
200