-- ───────────────────────────────────────────── -- SPB Drive — Personal Cloud Drive -- ───────────────────────────────────────────── -- Author : Simon-Pierre Boucher -- Contact : contact@spboucher.ai -- File : src/db/schema.sql -- Purpose : Full SQLite schema — nodes tree, blobs, shares, sessions, FTS5 -- License : MIT © Simon-Pierre Boucher -- ───────────────────────────────────────────── PRAGMA journal_mode = WAL; CREATE TABLE IF NOT EXISTS meta ( key TEXT PRIMARY KEY, value TEXT NOT NULL ); -- Content-addressed blob registry. Path on disk: files/// CREATE TABLE IF NOT EXISTS blobs ( sha TEXT PRIMARY KEY, size INTEGER NOT NULL, refcount INTEGER NOT NULL DEFAULT 0, created INTEGER NOT NULL ); -- Virtual folder tree. Root is id=1 (parent_id NULL, type 'folder'). CREATE TABLE IF NOT EXISTS nodes ( id INTEGER PRIMARY KEY AUTOINCREMENT, parent_id INTEGER REFERENCES nodes(id) ON DELETE CASCADE, name TEXT NOT NULL, type TEXT NOT NULL CHECK (type IN ('file', 'folder')), blob_sha TEXT REFERENCES blobs(sha), size INTEGER NOT NULL DEFAULT 0, mime TEXT, created INTEGER NOT NULL, modified INTEGER NOT NULL, starred INTEGER NOT NULL DEFAULT 0, color TEXT, emoji TEXT, trashed_at INTEGER, trash_orig_parent INTEGER ); CREATE INDEX IF NOT EXISTS idx_nodes_parent ON nodes(parent_id, trashed_at); CREATE INDEX IF NOT EXISTS idx_nodes_blob ON nodes(blob_sha); CREATE INDEX IF NOT EXISTS idx_nodes_trashed ON nodes(trashed_at) WHERE trashed_at IS NOT NULL; CREATE INDEX IF NOT EXISTS idx_nodes_starred ON nodes(starred) WHERE starred = 1; CREATE INDEX IF NOT EXISTS idx_nodes_recent ON nodes(modified DESC); CREATE TABLE IF NOT EXISTS shares ( id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT NOT NULL UNIQUE, node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, created INTEGER NOT NULL, expires_at INTEGER, password_hash TEXT, max_downloads INTEGER, downloads INTEGER NOT NULL DEFAULT 0, visits INTEGER NOT NULL DEFAULT 0, allow_download INTEGER NOT NULL DEFAULT 1, label TEXT, revoked_at INTEGER ); CREATE INDEX IF NOT EXISTS idx_shares_node ON shares(node_id); CREATE TABLE IF NOT EXISTS share_events ( id INTEGER PRIMARY KEY AUTOINCREMENT, share_id INTEGER NOT NULL REFERENCES shares(id) ON DELETE CASCADE, ts INTEGER NOT NULL, kind TEXT NOT NULL CHECK (kind IN ('visit', 'download')), ip TEXT, ua TEXT ); CREATE INDEX IF NOT EXISTS idx_share_events_share ON share_events(share_id, ts DESC); CREATE TABLE IF NOT EXISTS tags ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL UNIQUE, color TEXT NOT NULL DEFAULT '#4f8cff' ); CREATE TABLE IF NOT EXISTS node_tags ( node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, tag_id INTEGER NOT NULL REFERENCES tags(id) ON DELETE CASCADE, PRIMARY KEY (node_id, tag_id) ); -- Session ids are stored hashed (sha256) — a DB leak never leaks live cookies. CREATE TABLE IF NOT EXISTS sessions ( id INTEGER PRIMARY KEY AUTOINCREMENT, token_hash TEXT NOT NULL UNIQUE, created INTEGER NOT NULL, last_seen INTEGER NOT NULL, expires_at INTEGER NOT NULL, remember INTEGER NOT NULL DEFAULT 0, ip TEXT, ua TEXT ); CREATE TABLE IF NOT EXISTS api_tokens ( id INTEGER PRIMARY KEY AUTOINCREMENT, name TEXT NOT NULL, token_hash TEXT NOT NULL UNIQUE, created INTEGER NOT NULL, last_used INTEGER ); CREATE TABLE IF NOT EXISTS activity ( id INTEGER PRIMARY KEY AUTOINCREMENT, ts INTEGER NOT NULL, kind TEXT NOT NULL, node_id INTEGER, detail TEXT, ip TEXT ); CREATE INDEX IF NOT EXISTS idx_activity_ts ON activity(ts DESC); CREATE TABLE IF NOT EXISTS login_attempts ( ip TEXT PRIMARY KEY, fail_count INTEGER NOT NULL DEFAULT 0, last_fail INTEGER, locked_until INTEGER ); -- Resumable upload sessions survive server restarts. CREATE TABLE IF NOT EXISTS uploads ( id TEXT PRIMARY KEY, parent_id INTEGER NOT NULL, name TEXT NOT NULL, size INTEGER NOT NULL, chunk_size INTEGER NOT NULL, n_chunks INTEGER NOT NULL, created INTEGER NOT NULL ); -- Version history: superseded blobs kept when a file is replaced or edited. -- Each row holds one refcount on its blob (released when the row is pruned). CREATE TABLE IF NOT EXISTS node_versions ( id INTEGER PRIMARY KEY AUTOINCREMENT, node_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, blob_sha TEXT NOT NULL REFERENCES blobs(sha), size INTEGER NOT NULL, mime TEXT, created INTEGER NOT NULL, replaced_at INTEGER NOT NULL, origin TEXT NOT NULL DEFAULT 'replace' CHECK (origin IN ('replace', 'edit', 'restore')) ); CREATE INDEX IF NOT EXISTS idx_versions_node ON node_versions(node_id, replaced_at DESC); -- File requests: public links that let anyone upload INTO a chosen folder. CREATE TABLE IF NOT EXISTS file_requests ( id INTEGER PRIMARY KEY AUTOINCREMENT, token TEXT NOT NULL UNIQUE, folder_id INTEGER NOT NULL REFERENCES nodes(id) ON DELETE CASCADE, label TEXT, created INTEGER NOT NULL, expires_at INTEGER, max_files INTEGER, received INTEGER NOT NULL DEFAULT 0, closed_at INTEGER ); -- Full-text search over name, tags and extracted text content. CREATE VIRTUAL TABLE IF NOT EXISTS fts USING fts5( name, tags, content, tokenize = 'unicode61 remove_diacritics 2' ); -- Map fts rowid <-> node id explicitly (fts rowid == node id). CREATE TABLE IF NOT EXISTS fts_state ( node_id INTEGER PRIMARY KEY, content_sha TEXT );