82 lines
2.3 KiB
Go
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
|
|
}
|