Implement versiond M1: inotify monitor, versioning, diff, restore, systemd install
FastAPI service on 127.0.0.1:9922 that monitors user-chosen directories with pruned inotify watches, stores versions in a local content-addressed spool indexed in SQLite, coalesces bursts (first + last), and offers history, diff and dry-run-first bulk restore. Includes a CLI with a systemd user unit installer. Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
This commit is contained in:
co-authored by
Claude Opus 5.5
parent
3455499cea
commit
ddf09bea88
@@ -0,0 +1,186 @@
|
||||
"""SQLite metadata index."""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import sqlite3
|
||||
import time
|
||||
from pathlib import Path
|
||||
from typing import Any, Iterable
|
||||
|
||||
SCHEMA_VERSION = 1
|
||||
|
||||
SCHEMA = """
|
||||
CREATE TABLE IF NOT EXISTS roots (
|
||||
id INTEGER PRIMARY KEY,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
mode TEXT NOT NULL DEFAULT 'pending',
|
||||
watch_count INTEGER NOT NULL DEFAULT 0,
|
||||
polled_dirs INTEGER NOT NULL DEFAULT 0,
|
||||
baseline_state TEXT NOT NULL DEFAULT 'pending',
|
||||
baseline_files INTEGER NOT NULL DEFAULT 0,
|
||||
added_at REAL NOT NULL,
|
||||
last_scan_at REAL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS projects (
|
||||
id INTEGER PRIMARY KEY,
|
||||
root_id INTEGER REFERENCES roots(id) ON DELETE SET NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
detected_by TEXT NOT NULL,
|
||||
created_at REAL NOT NULL,
|
||||
last_activity_at REAL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS files (
|
||||
id INTEGER PRIMARY KEY,
|
||||
root_id INTEGER REFERENCES roots(id) ON DELETE SET NULL,
|
||||
project_id INTEGER REFERENCES projects(id) ON DELETE SET NULL,
|
||||
path TEXT NOT NULL UNIQUE,
|
||||
exists_on_disk INTEGER NOT NULL DEFAULT 1,
|
||||
inode INTEGER,
|
||||
mtime_ns INTEGER,
|
||||
size INTEGER,
|
||||
first_seen_at REAL NOT NULL,
|
||||
last_version_at REAL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS blobs (
|
||||
sha256 TEXT PRIMARY KEY,
|
||||
size INTEGER NOT NULL,
|
||||
stored_size INTEGER NOT NULL,
|
||||
created_at REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS versions (
|
||||
id INTEGER PRIMARY KEY,
|
||||
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||||
blob_sha256 TEXT NOT NULL REFERENCES blobs(sha256),
|
||||
captured_at REAL NOT NULL,
|
||||
source TEXT NOT NULL,
|
||||
reason TEXT NOT NULL,
|
||||
pinned INTEGER NOT NULL DEFAULT 0,
|
||||
durability TEXT NOT NULL DEFAULT 'local'
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS renames (
|
||||
id INTEGER PRIMARY KEY,
|
||||
file_id INTEGER NOT NULL REFERENCES files(id) ON DELETE CASCADE,
|
||||
old_path TEXT NOT NULL,
|
||||
new_path TEXT NOT NULL,
|
||||
at REAL NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS restore_plans (
|
||||
id TEXT PRIMARY KEY,
|
||||
created_at REAL NOT NULL,
|
||||
expires_at REAL NOT NULL,
|
||||
criteria_json TEXT NOT NULL,
|
||||
plan_json TEXT NOT NULL,
|
||||
status TEXT NOT NULL
|
||||
);
|
||||
CREATE TABLE IF NOT EXISTS audit_log (
|
||||
id INTEGER PRIMARY KEY,
|
||||
at REAL NOT NULL,
|
||||
action TEXT NOT NULL,
|
||||
details_json TEXT NOT NULL
|
||||
);
|
||||
CREATE INDEX IF NOT EXISTS versions_file_time ON versions(file_id, captured_at);
|
||||
CREATE INDEX IF NOT EXISTS versions_blob ON versions(blob_sha256);
|
||||
CREATE INDEX IF NOT EXISTS files_root ON files(root_id);
|
||||
CREATE INDEX IF NOT EXISTS files_project ON files(project_id);
|
||||
"""
|
||||
|
||||
|
||||
def row_dict(row: sqlite3.Row | None) -> dict[str, Any] | None:
|
||||
return dict(row) if row is not None else None
|
||||
|
||||
|
||||
class Database:
|
||||
def __init__(self, path: Path):
|
||||
self.conn = sqlite3.connect(path, check_same_thread=False, isolation_level=None)
|
||||
self.conn.row_factory = sqlite3.Row
|
||||
self.conn.execute("PRAGMA journal_mode=WAL")
|
||||
self.conn.execute("PRAGMA synchronous=NORMAL")
|
||||
self.conn.execute("PRAGMA foreign_keys=ON")
|
||||
self.conn.executescript(SCHEMA)
|
||||
current = self.conn.execute("PRAGMA user_version").fetchone()[0]
|
||||
if current < SCHEMA_VERSION:
|
||||
self.conn.execute(f"PRAGMA user_version={SCHEMA_VERSION}")
|
||||
|
||||
def close(self) -> None:
|
||||
self.conn.close()
|
||||
|
||||
def execute(self, sql: str, params: Iterable[Any] = ()) -> sqlite3.Cursor:
|
||||
return self.conn.execute(sql, tuple(params))
|
||||
|
||||
def one(self, sql: str, params: Iterable[Any] = ()) -> dict[str, Any] | None:
|
||||
return row_dict(self.conn.execute(sql, tuple(params)).fetchone())
|
||||
|
||||
def all(self, sql: str, params: Iterable[Any] = ()) -> list[dict[str, Any]]:
|
||||
return [dict(r) for r in self.conn.execute(sql, tuple(params)).fetchall()]
|
||||
|
||||
def transaction(self):
|
||||
return _Transaction(self.conn)
|
||||
|
||||
def audit(self, action: str, **details: Any) -> None:
|
||||
self.execute(
|
||||
"INSERT INTO audit_log(at, action, details_json) VALUES (?, ?, ?)",
|
||||
(time.time(), action, json.dumps(details, default=str)),
|
||||
)
|
||||
|
||||
# roots
|
||||
|
||||
def roots(self) -> list[dict[str, Any]]:
|
||||
return self.all("SELECT * FROM roots ORDER BY id")
|
||||
|
||||
def root(self, root_id: int) -> dict[str, Any] | None:
|
||||
return self.one("SELECT * FROM roots WHERE id = ?", (root_id,))
|
||||
|
||||
def update_root(self, root_id: int, **fields: Any) -> None:
|
||||
cols = ", ".join(f"{k} = ?" for k in fields)
|
||||
self.execute(f"UPDATE roots SET {cols} WHERE id = ?", (*fields.values(), root_id))
|
||||
|
||||
# files and versions
|
||||
|
||||
def file_by_path(self, path: str) -> dict[str, Any] | None:
|
||||
return self.one("SELECT * FROM files WHERE path = ?", (path,))
|
||||
|
||||
def file(self, file_id: int) -> dict[str, Any] | None:
|
||||
return self.one("SELECT * FROM files WHERE id = ?", (file_id,))
|
||||
|
||||
def latest_version(self, file_id: int) -> dict[str, Any] | None:
|
||||
return self.one(
|
||||
"SELECT * FROM versions WHERE file_id = ? ORDER BY captured_at DESC, id DESC LIMIT 1",
|
||||
(file_id,),
|
||||
)
|
||||
|
||||
def version(self, version_id: int) -> dict[str, Any] | None:
|
||||
return self.one(
|
||||
"SELECT v.*, f.path, b.size FROM versions v "
|
||||
"JOIN files f ON f.id = v.file_id JOIN blobs b ON b.sha256 = v.blob_sha256 "
|
||||
"WHERE v.id = ?",
|
||||
(version_id,),
|
||||
)
|
||||
|
||||
def version_at(self, file_id: int, as_of: float) -> dict[str, Any] | None:
|
||||
return self.one(
|
||||
"SELECT * FROM versions WHERE file_id = ? AND captured_at <= ? "
|
||||
"ORDER BY captured_at DESC, id DESC LIMIT 1",
|
||||
(file_id, as_of),
|
||||
)
|
||||
|
||||
def history(self, file_id: int) -> list[dict[str, Any]]:
|
||||
return self.all(
|
||||
"SELECT v.id, v.captured_at, v.source, v.reason, v.pinned, v.durability, "
|
||||
"v.blob_sha256, b.size FROM versions v JOIN blobs b ON b.sha256 = v.blob_sha256 "
|
||||
"WHERE v.file_id = ? ORDER BY v.captured_at DESC, v.id DESC",
|
||||
(file_id,),
|
||||
)
|
||||
|
||||
|
||||
class _Transaction:
|
||||
def __init__(self, conn: sqlite3.Connection):
|
||||
self.conn = conn
|
||||
|
||||
def __enter__(self):
|
||||
self.conn.execute("BEGIN IMMEDIATE")
|
||||
return self.conn
|
||||
|
||||
def __exit__(self, exc_type, exc, tb):
|
||||
self.conn.execute("ROLLBACK" if exc_type else "COMMIT")
|
||||
return False
|
||||
Reference in New Issue
Block a user