//! Sync domain models. use std::fmt; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use uuid::Uuid; use crate::config::ConflictResolution; use crate::model::{ContentHash, MediaId}; use crate::users::UserId; /// Unique identifier for a sync device. #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash, Serialize, Deserialize)] pub struct DeviceId(pub Uuid); impl DeviceId { pub fn new() -> Self { Self(Uuid::now_v7()) } } impl Default for DeviceId { fn default() -> Self { Self::new() } } impl fmt::Display for DeviceId { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { write!(f, "{}", self.0) } } /// Type of sync device. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "lowercase")] pub enum DeviceType { Desktop, Mobile, Tablet, Server, Other, } impl Default for DeviceType { fn default() -> Self { Self::Other } } impl fmt::Display for DeviceType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Desktop => write!(f, "desktop"), Self::Mobile => write!(f, "mobile"), Self::Tablet => write!(f, "tablet"), Self::Server => write!(f, "server"), Self::Other => write!(f, "other"), } } } impl std::str::FromStr for DeviceType { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "desktop" => Ok(Self::Desktop), "mobile" => Ok(Self::Mobile), "tablet" => Ok(Self::Tablet), "server" => Ok(Self::Server), "other" => Ok(Self::Other), _ => Err(format!("unknown device type: {}", s)), } } } /// A registered sync device. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncDevice { pub id: DeviceId, pub user_id: UserId, pub name: String, pub device_type: DeviceType, pub client_version: String, pub os_info: Option, pub last_sync_at: Option>, pub last_seen_at: DateTime, pub sync_cursor: Option, pub enabled: bool, pub created_at: DateTime, pub updated_at: DateTime, } impl SyncDevice { pub fn new( user_id: UserId, name: String, device_type: DeviceType, client_version: String, ) -> Self { let now = Utc::now(); Self { id: DeviceId::new(), user_id, name, device_type, client_version, os_info: None, last_sync_at: None, last_seen_at: now, sync_cursor: None, enabled: true, created_at: now, updated_at: now, } } } /// Type of change recorded in the sync log. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum SyncChangeType { Created, Modified, Deleted, Moved, MetadataUpdated, } impl fmt::Display for SyncChangeType { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Created => write!(f, "created"), Self::Modified => write!(f, "modified"), Self::Deleted => write!(f, "deleted"), Self::Moved => write!(f, "moved"), Self::MetadataUpdated => write!(f, "metadata_updated"), } } } impl std::str::FromStr for SyncChangeType { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "created" => Ok(Self::Created), "modified" => Ok(Self::Modified), "deleted" => Ok(Self::Deleted), "moved" => Ok(Self::Moved), "metadata_updated" => Ok(Self::MetadataUpdated), _ => Err(format!("unknown sync change type: {}", s)), } } } /// An entry in the sync log tracking a change. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncLogEntry { pub id: Uuid, pub sequence: i64, pub change_type: SyncChangeType, pub media_id: Option, pub path: String, pub content_hash: Option, pub file_size: Option, pub metadata_json: Option, pub changed_by_device: Option, pub timestamp: DateTime, } impl SyncLogEntry { pub fn new( change_type: SyncChangeType, path: String, media_id: Option, content_hash: Option, ) -> Self { Self { id: Uuid::now_v7(), sequence: 0, // Will be assigned by database change_type, media_id, path, content_hash, file_size: None, metadata_json: None, changed_by_device: None, timestamp: Utc::now(), } } } /// Sync status for a file on a device. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum FileSyncStatus { Synced, PendingUpload, PendingDownload, Conflict, Deleted, } impl fmt::Display for FileSyncStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Synced => write!(f, "synced"), Self::PendingUpload => write!(f, "pending_upload"), Self::PendingDownload => write!(f, "pending_download"), Self::Conflict => write!(f, "conflict"), Self::Deleted => write!(f, "deleted"), } } } impl std::str::FromStr for FileSyncStatus { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "synced" => Ok(Self::Synced), "pending_upload" => Ok(Self::PendingUpload), "pending_download" => Ok(Self::PendingDownload), "conflict" => Ok(Self::Conflict), "deleted" => Ok(Self::Deleted), _ => Err(format!("unknown file sync status: {}", s)), } } } /// Sync state for a specific file on a specific device. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct DeviceSyncState { pub device_id: DeviceId, pub path: String, pub local_hash: Option, pub server_hash: Option, pub local_mtime: Option, pub server_mtime: Option, pub sync_status: FileSyncStatus, pub last_synced_at: Option>, pub conflict_info_json: Option, } /// A sync conflict that needs resolution. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct SyncConflict { pub id: Uuid, pub device_id: DeviceId, pub path: String, pub local_hash: String, pub local_mtime: i64, pub server_hash: String, pub server_mtime: i64, pub detected_at: DateTime, pub resolved_at: Option>, pub resolution: Option, } impl SyncConflict { pub fn new( device_id: DeviceId, path: String, local_hash: String, local_mtime: i64, server_hash: String, server_mtime: i64, ) -> Self { Self { id: Uuid::now_v7(), device_id, path, local_hash, local_mtime, server_hash, server_mtime, detected_at: Utc::now(), resolved_at: None, resolution: None, } } } /// Status of an upload session. #[derive(Debug, Clone, Copy, PartialEq, Eq, Serialize, Deserialize)] #[serde(rename_all = "snake_case")] pub enum UploadStatus { Pending, InProgress, Completed, Failed, Expired, Cancelled, } impl fmt::Display for UploadStatus { fn fmt(&self, f: &mut fmt::Formatter<'_>) -> fmt::Result { match self { Self::Pending => write!(f, "pending"), Self::InProgress => write!(f, "in_progress"), Self::Completed => write!(f, "completed"), Self::Failed => write!(f, "failed"), Self::Expired => write!(f, "expired"), Self::Cancelled => write!(f, "cancelled"), } } } impl std::str::FromStr for UploadStatus { type Err = String; fn from_str(s: &str) -> Result { match s.to_lowercase().as_str() { "pending" => Ok(Self::Pending), "in_progress" => Ok(Self::InProgress), "completed" => Ok(Self::Completed), "failed" => Ok(Self::Failed), "expired" => Ok(Self::Expired), "cancelled" => Ok(Self::Cancelled), _ => Err(format!("unknown upload status: {}", s)), } } } /// A chunked upload session. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct UploadSession { pub id: Uuid, pub device_id: DeviceId, pub target_path: String, pub expected_hash: ContentHash, pub expected_size: u64, pub chunk_size: u64, pub chunk_count: u64, pub status: UploadStatus, pub created_at: DateTime, pub expires_at: DateTime, pub last_activity: DateTime, } impl UploadSession { pub fn new( device_id: DeviceId, target_path: String, expected_hash: ContentHash, expected_size: u64, chunk_size: u64, timeout_hours: u64, ) -> Self { let now = Utc::now(); let chunk_count = (expected_size + chunk_size - 1) / chunk_size; Self { id: Uuid::now_v7(), device_id, target_path, expected_hash, expected_size, chunk_size, chunk_count, status: UploadStatus::Pending, created_at: now, expires_at: now + chrono::Duration::hours(timeout_hours as i64), last_activity: now, } } } /// Information about an uploaded chunk. #[derive(Debug, Clone, Serialize, Deserialize)] pub struct ChunkInfo { pub upload_id: Uuid, pub chunk_index: u64, pub offset: u64, pub size: u64, pub hash: String, pub received_at: DateTime, }