Files
unibus/pkg/blobstore/blobstore.go
T

82 lines
2.3 KiB
Go

// Package blobstore is a content-addressed object store on local disk.
//
// The bus transports messages, not blobs. Media (images, files, large payloads)
// is encrypted by the client BEFORE being stored here, so the store only ever
// sees ciphertext. Objects are addressed by the sha256 hex of their (encrypted)
// bytes, which makes Put idempotent and deduplicating.
package blobstore
import (
"crypto/sha256"
"encoding/hex"
"fmt"
"os"
"path/filepath"
)
// Store is a directory-backed content-addressed blob store.
type Store struct {
dir string
}
// New creates a Store rooted at dir, creating the directory if needed.
func New(dir string) (*Store, error) {
if err := os.MkdirAll(dir, 0o755); err != nil {
return nil, fmt.Errorf("blobstore: mkdir %q: %w", dir, err)
}
return &Store{dir: dir}, nil
}
// path returns the on-disk path for a given content hash.
func (s *Store) path(hash string) string {
return filepath.Join(s.dir, hash)
}
// Put writes data to the store and returns its sha256 hex hash. If an object
// with the same content already exists, Put is a no-op and returns the hash.
func (s *Store) Put(data []byte) (string, error) {
sum := sha256.Sum256(data)
hash := hex.EncodeToString(sum[:])
p := s.path(hash)
if _, err := os.Stat(p); err == nil {
return hash, nil // already present (content-addressed: identical bytes)
}
// Write atomically: temp file + rename, so readers never see a partial blob.
tmp, err := os.CreateTemp(s.dir, ".tmp-*")
if err != nil {
return "", fmt.Errorf("blobstore: create temp: %w", err)
}
tmpName := tmp.Name()
if _, err := tmp.Write(data); err != nil {
tmp.Close()
os.Remove(tmpName)
return "", fmt.Errorf("blobstore: write temp: %w", err)
}
if err := tmp.Close(); err != nil {
os.Remove(tmpName)
return "", fmt.Errorf("blobstore: close temp: %w", err)
}
if err := os.Rename(tmpName, p); err != nil {
os.Remove(tmpName)
return "", fmt.Errorf("blobstore: rename: %w", err)
}
return hash, nil
}
// Get reads the object with the given hash.
func (s *Store) Get(hash string) ([]byte, error) {
data, err := os.ReadFile(s.path(hash))
if err != nil {
return nil, fmt.Errorf("blobstore: get %q: %w", hash, err)
}
return data, nil
}
// Has reports whether an object with the given hash exists.
func (s *Store) Has(hash string) bool {
_, err := os.Stat(s.path(hash))
return err == nil
}