package storage import ( "errors" "fmt" "io" "os" "path/filepath" "larc.wejust.rest/larc/internal/hash" ) /* BlobStore manages content-addressable blob storage. * Files are stored in objects/ directory with 2-char prefix subdirs. * All blobs are zstd compressed. * * Structure: objects/ab/cd1234567890ef (where abcd1234567890ef is the hash) */ var ( ErrBlobNotFound = errors.New("blob not found") ErrBlobExists = errors.New("blob already exists") ) // BlobStore provides content-addressable storage for file contents type BlobStore struct { root string // path to objects/ directory } // NewBlobStore creates a new blob store at the given root directory func NewBlobStore(root string) (*BlobStore, error) { if err := os.MkdirAll(root, 0755); err != nil { return nil, fmt.Errorf("create blob store dir: %w", err) } return &BlobStore{root: root}, nil } // blobPath returns the full path for a blob hash func (s *BlobStore) blobPath(h string) string { if len(h) < 2 { return filepath.Join(s.root, "00", h) } return filepath.Join(s.root, h[:2], h[2:]) } // Exists checks if a blob exists func (s *BlobStore) Exists(h string) bool { _, err := os.Stat(s.blobPath(h)) return err == nil } // Write stores data and returns its hash func (s *BlobStore) Write(data []byte) (string, error) { h := hash.Bytes(data) /* fast path: already exists */ if s.Exists(h) { return h, nil } /* compress data */ compressed, err := Compress(data) if err != nil { return "", fmt.Errorf("compress blob: %w", err) } /* ensure directory exists */ blobPath := s.blobPath(h) dir := filepath.Dir(blobPath) if err := os.MkdirAll(dir, 0755); err != nil { return "", fmt.Errorf("create blob subdir: %w", err) } /* write atomically via temp file */ tmpPath := blobPath + ".tmp" if err := os.WriteFile(tmpPath, compressed, 0644); err != nil { return "", fmt.Errorf("write blob tmp: %w", err) } if err := os.Rename(tmpPath, blobPath); err != nil { os.Remove(tmpPath) return "", fmt.Errorf("rename blob: %w", err) } return h, nil } // WriteFile stores a file and returns its hash func (s *BlobStore) WriteFile(path string) (string, int64, error) { data, err := os.ReadFile(path) if err != nil { return "", 0, fmt.Errorf("read file for blob: %w", err) } h, err := s.Write(data) if err != nil { return "", 0, err } return h, int64(len(data)), nil } // Read retrieves blob content by hash func (s *BlobStore) Read(h string) ([]byte, error) { compressed, err := os.ReadFile(s.blobPath(h)) if err != nil { if os.IsNotExist(err) { return nil, ErrBlobNotFound } return nil, fmt.Errorf("read blob: %w", err) } data, err := Decompress(compressed) if err != nil { return nil, fmt.Errorf("decompress blob: %w", err) } return data, nil } // ReadTo reads blob content to writer func (s *BlobStore) ReadTo(h string, w io.Writer) error { data, err := s.Read(h) if err != nil { return err } if _, err := w.Write(data); err != nil { return fmt.Errorf("write blob to dest: %w", err) } return nil } // Size returns the compressed size of a blob func (s *BlobStore) Size(h string) (int64, error) { info, err := os.Stat(s.blobPath(h)) if err != nil { if os.IsNotExist(err) { return 0, ErrBlobNotFound } return 0, fmt.Errorf("stat blob: %w", err) } return info.Size(), nil } // Delete removes a blob (use with caution!) func (s *BlobStore) Delete(h string) error { if err := os.Remove(s.blobPath(h)); err != nil { if os.IsNotExist(err) { return ErrBlobNotFound } return fmt.Errorf("delete blob: %w", err) } return nil } // Walk iterates over all blobs in the store func (s *BlobStore) Walk(fn func(hash string) error) error { entries, err := os.ReadDir(s.root) if err != nil { return fmt.Errorf("read blob root: %w", err) } for _, entry := range entries { if !entry.IsDir() || len(entry.Name()) != 2 { continue } prefix := entry.Name() subdir := filepath.Join(s.root, prefix) subEntries, err := os.ReadDir(subdir) if err != nil { continue } for _, subEntry := range subEntries { if subEntry.IsDir() { continue } hash := prefix + subEntry.Name() if err := fn(hash); err != nil { return err } } } return nil } // Count returns the total number of blobs func (s *BlobStore) Count() (int, error) { count := 0 err := s.Walk(func(_ string) error { count++ return nil }) return count, err }