diff --git a/src/storage.rs b/src/storage.rs index a096f22..58f2960 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -25,6 +25,18 @@ mod rosters; mod schema; mod sync_state; +const FOLDER_SCOPED_TABLES: &[&str] = &[ + "known_peers", + "join_invites", + "folder_rosters", + "manifests", + "manifest_history_compact", + "counters", + "sync_state", + "file_fingerprints", + "pending_materializations", +]; + pub use fingerprints::FileFingerprint; pub use manifests::ManifestRevision; pub use paths::AppPaths; @@ -226,17 +238,7 @@ impl StateStore { pub fn forget_folder(&self, folder: &FolderConfig) -> anyhow::Result<()> { let transaction = self.connection.unchecked_transaction()?; let folder_id = folder.id.to_string(); - for table in [ - "known_peers", - "join_invites", - "folder_rosters", - "manifests", - "manifest_history_compact", - "counters", - "sync_state", - "file_fingerprints", - "pending_materializations", - ] { + for table in FOLDER_SCOPED_TABLES { transaction.execute( &format!("DELETE FROM {table} WHERE folder_id = ?1"), params![folder_id], diff --git a/src/storage/manifests.rs b/src/storage/manifests.rs index aadec45..3bc8b2a 100644 --- a/src/storage/manifests.rs +++ b/src/storage/manifests.rs @@ -11,6 +11,8 @@ use crate::{ const HISTORY_CHECKPOINT_INTERVAL: u64 = 20; const COMPRESSION_LEVEL: i32 = 3; +const CHECKPOINT_RECORD_KIND: &str = "checkpoint"; +const DELTA_RECORD_KIND: &str = "delta"; #[derive(Clone, Debug)] pub struct ManifestRevision { @@ -141,12 +143,15 @@ fn history_payload( revision_count: u64, ) -> anyhow::Result<(&'static str, Vec)> { if previous.is_none() || revision_count.is_multiple_of(HISTORY_CHECKPOINT_INTERVAL) { - return Ok(("checkpoint", compress(manifest)?)); + return Ok((CHECKPOINT_RECORD_KIND, compress(manifest)?)); } let previous = previous.ok_or_else(|| anyhow::anyhow!("manifest history is missing its predecessor"))?; let previous = serde_json::from_str(previous)?; - Ok(("delta", compress(&manifest_delta(&previous, manifest))?)) + Ok(( + DELTA_RECORD_KIND, + compress(&manifest_delta(&previous, manifest))?, + )) } fn checkpoint_retention_boundary( @@ -163,8 +168,12 @@ fn checkpoint_retention_boundary( let boundary = &records[boundary_index]; let manifest = reconstruct_manifest(folder_id, &records[..=boundary_index])?; transaction.execute( - "UPDATE manifest_history_compact SET kind = 'checkpoint', payload = ?1 WHERE revision = ?2", - params![compress(&manifest)?, boundary.revision], + "UPDATE manifest_history_compact SET kind = ?1, payload = ?2 WHERE revision = ?3", + params![ + CHECKPOINT_RECORD_KIND, + compress(&manifest)?, + boundary.revision + ], )?; Ok(()) } @@ -217,8 +226,8 @@ fn apply_history_record( record: &HistoryRecord, ) -> anyhow::Result { match record.kind.as_str() { - "checkpoint" => Ok(decompress(&record.payload)?), - "delta" => { + CHECKPOINT_RECORD_KIND => Ok(decompress(&record.payload)?), + DELTA_RECORD_KIND => { let delta: ManifestDelta = decompress(&record.payload)?; for (path, entry) in delta.entries { match entry { diff --git a/src/storage/peers.rs b/src/storage/peers.rs index 499f815..6490cab 100644 --- a/src/storage/peers.rs +++ b/src/storage/peers.rs @@ -46,15 +46,15 @@ impl StateStore { })? .collect::, _>>()? .into_iter() - .map(peer_info_from_row) + .map(peer_info_from_values) .collect() } } -fn peer_info_from_row(row: (String, String, String)) -> anyhow::Result { +fn peer_info_from_values(values: (String, String, String)) -> anyhow::Result { Ok(PeerInfo { - device_id: row.0, - endpoint: serde_json::from_str(&row.1)?, - last_seen: OffsetDateTime::parse(&row.2, &Rfc3339)?, + device_id: values.0, + endpoint: serde_json::from_str(&values.1)?, + last_seen: OffsetDateTime::parse(&values.2, &Rfc3339)?, }) } diff --git a/src/storage/schema.rs b/src/storage/schema.rs index ace7cc7..60dd4dd 100644 --- a/src/storage/schema.rs +++ b/src/storage/schema.rs @@ -1,7 +1,70 @@ use rusqlite::Connection; +const TABLE_STATEMENTS: &[&str] = &[ + "CREATE TABLE IF NOT EXISTS folders ( + id TEXT PRIMARY KEY NOT NULL, + name TEXT NOT NULL, + path TEXT NOT NULL UNIQUE, + capability TEXT NOT NULL, + mode TEXT NOT NULL DEFAULT '\"send_receive\"' + );", + "CREATE TABLE IF NOT EXISTS known_peers ( + folder_id TEXT NOT NULL, + device_id TEXT NOT NULL, + endpoint TEXT NOT NULL, + last_seen TEXT NOT NULL, + PRIMARY KEY (folder_id, device_id) + );", + "CREATE TABLE IF NOT EXISTS join_invites ( + folder_id TEXT PRIMARY KEY NOT NULL, + invite TEXT NOT NULL + );", + "CREATE TABLE IF NOT EXISTS folder_rosters ( + folder_id TEXT PRIMARY KEY NOT NULL, + epoch INTEGER NOT NULL, + roster TEXT NOT NULL + );", + "CREATE TABLE IF NOT EXISTS manifests ( + folder_id TEXT PRIMARY KEY NOT NULL, + manifest TEXT NOT NULL + );", + "CREATE TABLE IF NOT EXISTS manifest_history_compact ( + revision INTEGER PRIMARY KEY AUTOINCREMENT, + folder_id TEXT NOT NULL, + saved_at TEXT NOT NULL, + kind TEXT NOT NULL, + payload BLOB NOT NULL + );", + "CREATE TABLE IF NOT EXISTS counters ( + folder_id TEXT NOT NULL, + device_id TEXT NOT NULL, + counter INTEGER NOT NULL, + PRIMARY KEY (folder_id, device_id) + );", + "CREATE TABLE IF NOT EXISTS sync_state ( + folder_id TEXT PRIMARY KEY NOT NULL, + last_successful_sync TEXT, + last_error TEXT + );", + "CREATE TABLE IF NOT EXISTS file_fingerprints ( + folder_id TEXT NOT NULL, + path TEXT NOT NULL, + size_bytes INTEGER NOT NULL, + modified_at_nanos INTEGER NOT NULL, + file_id INTEGER, + blob_hash TEXT NOT NULL, + PRIMARY KEY (folder_id, path) + );", + "CREATE TABLE IF NOT EXISTS pending_materializations ( + folder_id TEXT PRIMARY KEY NOT NULL, + entry TEXT NOT NULL, + resulting_manifest TEXT NOT NULL + );", +]; + pub(super) fn initialize(connection: &Connection) -> rusqlite::Result<()> { - connection.execute_batch( - "CREATE TABLE IF NOT EXISTS folders (id TEXT PRIMARY KEY NOT NULL, name TEXT NOT NULL, path TEXT NOT NULL UNIQUE, capability TEXT NOT NULL, mode TEXT NOT NULL DEFAULT '\"send_receive\"'); CREATE TABLE IF NOT EXISTS known_peers (folder_id TEXT NOT NULL, device_id TEXT NOT NULL, endpoint TEXT NOT NULL, last_seen TEXT NOT NULL, PRIMARY KEY (folder_id, device_id)); CREATE TABLE IF NOT EXISTS join_invites (folder_id TEXT PRIMARY KEY NOT NULL, invite TEXT NOT NULL); CREATE TABLE IF NOT EXISTS folder_rosters (folder_id TEXT PRIMARY KEY NOT NULL, epoch INTEGER NOT NULL, roster TEXT NOT NULL); CREATE TABLE IF NOT EXISTS manifests (folder_id TEXT PRIMARY KEY NOT NULL, manifest TEXT NOT NULL); CREATE TABLE IF NOT EXISTS manifest_history_compact (revision INTEGER PRIMARY KEY AUTOINCREMENT, folder_id TEXT NOT NULL, saved_at TEXT NOT NULL, kind TEXT NOT NULL, payload BLOB NOT NULL); CREATE TABLE IF NOT EXISTS counters (folder_id TEXT NOT NULL, device_id TEXT NOT NULL, counter INTEGER NOT NULL, PRIMARY KEY (folder_id, device_id)); CREATE TABLE IF NOT EXISTS sync_state (folder_id TEXT PRIMARY KEY NOT NULL, last_successful_sync TEXT, last_error TEXT); CREATE TABLE IF NOT EXISTS file_fingerprints (folder_id TEXT NOT NULL, path TEXT NOT NULL, size_bytes INTEGER NOT NULL, modified_at_nanos INTEGER NOT NULL, file_id INTEGER, blob_hash TEXT NOT NULL, PRIMARY KEY (folder_id, path)); CREATE TABLE IF NOT EXISTS pending_materializations (folder_id TEXT PRIMARY KEY NOT NULL, entry TEXT NOT NULL, resulting_manifest TEXT NOT NULL);", - ) + for statement in TABLE_STATEMENTS { + connection.execute_batch(statement)?; + } + Ok(()) } diff --git a/src/storage/tests.rs b/src/storage/tests.rs index 92386b3..f6cb4a5 100644 --- a/src/storage/tests.rs +++ b/src/storage/tests.rs @@ -1,7 +1,7 @@ use std::fs; use tempfile::TempDir; -use time::OffsetDateTime; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use super::{ AppPaths, FileFingerprint, StateStore, prune_manifest_history, validate_history_revision_limit, @@ -328,10 +328,10 @@ fn prunes_manifest_history_to_its_configured_limit() -> anyhow::Result<()> { let paths = AppPaths::from_data_directory(directory.path().join("state"))?; let store = StateStore::open(&paths)?; let folder_id = uuid::Uuid::new_v4(); - for revision in 0..3 { + for _ in 0..3 { store.connection.execute( "INSERT INTO manifest_history_compact (folder_id, saved_at, kind, payload) VALUES (?1, ?2, ?3, ?4)", - rusqlite::params![folder_id.to_string(), revision.to_string(), "checkpoint", vec![0_u8]], + rusqlite::params![folder_id.to_string(), OffsetDateTime::now_utc().format(&Rfc3339)?, "checkpoint", vec![0_u8]], )?; } let transaction = store.connection.unchecked_transaction()?; @@ -344,6 +344,10 @@ fn prunes_manifest_history_to_its_configured_limit() -> anyhow::Result<()> { )?; assert_eq!(remaining, 1); - assert!(validate_history_revision_limit(0).is_err()); Ok(()) } + +#[test] +fn rejects_a_zero_history_revision_limit() { + assert!(validate_history_revision_limit(0).is_err()); +}