diff --git a/crates/domain/src/entities/mod.rs b/crates/domain/src/entities/mod.rs index 2b794e3..2a7f1e3 100644 --- a/crates/domain/src/entities/mod.rs +++ b/crates/domain/src/entities/mod.rs @@ -45,3 +45,9 @@ pub use share_target::{ShareTarget, TargetType}; pub use share_link::{LinkAccessLevel, ShareLink}; pub use invite_code::InviteCode; pub use visibility_filter::VisibilityFilter; + +mod sidecar_record; +mod sidecar_config; + +pub use sidecar_record::{SidecarRecord, SyncStatus}; +pub use sidecar_config::{ConflictPolicy, SidecarConfig, SyncMode}; diff --git a/crates/domain/src/entities/sidecar_config.rs b/crates/domain/src/entities/sidecar_config.rs new file mode 100644 index 0000000..7ab71e7 --- /dev/null +++ b/crates/domain/src/entities/sidecar_config.rs @@ -0,0 +1,30 @@ +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SyncMode { + Auto, + Scheduled, + Manual, +} + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum ConflictPolicy { + DbWins, + FileWins, + RequireUserDecision, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SidecarConfig { + pub export_base_path: String, + pub sync_mode: SyncMode, + pub conflict_resolution_policy: ConflictPolicy, +} + +impl Default for SidecarConfig { + fn default() -> Self { + Self { + export_base_path: "/kphotos/sidecars".to_string(), + sync_mode: SyncMode::Auto, + conflict_resolution_policy: ConflictPolicy::DbWins, + } + } +} diff --git a/crates/domain/src/entities/sidecar_record.rs b/crates/domain/src/entities/sidecar_record.rs new file mode 100644 index 0000000..ade3f8a --- /dev/null +++ b/crates/domain/src/entities/sidecar_record.rs @@ -0,0 +1,57 @@ +use crate::value_objects::{Checksum, DateTimeStamp, SystemId}; + +#[derive(Debug, Clone, Copy, PartialEq, Eq, serde::Serialize, serde::Deserialize)] +pub enum SyncStatus { + InSync, + PendingWrite, + PendingRead, + Conflict, + Error, +} + +#[derive(Debug, Clone, serde::Serialize, serde::Deserialize)] +pub struct SidecarRecord { + pub asset_id: SystemId, + pub sync_status: SyncStatus, + pub sidecar_storage_path: String, + pub last_synced_at: Option, + pub last_known_file_hash: Option, + pub error_message: Option, +} + +impl SidecarRecord { + pub fn new(asset_id: SystemId, path: impl Into) -> Self { + Self { + asset_id, + sync_status: SyncStatus::PendingWrite, + sidecar_storage_path: path.into(), + last_synced_at: None, + last_known_file_hash: None, + error_message: None, + } + } + + pub fn mark_synced(&mut self, file_hash: Checksum) { + self.sync_status = SyncStatus::InSync; + self.last_synced_at = Some(DateTimeStamp::now()); + self.last_known_file_hash = Some(file_hash); + self.error_message = None; + } + + pub fn mark_pending_write(&mut self) { + self.sync_status = SyncStatus::PendingWrite; + } + + pub fn mark_pending_read(&mut self) { + self.sync_status = SyncStatus::PendingRead; + } + + pub fn mark_conflict(&mut self) { + self.sync_status = SyncStatus::Conflict; + } + + pub fn mark_error(&mut self, message: impl Into) { + self.sync_status = SyncStatus::Error; + self.error_message = Some(message.into()); + } +} diff --git a/crates/domain/src/ports/mod.rs b/crates/domain/src/ports/mod.rs index 1719354..05375cb 100644 --- a/crates/domain/src/ports/mod.rs +++ b/crates/domain/src/ports/mod.rs @@ -45,3 +45,9 @@ mod visibility_filter_repo; pub use share_repo::ShareRepository; pub use visibility_filter_repo::VisibilityFilterRepository; + +mod sidecar_repo; +mod sidecar_writer; + +pub use sidecar_repo::SidecarRepository; +pub use sidecar_writer::SidecarWriterPort; diff --git a/crates/domain/src/ports/sidecar_repo.rs b/crates/domain/src/ports/sidecar_repo.rs new file mode 100644 index 0000000..36ef14c --- /dev/null +++ b/crates/domain/src/ports/sidecar_repo.rs @@ -0,0 +1,10 @@ +use async_trait::async_trait; +use crate::{entities::{SidecarRecord, SyncStatus}, errors::DomainError, value_objects::SystemId}; + +#[async_trait] +pub trait SidecarRepository: Send + Sync { + async fn find_by_asset(&self, asset_id: &SystemId) -> Result, DomainError>; + async fn find_by_status(&self, status: SyncStatus) -> Result, DomainError>; + async fn save(&self, record: &SidecarRecord) -> Result<(), DomainError>; + async fn delete(&self, asset_id: &SystemId) -> Result<(), DomainError>; +} diff --git a/crates/domain/src/ports/sidecar_writer.rs b/crates/domain/src/ports/sidecar_writer.rs new file mode 100644 index 0000000..aaf56ec --- /dev/null +++ b/crates/domain/src/ports/sidecar_writer.rs @@ -0,0 +1,9 @@ +use async_trait::async_trait; +use crate::{errors::DomainError, value_objects::StructuredData}; + +#[async_trait] +pub trait SidecarWriterPort: Send + Sync { + fn format_name(&self) -> &str; + async fn write_sidecar(&self, data: &StructuredData, path: &str) -> Result<(), DomainError>; + async fn read_sidecar(&self, path: &str) -> Result; +} diff --git a/crates/domain/tests/entities/mod.rs b/crates/domain/tests/entities/mod.rs index 3e2d312..a37b7a1 100644 --- a/crates/domain/tests/entities/mod.rs +++ b/crates/domain/tests/entities/mod.rs @@ -15,3 +15,4 @@ mod album; mod tag; mod share_scope; mod share_link; +mod sidecar_record; diff --git a/crates/domain/tests/entities/sidecar_record.rs b/crates/domain/tests/entities/sidecar_record.rs new file mode 100644 index 0000000..95b8fd2 --- /dev/null +++ b/crates/domain/tests/entities/sidecar_record.rs @@ -0,0 +1,31 @@ +use domain::entities::{SidecarRecord, SyncStatus}; +use domain::value_objects::{Checksum, SystemId}; + +#[test] +fn new_is_pending_write() { + let record = SidecarRecord::new(SystemId::new(), "/path/to/sidecar.xmp"); + assert_eq!(record.sync_status, SyncStatus::PendingWrite); + assert!(record.last_synced_at.is_none()); + assert!(record.last_known_file_hash.is_none()); +} + +#[test] +fn sync_lifecycle() { + let mut record = SidecarRecord::new(SystemId::new(), "/path/to/sidecar.xmp"); + + let hash = Checksum::new("a".repeat(64)).unwrap(); + record.mark_synced(hash); + assert_eq!(record.sync_status, SyncStatus::InSync); + assert!(record.last_synced_at.is_some()); + assert!(record.last_known_file_hash.is_some()); + + record.mark_pending_read(); + assert_eq!(record.sync_status, SyncStatus::PendingRead); + + record.mark_conflict(); + assert_eq!(record.sync_status, SyncStatus::Conflict); + + record.mark_error("disk full"); + assert_eq!(record.sync_status, SyncStatus::Error); + assert_eq!(record.error_message.as_deref(), Some("disk full")); +}