from pathlib import Path import hashlib import sqlite3 import time from lane_common.throttle import WriteThrottle from lane_common.crypto_base import prefetch_container_assets from .crypto import SovereignVault class AccountBucketStorage: def __init__(self, base_dir: Path, db_path: Path): self.base_dir = base_dir self.db_path = db_path self._throttle = WriteThrottle(min_interval_seconds=3.0) self._init_db() def _bucket_dir(self, account_id: int) -> Path: return self.base_dir / f"acct_{account_id}" def _init_db(self) -> None: conn = sqlite3.connect(self.db_path) cur = conn.cursor() cur.execute(""" CREATE TABLE IF NOT EXISTS objects ( account_id INTEGER, object_hash TEXT, logical_key TEXT, created_at INTEGER, tag BLOB, PRIMARY KEY (account_id, object_hash) )""") conn.commit() conn.close() def _hash_bytes(self, content: bytes) -> str: h = hashlib.sha256() h.update(content) return h.hexdigest() def store(self, account_id: int, device_id: str, content: bytes, logical_key: str) -> str: prefetch_container_assets(self.base_dir) self._throttle.before_write() bucket_dir = self._bucket_dir(account_id) bucket_dir.mkdir(parents=True, exist_ok=True) object_hash = self._hash_bytes(content) vault = SovereignVault(account_id, device_id) key = vault.derive_object_key(object_hash) blob, tag = vault.encrypt(key, content) prefix = object_hash[:2] obj_dir = bucket_dir / prefix obj_dir.mkdir(parents=True, exist_ok=True) obj_path = obj_dir / object_hash if not obj_path.exists(): obj_path.write_bytes(blob) conn = sqlite3.connect(self.db_path) cur = conn.cursor() cur.execute(""" INSERT OR IGNORE INTO objects (account_id, object_hash, logical_key, created_at, tag) VALUES (?, ?, ?, ?, ?)""", (account_id, object_hash, logical_key, int(time.time()), tag)) conn.commit() conn.close() return object_hash def load(self, account_id: int, device_id: str, object_hash: str) -> bytes | None: bucket_dir = self._bucket_dir(account_id) prefix = object_hash[:2] obj_path = bucket_dir / prefix / object_hash if not obj_path.exists(): return None conn = sqlite3.connect(self.db_path) cur = conn.cursor() cur.execute(""" SELECT tag FROM objects WHERE account_id = ? AND object_hash = ?""", (account_id, object_hash)) row = cur.fetchone() conn.close() if row is None: return None tag = row[0] vault = SovereignVault(account_id, device_id) key = vault.derive_object_key(object_hash) blob = obj_path.read_bytes() return vault.decrypt(key, blob, tag)