domain: add Sidecar Sync entities and ports (SidecarRecord, SidecarConfig, SidecarWriterPort)

This commit is contained in:
2026-05-31 03:34:04 +02:00
parent ba53e0fa70
commit ee79be0351
8 changed files with 150 additions and 0 deletions

View File

@@ -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};

View File

@@ -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,
}
}
}

View File

@@ -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<DateTimeStamp>,
pub last_known_file_hash: Option<Checksum>,
pub error_message: Option<String>,
}
impl SidecarRecord {
pub fn new(asset_id: SystemId, path: impl Into<String>) -> 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<String>) {
self.sync_status = SyncStatus::Error;
self.error_message = Some(message.into());
}
}

View File

@@ -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;

View File

@@ -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<Option<SidecarRecord>, DomainError>;
async fn find_by_status(&self, status: SyncStatus) -> Result<Vec<SidecarRecord>, DomainError>;
async fn save(&self, record: &SidecarRecord) -> Result<(), DomainError>;
async fn delete(&self, asset_id: &SystemId) -> Result<(), DomainError>;
}

View File

@@ -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<StructuredData, DomainError>;
}

View File

@@ -15,3 +15,4 @@ mod album;
mod tag;
mod share_scope;
mod share_link;
mod sidecar_record;

View File

@@ -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"));
}