From c42229ed658e892230aafbebb3dea6b67db9116a Mon Sep 17 00:00:00 2001 From: Aly Raffauf Date: Sun, 2 Aug 2026 18:03:01 -0400 Subject: [PATCH] show detailed sync status --- src/app.rs | 30 ++++++++++++++++++++++++++++-- src/cli.rs | 17 ++++++++++++++--- src/storage.rs | 29 ++++++++++++++++++++++++++++- 3 files changed, 70 insertions(+), 6 deletions(-) diff --git a/src/app.rs b/src/app.rs index 9282cf7..16993ab 100644 --- a/src/app.rs +++ b/src/app.rs @@ -29,7 +29,11 @@ pub struct AppaService { pub struct FolderStatus { pub folder: FolderConfig, pub peer_count: usize, - pub entry_count: usize, + pub file_count: usize, + pub directory_count: usize, + pub deleted_count: usize, + pub conflict_count: usize, + pub last_successful_sync: Option, } #[derive(Clone, Debug)] @@ -75,9 +79,30 @@ impl AppaService { pub fn status(&self, folder_path: &Path) -> AppResult { let folder = self.require_folder(folder_path)?; + let manifest = self.state_store.load_manifest(folder.id)?; Ok(FolderStatus { peer_count: self.state_store.peers(folder.id)?.len(), - entry_count: self.state_store.load_manifest(folder.id)?.entries.len(), + file_count: manifest + .entries + .values() + .filter(|entry| entry.kind == EntryKind::File) + .count(), + directory_count: manifest + .entries + .values() + .filter(|entry| entry.kind == EntryKind::Directory) + .count(), + deleted_count: manifest + .entries + .values() + .filter(|entry| entry.kind == EntryKind::Deleted) + .count(), + conflict_count: manifest + .entries + .keys() + .filter(|path| path.contains(" (conflict-")) + .count(), + last_successful_sync: self.state_store.last_successful_sync(folder.id)?, folder, }) } @@ -158,6 +183,7 @@ impl AppaService { node.publish_manifest(local_manifest.clone()).await; } self.save_discovered_peers(folder, node)?; + self.state_store.record_successful_sync(folder.id)?; Ok(synchronized_count) } diff --git a/src/cli.rs b/src/cli.rs index 9590bf7..f264aa3 100644 --- a/src/cli.rs +++ b/src/cli.rs @@ -99,16 +99,27 @@ fn print_status(appa: &AppaService, folder: PathBuf, json: bool) -> anyhow::Resu "folder": status.folder.path, "folder_id": status.folder.id, "peers": status.peer_count, - "entries": status.entry_count, + "files": status.file_count, + "directories": status.directory_count, + "tombstones": status.deleted_count, + "conflicts": status.conflict_count, + "last_successful_sync": status.last_successful_sync, }) ); return Ok(()); } println!( - "{}\nPeers: {}\nEntries: {}", + "{}\nKnown peers: {}\nFiles: {}\nDirectories: {}\nTombstones: {}\nConflicts: {}\nLast successful sync: {}", status.folder.path.display(), status.peer_count, - status.entry_count, + status.file_count, + status.directory_count, + status.deleted_count, + status.conflict_count, + status + .last_successful_sync + .map(|time| time.to_string()) + .unwrap_or_else(|| "never".to_owned()), ); Ok(()) } diff --git a/src/storage.rs b/src/storage.rs index b8d4035..b353f75 100644 --- a/src/storage.rs +++ b/src/storage.rs @@ -9,6 +9,7 @@ use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD}; use directories::ProjectDirs; use iroh::{EndpointAddr, SecretKey}; use rusqlite::{Connection, OptionalExtension, params}; +use time::{OffsetDateTime, format_description::well_known::Rfc3339}; use uuid::Uuid; use crate::{ @@ -206,8 +207,34 @@ impl StateStore { u64::try_from(count).map_err(Into::into) } + pub fn record_successful_sync(&self, folder_id: FolderId) -> anyhow::Result<()> { + let completed_at = OffsetDateTime::now_utc().format(&Rfc3339)?; + self.connection.execute( + "INSERT INTO sync_state (folder_id, last_successful_sync) VALUES (?1, ?2) ON CONFLICT(folder_id) DO UPDATE SET last_successful_sync = excluded.last_successful_sync", + params![folder_id.to_string(), completed_at], + )?; + Ok(()) + } + + pub fn last_successful_sync( + &self, + folder_id: FolderId, + ) -> anyhow::Result> { + let timestamp: Option = self + .connection + .query_row( + "SELECT last_successful_sync FROM sync_state WHERE folder_id = ?1", + params![folder_id.to_string()], + |row| row.get(0), + ) + .optional()?; + timestamp + .map(|value| OffsetDateTime::parse(&value, &Rfc3339).map_err(Into::into)) + .transpose() + } + fn migrate(&self) -> anyhow::Result<()> { - self.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); CREATE TABLE IF NOT EXISTS peers (folder_id TEXT NOT NULL, endpoint TEXT NOT NULL, PRIMARY KEY (folder_id, endpoint)); CREATE TABLE IF NOT EXISTS manifests (folder_id TEXT PRIMARY KEY NOT NULL, manifest TEXT 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));")?; + self.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); CREATE TABLE IF NOT EXISTS peers (folder_id TEXT NOT NULL, endpoint TEXT NOT NULL, PRIMARY KEY (folder_id, endpoint)); CREATE TABLE IF NOT EXISTS manifests (folder_id TEXT PRIMARY KEY NOT NULL, manifest TEXT 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 NOT NULL);")?; Ok(()) } } -- knotmirror 0.0.1