feat: initial scaffold of unibus message bus (membership service + client lib + demo peers)
This commit is contained in:
@@ -0,0 +1,81 @@
|
||||
// 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
|
||||
}
|
||||
Reference in New Issue
Block a user