131 lines
3.3 KiB
Go
131 lines
3.3 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(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)
|
|
}
|