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(projectID string, data []byte) (string, error) Read(projectID, objectID string) ([]byte, error) Delete(objectID string) error Exists(objectID 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 MasterKey []byte } // 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) } // Write encrypts data and writes to store. Returns the object ID. func (s *LocalStore) Write(projectID string, data []byte) (string, error) { // Derive project-specific key if master key is set if len(s.MasterKey) > 0 { key, err := DeriveProjectKey(s.MasterKey, projectID) if err != nil { return "", err } encrypted, err := ObjectEncrypt(key, data) if err != nil { return "", err } data = encrypted } // Compute content-addressable ID id := ObjectID(data) path := s.objectPath(id) if err := os.MkdirAll(filepath.Dir(path), 0700); err != nil { return "", err } if err := os.WriteFile(path, data, 0600); err != nil { return "", err } return id, nil } // Read reads and decrypts data from store. func (s *LocalStore) Read(projectID, objectID string) ([]byte, error) { data, err := os.ReadFile(s.objectPath(objectID)) if errors.Is(err, os.ErrNotExist) { return nil, ErrObjectNotFound } if err != nil { return nil, err } // Decrypt if master key is set if len(s.MasterKey) > 0 { key, err := DeriveProjectKey(s.MasterKey, projectID) if err != nil { return nil, err } data, err = ObjectDecrypt(key, data) if err != nil { return nil, err } } return data, nil } func (s *LocalStore) Delete(objectID string) error { err := os.Remove(s.objectPath(objectID)) if errors.Is(err, os.ErrNotExist) { return nil } return err } func (s *LocalStore) Exists(objectID string) bool { _, err := os.Stat(s.objectPath(objectID)) 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) { return store.Write(projectID, data) } // ObjectRead reads and decrypts data from store. func ObjectRead(db *DB, store ObjectStore, cfg *Config, projectID, objectID string) ([]byte, error) { return store.Read(projectID, objectID) } // ObjectDelete removes an object from store. func ObjectDelete(store ObjectStore, objectID string) error { return store.Delete(objectID) }