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) }