//! Stable application API for Appa frontends. use std::path::{Path, PathBuf}; use serde::{Deserialize, Serialize}; use time::OffsetDateTime; use crate::{ app::{AppaService, ConflictInfo, FolderStatus, MemberInfo}, service, }; pub type Result = anyhow::Result; #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Dashboard { pub folders: Vec, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct FolderSummary { pub name: String, pub path: PathBuf, pub member_count: usize, pub peer_count: usize, pub file_count: usize, pub conflict_count: usize, pub last_successful_sync: Option, pub last_sync_error: Option, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct FolderDetails { pub summary: FolderSummary, pub conflicts: Vec, pub members: Vec, pub history_revision_count: u64, pub can_revoke_members: bool, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Conflict { pub path: String, pub author_device_id: String, pub modified_at: OffsetDateTime, } #[derive(Clone, Debug, Deserialize, Serialize)] pub struct Member { pub device_id: String, pub label: Option, } /// A handle for Appa operations that do not require direct access to storage /// or transport internals. pub struct Appa { service: AppaService, } impl Appa { pub fn open() -> Result { Ok(Self { service: AppaService::open()?, }) } #[cfg(test)] pub(crate) fn open_at(paths: crate::storage::AppPaths) -> Result { Ok(Self { service: AppaService::open_at(paths)?, }) } pub fn dashboard(&self) -> Result { Ok(Dashboard { folders: self .service .statuses()? .iter() .map(FolderSummary::from) .collect(), }) } pub fn folder_details(&self, folder_path: &Path) -> Result { let status = self.service.status(folder_path)?; Ok(FolderDetails { summary: FolderSummary::from(&status), conflicts: self .service .conflicts(folder_path)? .into_iter() .map(Conflict::from) .collect(), members: self .service .members(folder_path)? .into_iter() .map(Member::from) .collect(), history_revision_count: status.history_revision_count, can_revoke_members: self.service.can_revoke_members(folder_path)?, }) } pub fn register_folder(&self, folder_path: &Path) -> Result<()> { self.service.register_folder(folder_path)?; Ok(()) } pub fn join_folder(&self, folder_path: &Path, invitation: &str) -> Result<()> { self.service.join_folder(folder_path, invitation)?; Ok(()) } pub async fn create_invite(&self, folder_path: &Path) -> Result { self.service.create_invite(folder_path).await } pub fn revoke_member(&self, folder_path: &Path, device_id: &str) -> Result<()> { self.service.revoke_member(folder_path, device_id) } pub fn install_service(&self) -> Result<()> { service::install()?; Ok(()) } } impl From<&FolderStatus> for FolderSummary { fn from(status: &FolderStatus) -> Self { Self { name: status.folder.name.clone(), path: status.folder.path.clone(), member_count: status.member_count, peer_count: status.peers.len(), file_count: status.file_count, conflict_count: status.conflict_count, last_successful_sync: status.last_successful_sync, last_sync_error: status.last_sync_error.clone(), } } } impl From for Conflict { fn from(conflict: ConflictInfo) -> Self { Self { path: conflict.path, author_device_id: conflict.author_device_id, modified_at: conflict.modified_at, } } } impl From for Member { fn from(member: MemberInfo) -> Self { Self { device_id: member.device_id, label: member.label, } } } #[cfg(test)] mod tests { use tempfile::TempDir; use super::Appa; use crate::storage::AppPaths; #[test] fn dashboard_summarizes_a_registered_folder() -> anyhow::Result<()> { let state_directory = TempDir::new()?; let folder_directory = TempDir::new()?; let appa = Appa::open_at(AppPaths::from_data_directory( state_directory.path().join("state"), )?)?; appa.register_folder(folder_directory.path())?; let dashboard = appa.dashboard()?; assert_eq!(dashboard.folders.len(), 1); assert_eq!( dashboard.folders[0].path, folder_directory.path().canonicalize()? ); Ok(()) } }