dealspace/lib/store.go

118 lines
2.8 KiB
Go

package lib
import (
"crypto/sha256"
"encoding/hex"
"errors"
"os"
"path/filepath"
)
var (
ErrObjectNotFound = errors.New("object not found")
)
// ObjectStore is the interface for encrypted file storage.
type ObjectStore interface {
Write(id string, data []byte) error
Read(id string) ([]byte, error)
Delete(id string) error
Exists(id string) bool
}
// LocalStore implements ObjectStore using the local filesystem.
// Files are stored in a two-level directory structure based on the first 4 hex chars of the ID.
type LocalStore struct {
BasePath string
}
// NewLocalStore creates a new local filesystem object store.
func NewLocalStore(basePath string) (*LocalStore, error) {
if err := os.MkdirAll(basePath, 0700); err != nil {
return nil, err
}
return &LocalStore{BasePath: basePath}, nil
}
func (s *LocalStore) objectPath(id string) string {
// Two-level sharding: ab/cd/abcdef...
if len(id) < 4 {
return filepath.Join(s.BasePath, id)
}
return filepath.Join(s.BasePath, id[:2], id[2:4], id)
}
func (s *LocalStore) Write(id string, data []byte) error {
path := s.objectPath(id)
if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil {
return err
}
return os.WriteFile(path, data, 0600)
}
func (s *LocalStore) Read(id string) ([]byte, error) {
data, err := os.ReadFile(s.objectPath(id))
if errors.Is(err, os.ErrNotExist) {
return nil, ErrObjectNotFound
}
return data, err
}
func (s *LocalStore) Delete(id string) error {
err := os.Remove(s.objectPath(id))
if errors.Is(err, os.ErrNotExist) {
return nil
}
return err
}
func (s *LocalStore) Exists(id string) bool {
_, err := os.Stat(s.objectPath(id))
return err == nil
}
// ObjectID computes the content-addressable ID (hex SHA-256 of encrypted content).
func ObjectID(encryptedData []byte) string {
h := sha256.Sum256(encryptedData)
return hex.EncodeToString(h[:])
}
// ObjectWrite encrypts data and writes to store. Returns the object ID.
func ObjectWrite(db *DB, store ObjectStore, cfg *Config, projectID string, data []byte) (string, error) {
key, err := DeriveProjectKey(cfg.MasterKey, projectID)
if err != nil {
return "", err
}
encrypted, err := ObjectEncrypt(key, data)
if err != nil {
return "", err
}
id := ObjectID(encrypted)
if err := store.Write(id, encrypted); err != nil {
return "", err
}
return id, nil
}
// ObjectRead reads and decrypts data from store.
func ObjectRead(db *DB, store ObjectStore, cfg *Config, projectID, objectID string) ([]byte, error) {
encrypted, err := store.Read(objectID)
if err != nil {
return nil, err
}
key, err := DeriveProjectKey(cfg.MasterKey, projectID)
if err != nil {
return nil, err
}
return ObjectDecrypt(key, encrypted)
}
// ObjectDelete removes an object from store.
func ObjectDelete(store ObjectStore, objectID string) error {
return store.Delete(objectID)
}