diff --git a/src/app/manifest.rs b/src/app/manifest.rs index 2d446a3..2a80633 100644 --- a/src/app/manifest.rs +++ b/src/app/manifest.rs @@ -31,6 +31,7 @@ pub(super) async fn build_manifest(context: &SyncContext<'_>) -> anyhow::Result< let mut file_candidates = Vec::new(); let mut blob_hashes = BTreeMap::new(); let mut files_to_import = Vec::new(); + let cached_blob_hashes = context.state_store.cached_blob_hashes(context.folder.id)?; let collected_entries = collect_entries(&context.folder.path, &ignore_rules)?; for file_path in collected_entries.files { file_count += 1; @@ -45,26 +46,30 @@ pub(super) async fn build_manifest(context: &SyncContext<'_>) -> anyhow::Result< metadata, fingerprint, }; - match context.state_store.cached_blob_hash( - context.folder.id, - &path, - &candidate.fingerprint, - )? { - Some(blob_hash) => { - blob_hashes.insert(path, blob_hash); + match cached_blob_hashes.get(&path) { + Some(cached) if cached.fingerprint == candidate.fingerprint => { + blob_hashes.insert(path, cached.blob_hash.clone()); file_candidates.push(candidate); } - None => files_to_import.push(candidate), + _ => files_to_import.push(candidate), } } let imported_file_count = files_to_import.len(); - for (candidate, blob_hash) in import_files(context.node, files_to_import).await? { - context.state_store.save_file_fingerprint( - context.folder.id, - &candidate.path, - &candidate.fingerprint, - &blob_hash, - )?; + let imported_files = import_files(context.node, files_to_import).await?; + let fingerprint_updates = imported_files + .iter() + .map(|(candidate, blob_hash)| { + ( + candidate.path.clone(), + candidate.fingerprint.clone(), + blob_hash.clone(), + ) + }) + .collect::>(); + context + .state_store + .save_file_fingerprints(context.folder.id, &fingerprint_updates)?; + for (candidate, blob_hash) in imported_files { blob_hashes.insert(candidate.path.clone(), blob_hash); file_candidates.push(candidate); } diff --git a/src/storage/fingerprints.rs b/src/storage/fingerprints.rs index f7bca2f..b990d1e 100644 --- a/src/storage/fingerprints.rs +++ b/src/storage/fingerprints.rs @@ -1,6 +1,11 @@ -use std::{collections::BTreeSet, fs::Metadata}; +use std::{ + collections::{BTreeMap, BTreeSet}, + fs::Metadata, +}; -use rusqlite::{OptionalExtension, params}; +#[cfg(test)] +use rusqlite::OptionalExtension; +use rusqlite::params; use time::OffsetDateTime; use crate::{domain::FolderId, storage::StateStore}; @@ -12,6 +17,11 @@ pub struct FileFingerprint { file_id: Option, } +pub(crate) struct CachedBlobHash { + pub(crate) fingerprint: FileFingerprint, + pub(crate) blob_hash: String, +} + impl FileFingerprint { pub fn from_metadata(metadata: &Metadata) -> anyhow::Result { Ok(Self { @@ -25,6 +35,32 @@ impl FileFingerprint { } impl StateStore { + pub fn cached_blob_hashes( + &self, + folder_id: FolderId, + ) -> anyhow::Result> { + let mut statement = self.connection.prepare( + "SELECT path, size_bytes, modified_at_nanos, file_id, blob_hash FROM file_fingerprints WHERE folder_id = ?1", + )?; + statement + .query_map(params![folder_id.to_string()], |row| { + Ok(( + row.get::<_, String>(0)?, + CachedBlobHash { + fingerprint: FileFingerprint { + size_bytes: row.get(1)?, + modified_at_nanos: row.get(2)?, + file_id: row.get(3)?, + }, + blob_hash: row.get(4)?, + }, + )) + })? + .collect::, _>>() + .map_err(Into::into) + } + + #[cfg(test)] pub fn cached_blob_hash( &self, folder_id: FolderId, @@ -47,6 +83,7 @@ impl StateStore { .map_err(Into::into) } + #[cfg(test)] pub fn save_file_fingerprint( &self, folder_id: FolderId, @@ -68,6 +105,32 @@ impl StateStore { Ok(()) } + pub fn save_file_fingerprints( + &self, + folder_id: FolderId, + fingerprints: &[(String, FileFingerprint, String)], + ) -> anyhow::Result<()> { + if fingerprints.is_empty() { + return Ok(()); + } + let transaction = self.connection.unchecked_transaction()?; + for (path, fingerprint, blob_hash) in fingerprints { + transaction.execute( + "INSERT INTO file_fingerprints (folder_id, path, size_bytes, modified_at_nanos, file_id, blob_hash) VALUES (?1, ?2, ?3, ?4, ?5, ?6) ON CONFLICT(folder_id, path) DO UPDATE SET size_bytes = excluded.size_bytes, modified_at_nanos = excluded.modified_at_nanos, file_id = excluded.file_id, blob_hash = excluded.blob_hash", + params![ + folder_id.to_string(), + path, + fingerprint.size_bytes, + fingerprint.modified_at_nanos, + fingerprint.file_id, + blob_hash, + ], + )?; + } + transaction.commit()?; + Ok(()) + } + pub fn remove_missing_file_fingerprints( &self, folder_id: FolderId,