domain: add Sidecar Sync entities and ports (SidecarRecord, SidecarConfig, SidecarWriterPort)
This commit is contained in:
@@ -45,3 +45,9 @@ pub use share_target::{ShareTarget, TargetType};
|
|||||||
pub use share_link::{LinkAccessLevel, ShareLink};
|
pub use share_link::{LinkAccessLevel, ShareLink};
|
||||||
pub use invite_code::InviteCode;
|
pub use invite_code::InviteCode;
|
||||||
pub use visibility_filter::VisibilityFilter;
|
pub use visibility_filter::VisibilityFilter;
|
||||||
|
|
||||||
|
mod sidecar_record;
|
||||||
|
mod sidecar_config;
|
||||||
|
|
||||||
|
pub use sidecar_record::{SidecarRecord, SyncStatus};
|
||||||
|
pub use sidecar_config::{ConflictPolicy, SidecarConfig, SyncMode};
|
||||||
|
|||||||
30
crates/domain/src/entities/sidecar_config.rs
Normal file
30
crates/domain/src/entities/sidecar_config.rs
Normal 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,
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
57
crates/domain/src/entities/sidecar_record.rs
Normal file
57
crates/domain/src/entities/sidecar_record.rs
Normal 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());
|
||||||
|
}
|
||||||
|
}
|
||||||
@@ -45,3 +45,9 @@ mod visibility_filter_repo;
|
|||||||
|
|
||||||
pub use share_repo::ShareRepository;
|
pub use share_repo::ShareRepository;
|
||||||
pub use visibility_filter_repo::VisibilityFilterRepository;
|
pub use visibility_filter_repo::VisibilityFilterRepository;
|
||||||
|
|
||||||
|
mod sidecar_repo;
|
||||||
|
mod sidecar_writer;
|
||||||
|
|
||||||
|
pub use sidecar_repo::SidecarRepository;
|
||||||
|
pub use sidecar_writer::SidecarWriterPort;
|
||||||
|
|||||||
10
crates/domain/src/ports/sidecar_repo.rs
Normal file
10
crates/domain/src/ports/sidecar_repo.rs
Normal 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>;
|
||||||
|
}
|
||||||
9
crates/domain/src/ports/sidecar_writer.rs
Normal file
9
crates/domain/src/ports/sidecar_writer.rs
Normal 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>;
|
||||||
|
}
|
||||||
@@ -15,3 +15,4 @@ mod album;
|
|||||||
mod tag;
|
mod tag;
|
||||||
mod share_scope;
|
mod share_scope;
|
||||||
mod share_link;
|
mod share_link;
|
||||||
|
mod sidecar_record;
|
||||||
|
|||||||
31
crates/domain/tests/entities/sidecar_record.rs
Normal file
31
crates/domain/tests/entities/sidecar_record.rs
Normal 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"));
|
||||||
|
}
|
||||||
Reference in New Issue
Block a user