Signed-off-by: NotAShelf <raf@notashelf.dev> Change-Id: Id99798df6f7e4470caae8a193c2654aa6a6a6964
380 lines
10 KiB
Rust
380 lines
10 KiB
Rust
//! 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<Self, Self::Err> {
|
|
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<String>,
|
|
pub last_sync_at: Option<DateTime<Utc>>,
|
|
pub last_seen_at: DateTime<Utc>,
|
|
pub sync_cursor: Option<i64>,
|
|
pub enabled: bool,
|
|
pub created_at: DateTime<Utc>,
|
|
pub updated_at: DateTime<Utc>,
|
|
}
|
|
|
|
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<Self, Self::Err> {
|
|
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<MediaId>,
|
|
pub path: String,
|
|
pub content_hash: Option<ContentHash>,
|
|
pub file_size: Option<u64>,
|
|
pub metadata_json: Option<String>,
|
|
pub changed_by_device: Option<DeviceId>,
|
|
pub timestamp: DateTime<Utc>,
|
|
}
|
|
|
|
impl SyncLogEntry {
|
|
pub fn new(
|
|
change_type: SyncChangeType,
|
|
path: String,
|
|
media_id: Option<MediaId>,
|
|
content_hash: Option<ContentHash>,
|
|
) -> 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<Self, Self::Err> {
|
|
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<String>,
|
|
pub server_hash: Option<String>,
|
|
pub local_mtime: Option<i64>,
|
|
pub server_mtime: Option<i64>,
|
|
pub sync_status: FileSyncStatus,
|
|
pub last_synced_at: Option<DateTime<Utc>>,
|
|
pub conflict_info_json: Option<String>,
|
|
}
|
|
|
|
/// 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<Utc>,
|
|
pub resolved_at: Option<DateTime<Utc>>,
|
|
pub resolution: Option<ConflictResolution>,
|
|
}
|
|
|
|
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<Self, Self::Err> {
|
|
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<Utc>,
|
|
pub expires_at: DateTime<Utc>,
|
|
pub last_activity: DateTime<Utc>,
|
|
}
|
|
|
|
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<Utc>,
|
|
}
|