diff --git a/Cargo.lock b/Cargo.lock index 4193e765c..18f6e9368 100644 Binary files a/Cargo.lock and b/Cargo.lock differ diff --git a/Cargo.toml b/Cargo.toml index bdda88efe..5cd58af72 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [workspace] members = [ - "apps/cloud", + # "apps/cloud", "apps/desktop/crates/*", "apps/desktop/src-tauri", "apps/mobile/modules/sd-core/android/crate", diff --git a/Cargo.toml.bak b/Cargo.toml.bak new file mode 100644 index 000000000..bdda88efe --- /dev/null +++ b/Cargo.toml.bak @@ -0,0 +1,158 @@ +[workspace] +members = [ + "apps/cloud", + "apps/desktop/crates/*", + "apps/desktop/src-tauri", + "apps/mobile/modules/sd-core/android/crate", + "apps/mobile/modules/sd-core/core", + "apps/mobile/modules/sd-core/ios/crate", + "apps/server", + "core", + "core/crates/*", + "crates/*" +] +resolver = "2" + +[workspace.package] +edition = "2021" +license = "AGPL-3.0-only" +repository = "https://github.com/spacedriveapp/spacedrive" +rust-version = "1.81" + +[workspace.dependencies] +# First party dependencies +# sd-cloud-schema = { git = "https://github.com/spacedriveapp/cloud-services-schema", rev = "515ba740ea" } + +# Third party dependencies used by one or more of our crates +anyhow = "1.0.94" +async-channel = "2.3" +async-stream = "0.3.6" +async-trait = "0.1.83" +axum = "0.7.9" +axum-extra = "0.9.6" +base64 = "0.22.1" +blake3 = "1.5.5" +bytes = "1.9.0" +chrono = "0.4.39" +ed25519-dalek = "2.1" +flume = "0.11.0" +futures = "0.3.31" +futures-concurrency = "7.6.2" +globset = "0.4.15" +http = "1.2.0" +hyper = "1.5.2" +image = "0.25.5" +iroh = "0.29.0" +itertools = "0.13.0" +lending-stream = "1.0" +libc = "0.2.169" +mimalloc = "0.1.43" +normpath = "1.3" +pin-project-lite = "0.2.15" +quic-rpc = "0.17.3" +rand = "0.9.0-alpha.2" +regex = "1.11.1" +reqwest = { version = "0.12.9", default-features = false } +rmp = "0.8.14" +rmp-serde = "1.3" +rmpv = { version = "1.3", features = ["with-serde"] } +serde = "1.0.216" +serde_json = "1.0.133" +specta = "=2.0.0-rc.20" +strum = "0.26" +strum_macros = "0.26" +tempfile = "3.14.0" +thiserror = "2.0.8" +tokio = "1.42.0" +tokio-stream = "0.1.17" +tokio-util = "0.7.13" +tracing = "0.1.41" +tracing-subscriber = "0.3.19" +tracing-test = "0.2.5" +uhlc = "0.8.0" # Must follow version used by specta +uuid = "1.10" # Must follow version used by specta +webp = "0.3.0" +zeroize = "1.8" + +[workspace.dependencies.rspc] +git = "https://github.com/spacedriveapp/rspc.git" +rev = "6a77167495" + +[workspace.dependencies.prisma-client-rust] +default-features = false +features = ["migrations", "specta", "sqlite", "sqlite-create-many"] +git = "https://github.com/spacedriveapp/prisma-client-rust" +rev = "b22ad7dc7d" + +[workspace.dependencies.prisma-client-rust-sdk] +default-features = false +features = ["sqlite"] +git = "https://github.com/spacedriveapp/prisma-client-rust" +rev = "b22ad7dc7d" + +# Proper IOS Support +[patch.crates-io.if-watch] +git = "https://github.com/spacedriveapp/if-watch.git" +rev = "a92c17d3f8" + +# Add `Control::open_stream_with_addrs` +[patch.crates-io.libp2p] +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" +[patch.crates-io.libp2p-core] +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" +[patch.crates-io.libp2p-identity] +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" +[patch.crates-io.libp2p-swarm] +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" +[patch.crates-io.libp2p-stream] +git = "https://github.com/spacedriveapp/rust-libp2p" +rev = "1024411ffa" + +[profile.dev] +# Make compilation faster on macOS +codegen-units = 256 +debug = 0 +incremental = true +lto = false +opt-level = 0 +split-debuginfo = "unpacked" +strip = "none" + +[profile.dev-debug] +inherits = "dev" +# Enables debugger +codegen-units = 256 +debug = "full" +incremental = true +lto = "off" +opt-level = 0 +split-debuginfo = "none" +strip = "none" + +# Set the settings for build scripts and proc-macros. +[profile.dev.build-override] +opt-level = 3 + +# Set the default for dependencies, except workspace members. +[profile.dev.package."*"] +incremental = false +opt-level = 3 + +# Set the default for dependencies, except workspace members. +[profile.dev-debug.package."*"] +debug = "full" +incremental = false +inherits = "dev" +opt-level = 3 + +# Optimize release builds +[profile.release] +codegen-units = 1 # Compile crates one after another so the compiler can optimize better +lto = true # Enables link to optimizations +opt-level = "s" # Optimize for binary size +panic = "unwind" # Sadly we need unwind to avoid unexpected crashes on third party crates +strip = true # Remove debug symbols diff --git a/apps/desktop/src-tauri/Cargo.toml b/apps/desktop/src-tauri/Cargo.toml index c9c850825..e95931bf5 100644 --- a/apps/desktop/src-tauri/Cargo.toml +++ b/apps/desktop/src-tauri/Cargo.toml @@ -13,7 +13,6 @@ repository.workspace = true # Spacedrive Sub-crates sd-core = { path = "../../../core", features = ["ffmpeg", "heif"] } sd-fda = { path = "../../../crates/fda" } -sd-prisma = { path = "../../../crates/prisma" } # Workspace dependencies axum = { workspace = true, features = ["query"] } @@ -22,7 +21,6 @@ base64 = { workspace = true } futures = { workspace = true } http = { workspace = true } hyper = { workspace = true } -prisma-client-rust = { workspace = true } rand = { workspace = true } rspc = { workspace = true, features = ["tauri"] } serde = { workspace = true } diff --git a/core/Cargo.toml b/core/Cargo.toml index cbb8fc0ef..236407e3f 100644 --- a/core/Cargo.toml +++ b/core/Cargo.toml @@ -7,9 +7,13 @@ version = "0.1.0" default = [] # FFmpeg support for video thumbnails ffmpeg = ["dep:sd-ffmpeg"] +# AI models support +ai = [] +# HEIF image support +heif = [] +# Mobile platform support +mobile = [] -[workspace] -members = ["benchmarks", "crates/*"] [dependencies] # Async runtime @@ -65,13 +69,13 @@ globset = { version = "0.4", features = ["serde1"] } inventory = "0.3" # Automatic job registration rmp = "0.8" # MessagePack core types rmp-serde = "1.3" # MessagePack serialization for job state -sd-task-system = { path = "crates/task-system" } +sd-task-system = { path = "../crates/task-system" } spacedrive-jobs-derive = { path = "crates/spacedrive-jobs-derive" } # Job derive macros # Media processing dependencies image = "0.25" -sd-ffmpeg = { path = "crates/ffmpeg", optional = true } -sd-images = { path = "crates/images" } +sd-ffmpeg = { path = "../crates/ffmpeg", optional = true } +sd-images = { path = "../crates/images" } tokio-rustls = "0.26" webp = "0.3" diff --git a/core/src/shared/errors.rs b/core/src/common/errors.rs similarity index 100% rename from core/src/shared/errors.rs rename to core/src/common/errors.rs diff --git a/core/src/shared/mod.rs b/core/src/common/mod.rs similarity index 100% rename from core/src/shared/mod.rs rename to core/src/common/mod.rs diff --git a/core/src/shared/types.rs b/core/src/common/types.rs similarity index 100% rename from core/src/shared/types.rs rename to core/src/common/types.rs diff --git a/core/src/shared/utils.rs b/core/src/common/utils.rs similarity index 100% rename from core/src/shared/utils.rs rename to core/src/common/utils.rs diff --git a/core/src/context.rs b/core/src/context.rs index bb656e2a0..ecbff3e14 100644 --- a/core/src/context.rs +++ b/core/src/context.rs @@ -3,11 +3,9 @@ //! Shared context providing access to core application components. use crate::{ - config::JobLoggingConfig, - device::DeviceManager, infrastructure::events::EventBus, - keys::library_key_manager::LibraryKeyManager, library::LibraryManager, - infrastructure::actions::manager::ActionManager, - services::networking::NetworkingService, volume::VolumeManager, + config::JobLoggingConfig, device::DeviceManager, infra::actions::manager::ActionManager, + infra::events::EventBus, keys::library_key_manager::LibraryKeyManager, library::LibraryManager, + service::networking::NetworkingService, volume::VolumeManager, }; use std::{path::PathBuf, sync::Arc}; use tokio::sync::RwLock; @@ -49,7 +47,7 @@ impl CoreContext { job_logs_dir: None, } } - + /// Set job logging configuration pub fn set_job_logging(&mut self, config: JobLoggingConfig, logs_dir: PathBuf) { self.job_logging_config = Some(config); diff --git a/core/src/keys/device_key_manager.rs b/core/src/crypto/device_key_manager.rs similarity index 100% rename from core/src/keys/device_key_manager.rs rename to core/src/crypto/device_key_manager.rs diff --git a/core/src/keys/library_key_manager.rs b/core/src/crypto/library_key_manager.rs similarity index 100% rename from core/src/keys/library_key_manager.rs rename to core/src/crypto/library_key_manager.rs diff --git a/core/src/keys/mod.rs b/core/src/crypto/mod.rs similarity index 100% rename from core/src/keys/mod.rs rename to core/src/crypto/mod.rs diff --git a/core/src/domain/addressing.rs b/core/src/domain/addressing.rs index d4db27837..592ef2640 100644 --- a/core/src/domain/addressing.rs +++ b/core/src/domain/addressing.rs @@ -1,6 +1,6 @@ //! Core addressing data structures for the Virtual Distributed File System //! -//! This module contains the fundamental "nouns" of the addressing system - +//! This module contains the fundamental "nouns" of the addressing system - //! the data structures that represent paths in Spacedrive's distributed //! file system. @@ -56,7 +56,7 @@ impl SdPath { /// Create an SdPath for a local file on this device pub fn local(path: impl Into) -> Self { Self::Physical { - device_id: crate::shared::utils::get_current_device_id(), + device_id: crate::common::utils::get_current_device_id(), path: path.into(), } } @@ -64,7 +64,7 @@ impl SdPath { /// Check if this path is on the current device pub fn is_local(&self) -> bool { match self { - Self::Physical { device_id, .. } => *device_id == crate::shared::utils::get_current_device_id(), + Self::Physical { device_id, .. } => *device_id == crate::common::utils::get_current_device_id(), Self::Content { .. } => false, // Content paths are abstract, not inherently local } } @@ -73,7 +73,7 @@ impl SdPath { pub fn as_local_path(&self) -> Option<&Path> { match self { Self::Physical { device_id, path } => { - if *device_id == crate::shared::utils::get_current_device_id() { + if *device_id == crate::common::utils::get_current_device_id() { Some(path) } else { None @@ -87,7 +87,7 @@ impl SdPath { pub fn display(&self) -> String { match self { Self::Physical { device_id, path } => { - if *device_id == crate::shared::utils::get_current_device_id() { + if *device_id == crate::common::utils::get_current_device_id() { path.display().to_string() } else { format!("sd://{}/{}", device_id, path.display()) @@ -147,7 +147,7 @@ impl SdPath { Self::Content { .. } => None, // Content paths don't have volumes until resolved } } - + /// Check if this path is on the same volume as another path pub async fn same_volume(&self, other: &SdPath, volume_manager: &crate::volume::VolumeManager) -> bool { match (self, other) { @@ -155,7 +155,7 @@ impl SdPath { if !self.is_local() || !other.is_local() { return false; } - + if let (Some(self_path), Some(other_path)) = (self.as_local_path(), other.as_local_path()) { volume_manager.same_volume(self_path, other_path).await } else { @@ -174,7 +174,7 @@ impl SdPath { pub fn from_uri(uri: &str) -> Result { if uri.starts_with("sd://") { let uri = &uri[5..]; // Strip "sd://" - + if let Some(content_id_str) = uri.strip_prefix("content/") { // Parse content path let content_id = Uuid::parse_str(content_id_str) @@ -186,11 +186,11 @@ impl SdPath { if parts.len() != 2 { return Err(SdPathParseError::InvalidFormat); } - + let device_id = Uuid::parse_str(parts[0]) .map_err(|_| SdPathParseError::InvalidDeviceId)?; let path = PathBuf::from("/").join(parts[1]); - + Ok(Self::Physical { device_id, path }) } } else { @@ -252,14 +252,14 @@ impl SdPath { &self, context: &crate::context::CoreContext ) -> Result { - let resolver = crate::operations::addressing::PathResolver; + let resolver = crate::ops::addressing::PathResolver; resolver.resolve(self, context).await } - + /// Resolve this path using a JobContext pub async fn resolve_in_job<'a>( &self, - job_ctx: &crate::infrastructure::jobs::context::JobContext<'a> + job_ctx: &crate::infra::jobs::context::JobContext<'a> ) -> Result { // For now, if it's already physical, just return it // TODO: Implement proper resolution using job context's library and networking diff --git a/core/src/domain/content_identity.rs b/core/src/domain/content_identity.rs index 783dceab9..505956b5f 100644 --- a/core/src/domain/content_identity.rs +++ b/core/src/domain/content_identity.rs @@ -133,7 +133,7 @@ impl ContentKind { } /// Get content kind from file type - pub fn from_file_type(file_type: &crate::file_type::FileType) -> Self { + pub fn from_file_type(file_type: &crate::filetype::FileType) -> Self { file_type.category } } @@ -171,10 +171,10 @@ impl ContentHashGenerator { let mut hasher = Hasher::new(); hasher.update(&size.to_le_bytes()); - + let content = tokio::fs::read(path).await?; hasher.update(&content); - + Ok(hasher.finalize().to_hex()[..16].to_string()) } diff --git a/core/src/domain/device.rs b/core/src/domain/device.rs index 1941883ed..d97ec76a0 100644 --- a/core/src/domain/device.rs +++ b/core/src/domain/device.rs @@ -11,265 +11,269 @@ use uuid::Uuid; /// A device running Spacedrive #[derive(Debug, Clone, Serialize, Deserialize)] pub struct Device { - /// Unique identifier for this device - pub id: Uuid, + /// Unique identifier for this device + pub id: Uuid, - /// Human-readable name - pub name: String, + /// Human-readable name + pub name: String, - /// Operating system - pub os: OperatingSystem, + /// Operating system + pub os: OperatingSystem, - /// Hardware model (e.g., "MacBook Pro", "iPhone 15") - pub hardware_model: Option, + /// Hardware model (e.g., "MacBook Pro", "iPhone 15") + pub hardware_model: Option, - /// Network addresses for P2P connections - pub network_addresses: Vec, + /// Network addresses for P2P connections + pub network_addresses: Vec, - /// Whether this device is currently online - pub is_online: bool, + /// Whether this device is currently online + pub is_online: bool, - /// Sync leadership status per library - pub sync_leadership: HashMap, + /// Sync leadership status per library + pub sync_leadership: HashMap, - /// Last time this device was seen - pub last_seen_at: DateTime, + /// Last time this device was seen + pub last_seen_at: DateTime, - /// When this device was first added - pub created_at: DateTime, + /// When this device was first added + pub created_at: DateTime, - /// When this device info was last updated - pub updated_at: DateTime, + /// When this device info was last updated + pub updated_at: DateTime, } /// Sync role for a device in a specific library #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum SyncRole { - /// This device maintains the sync log for the library - Leader, + /// This device maintains the sync log for the library + Leader, - /// This device syncs from the leader - Follower, + /// This device syncs from the leader + Follower, - /// This device doesn't participate in sync for this library - Inactive, + /// This device doesn't participate in sync for this library + Inactive, } /// Operating system types #[derive(Debug, Clone, Copy, Serialize, Deserialize, PartialEq)] pub enum OperatingSystem { - MacOS, - Windows, - Linux, - IOs, - Android, - Other, + MacOS, + Windows, + Linux, + IOs, + Android, + Other, } impl Device { - /// Create a new device - pub fn new(name: String) -> Self { - let now = Utc::now(); - Self { - id: Uuid::new_v4(), - name, - os: detect_operating_system(), - hardware_model: detect_hardware_model(), - network_addresses: Vec::new(), - is_online: true, - sync_leadership: HashMap::new(), - last_seen_at: now, - created_at: now, - updated_at: now, - } - } + /// Create a new device + pub fn new(name: String) -> Self { + let now = Utc::now(); + Self { + id: Uuid::new_v4(), + name, + os: detect_operating_system(), + hardware_model: detect_hardware_model(), + network_addresses: Vec::new(), + is_online: true, + sync_leadership: HashMap::new(), + last_seen_at: now, + created_at: now, + updated_at: now, + } + } - /// Create the current device - pub fn current() -> Self { - Self::new(get_device_name()) - } + /// Create the current device + pub fn current() -> Self { + Self::new(get_device_name()) + } - /// Update network addresses - pub fn update_network_addresses(&mut self, addresses: Vec) { - self.network_addresses = addresses; - self.updated_at = Utc::now(); - } + /// Update network addresses + pub fn update_network_addresses(&mut self, addresses: Vec) { + self.network_addresses = addresses; + self.updated_at = Utc::now(); + } - /// Mark device as online - pub fn mark_online(&mut self) { - self.is_online = true; - self.last_seen_at = Utc::now(); - self.updated_at = Utc::now(); - } + /// Mark device as online + pub fn mark_online(&mut self) { + self.is_online = true; + self.last_seen_at = Utc::now(); + self.updated_at = Utc::now(); + } - /// Mark device as offline - pub fn mark_offline(&mut self) { - self.is_online = false; - self.updated_at = Utc::now(); - } + /// Mark device as offline + pub fn mark_offline(&mut self) { + self.is_online = false; + self.updated_at = Utc::now(); + } - /// Check if this is the current device - pub fn is_current(&self) -> bool { - self.id == crate::shared::utils::get_current_device_id() - } + /// Check if this is the current device + pub fn is_current(&self) -> bool { + self.id == crate::common::utils::get_current_device_id() + } - /// Set sync role for a library - pub fn set_sync_role(&mut self, library_id: Uuid, role: SyncRole) { - self.sync_leadership.insert(library_id, role); - self.updated_at = Utc::now(); - } + /// Set sync role for a library + pub fn set_sync_role(&mut self, library_id: Uuid, role: SyncRole) { + self.sync_leadership.insert(library_id, role); + self.updated_at = Utc::now(); + } - /// Get sync role for a library - pub fn sync_role(&self, library_id: &Uuid) -> SyncRole { - self.sync_leadership.get(library_id).copied().unwrap_or(SyncRole::Inactive) - } + /// Get sync role for a library + pub fn sync_role(&self, library_id: &Uuid) -> SyncRole { + self.sync_leadership + .get(library_id) + .copied() + .unwrap_or(SyncRole::Inactive) + } - /// Check if this device is the sync leader for a library - pub fn is_sync_leader(&self, library_id: &Uuid) -> bool { - matches!(self.sync_role(library_id), SyncRole::Leader) - } + /// Check if this device is the sync leader for a library + pub fn is_sync_leader(&self, library_id: &Uuid) -> bool { + matches!(self.sync_role(library_id), SyncRole::Leader) + } - /// Get all libraries where this device is the leader - pub fn leader_libraries(&self) -> Vec { - self.sync_leadership - .iter() - .filter_map(|(lib_id, role)| { - if *role == SyncRole::Leader { - Some(*lib_id) - } else { - None - } - }) - .collect() - } + /// Get all libraries where this device is the leader + pub fn leader_libraries(&self) -> Vec { + self.sync_leadership + .iter() + .filter_map(|(lib_id, role)| { + if *role == SyncRole::Leader { + Some(*lib_id) + } else { + None + } + }) + .collect() + } } /// Get the device name from the system fn get_device_name() -> String { - #[cfg(target_os = "macos")] - { - return whoami::devicename(); - } + #[cfg(target_os = "macos")] + { + return whoami::devicename(); + } - #[cfg(any(target_os = "windows", target_os = "linux"))] - { - if let Ok(name) = hostname::get() { - if let Ok(name_str) = name.into_string() { - return name_str; - } - } - } + #[cfg(any(target_os = "windows", target_os = "linux"))] + { + if let Ok(name) = hostname::get() { + if let Ok(name_str) = name.into_string() { + return name_str; + } + } + } - "Unknown Device".to_string() + "Unknown Device".to_string() } /// Detect the operating system fn detect_operating_system() -> OperatingSystem { - #[cfg(target_os = "macos")] - return OperatingSystem::MacOS; + #[cfg(target_os = "macos")] + return OperatingSystem::MacOS; - #[cfg(target_os = "windows")] - return OperatingSystem::Windows; + #[cfg(target_os = "windows")] + return OperatingSystem::Windows; - #[cfg(target_os = "linux")] - return OperatingSystem::Linux; + #[cfg(target_os = "linux")] + return OperatingSystem::Linux; - #[cfg(target_os = "ios")] - return OperatingSystem::IOs; + #[cfg(target_os = "ios")] + return OperatingSystem::IOs; - #[cfg(target_os = "android")] - return OperatingSystem::Android; + #[cfg(target_os = "android")] + return OperatingSystem::Android; - #[cfg(not(any( - target_os = "macos", - target_os = "windows", - target_os = "linux", - target_os = "ios", - target_os = "android" - )))] - return OperatingSystem::Other; + #[cfg(not(any( + target_os = "macos", + target_os = "windows", + target_os = "linux", + target_os = "ios", + target_os = "android" + )))] + return OperatingSystem::Other; } /// Get hardware model information fn detect_hardware_model() -> Option { - // This would use platform-specific APIs - // For now, return None - None + // This would use platform-specific APIs + // For now, return None + None } impl std::fmt::Display for OperatingSystem { - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - OperatingSystem::MacOS => write!(f, "macOS"), - OperatingSystem::Windows => write!(f, "Windows"), - OperatingSystem::Linux => write!(f, "Linux"), - OperatingSystem::IOs => write!(f, "iOS"), - OperatingSystem::Android => write!(f, "Android"), - OperatingSystem::Other => write!(f, "Other"), - } - } + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + match self { + OperatingSystem::MacOS => write!(f, "macOS"), + OperatingSystem::Windows => write!(f, "Windows"), + OperatingSystem::Linux => write!(f, "Linux"), + OperatingSystem::IOs => write!(f, "iOS"), + OperatingSystem::Android => write!(f, "Android"), + OperatingSystem::Other => write!(f, "Other"), + } + } } // Conversion implementations for database entities -use crate::infrastructure::database::entities; +use crate::infra::database::entities; use sea_orm::ActiveValue; impl From for entities::device::ActiveModel { - fn from(device: Device) -> Self { - use sea_orm::ActiveValue::*; + fn from(device: Device) -> Self { + use sea_orm::ActiveValue::*; - entities::device::ActiveModel { - id: NotSet, // Auto-increment - uuid: Set(device.id), - name: Set(device.name), - os: Set(device.os.to_string()), - os_version: Set(None), // TODO: Add to domain model if needed - hardware_model: Set(device.hardware_model), - network_addresses: Set(serde_json::json!(device.network_addresses)), - is_online: Set(device.is_online), - last_seen_at: Set(device.last_seen_at), - capabilities: Set(serde_json::json!({ - "indexing": true, - "p2p": true, - "volume_detection": true - })), - sync_leadership: Set(serde_json::json!(device.sync_leadership)), - created_at: Set(device.created_at), - updated_at: Set(device.updated_at), - } - } + entities::device::ActiveModel { + id: NotSet, // Auto-increment + uuid: Set(device.id), + name: Set(device.name), + os: Set(device.os.to_string()), + os_version: Set(None), // TODO: Add to domain model if needed + hardware_model: Set(device.hardware_model), + network_addresses: Set(serde_json::json!(device.network_addresses)), + is_online: Set(device.is_online), + last_seen_at: Set(device.last_seen_at), + capabilities: Set(serde_json::json!({ + "indexing": true, + "p2p": true, + "volume_detection": true + })), + sync_leadership: Set(serde_json::json!(device.sync_leadership)), + created_at: Set(device.created_at), + updated_at: Set(device.updated_at), + } + } } impl TryFrom for Device { - type Error = serde_json::Error; + type Error = serde_json::Error; - fn try_from(model: entities::device::Model) -> Result { - let network_addresses: Vec = serde_json::from_value(model.network_addresses)?; - let sync_leadership: HashMap = serde_json::from_value(model.sync_leadership)?; + fn try_from(model: entities::device::Model) -> Result { + let network_addresses: Vec = serde_json::from_value(model.network_addresses)?; + let sync_leadership: HashMap = + serde_json::from_value(model.sync_leadership)?; - Ok(Device { - id: model.uuid, - name: model.name, - os: parse_operating_system(&model.os), - hardware_model: model.hardware_model, - network_addresses, - is_online: model.is_online, - sync_leadership, - last_seen_at: model.last_seen_at, - created_at: model.created_at, - updated_at: model.updated_at, - }) - } + Ok(Device { + id: model.uuid, + name: model.name, + os: parse_operating_system(&model.os), + hardware_model: model.hardware_model, + network_addresses, + is_online: model.is_online, + sync_leadership, + last_seen_at: model.last_seen_at, + created_at: model.created_at, + updated_at: model.updated_at, + }) + } } /// Parse OS string to enum fn parse_operating_system(os_str: &str) -> OperatingSystem { - match os_str { - "macOS" => OperatingSystem::MacOS, - "Windows" => OperatingSystem::Windows, - "Linux" => OperatingSystem::Linux, - "iOS" => OperatingSystem::IOs, - "Android" => OperatingSystem::Android, - _ => OperatingSystem::Other, - } -} \ No newline at end of file + match os_str { + "macOS" => OperatingSystem::MacOS, + "Windows" => OperatingSystem::Windows, + "Linux" => OperatingSystem::Linux, + "iOS" => OperatingSystem::IOs, + "Android" => OperatingSystem::Android, + _ => OperatingSystem::Other, + } +} diff --git a/core/src/file_type/builtin.rs b/core/src/filetype/builtin.rs similarity index 100% rename from core/src/file_type/builtin.rs rename to core/src/filetype/builtin.rs diff --git a/core/src/file_type/definitions/archives.toml b/core/src/filetype/definitions/archives.toml similarity index 100% rename from core/src/file_type/definitions/archives.toml rename to core/src/filetype/definitions/archives.toml diff --git a/core/src/file_type/definitions/audio.toml b/core/src/filetype/definitions/audio.toml similarity index 100% rename from core/src/file_type/definitions/audio.toml rename to core/src/filetype/definitions/audio.toml diff --git a/core/src/file_type/definitions/code.toml b/core/src/filetype/definitions/code.toml similarity index 100% rename from core/src/file_type/definitions/code.toml rename to core/src/filetype/definitions/code.toml diff --git a/core/src/file_type/definitions/documents.toml b/core/src/filetype/definitions/documents.toml similarity index 100% rename from core/src/file_type/definitions/documents.toml rename to core/src/filetype/definitions/documents.toml diff --git a/core/src/file_type/definitions/images.toml b/core/src/filetype/definitions/images.toml similarity index 100% rename from core/src/file_type/definitions/images.toml rename to core/src/filetype/definitions/images.toml diff --git a/core/src/file_type/definitions/misc.toml b/core/src/filetype/definitions/misc.toml similarity index 100% rename from core/src/file_type/definitions/misc.toml rename to core/src/filetype/definitions/misc.toml diff --git a/core/src/file_type/definitions/video.toml b/core/src/filetype/definitions/video.toml similarity index 100% rename from core/src/file_type/definitions/video.toml rename to core/src/filetype/definitions/video.toml diff --git a/core/src/file_type/magic.rs b/core/src/filetype/magic.rs similarity index 100% rename from core/src/file_type/magic.rs rename to core/src/filetype/magic.rs diff --git a/core/src/file_type/mod.rs b/core/src/filetype/mod.rs similarity index 100% rename from core/src/file_type/mod.rs rename to core/src/filetype/mod.rs diff --git a/core/src/file_type/registry.rs b/core/src/filetype/registry.rs similarity index 97% rename from core/src/file_type/registry.rs rename to core/src/filetype/registry.rs index 7c9c722b0..4b05678c8 100644 --- a/core/src/file_type/registry.rs +++ b/core/src/filetype/registry.rs @@ -2,7 +2,7 @@ use super::{FileType, FileTypeError, IdentificationMethod, IdentificationResult, Result}; use crate::domain::ContentKind; -use crate::file_type::magic::MagicBytePattern; +use crate::filetype::magic::MagicBytePattern; use serde::Deserialize; use std::collections::HashMap; use std::path::Path; @@ -50,10 +50,10 @@ struct MagicByteDefinition { pub struct FileTypeRegistry { /// All registered file types by ID types: HashMap, - + /// Extension to type IDs mapping extension_map: HashMap>, - + /// MIME type to type ID mapping mime_map: HashMap, } @@ -66,18 +66,18 @@ impl FileTypeRegistry { extension_map: HashMap::new(), mime_map: HashMap::new(), }; - + // Load built-in types registry.load_builtin_types(); - + registry } - + /// Load built-in file type definitions fn load_builtin_types(&mut self) { // Load all TOML definitions from the builtin module let toml_definitions = super::builtin::get_builtin_toml_definitions(); - + for toml_content in toml_definitions { // Use the loader to parse TOML if let Err(e) = self.load_from_toml(toml_content) { @@ -85,12 +85,12 @@ impl FileTypeRegistry { } } } - + /// Register a file type pub fn register(&mut self, file_type: FileType) -> Result<()> { // Add to main registry let id = file_type.id.clone(); - + // Update extension map for ext in &file_type.extensions { self.extension_map @@ -98,26 +98,26 @@ impl FileTypeRegistry { .or_insert_with(Vec::new) .push(id.clone()); } - + // Update MIME map for mime in &file_type.mime_types { self.mime_map.insert(mime.clone(), id.clone()); } - + self.types.insert(id, file_type); - + Ok(()) } - + /// Get a file type by ID pub fn get(&self, id: &str) -> Option<&FileType> { self.types.get(id) } - + /// Get file types by extension pub fn get_by_extension(&self, ext: &str) -> Vec<&FileType> { let ext = ext.trim_start_matches('.').to_lowercase(); - + self.extension_map .get(&ext) .map(|ids| { @@ -127,14 +127,14 @@ impl FileTypeRegistry { }) .unwrap_or_default() } - + /// Get file type by MIME type pub fn get_by_mime(&self, mime: &str) -> Option<&FileType> { self.mime_map .get(mime) .and_then(|id| self.types.get(id)) } - + /// Identify a file type from a path pub async fn identify(&self, path: &Path) -> Result { // Get extension @@ -142,10 +142,10 @@ impl FileTypeRegistry { .extension() .and_then(|s| s.to_str()) .unwrap_or(""); - + // Get possible types by extension let candidates = self.get_by_extension(extension); - + match candidates.len() { 0 => { // No extension match, try magic bytes on all types @@ -183,7 +183,7 @@ impl FileTypeRegistry { } } } - + /// Identify by magic bytes from a set of candidates async fn identify_by_magic_bytes( &self, @@ -195,10 +195,10 @@ impl FileTypeRegistry { let mut buffer = vec![0u8; MAX_MAGIC_BYTES]; let bytes_read = file.read(&mut buffer).await?; buffer.truncate(bytes_read); - + // Check each candidate let mut matches: Vec<(&FileType, u8)> = Vec::new(); - + for candidate in candidates { for pattern in &candidate.magic_bytes { if pattern.matches(&buffer) { @@ -207,10 +207,10 @@ impl FileTypeRegistry { } } } - + // Sort by priority (highest first) matches.sort_by_key(|(_, priority)| std::cmp::Reverse(*priority)); - + if let Some((file_type, _)) = matches.first() { Ok(IdentificationResult { file_type: (*file_type).clone(), @@ -228,21 +228,21 @@ impl FileTypeRegistry { } } } - + /// Check if a specific file type's magic bytes match async fn check_magic_bytes(&self, path: &Path, file_type: &FileType) -> Result { if file_type.magic_bytes.is_empty() { return Ok(true); } - + let mut file = File::open(path).await?; let mut buffer = vec![0u8; MAX_MAGIC_BYTES]; let bytes_read = file.read(&mut buffer).await?; buffer.truncate(bytes_read); - + Ok(file_type.magic_bytes.iter().any(|pattern| pattern.matches(&buffer))) } - + /// Identify by content analysis (for text files) async fn identify_by_content( &self, @@ -254,7 +254,7 @@ impl FileTypeRegistry { let mut buffer = vec![0u8; MAX_CONTENT_BYTES]; let bytes_read = file.read(&mut buffer).await?; buffer.truncate(bytes_read); - + // Try to convert to string if let Ok(content) = String::from_utf8(buffer) { // Simple heuristics for now @@ -269,7 +269,7 @@ impl FileTypeRegistry { } } } - + // Default to first text candidate if let Some(text_type) = candidates.iter().find(|ft| { matches!(ft.category, ContentKind::Text | ContentKind::Code) @@ -283,20 +283,20 @@ impl FileTypeRegistry { Err(FileTypeError::UnknownType) } } - + /// Load definitions from a TOML string pub fn load_from_toml(&mut self, content: &str) -> Result<()> { let defs: FileTypeDefinitions = toml::from_str(content) .map_err(|e| FileTypeError::InvalidConfig(format!("TOML parse error: {}", e)))?; - + for def in defs.file_types { let file_type = self.definition_to_file_type(def)?; self.register(file_type)?; } - + Ok(()) } - + /// Convert a definition to a FileType fn definition_to_file_type(&self, def: FileTypeDefinition) -> Result { // Parse category @@ -318,7 +318,7 @@ impl FileTypeRegistry { "key" => ContentKind::Key, _ => ContentKind::Unknown, }; - + // Parse magic bytes let mut magic_bytes = Vec::new(); for mb_def in def.magic_bytes { @@ -329,7 +329,7 @@ impl FileTypeRegistry { ).map_err(|e| FileTypeError::InvalidConfig(format!("Invalid magic bytes: {}", e)))?; magic_bytes.push(pattern); } - + Ok(FileType { id: def.id, name: def.name, @@ -353,21 +353,21 @@ impl Default for FileTypeRegistry { #[cfg(test)] mod tests { use super::*; - + #[tokio::test] async fn test_registry_basic() { let registry = FileTypeRegistry::new(); - + // Test getting by extension let jpeg_types = registry.get_by_extension("jpg"); assert_eq!(jpeg_types.len(), 1); assert_eq!(jpeg_types[0].id, "image/jpeg"); - + // Test getting by MIME let png_type = registry.get_by_mime("image/png"); assert!(png_type.is_some()); assert_eq!(png_type.unwrap().id, "image/png"); - + // Test extension conflict let ts_types = registry.get_by_extension("ts"); assert_eq!(ts_types.len(), 2); // TypeScript and MPEG-TS diff --git a/core/src/infrastructure/actions/BUILDER_REFACTOR_PLAN.md b/core/src/infra/action/BUILDER_REFACTOR_PLAN.md similarity index 100% rename from core/src/infrastructure/actions/BUILDER_REFACTOR_PLAN.md rename to core/src/infra/action/BUILDER_REFACTOR_PLAN.md diff --git a/core/src/infrastructure/actions/builder.rs b/core/src/infra/action/builder.rs similarity index 100% rename from core/src/infrastructure/actions/builder.rs rename to core/src/infra/action/builder.rs diff --git a/core/src/infrastructure/actions/error.rs b/core/src/infra/action/error.rs similarity index 95% rename from core/src/infrastructure/actions/error.rs rename to core/src/infra/action/error.rs index 724de044a..068a4caf0 100644 --- a/core/src/infrastructure/actions/error.rs +++ b/core/src/infra/action/error.rs @@ -1,9 +1,9 @@ //! Error types for the Action System use crate::{ - infrastructure::jobs::error::JobError, + infra::jobs::error::JobError, library::LibraryError, - shared::errors::CoreError, + common::errors::CoreError, }; use serde::{Deserialize, Serialize}; use thiserror::Error; @@ -18,83 +18,83 @@ pub enum ActionError { /// Action type not registered in the registry #[error("Action type '{0}' is not registered")] ActionNotRegistered(String), - + /// Invalid action type for the handler #[error("Invalid action type for this handler")] InvalidActionType, - + /// Invalid input provided to action #[error("Invalid input: {0}")] InvalidInput(String), - + /// Permission denied for this action #[error("Permission denied for action '{action}': {reason}")] PermissionDenied { action: String, reason: String, }, - + /// Library not found #[error("Library {0} not found")] LibraryNotFound(Uuid), - + /// Location not found #[error("Location {0} not found")] LocationNotFound(Uuid), - + /// Device not found #[error("Device {0} not found")] DeviceNotFound(Uuid), - + /// File system error #[error("File system error at '{path}': {error}")] FileSystem { path: String, error: String, }, - + /// Network error for cross-device operations #[error("Network error with device {device_id}: {error}")] Network { device_id: Uuid, error: String, }, - + /// Job creation or execution error #[error("Job error: {0}")] Job(#[from] JobError), - + /// Database operation error #[error("Database error: {0}")] Database(String), - + /// Validation error #[error("Validation error for field '{field}': {message}")] Validation { field: String, message: String, }, - + /// Action execution timeout #[error("Action execution timed out")] Timeout, - + /// Action was cancelled #[error("Action was cancelled")] Cancelled, - + /// Device manager error #[error("Device manager error: {0}")] DeviceManager(String), - + /// JSON serialization error #[error("JSON serialization error: {0}")] JsonSerialization(#[from] serde_json::Error), - + /// Sea-ORM database error #[error("Database operation failed: {0}")] SeaOrm(#[from] sea_orm::DbErr), - + /// IO error #[error("IO error at '{path}': {source}")] Io { @@ -102,7 +102,7 @@ pub enum ActionError { #[source] source: std::io::Error, }, - + /// Generic internal error #[error("Internal error: {0}")] Internal(String), @@ -140,7 +140,7 @@ impl ActionError { source: error, } } - + pub fn device_manager_error(error: impl std::fmt::Display) -> Self { Self::DeviceManager(error.to_string()) } diff --git a/core/src/infrastructure/actions/handler.rs b/core/src/infra/action/handler.rs similarity index 100% rename from core/src/infrastructure/actions/handler.rs rename to core/src/infra/action/handler.rs diff --git a/core/src/infrastructure/actions/manager.rs b/core/src/infra/action/manager.rs similarity index 97% rename from core/src/infrastructure/actions/manager.rs rename to core/src/infra/action/manager.rs index 699c3bf63..37b2798a4 100644 --- a/core/src/infrastructure/actions/manager.rs +++ b/core/src/infra/action/manager.rs @@ -5,8 +5,8 @@ use super::{ }; use crate::{ context::CoreContext, - infrastructure::database::entities::{audit_log, AuditLog, AuditLogActive}, - shared::utils::get_current_device_id, + infra::database::entities::{audit_log, AuditLog, AuditLogActive}, + common::utils::get_current_device_id, }; use sea_orm::{ActiveModelTrait, ColumnTrait, DatabaseConnection, EntityTrait, QueryFilter, QuerySelect, Set}; use std::sync::Arc; @@ -63,7 +63,7 @@ impl ActionManager { ) -> ActionResult { let library = self.get_library(library_id).await?; let db = library.db().conn(); - + let audit_entry = AuditLogActive { uuid: Set(Uuid::new_v4().to_string()), action_type: Set(action.kind().to_string()), @@ -108,7 +108,7 @@ impl ActionManager { // Convert to active model and explicitly mark changed fields let mut active_model: AuditLogActive = entry.into(); - + // Explicitly mark the fields we want to update as "Set" (changed) match result { Ok(output) => { @@ -124,7 +124,7 @@ impl ActionManager { active_model.error_message = Set(Some(error.to_string())); } } - + active_model .update(db) .await @@ -152,13 +152,13 @@ impl ActionManager { ) -> ActionResult> { let library = self.get_library(library_id).await?; let db = library.db().conn(); - + let mut query = AuditLog::find(); - + if let Some(limit) = limit { query = query.limit(limit); } - + if let Some(offset) = offset { query = query.offset(offset); } @@ -177,7 +177,7 @@ impl ActionManager { ) -> ActionResult> { let library = self.get_library(library_id).await?; let db = library.db().conn(); - + AuditLog::find() .filter(audit_log::Column::Uuid.eq(action_uuid)) .one(db) diff --git a/core/src/infrastructure/actions/mod.rs b/core/src/infra/action/mod.rs similarity index 100% rename from core/src/infrastructure/actions/mod.rs rename to core/src/infra/action/mod.rs diff --git a/core/src/infrastructure/actions/output.rs b/core/src/infra/action/output.rs similarity index 100% rename from core/src/infrastructure/actions/output.rs rename to core/src/infra/action/output.rs diff --git a/core/src/infrastructure/actions/receipt.rs b/core/src/infra/action/receipt.rs similarity index 95% rename from core/src/infrastructure/actions/receipt.rs rename to core/src/infra/action/receipt.rs index 30d8d85b0..9519a305d 100644 --- a/core/src/infrastructure/actions/receipt.rs +++ b/core/src/infra/action/receipt.rs @@ -1,6 +1,6 @@ //! Action execution receipts -use crate::infrastructure::jobs::handle::JobHandle; +use crate::infra::jobs::handle::JobHandle; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -9,14 +9,14 @@ use uuid::Uuid; pub struct ActionReceipt { /// Unique identifier for the action execution pub action_id: Uuid, - + /// Optional job handle if the action created a background job #[serde(skip)] pub job_handle: Option, - + /// Optional result payload (for immediate actions) pub result_payload: Option, - + /// Whether the action completed immediately or is running in background pub is_immediate: bool, } @@ -31,7 +31,7 @@ impl ActionReceipt { is_immediate: true, } } - + /// Create a new receipt for a job-based action pub fn job_based(action_id: Uuid, job_handle: JobHandle) -> Self { Self { @@ -41,7 +41,7 @@ impl ActionReceipt { is_immediate: false, } } - + /// Create a new receipt for a hybrid action (immediate with optional job) pub fn hybrid( action_id: Uuid, diff --git a/core/src/infrastructure/actions/registry.rs b/core/src/infra/action/registry.rs similarity index 100% rename from core/src/infrastructure/actions/registry.rs rename to core/src/infra/action/registry.rs diff --git a/core/src/infrastructure/actions/tests.rs b/core/src/infra/action/tests.rs similarity index 96% rename from core/src/infrastructure/actions/tests.rs rename to core/src/infra/action/tests.rs index e27b51759..53710eda1 100644 --- a/core/src/infrastructure/actions/tests.rs +++ b/core/src/infra/action/tests.rs @@ -5,7 +5,7 @@ mod tests { use super::*; use crate::{ context::CoreContext, - infrastructure::actions::{Action, registry::REGISTRY}, + infra::actions::{Action, registry::REGISTRY}, }; #[test] @@ -53,7 +53,7 @@ mod tests { assert!(REGISTRY.has_action("location.add")); assert!(REGISTRY.has_action("location.remove")); assert!(REGISTRY.has_action("location.index")); - + // Test that unknown actions are not registered assert!(!REGISTRY.has_action("unknown.action")); } @@ -62,7 +62,7 @@ mod tests { fn test_action_registry_get_handler() { let handler = REGISTRY.get("library.create"); assert!(handler.is_some()); - + let handler = REGISTRY.get("unknown.action"); assert!(handler.is_none()); } diff --git a/core/src/infrastructure/cli/README.md b/core/src/infra/cli/README.md similarity index 100% rename from core/src/infrastructure/cli/README.md rename to core/src/infra/cli/README.md diff --git a/core/src/infrastructure/cli/adapters/copy.rs b/core/src/infra/cli/adapters/copy.rs similarity index 100% rename from core/src/infrastructure/cli/adapters/copy.rs rename to core/src/infra/cli/adapters/copy.rs diff --git a/core/src/infrastructure/cli/adapters/mod.rs b/core/src/infra/cli/adapters/mod.rs similarity index 100% rename from core/src/infrastructure/cli/adapters/mod.rs rename to core/src/infra/cli/adapters/mod.rs diff --git a/core/src/infrastructure/cli/commands/daemon.rs b/core/src/infra/cli/commands/daemon.rs similarity index 98% rename from core/src/infrastructure/cli/commands/daemon.rs rename to core/src/infra/cli/commands/daemon.rs index 860376be4..1f6f16e58 100644 --- a/core/src/infrastructure/cli/commands/daemon.rs +++ b/core/src/infra/cli/commands/daemon.rs @@ -5,11 +5,11 @@ //! - Checking daemon status //! - Managing multiple daemon instances -use crate::infrastructure::cli::daemon::{ +use crate::infra::cli::daemon::{ Daemon, DaemonClient, DaemonCommand, DaemonConfig, DaemonResponse, }; -use crate::infrastructure::cli::output::messages::LibraryInfo as OutputLibraryInfo; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::output::messages::LibraryInfo as OutputLibraryInfo; +use crate::infra::cli::output::{CliOutput, Message}; use clap::Subcommand; use comfy_table::Table; use std::path::PathBuf; @@ -215,11 +215,11 @@ async fn handle_stop_daemon( output.print(Message::DaemonStopped { instance: instance_display.to_string(), })?; - + // If reset flag is set, remove the data directory if reset { output.warning("Removing all Spacedrive data...")?; - + if data_dir.exists() { match std::fs::remove_dir_all(&data_dir) { Ok(_) => { diff --git a/core/src/infrastructure/cli/commands/file.rs b/core/src/infra/cli/commands/file.rs similarity index 96% rename from core/src/infrastructure/cli/commands/file.rs rename to core/src/infra/cli/commands/file.rs index 3d4e514fa..9f05f52a4 100644 --- a/core/src/infrastructure/cli/commands/file.rs +++ b/core/src/infra/cli/commands/file.rs @@ -5,9 +5,9 @@ //! - Indexing operations with enhanced scope options //! - Legacy scanning operations -use crate::infrastructure::cli::adapters::FileCopyCliArgs; -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::adapters::FileCopyCliArgs; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; +use crate::infra::cli::output::{CliOutput, Message}; use clap::{Subcommand, ValueEnum}; use std::path::PathBuf; @@ -129,7 +129,11 @@ async fn handle_copy_command( // Send copy command to daemon match client .send_command(DaemonCommand::Copy { - sources: input.sources.iter().map(|p| p.display().to_string()).collect(), + sources: input + .sources + .iter() + .map(|p| p.display().to_string()) + .collect(), destination: input.destination.display().to_string(), overwrite: input.overwrite, verify: input.verify_checksum, diff --git a/core/src/infrastructure/cli/commands/job.rs b/core/src/infra/cli/commands/job.rs similarity index 96% rename from core/src/infrastructure/cli/commands/job.rs rename to core/src/infra/cli/commands/job.rs index 992244a49..1c46ddfad 100644 --- a/core/src/infrastructure/cli/commands/job.rs +++ b/core/src/infra/cli/commands/job.rs @@ -5,12 +5,10 @@ //! - Getting detailed job information //! - Monitoring job progress in real-time -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; -use crate::infrastructure::cli::output::messages::{ - JobInfo as OutputJobInfo, JobStatus as OutputJobStatus, -}; -use crate::infrastructure::cli::output::{CliOutput, Message}; -use crate::infrastructure::cli::utils::{format_bytes, progress_styles}; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; +use crate::infra::cli::output::messages::{JobInfo as OutputJobInfo, JobStatus as OutputJobStatus}; +use crate::infra::cli::output::{CliOutput, Message}; +use crate::infra::cli::utils::{format_bytes, progress_styles}; use clap::Subcommand; use comfy_table::Table; use indicatif::{MultiProgress, ProgressBar}; @@ -102,7 +100,7 @@ pub async fn handle_job_command( .map(|job| { let status = job .status - .parse::() + .parse::() .map(OutputJobStatus::from) .unwrap_or(OutputJobStatus::Queued); @@ -119,7 +117,7 @@ pub async fn handle_job_command( if matches!( output.format(), - crate::infrastructure::cli::output::OutputFormat::Json + crate::infra::cli::output::OutputFormat::Json ) { output.print(Message::JobList { jobs: output_jobs })?; } else { diff --git a/core/src/infrastructure/cli/commands/library.rs b/core/src/infra/cli/commands/library.rs similarity index 95% rename from core/src/infrastructure/cli/commands/library.rs rename to core/src/infra/cli/commands/library.rs index 7b4c517eb..adbef6d6f 100644 --- a/core/src/infrastructure/cli/commands/library.rs +++ b/core/src/infra/cli/commands/library.rs @@ -6,9 +6,9 @@ //! - Switching between libraries //! - Getting current library info -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; -use crate::infrastructure::cli::output::messages::LibraryInfo as OutputLibraryInfo; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; +use crate::infra::cli::output::messages::LibraryInfo as OutputLibraryInfo; +use crate::infra::cli::output::{CliOutput, Message}; use clap::Subcommand; use comfy_table::Table; use std::path::PathBuf; @@ -123,7 +123,7 @@ pub async fn handle_library_command( if detailed || matches!( output.format(), - crate::infrastructure::cli::output::OutputFormat::Json + crate::infra::cli::output::OutputFormat::Json ) { output.print(Message::LibraryList { libraries: output_libs, diff --git a/core/src/infrastructure/cli/commands/location.rs b/core/src/infra/cli/commands/location.rs similarity index 96% rename from core/src/infrastructure/cli/commands/location.rs rename to core/src/infra/cli/commands/location.rs index 942a7d6b0..44581e277 100644 --- a/core/src/infrastructure/cli/commands/location.rs +++ b/core/src/infra/cli/commands/location.rs @@ -6,9 +6,9 @@ //! - Removing locations //! - Rescanning locations for changes -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; -use crate::infrastructure::cli::output::messages::LocationInfo as OutputLocationInfo; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; +use crate::infra::cli::output::messages::LocationInfo as OutputLocationInfo; +use crate::infra::cli::output::{CliOutput, Message}; use clap::{Subcommand, ValueEnum}; use comfy_table::Table; use std::path::PathBuf; @@ -84,7 +84,7 @@ pub async fn handle_location_command( name, }) .await; - + match response { Ok(DaemonResponse::LocationAdded { location_id, @@ -177,7 +177,7 @@ pub async fn handle_location_command( if matches!( output.format(), - crate::infrastructure::cli::output::OutputFormat::Json + crate::infra::cli::output::OutputFormat::Json ) { output.print(Message::LocationList { locations: output_locations, diff --git a/core/src/infrastructure/cli/commands/mod.rs b/core/src/infra/cli/commands/mod.rs similarity index 100% rename from core/src/infrastructure/cli/commands/mod.rs rename to core/src/infra/cli/commands/mod.rs diff --git a/core/src/infrastructure/cli/commands/network.rs b/core/src/infra/cli/commands/network.rs similarity index 97% rename from core/src/infrastructure/cli/commands/network.rs rename to core/src/infra/cli/commands/network.rs index 6ec68db11..7b493dc58 100644 --- a/core/src/infrastructure/cli/commands/network.rs +++ b/core/src/infra/cli/commands/network.rs @@ -6,11 +6,11 @@ //! - Pairing operations with other devices //! - Spacedrop file sharing -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; -use crate::infrastructure::cli::output::{CliOutput, Message}; -use crate::infrastructure::cli::output::messages::{DeviceInfo as OutputDeviceInfo, DeviceStatus, PairingRequest}; -use crate::infrastructure::cli::utils::format_bytes_parts; -use crate::services::networking::DeviceInfo; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; +use crate::infra::cli::output::{CliOutput, Message}; +use crate::infra::cli::output::messages::{DeviceInfo as OutputDeviceInfo, DeviceStatus, PairingRequest}; +use crate::infra::cli::utils::format_bytes_parts; +use crate::service::networking::DeviceInfo; use clap::Subcommand; use comfy_table::{presets::UTF8_FULL, Table}; use std::path::PathBuf; @@ -24,7 +24,7 @@ pub enum NetworkCommands { /// Start networking services Start, - /// Stop networking services + /// Stop networking services Stop, /// List discovered devices @@ -184,8 +184,8 @@ pub async fn handle_network_command( peer_id: None, // TODO: Get from daemon if available }) .collect(); - - if matches!(output.format(), crate::infrastructure::cli::output::OutputFormat::Json) { + + if matches!(output.format(), crate::infra::cli::output::OutputFormat::Json) { output.print(Message::DevicesList { devices: output_devices })?; } else { // For human output, use a table @@ -226,7 +226,7 @@ pub async fn handle_network_command( return Ok(()); } }; - + match client .send_command(DaemonCommand::RevokeDevice { device_id: device_uuid }) .await? @@ -257,10 +257,10 @@ pub async fn handle_network_command( return Ok(()); } }; - + // Use sender name or default let sender_name = sender.unwrap_or_else(|| "Anonymous".to_string()); - + match client .send_command(DaemonCommand::SendSpacedrop { device_id: device_uuid, @@ -313,7 +313,7 @@ async fn handle_pairing_command( expires_in_seconds, } => { output.print(Message::PairingCodeGenerated { code: code.clone() })?; - + output.section() .empty_line() .text(&format!("This code expires in {} seconds", expires_in_seconds)) @@ -345,8 +345,8 @@ async fn handle_pairing_command( .await? { DaemonResponse::PairingInProgress => { - output.print(Message::PairingInProgress { - device_name: "remote device".to_string() + output.print(Message::PairingInProgress { + device_name: "remote device".to_string() })?; output.info("Pairing process started - this may take a few moments...")?; @@ -465,9 +465,9 @@ async fn handle_pairing_command( let section = output.section() .title("Current Pairing Status") .item("Status", &status); - + let section = if let Some(device) = remote_device { - section.item("Connected Device", &format!("{} ({})", + section.item("Connected Device", &format!("{} ({})", device.device_name, &device.device_id.to_string()[..8] )) @@ -506,7 +506,7 @@ async fn handle_pairing_command( timestamp: 0, // TODO: Parse from received_at string }) .collect(); - + output.print(Message::PairingStatus { status: "active".to_string(), pending_requests, @@ -577,7 +577,7 @@ async fn handle_pairing_command( return Ok(()); } }; - + match client .send_command(DaemonCommand::AcceptPairing { request_id: request_uuid }) .await? @@ -602,7 +602,7 @@ async fn handle_pairing_command( return Ok(()); } }; - + match client .send_command(DaemonCommand::RejectPairing { request_id: request_uuid }) .await? diff --git a/core/src/infrastructure/cli/commands/system.rs b/core/src/infra/cli/commands/system.rs similarity index 97% rename from core/src/infrastructure/cli/commands/system.rs rename to core/src/infra/cli/commands/system.rs index b26589a68..c5311cf70 100644 --- a/core/src/infrastructure/cli/commands/system.rs +++ b/core/src/infra/cli/commands/system.rs @@ -5,8 +5,8 @@ //! - Log viewing and following //! - Real-time monitoring -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonConfig, DaemonResponse}; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonConfig, DaemonResponse}; +use crate::infra::cli::output::{CliOutput, Message}; use clap::Subcommand; use std::fs::File; use std::io::{BufRead, BufReader, Seek, SeekFrom}; @@ -30,7 +30,7 @@ pub enum SystemCommands { /// Monitor all system activity in real-time Monitor, - + /// Launch Terminal User Interface for real-time monitoring Tui, } @@ -61,20 +61,20 @@ async fn handle_status_command( output: &mut CliOutput, ) -> Result<(), Box> { let mut client = DaemonClient::new_with_instance(instance_name.clone()); - + match client.send_command(DaemonCommand::GetStatus).await { Ok(DaemonResponse::Status(status)) => { let section = output.section() .title("System Status") .item("Version", &status.version) .item("Uptime", &format!("{} seconds", status.uptime_secs)); - + let section = if let Some(library_id) = status.current_library { section.item("Current Library", &library_id.to_string()) } else { section.item("Current Library", "None") }; - + section.item("Active Jobs", &status.active_jobs.to_string()) .item("Total Locations", &status.total_locations.to_string()) .empty_line() @@ -92,7 +92,7 @@ async fn handle_status_command( output.error(Message::Error("Unexpected response from daemon".to_string()))?; } } - + Ok(()) } @@ -209,7 +209,7 @@ fn format_log_line(line: &str, output: &CliOutput) -> String { if !output.use_color() { return line.to_string(); } - + use owo_colors::OwoColorize; if line.contains("ERROR") { line.red().to_string() diff --git a/core/src/infrastructure/cli/commands/volume.rs b/core/src/infra/cli/commands/volume.rs similarity index 98% rename from core/src/infrastructure/cli/commands/volume.rs rename to core/src/infra/cli/commands/volume.rs index 6b54755e2..a631b4b1d 100644 --- a/core/src/infrastructure/cli/commands/volume.rs +++ b/core/src/infra/cli/commands/volume.rs @@ -1,7 +1,7 @@ //! Volume CLI commands -use crate::infrastructure::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::daemon::{DaemonClient, DaemonCommand, DaemonResponse}; +use crate::infra::cli::output::{CliOutput, Message}; use crate::volume::types::VolumeFingerprint; use clap::Subcommand; use comfy_table::Table; diff --git a/core/src/infrastructure/cli/daemon/client.rs b/core/src/infra/cli/daemon/client.rs similarity index 100% rename from core/src/infrastructure/cli/daemon/client.rs rename to core/src/infra/cli/daemon/client.rs diff --git a/core/src/infrastructure/cli/daemon/config.rs b/core/src/infra/cli/daemon/config.rs similarity index 100% rename from core/src/infrastructure/cli/daemon/config.rs rename to core/src/infra/cli/daemon/config.rs diff --git a/core/src/infrastructure/cli/daemon/handlers/core.rs b/core/src/infra/cli/daemon/handlers/core.rs similarity index 92% rename from core/src/infrastructure/cli/daemon/handlers/core.rs rename to core/src/infra/cli/daemon/handlers/core.rs index 80d1f154d..4b65e8c46 100644 --- a/core/src/infrastructure/cli/daemon/handlers/core.rs +++ b/core/src/infra/cli/daemon/handlers/core.rs @@ -5,9 +5,9 @@ use std::sync::Arc; use tokio::sync::oneshot; use tracing::error; +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{DaemonCommand, DaemonResponse, DaemonStatus}; use crate::Core; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{DaemonCommand, DaemonResponse, DaemonStatus}; use super::CommandHandler; @@ -78,4 +78,4 @@ impl CommandHandler for CoreHandler { DaemonCommand::Ping | DaemonCommand::Shutdown | DaemonCommand::GetStatus ) } -} \ No newline at end of file +} diff --git a/core/src/infrastructure/cli/daemon/handlers/file.rs b/core/src/infra/cli/daemon/handlers/file.rs similarity index 87% rename from core/src/infrastructure/cli/daemon/handlers/file.rs rename to core/src/infra/cli/daemon/handlers/file.rs index 408858748..3d30dd14f 100644 --- a/core/src/infrastructure/cli/daemon/handlers/file.rs +++ b/core/src/infra/cli/daemon/handlers/file.rs @@ -5,9 +5,9 @@ use std::path::PathBuf; use std::sync::Arc; use uuid::Uuid; -use crate::infrastructure::actions::builder::{ActionBuildError, ActionBuilder}; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{DaemonCommand, DaemonResponse}; +use crate::infra::actions::builder::{ActionBuildError, ActionBuilder}; +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{DaemonCommand, DaemonResponse}; use crate::Core; use super::CommandHandler; @@ -37,21 +37,21 @@ impl CommandHandler for FileHandler { let library_id = library.id(); // Create copy options from parameters - let options = crate::operations::files::copy::CopyOptions { + let options = crate::ops::files::copy::CopyOptions { overwrite, verify_checksum: verify, preserve_timestamps, delete_after_copy: move_files, - move_mode: if move_files { - Some(crate::operations::files::copy::MoveMode::Move) + move_mode: if move_files { + Some(crate::ops::files::copy::MoveMode::Move) } else { None }, - copy_method: crate::operations::files::copy::input::CopyMethod::Auto, + copy_method: crate::ops::files::copy::input::CopyMethod::Auto, }; // Create action directly from URIs - let action = match crate::operations::files::copy::action::FileCopyActionBuilder::from_uris( + let action = match crate::ops::files::copy::action::FileCopyActionBuilder::from_uris( sources, destination, options, @@ -70,7 +70,7 @@ impl CommandHandler for FileHandler { Some(action_manager) => { // Create the full Action enum - let full_action = crate::infrastructure::actions::Action::FileCopy { + let full_action = crate::infra::actions::Action::FileCopy { library_id, action, }; @@ -152,7 +152,7 @@ impl CommandHandler for FileHandler { }; browse_entries.push( - crate::infrastructure::cli::daemon::types::common::BrowseEntry { + crate::infra::cli::daemon::types::common::BrowseEntry { name: file_name, path: file_path, is_dir, @@ -208,9 +208,9 @@ impl CommandHandler for FileHandler { // Dispatch LocationIndexAction for each location for location in locations { - let action = crate::infrastructure::actions::Action::LocationIndex { + let action = crate::infra::actions::Action::LocationIndex { library_id, - action: crate::operations::locations::index::action::LocationIndexAction { + action: crate::ops::locations::index::action::LocationIndexAction { location_id: location.id, mode: location.index_mode.into(), }, @@ -267,11 +267,11 @@ impl CommandHandler for FileHandler { match core.context.get_action_manager().await { Some(action_manager) => { // Create LocationIndexAction - let action = crate::infrastructure::actions::Action::LocationIndex { + let action = crate::infra::actions::Action::LocationIndex { library_id, - action: crate::operations::locations::index::action::LocationIndexAction { + action: crate::ops::locations::index::action::LocationIndexAction { location_id, - mode: crate::operations::indexing::IndexMode::Content, + mode: crate::ops::indexing::IndexMode::Content, }, }; diff --git a/core/src/infrastructure/cli/daemon/handlers/job.rs b/core/src/infra/cli/daemon/handlers/job.rs similarity index 91% rename from core/src/infrastructure/cli/daemon/handlers/job.rs rename to core/src/infra/cli/daemon/handlers/job.rs index 6ed1c5b29..92b22704b 100644 --- a/core/src/infrastructure/cli/daemon/handlers/job.rs +++ b/core/src/infra/cli/daemon/handlers/job.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use std::sync::Arc; use uuid::Uuid; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{DaemonCommand, DaemonResponse, JobInfo}; +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{DaemonCommand, DaemonResponse, JobInfo}; use crate::Core; use super::CommandHandler; @@ -47,7 +47,7 @@ impl CommandHandler for JobHandler { // For other statuses, query the database let status_filter = status.and_then(|s| { - s.parse::() + s.parse::() .ok() }); @@ -95,8 +95,8 @@ impl CommandHandler for JobHandler { // Get current library from CLI state if let Some(library) = state_service.get_current_library(core).await { let job_manager = library.jobs(); - let job_id = crate::infrastructure::jobs::types::JobId(id); - + let job_id = crate::infra::jobs::types::JobId(id); + match job_manager.pause_job(job_id).await { Ok(_) => DaemonResponse::Ok, Err(e) => DaemonResponse::Error(e.to_string()), @@ -110,8 +110,8 @@ impl CommandHandler for JobHandler { // Get current library from CLI state if let Some(library) = state_service.get_current_library(core).await { let job_manager = library.jobs(); - let job_id = crate::infrastructure::jobs::types::JobId(id); - + let job_id = crate::infra::jobs::types::JobId(id); + match job_manager.resume_job(job_id).await { Ok(_) => DaemonResponse::Ok, Err(e) => DaemonResponse::Error(e.to_string()), diff --git a/core/src/infrastructure/cli/daemon/handlers/library.rs b/core/src/infra/cli/daemon/handlers/library.rs similarity index 95% rename from core/src/infrastructure/cli/daemon/handlers/library.rs rename to core/src/infra/cli/daemon/handlers/library.rs index 0a9af119e..e2d98c92e 100644 --- a/core/src/infrastructure/cli/daemon/handlers/library.rs +++ b/core/src/infra/cli/daemon/handlers/library.rs @@ -6,8 +6,8 @@ use tracing::warn; use uuid::Uuid; use crate::Core; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{ +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{ DaemonCommand, DaemonResponse, LibraryInfo, }; @@ -35,7 +35,7 @@ impl CommandHandler for LibraryHandler { // Auto-select the newly created library let library_id = library.id(); let library_path = library.path().to_path_buf(); - + // Try to set the new library as current if let Err(e) = state_service .switch_library(library_id, library_path.clone()) @@ -43,7 +43,7 @@ impl CommandHandler for LibraryHandler { { warn!("Failed to auto-select new library: {}", e); } - + DaemonResponse::LibraryCreated { id: library_id, name: name.clone(), // Use the name passed in instead of reading from library diff --git a/core/src/infrastructure/cli/daemon/handlers/location.rs b/core/src/infra/cli/daemon/handlers/location.rs similarity index 85% rename from core/src/infrastructure/cli/daemon/handlers/location.rs rename to core/src/infra/cli/daemon/handlers/location.rs index 2fbf563b6..ddef51b8f 100644 --- a/core/src/infrastructure/cli/daemon/handlers/location.rs +++ b/core/src/infra/cli/daemon/handlers/location.rs @@ -5,9 +5,9 @@ use std::path::PathBuf; use std::sync::Arc; use uuid::Uuid; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{DaemonCommand, DaemonResponse, LocationInfo}; -use crate::infrastructure::actions::output::ActionOutput; +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{DaemonCommand, DaemonResponse, LocationInfo}; +use crate::infra::actions::output::ActionOutput; use crate::Core; use super::CommandHandler; @@ -33,13 +33,13 @@ impl CommandHandler for LocationHandler { match core.context.get_action_manager().await { Some(action_manager) => { // Create the location add action - let action = crate::infrastructure::actions::Action::LocationAdd { + let action = crate::infra::actions::Action::LocationAdd { library_id, action: - crate::operations::locations::add::action::LocationAddAction { + crate::ops::locations::add::action::LocationAddAction { path: path.clone(), name, - mode: crate::operations::indexing::IndexMode::Content, + mode: crate::ops::indexing::IndexMode::Content, }, }; @@ -77,8 +77,8 @@ impl CommandHandler for LocationHandler { // Get current library from CLI state if let Some(library) = state_service.get_current_library(core).await { // For listing, we can directly query the database since it's a read operation - use crate::infrastructure::database::entities; - use crate::operations::indexing::PathResolver; + use crate::infra::database::entities; + use crate::ops::indexing::PathResolver; use sea_orm::EntityTrait; match entities::location::Entity::find() @@ -123,9 +123,9 @@ impl CommandHandler for LocationHandler { match core.context.get_action_manager().await { Some(action_manager) => { // Create the location remove action - let action = crate::infrastructure::actions::Action::LocationRemove { + let action = crate::infra::actions::Action::LocationRemove { library_id, - action: crate::operations::locations::remove::action::LocationRemoveAction { + action: crate::ops::locations::remove::action::LocationRemoveAction { location_id: id, }, }; @@ -155,9 +155,9 @@ impl CommandHandler for LocationHandler { match core.context.get_action_manager().await { Some(action_manager) => { // Create LocationRescanAction - let action = crate::infrastructure::actions::Action::LocationRescan { + let action = crate::infra::actions::Action::LocationRescan { library_id, - action: crate::operations::locations::rescan::action::LocationRescanAction { + action: crate::ops::locations::rescan::action::LocationRescanAction { location_id: id, full_rescan: false, }, diff --git a/core/src/infrastructure/cli/daemon/handlers/mod.rs b/core/src/infra/cli/daemon/handlers/mod.rs similarity index 93% rename from core/src/infrastructure/cli/daemon/handlers/mod.rs rename to core/src/infra/cli/daemon/handlers/mod.rs index af0feec44..9b254cbc9 100644 --- a/core/src/infrastructure/cli/daemon/handlers/mod.rs +++ b/core/src/infra/cli/daemon/handlers/mod.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use std::sync::Arc; use crate::Core; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{DaemonCommand, DaemonResponse}; +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{DaemonCommand, DaemonResponse}; pub mod core; pub mod file; diff --git a/core/src/infrastructure/cli/daemon/handlers/network.rs b/core/src/infra/cli/daemon/handlers/network.rs similarity index 97% rename from core/src/infrastructure/cli/daemon/handlers/network.rs rename to core/src/infra/cli/daemon/handlers/network.rs index 68dceb3d8..74367c830 100644 --- a/core/src/infrastructure/cli/daemon/handlers/network.rs +++ b/core/src/infra/cli/daemon/handlers/network.rs @@ -4,8 +4,8 @@ use async_trait::async_trait; use std::sync::Arc; use uuid::Uuid; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{ +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{ ConnectedDeviceInfo, DaemonCommand, DaemonResponse, PairingRequestInfo, }; use crate::Core; @@ -95,10 +95,10 @@ impl CommandHandler for NetworkHandler { match core.context.get_action_manager().await { Some(action_manager) => { // Create DeviceRevokeAction - let action = crate::infrastructure::actions::Action::DeviceRevoke { + let action = crate::infra::actions::Action::DeviceRevoke { library_id, action: - crate::operations::devices::revoke::action::DeviceRevokeAction { + crate::ops::devices::revoke::action::DeviceRevokeAction { device_id, reason: Some("Revoked via CLI".to_string()), }, diff --git a/core/src/infrastructure/cli/daemon/handlers/system.rs b/core/src/infra/cli/daemon/handlers/system.rs similarity index 86% rename from core/src/infrastructure/cli/daemon/handlers/system.rs rename to core/src/infra/cli/daemon/handlers/system.rs index caa97f2d8..571cdc83d 100644 --- a/core/src/infrastructure/cli/daemon/handlers/system.rs +++ b/core/src/infra/cli/daemon/handlers/system.rs @@ -4,9 +4,9 @@ use async_trait::async_trait; use std::path::PathBuf; use std::sync::Arc; +use crate::infra::cli::daemon::services::StateService; +use crate::infra::cli::daemon::types::{DaemonCommand, DaemonResponse}; use crate::Core; -use crate::infrastructure::cli::daemon::services::StateService; -use crate::infrastructure::cli::daemon::types::{DaemonCommand, DaemonResponse}; use super::CommandHandler; @@ -42,4 +42,4 @@ impl CommandHandler for SystemHandler { fn can_handle(&self, cmd: &DaemonCommand) -> bool { matches!(cmd, DaemonCommand::SubscribeEvents) } -} \ No newline at end of file +} diff --git a/core/src/infrastructure/cli/daemon/handlers/volume.rs b/core/src/infra/cli/daemon/handlers/volume.rs similarity index 99% rename from core/src/infrastructure/cli/daemon/handlers/volume.rs rename to core/src/infra/cli/daemon/handlers/volume.rs index de85adcc4..70d51a04c 100644 --- a/core/src/infrastructure/cli/daemon/handlers/volume.rs +++ b/core/src/infra/cli/daemon/handlers/volume.rs @@ -5,7 +5,7 @@ use std::sync::Arc; use tracing::debug; use crate::{ - infrastructure::{ + infra::{ actions::{manager::ActionManager, Action}, cli::{ commands::VolumeCommands, @@ -15,7 +15,7 @@ use crate::{ }, }, }, - operations::volumes::{ + ops::volumes::{ speed_test::action::VolumeSpeedTestAction, track::action::VolumeTrackAction, untrack::action::VolumeUntrackAction, }, diff --git a/core/src/infrastructure/cli/daemon/mod.rs b/core/src/infra/cli/daemon/mod.rs similarity index 98% rename from core/src/infrastructure/cli/daemon/mod.rs rename to core/src/infra/cli/daemon/mod.rs index 668f823fc..76aa67f57 100644 --- a/core/src/infrastructure/cli/daemon/mod.rs +++ b/core/src/infra/cli/daemon/mod.rs @@ -3,7 +3,7 @@ //! The daemon runs in the background and handles all core operations. //! The CLI communicates with it via Unix domain socket (on Unix) or named pipe (on Windows). -use crate::{infrastructure::cli::state::CliState, Core}; +use crate::{infra::cli::state::CliState, Core}; use std::path::PathBuf; use std::sync::Arc; use tokio::io::{AsyncBufReadExt, AsyncWriteExt, BufReader}; @@ -158,7 +158,7 @@ impl Daemon { // Emit CoreStarted event to signal daemon is ready self.core .events - .emit(crate::infrastructure::events::Event::CoreStarted); + .emit(crate::infra::events::Event::CoreStarted); // Set up shutdown channel let (shutdown_tx, mut shutdown_rx) = oneshot::channel(); diff --git a/core/src/infrastructure/cli/daemon/services/helpers.rs b/core/src/infra/cli/daemon/services/helpers.rs similarity index 100% rename from core/src/infrastructure/cli/daemon/services/helpers.rs rename to core/src/infra/cli/daemon/services/helpers.rs diff --git a/core/src/infrastructure/cli/daemon/services/mod.rs b/core/src/infra/cli/daemon/services/mod.rs similarity index 100% rename from core/src/infrastructure/cli/daemon/services/mod.rs rename to core/src/infra/cli/daemon/services/mod.rs diff --git a/core/src/infrastructure/cli/daemon/services/state.rs b/core/src/infra/cli/daemon/services/state.rs similarity index 97% rename from core/src/infrastructure/cli/daemon/services/state.rs rename to core/src/infra/cli/daemon/services/state.rs index 197e21496..5960a53eb 100644 --- a/core/src/infrastructure/cli/daemon/services/state.rs +++ b/core/src/infra/cli/daemon/services/state.rs @@ -1,6 +1,6 @@ //! State management service for the daemon -use crate::infrastructure::cli::state::CliState; +use crate::infra::cli::state::CliState; use crate::library::Library; use crate::Core; use std::path::PathBuf; @@ -72,4 +72,4 @@ impl StateService { Ok(()) } -} \ No newline at end of file +} diff --git a/core/src/infrastructure/cli/daemon/types/commands.rs b/core/src/infra/cli/daemon/types/commands.rs similarity index 97% rename from core/src/infrastructure/cli/daemon/types/commands.rs rename to core/src/infra/cli/daemon/types/commands.rs index e3b383514..cb2214175 100644 --- a/core/src/infrastructure/cli/daemon/types/commands.rs +++ b/core/src/infra/cli/daemon/types/commands.rs @@ -3,8 +3,8 @@ use serde::{Deserialize, Serialize}; use std::path::PathBuf; use uuid::Uuid; -use crate::infrastructure::cli::commands::{ - LibraryCommands, LocationCommands, JobCommands, FileCommands, +use crate::infra::cli::commands::{ + LibraryCommands, LocationCommands, JobCommands, FileCommands, NetworkCommands, SystemCommands, VolumeCommands }; diff --git a/core/src/infrastructure/cli/daemon/types/common.rs b/core/src/infra/cli/daemon/types/common.rs similarity index 100% rename from core/src/infrastructure/cli/daemon/types/common.rs rename to core/src/infra/cli/daemon/types/common.rs diff --git a/core/src/infrastructure/cli/daemon/types/mod.rs b/core/src/infra/cli/daemon/types/mod.rs similarity index 100% rename from core/src/infrastructure/cli/daemon/types/mod.rs rename to core/src/infra/cli/daemon/types/mod.rs diff --git a/core/src/infrastructure/cli/daemon/types/responses.rs b/core/src/infra/cli/daemon/types/responses.rs similarity index 95% rename from core/src/infrastructure/cli/daemon/types/responses.rs rename to core/src/infra/cli/daemon/types/responses.rs index cc37d24c3..190187626 100644 --- a/core/src/infrastructure/cli/daemon/types/responses.rs +++ b/core/src/infra/cli/daemon/types/responses.rs @@ -8,7 +8,7 @@ use super::common::{ BrowseEntry, ConnectedDeviceInfo, JobInfo, LibraryInfo, LocationInfo, PairingRequestInfo, VolumeListItem, }; -use crate::{infrastructure::actions::output::ActionOutput, volume::Volume}; +use crate::{infra::actions::output::ActionOutput, volume::Volume}; /// Responses from the daemon #[derive(Debug, Serialize, Deserialize)] diff --git a/core/src/infrastructure/cli/mod.rs b/core/src/infra/cli/mod.rs similarity index 98% rename from core/src/infrastructure/cli/mod.rs rename to core/src/infra/cli/mod.rs index 686ce70d0..dd85dcb57 100644 --- a/core/src/infrastructure/cli/mod.rs +++ b/core/src/infra/cli/mod.rs @@ -6,7 +6,7 @@ pub mod pairing_ui; pub mod state; pub mod utils; -use crate::infrastructure::cli::commands::{ +use crate::infra::cli::commands::{ daemon::{handle_daemon_command, DaemonCommands}, file::{handle_file_command, FileCommands}, job::{handle_job_command, JobCommands}, @@ -16,7 +16,7 @@ use crate::infrastructure::cli::commands::{ system::{handle_system_command, SystemCommands}, volume::{handle_volume_command, VolumeCommands}, }; -use crate::infrastructure::cli::output::{CliOutput, Message}; +use crate::infra::cli::output::{CliOutput, Message}; use clap::{Parser, Subcommand}; use std::path::PathBuf; diff --git a/core/src/infrastructure/cli/output/context.rs b/core/src/infra/cli/output/context.rs similarity index 100% rename from core/src/infrastructure/cli/output/context.rs rename to core/src/infra/cli/output/context.rs diff --git a/core/src/infrastructure/cli/output/formatters.rs b/core/src/infra/cli/output/formatters.rs similarity index 100% rename from core/src/infrastructure/cli/output/formatters.rs rename to core/src/infra/cli/output/formatters.rs diff --git a/core/src/infrastructure/cli/output/messages.rs b/core/src/infra/cli/output/messages.rs similarity index 88% rename from core/src/infrastructure/cli/output/messages.rs rename to core/src/infra/cli/output/messages.rs index 1e1c08cdd..41fa9e603 100644 --- a/core/src/infrastructure/cli/output/messages.rs +++ b/core/src/infra/cli/output/messages.rs @@ -272,15 +272,15 @@ pub enum JobStatus { Cancelled, } -impl From for JobStatus { - fn from(status: crate::infrastructure::jobs::types::JobStatus) -> Self { +impl From for JobStatus { + fn from(status: crate::infra::jobs::types::JobStatus) -> Self { match status { - crate::infrastructure::jobs::types::JobStatus::Queued => Self::Queued, - crate::infrastructure::jobs::types::JobStatus::Running => Self::Running, - crate::infrastructure::jobs::types::JobStatus::Completed => Self::Completed, - crate::infrastructure::jobs::types::JobStatus::Failed => Self::Failed, - crate::infrastructure::jobs::types::JobStatus::Paused => Self::Paused, - crate::infrastructure::jobs::types::JobStatus::Cancelled => Self::Cancelled, + crate::infra::jobs::types::JobStatus::Queued => Self::Queued, + crate::infra::jobs::types::JobStatus::Running => Self::Running, + crate::infra::jobs::types::JobStatus::Completed => Self::Completed, + crate::infra::jobs::types::JobStatus::Failed => Self::Failed, + crate::infra::jobs::types::JobStatus::Paused => Self::Paused, + crate::infra::jobs::types::JobStatus::Cancelled => Self::Cancelled, } } } diff --git a/core/src/infrastructure/cli/output/mod.rs b/core/src/infra/cli/output/mod.rs similarity index 100% rename from core/src/infrastructure/cli/output/mod.rs rename to core/src/infra/cli/output/mod.rs diff --git a/core/src/infrastructure/cli/output/section.rs b/core/src/infra/cli/output/section.rs similarity index 97% rename from core/src/infrastructure/cli/output/section.rs rename to core/src/infra/cli/output/section.rs index 10d1598bc..9b6a7d19e 100644 --- a/core/src/infrastructure/cli/output/section.rs +++ b/core/src/infra/cli/output/section.rs @@ -152,13 +152,13 @@ impl<'a> HelpSection<'a> { pub fn end(mut self) -> OutputSection<'a> { if !self.items.is_empty() { self.parent.lines.push(Line::Empty); - + if self.parent.context.use_emoji() { self.parent.lines.push(Line::Text("💡 Tips:".to_string())); } else { self.parent.lines.push(Line::Text("Tips:".to_string())); } - + for item in self.items { self.parent.lines.push(Line::Text(format!(" • {}", item))); } @@ -175,12 +175,12 @@ impl<'a> HelpSection<'a> { #[cfg(test)] mod tests { use super::*; - use crate::infrastructure::cli::output::{CliOutput, OutputFormat}; + use crate::infra::cli::output::{CliOutput, OutputFormat}; #[test] fn test_section_builder() { let (mut output, buffer) = CliOutput::test(); - + output.section() .title("Test Section") .status("Status", "Active") @@ -189,7 +189,7 @@ mod tests { .item("Item 2", "Value 2") .render() .unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); assert!(result.contains("Test Section")); assert!(result.contains("Status: Active")); @@ -199,7 +199,7 @@ mod tests { #[test] fn test_help_section() { let (mut output, buffer) = CliOutput::test(); - + output.section() .title("Commands") .help() @@ -207,7 +207,7 @@ mod tests { .item("Use 'delete' to remove items") .render() .unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); assert!(result.contains("Commands")); assert!(result.contains("Tips:")); diff --git a/core/src/infrastructure/cli/output/tests.rs b/core/src/infra/cli/output/tests.rs similarity index 94% rename from core/src/infrastructure/cli/output/tests.rs rename to core/src/infra/cli/output/tests.rs index 96d989c0a..eafca8c1f 100644 --- a/core/src/infrastructure/cli/output/tests.rs +++ b/core/src/infra/cli/output/tests.rs @@ -2,22 +2,22 @@ #[cfg(test)] mod tests { - use crate::infrastructure::cli::output::*; - use crate::infrastructure::cli::output::messages::*; + use crate::infra::cli::output::*; + use crate::infra::cli::output::messages::*; use std::path::PathBuf; use uuid::Uuid; #[test] fn test_human_format_basic_messages() { let (mut output, buffer) = CliOutput::test(); - + output.success("Operation successful").unwrap(); output.error(Message::Error("Something went wrong".to_string())).unwrap(); output.warning("This is a warning").unwrap(); output.info("Some information").unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Operation successful")); assert!(result.contains("Something went wrong")); assert!(result.contains("This is a warning")); @@ -28,24 +28,24 @@ mod tests { fn test_json_format() { use std::sync::{Arc, Mutex}; use super::super::TestWriter; - + let buffer = Arc::new(Mutex::new(Vec::new())); let writer = TestWriter { buffer: buffer.clone() }; let mut output = CliOutput { context: OutputContext::test(Box::new(writer)), }; output.context.format = OutputFormat::Json; - output.context.formatter = Box::new(crate::infrastructure::cli::output::formatters::JsonFormatter); - + output.context.formatter = Box::new(crate::infra::cli::output::formatters::JsonFormatter); + output.print(Message::LibraryCreated { name: "Test Library".to_string(), id: Uuid::new_v4(), path: PathBuf::from("/test/path"), }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); let json: serde_json::Value = serde_json::from_str(&result.trim()).unwrap(); - + assert_eq!(json["type"], "library_created"); assert_eq!(json["success"], true); assert_eq!(json["data"]["name"], "Test Library"); @@ -55,23 +55,23 @@ mod tests { fn test_quiet_format() { use std::sync::{Arc, Mutex}; use super::super::TestWriter; - + let buffer = Arc::new(Mutex::new(Vec::new())); let writer = TestWriter { buffer: buffer.clone() }; let mut output = CliOutput { context: OutputContext::test(Box::new(writer)), }; output.context.format = OutputFormat::Quiet; - + // Normal messages should not appear in quiet mode output.info("This should not appear").unwrap(); output.success("This also should not appear").unwrap(); - + // Errors should always appear output.error(Message::Error("This error should appear".to_string())).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(!result.contains("This should not appear")); assert!(!result.contains("This also should not appear")); assert!(result.contains("This error should appear")); @@ -81,7 +81,7 @@ mod tests { fn test_verbosity_levels() { use std::sync::{Arc, Mutex}; use super::super::TestWriter; - + let buffer = Arc::new(Mutex::new(Vec::new())); let writer = TestWriter { buffer: buffer.clone() }; let mut output = CliOutput::with_options( @@ -90,22 +90,22 @@ mod tests { ColorMode::Never ); output.context = OutputContext::test(Box::new(writer)); - + // Normal messages should appear output.info("Normal info").unwrap(); - + // Debug messages should not appear at normal verbosity output.print(Message::Debug("Debug info".to_string())).unwrap(); - + // Progress messages (verbose level) should not appear output.print(Message::IndexingProgress { current: 10, total: 100, location: "test".to_string(), }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Normal info")); assert!(!result.contains("Debug info")); assert!(!result.contains("Indexing")); @@ -114,16 +114,16 @@ mod tests { #[test] fn test_library_messages() { let (mut output, buffer) = CliOutput::test(); - + let lib_id = Uuid::new_v4(); output.print(Message::LibraryCreated { name: "My Library".to_string(), id: lib_id, path: PathBuf::from("/home/user/library"), }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Library 'My Library' created successfully")); assert!(result.contains(&lib_id.to_string())); assert!(result.contains("/home/user/library")); @@ -132,7 +132,7 @@ mod tests { #[test] fn test_device_list() { let (mut output, buffer) = CliOutput::test(); - + let devices = vec![ DeviceInfo { id: "device1".to_string(), @@ -147,11 +147,11 @@ mod tests { peer_id: Some("peer123".to_string()), }, ]; - + output.print(Message::DevicesList { devices }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Discovered devices")); assert!(result.contains("My Computer")); assert!(result.contains("My Phone")); @@ -162,7 +162,7 @@ mod tests { #[test] fn test_section_builder_basic() { let (mut output, buffer) = CliOutput::test(); - + output.section() .title("Test Section") .status("Version", "1.0.0") @@ -171,9 +171,9 @@ mod tests { .text("Some additional text") .render() .unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Test Section")); assert!(result.contains("Version: 1.0.0")); assert!(result.contains("Status: Running")); @@ -183,20 +183,20 @@ mod tests { #[test] fn test_section_builder_with_table() { let (mut output, buffer) = CliOutput::test(); - + let mut table = comfy_table::Table::new(); table.set_header(vec!["ID", "Name", "Status"]); table.add_row(vec!["1", "Item 1", "Active"]); table.add_row(vec!["2", "Item 2", "Inactive"]); - + output.section() .title("Items") .table(table) .render() .unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Items")); assert!(result.contains("ID")); assert!(result.contains("Name")); @@ -208,7 +208,7 @@ mod tests { #[test] fn test_help_section() { let (mut output, buffer) = CliOutput::test(); - + output.section() .title("Available Commands") .help() @@ -217,9 +217,9 @@ mod tests { .item("delete - Delete an item") .render() .unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Available Commands")); assert!(result.contains("Tips:")); assert!(result.contains("• create - Create a new item")); @@ -231,7 +231,7 @@ mod tests { fn test_progress_messages() { use std::sync::{Arc, Mutex}; use super::super::TestWriter; - + let buffer = Arc::new(Mutex::new(Vec::new())); let writer = TestWriter { buffer: buffer.clone() }; let mut output = CliOutput::with_options( @@ -241,21 +241,21 @@ mod tests { ); output.context = OutputContext::test(Box::new(writer)); output.context.verbosity = VerbosityLevel::Verbose; - + output.print(Message::IndexingProgress { current: 150, total: 1000, location: "/home/user/documents".to_string(), }).unwrap(); - + output.print(Message::CopyProgress { current: 5, total: 10, current_file: Some("file.txt".to_string()), }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Indexing /home/user/documents: 150/1000 files")); assert!(result.contains("Copying file.txt: 5/10 files")); } @@ -263,18 +263,18 @@ mod tests { #[test] fn test_pairing_messages() { let (mut output, buffer) = CliOutput::test(); - + output.print(Message::PairingCodeGenerated { code: "ABC123".to_string(), }).unwrap(); - + output.print(Message::PairingSuccess { device_name: "John's Phone".to_string(), device_id: "device123".to_string(), }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Pairing code generated")); assert!(result.contains("ABC123")); assert!(result.contains("Successfully paired with John's Phone")); @@ -283,28 +283,28 @@ mod tests { #[test] fn test_job_messages() { let (mut output, buffer) = CliOutput::test(); - + let job_id = Uuid::new_v4(); - + output.print(Message::JobStarted { id: job_id, name: "File Copy".to_string(), }).unwrap(); - + output.print(Message::JobCompleted { id: job_id, name: "File Copy".to_string(), duration: 42, }).unwrap(); - + output.print(Message::JobFailed { id: job_id, name: "File Validation".to_string(), error: "Checksum mismatch".to_string(), }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Job started: File Copy")); assert!(result.contains("Job completed: File Copy (42s)")); assert!(result.contains("Job failed: File Validation")); @@ -314,7 +314,7 @@ mod tests { #[test] fn test_empty_line_deduplication() { let (mut output, buffer) = CliOutput::test(); - + output.section() .title("Test") .empty_line() @@ -323,17 +323,17 @@ mod tests { .text("Content") .render() .unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); let lines: Vec<&str> = result.lines().collect(); - + // Count empty lines between "Test" and "Content" let empty_count = lines.iter() .skip_while(|&&line| !line.contains("Test")) .take_while(|&&line| !line.contains("Content")) .filter(|&&line| line.trim().is_empty()) .count(); - + assert_eq!(empty_count, 1, "Should have exactly one empty line"); } @@ -347,7 +347,7 @@ mod tests { ); // In test environment, should detect no color support assert!(!ctx.use_color()); - + // Test always mode let ctx = OutputContext::with_options( OutputFormat::Human, @@ -355,7 +355,7 @@ mod tests { ColorMode::Always ); assert!(ctx.use_color()); - + // Test never mode let ctx = OutputContext::with_options( OutputFormat::Human, @@ -368,7 +368,7 @@ mod tests { #[test] fn test_daemon_status_message() { let (mut output, buffer) = CliOutput::test(); - + output.print(Message::DaemonStatus { version: "2.0.0".to_string(), uptime: 3600, @@ -382,9 +382,9 @@ mod tests { } ], }).unwrap(); - + let result = String::from_utf8(buffer.lock().unwrap().clone()).unwrap(); - + assert!(result.contains("Spacedrive Daemon Status")); assert!(result.contains("Version: 2.0.0")); assert!(result.contains("Instance: default")); diff --git a/core/src/infrastructure/cli/pairing_ui.rs b/core/src/infra/cli/pairing_ui.rs similarity index 100% rename from core/src/infrastructure/cli/pairing_ui.rs rename to core/src/infra/cli/pairing_ui.rs diff --git a/core/src/infrastructure/cli/state.rs b/core/src/infra/cli/state.rs similarity index 100% rename from core/src/infrastructure/cli/state.rs rename to core/src/infra/cli/state.rs diff --git a/core/src/infrastructure/cli/tui.rs b/core/src/infra/cli/tui.rs similarity index 100% rename from core/src/infrastructure/cli/tui.rs rename to core/src/infra/cli/tui.rs diff --git a/core/src/infrastructure/cli/utils.rs b/core/src/infra/cli/utils.rs similarity index 100% rename from core/src/infrastructure/cli/utils.rs rename to core/src/infra/cli/utils.rs diff --git a/core/src/infrastructure/database/entities/audit_log.rs b/core/src/infra/db/entities/audit_log.rs similarity index 100% rename from core/src/infrastructure/database/entities/audit_log.rs rename to core/src/infra/db/entities/audit_log.rs diff --git a/core/src/infrastructure/database/entities/collection.rs b/core/src/infra/db/entities/collection.rs similarity index 100% rename from core/src/infrastructure/database/entities/collection.rs rename to core/src/infra/db/entities/collection.rs diff --git a/core/src/infrastructure/database/entities/collection_entry.rs b/core/src/infra/db/entities/collection_entry.rs similarity index 100% rename from core/src/infrastructure/database/entities/collection_entry.rs rename to core/src/infra/db/entities/collection_entry.rs diff --git a/core/src/infrastructure/database/entities/content_identity.rs b/core/src/infra/db/entities/content_identity.rs similarity index 100% rename from core/src/infrastructure/database/entities/content_identity.rs rename to core/src/infra/db/entities/content_identity.rs diff --git a/core/src/infrastructure/database/entities/content_kind.rs b/core/src/infra/db/entities/content_kind.rs similarity index 100% rename from core/src/infrastructure/database/entities/content_kind.rs rename to core/src/infra/db/entities/content_kind.rs diff --git a/core/src/infrastructure/database/entities/device.rs b/core/src/infra/db/entities/device.rs similarity index 100% rename from core/src/infrastructure/database/entities/device.rs rename to core/src/infra/db/entities/device.rs diff --git a/core/src/infrastructure/database/entities/directory_paths.rs b/core/src/infra/db/entities/directory_paths.rs similarity index 100% rename from core/src/infrastructure/database/entities/directory_paths.rs rename to core/src/infra/db/entities/directory_paths.rs diff --git a/core/src/infrastructure/database/entities/entry.rs b/core/src/infra/db/entities/entry.rs similarity index 100% rename from core/src/infrastructure/database/entities/entry.rs rename to core/src/infra/db/entities/entry.rs diff --git a/core/src/infrastructure/database/entities/entry_closure.rs b/core/src/infra/db/entities/entry_closure.rs similarity index 100% rename from core/src/infrastructure/database/entities/entry_closure.rs rename to core/src/infra/db/entities/entry_closure.rs diff --git a/core/src/infrastructure/database/entities/indexer_rule.rs b/core/src/infra/db/entities/indexer_rule.rs similarity index 100% rename from core/src/infrastructure/database/entities/indexer_rule.rs rename to core/src/infra/db/entities/indexer_rule.rs diff --git a/core/src/infrastructure/database/entities/label.rs b/core/src/infra/db/entities/label.rs similarity index 100% rename from core/src/infrastructure/database/entities/label.rs rename to core/src/infra/db/entities/label.rs diff --git a/core/src/infrastructure/database/entities/location.rs b/core/src/infra/db/entities/location.rs similarity index 100% rename from core/src/infrastructure/database/entities/location.rs rename to core/src/infra/db/entities/location.rs diff --git a/core/src/infrastructure/database/entities/metadata_label.rs b/core/src/infra/db/entities/metadata_label.rs similarity index 100% rename from core/src/infrastructure/database/entities/metadata_label.rs rename to core/src/infra/db/entities/metadata_label.rs diff --git a/core/src/infrastructure/database/entities/metadata_tag.rs b/core/src/infra/db/entities/metadata_tag.rs similarity index 100% rename from core/src/infrastructure/database/entities/metadata_tag.rs rename to core/src/infra/db/entities/metadata_tag.rs diff --git a/core/src/infrastructure/database/entities/mime_type.rs b/core/src/infra/db/entities/mime_type.rs similarity index 100% rename from core/src/infrastructure/database/entities/mime_type.rs rename to core/src/infra/db/entities/mime_type.rs diff --git a/core/src/infrastructure/database/entities/mod.rs b/core/src/infra/db/entities/mod.rs similarity index 100% rename from core/src/infrastructure/database/entities/mod.rs rename to core/src/infra/db/entities/mod.rs diff --git a/core/src/infrastructure/database/entities/sidecar.rs b/core/src/infra/db/entities/sidecar.rs similarity index 100% rename from core/src/infrastructure/database/entities/sidecar.rs rename to core/src/infra/db/entities/sidecar.rs diff --git a/core/src/infrastructure/database/entities/sidecar_availability.rs b/core/src/infra/db/entities/sidecar_availability.rs similarity index 100% rename from core/src/infrastructure/database/entities/sidecar_availability.rs rename to core/src/infra/db/entities/sidecar_availability.rs diff --git a/core/src/infrastructure/database/entities/tag.rs b/core/src/infra/db/entities/tag.rs similarity index 100% rename from core/src/infrastructure/database/entities/tag.rs rename to core/src/infra/db/entities/tag.rs diff --git a/core/src/infrastructure/database/entities/user_metadata.rs b/core/src/infra/db/entities/user_metadata.rs similarity index 100% rename from core/src/infrastructure/database/entities/user_metadata.rs rename to core/src/infra/db/entities/user_metadata.rs diff --git a/core/src/infrastructure/database/entities/volume.rs b/core/src/infra/db/entities/volume.rs similarity index 100% rename from core/src/infrastructure/database/entities/volume.rs rename to core/src/infra/db/entities/volume.rs diff --git a/core/src/infrastructure/database/migration/m20240101_000001_initial_schema.rs b/core/src/infra/db/migration/m20240101_000001_initial_schema.rs similarity index 100% rename from core/src/infrastructure/database/migration/m20240101_000001_initial_schema.rs rename to core/src/infra/db/migration/m20240101_000001_initial_schema.rs diff --git a/core/src/infrastructure/database/migration/m20240102_000001_populate_lookups.rs b/core/src/infra/db/migration/m20240102_000001_populate_lookups.rs similarity index 100% rename from core/src/infrastructure/database/migration/m20240102_000001_populate_lookups.rs rename to core/src/infra/db/migration/m20240102_000001_populate_lookups.rs diff --git a/core/src/infrastructure/database/migration/m20240107_000001_create_collections.rs b/core/src/infra/db/migration/m20240107_000001_create_collections.rs similarity index 100% rename from core/src/infrastructure/database/migration/m20240107_000001_create_collections.rs rename to core/src/infra/db/migration/m20240107_000001_create_collections.rs diff --git a/core/src/infrastructure/database/migration/m20250109_000001_create_sidecars.rs b/core/src/infra/db/migration/m20250109_000001_create_sidecars.rs similarity index 100% rename from core/src/infrastructure/database/migration/m20250109_000001_create_sidecars.rs rename to core/src/infra/db/migration/m20250109_000001_create_sidecars.rs diff --git a/core/src/infrastructure/database/migration/m20250110_000001_refactor_volumes_table.rs b/core/src/infra/db/migration/m20250110_000001_refactor_volumes_table.rs similarity index 100% rename from core/src/infrastructure/database/migration/m20250110_000001_refactor_volumes_table.rs rename to core/src/infra/db/migration/m20250110_000001_refactor_volumes_table.rs diff --git a/core/src/infrastructure/database/migration/m20250112_000001_create_indexer_rules.rs b/core/src/infra/db/migration/m20250112_000001_create_indexer_rules.rs similarity index 100% rename from core/src/infrastructure/database/migration/m20250112_000001_create_indexer_rules.rs rename to core/src/infra/db/migration/m20250112_000001_create_indexer_rules.rs diff --git a/core/src/infrastructure/database/migration/mod.rs b/core/src/infra/db/migration/mod.rs similarity index 100% rename from core/src/infrastructure/database/migration/mod.rs rename to core/src/infra/db/migration/mod.rs diff --git a/core/src/infrastructure/database/mod.rs b/core/src/infra/db/mod.rs similarity index 100% rename from core/src/infrastructure/database/mod.rs rename to core/src/infra/db/mod.rs diff --git a/core/src/infrastructure/events/mod.rs b/core/src/infra/event/mod.rs similarity index 91% rename from core/src/infrastructure/events/mod.rs rename to core/src/infra/event/mod.rs index 712f3c23a..7b4aa2cb0 100644 --- a/core/src/infrastructure/events/mod.rs +++ b/core/src/infra/event/mod.rs @@ -1,6 +1,6 @@ //! Event bus for decoupled communication -use crate::infrastructure::jobs::output::JobOutput; +use crate::infra::jobs::output::JobOutput; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use tokio::sync::broadcast; @@ -24,17 +24,17 @@ pub enum Event { EntryCreated { library_id: Uuid, entry_id: Uuid }, EntryModified { library_id: Uuid, entry_id: Uuid }, EntryDeleted { library_id: Uuid, entry_id: Uuid }, - EntryMoved { - library_id: Uuid, - entry_id: Uuid, - old_path: String, - new_path: String + EntryMoved { + library_id: Uuid, + entry_id: Uuid, + old_path: String, + new_path: String }, // Volume events VolumeAdded(crate::volume::Volume), - VolumeRemoved { - fingerprint: crate::volume::VolumeFingerprint + VolumeRemoved { + fingerprint: crate::volume::VolumeFingerprint }, VolumeUpdated { fingerprint: crate::volume::VolumeFingerprint, @@ -58,19 +58,19 @@ pub enum Event { // Job events JobQueued { job_id: String, job_type: String }, JobStarted { job_id: String, job_type: String }, - JobProgress { - job_id: String, + JobProgress { + job_id: String, job_type: String, - progress: f64, + progress: f64, message: Option, // Enhanced progress data - serialized GenericProgress generic_progress: Option, }, JobCompleted { job_id: String, job_type: String, output: JobOutput }, - JobFailed { - job_id: String, - job_type: String, - error: String + JobFailed { + job_id: String, + job_type: String, + error: String }, JobCancelled { job_id: String, job_type: String }, JobPaused { job_id: String }, @@ -78,15 +78,15 @@ pub enum Event { // Indexing events IndexingStarted { location_id: Uuid }, - IndexingProgress { - location_id: Uuid, - processed: u64, - total: Option + IndexingProgress { + location_id: Uuid, + processed: u64, + total: Option }, - IndexingCompleted { - location_id: Uuid, - total_files: u64, - total_dirs: u64 + IndexingCompleted { + location_id: Uuid, + total_files: u64, + total_dirs: u64 }, IndexingFailed { location_id: Uuid, error: String }, @@ -124,9 +124,9 @@ pub enum Event { }, // Custom events for extensibility - Custom { - event_type: String, - data: serde_json::Value + Custom { + event_type: String, + data: serde_json::Value }, } @@ -151,7 +151,7 @@ impl EventBus { let (sender, _) = broadcast::channel(capacity); Self { sender } } - + /// Emit an event to all subscribers pub fn emit(&self, event: Event) { match self.sender.send(event.clone()) { @@ -164,14 +164,14 @@ impl EventBus { } } } - + /// Subscribe to events pub fn subscribe(&self) -> EventSubscriber { EventSubscriber { receiver: self.sender.subscribe(), } } - + /// Get the number of active subscribers pub fn subscriber_count(&self) -> usize { self.sender.receiver_count() diff --git a/core/src/infrastructure/jobs/context.rs b/core/src/infra/job/context.rs similarity index 100% rename from core/src/infrastructure/jobs/context.rs rename to core/src/infra/job/context.rs diff --git a/core/src/infrastructure/jobs/database.rs b/core/src/infra/job/database.rs similarity index 100% rename from core/src/infrastructure/jobs/database.rs rename to core/src/infra/job/database.rs diff --git a/core/src/infrastructure/jobs/error.rs b/core/src/infra/job/error.rs similarity index 100% rename from core/src/infrastructure/jobs/error.rs rename to core/src/infra/job/error.rs diff --git a/core/src/infrastructure/jobs/executor.rs b/core/src/infra/job/executor.rs similarity index 99% rename from core/src/infrastructure/jobs/executor.rs rename to core/src/infra/job/executor.rs index 9d573aa5a..1f4e0967a 100644 --- a/core/src/infrastructure/jobs/executor.rs +++ b/core/src/infra/job/executor.rs @@ -74,7 +74,7 @@ impl JobExecutor { } else { None }; - + Self { job, state: JobExecutorState { @@ -146,9 +146,9 @@ impl Task for JobExecutor { if let Some(logger) = &self.state.file_logger { let _ = logger.log("INFO", &format!("Starting job {}: {}", self.state.job_id, J::NAME)); } - + let result = self.run_inner(interrupter).await; - + // Log job completion if let Some(logger) = &self.state.file_logger { match &result { @@ -166,7 +166,7 @@ impl Task for JobExecutor { } } } - + result } } @@ -365,7 +365,7 @@ impl ErasedJob for JobExecutor { self: Box, job_id: JobId, library: std::sync::Arc, - job_db: std::sync::Arc, + job_db: std::sync::Arc, status_tx: tokio::sync::watch::Sender, progress_tx: tokio::sync::mpsc::UnboundedSender, broadcast_tx: tokio::sync::broadcast::Sender, @@ -394,7 +394,7 @@ impl ErasedJob for JobExecutor { } else { None }; - + executor.state = JobExecutorState { job_id, library, diff --git a/core/src/infrastructure/jobs/generic_progress.rs b/core/src/infra/job/generic_progress.rs similarity index 100% rename from core/src/infrastructure/jobs/generic_progress.rs rename to core/src/infra/job/generic_progress.rs diff --git a/core/src/infrastructure/jobs/handle.rs b/core/src/infra/job/handle.rs similarity index 100% rename from core/src/infrastructure/jobs/handle.rs rename to core/src/infra/job/handle.rs diff --git a/core/src/infrastructure/jobs/logger.rs b/core/src/infra/job/logger.rs similarity index 100% rename from core/src/infrastructure/jobs/logger.rs rename to core/src/infra/job/logger.rs diff --git a/core/src/infrastructure/jobs/manager.rs b/core/src/infra/job/manager.rs similarity index 98% rename from core/src/infrastructure/jobs/manager.rs rename to core/src/infra/job/manager.rs index 5c61b592d..f1095c327 100644 --- a/core/src/infrastructure/jobs/manager.rs +++ b/core/src/infra/job/manager.rs @@ -15,7 +15,7 @@ use super::{ }; use crate::{ context::CoreContext, - infrastructure::events::{Event, EventBus}, + infra::events::{Event, EventBus}, library::Library, }; use async_trait::async_trait; @@ -164,7 +164,7 @@ impl JobManager { let _ = broadcast_tx_clone.send(progress.clone()); // Emit enhanced progress event - use crate::infrastructure::events::Event; + use crate::infra::events::Event; // Extract generic progress data if available let generic_progress = match &progress { @@ -174,7 +174,7 @@ impl JobManager { crate::operations::files::copy::CopyProgress, >(value.clone()) { - use crate::infrastructure::jobs::generic_progress::ToGenericProgress; + use crate::infra::jobs::generic_progress::ToGenericProgress; Some(serde_json::to_value(copy_progress.to_generic_progress()).ok()) } else { None @@ -410,7 +410,7 @@ impl JobManager { } // Emit enhanced progress event - use crate::infrastructure::events::Event; + use crate::infra::events::Event; // Extract generic progress data if available let generic_progress = match &progress { @@ -420,7 +420,7 @@ impl JobManager { crate::operations::files::copy::CopyProgress, >(value.clone()) { - use crate::infrastructure::jobs::generic_progress::ToGenericProgress; + use crate::infra::jobs::generic_progress::ToGenericProgress; Some(serde_json::to_value(copy_progress.to_generic_progress()).ok()) } else { None @@ -532,7 +532,11 @@ impl JobManager { let output = { let jobs = running_jobs.read().await; if let Some(job) = jobs.get(&job_id_clone) { - job.handle.output.lock().await.clone() + job.handle + .output + .lock() + .await + .clone() .unwrap_or(Ok(JobOutput::Success)) } else { Ok(JobOutput::Success) @@ -999,7 +1003,12 @@ impl JobManager { let output = { let jobs = running_jobs.read().await; jobs.get(&job_id_clone) - .and_then(|job| job.handle.output.blocking_lock().clone()) + .and_then(|job| { + job.handle + .output + .blocking_lock() + .clone() + }) .unwrap_or(Ok(JobOutput::Success)) }; @@ -1430,4 +1439,4 @@ impl CheckpointHandler for DbCheckpointHandler { } } -use uuid::Uuid; \ No newline at end of file +use uuid::Uuid; diff --git a/core/src/infrastructure/jobs/mod.rs b/core/src/infra/job/mod.rs similarity index 100% rename from core/src/infrastructure/jobs/mod.rs rename to core/src/infra/job/mod.rs diff --git a/core/src/infrastructure/jobs/output.rs b/core/src/infra/job/output.rs similarity index 93% rename from core/src/infrastructure/jobs/output.rs rename to core/src/infra/job/output.rs index 47b9a4239..31dd0217e 100644 --- a/core/src/infrastructure/jobs/output.rs +++ b/core/src/infra/job/output.rs @@ -11,25 +11,25 @@ use super::progress::Progress; pub enum JobOutput { /// Job completed successfully with no specific output Success, - + /// File copy job output FileCopy { copied_count: usize, total_bytes: u64, }, - + /// Indexer job output Indexed { stats: IndexerStats, metrics: IndexerMetrics, }, - + /// Thumbnail generation output ThumbnailsGenerated { generated_count: usize, failed_count: usize, }, - + /// Thumbnail generation output (detailed) ThumbnailGeneration { generated_count: u64, @@ -37,35 +37,35 @@ pub enum JobOutput { error_count: u64, total_size_bytes: u64, }, - + /// File move/rename operation output FileMove { moved_count: usize, failed_count: usize, total_bytes: u64, }, - + /// File delete operation output FileDelete { deleted_count: usize, failed_count: usize, total_bytes: u64, }, - + /// Duplicate detection output DuplicateDetection { duplicate_groups: usize, total_duplicates: usize, potential_savings: u64, }, - + /// File validation output FileValidation { validated_count: usize, issues_found: usize, total_bytes_validated: u64, }, - + /// Generic output with custom data Custom(serde_json::Value), } @@ -75,7 +75,7 @@ impl JobOutput { pub fn custom(data: T) -> Self { Self::Custom(serde_json::to_value(data).unwrap_or(serde_json::Value::Null)) } - + /// Get indexed output if this is an indexed job pub fn as_indexed(&self) -> Option { match self { @@ -89,14 +89,14 @@ impl JobOutput { _ => None, } } - + /// Convert output to a progress representation (for final progress) pub fn as_progress(&self) -> Option { match self { Self::Success => Some(Progress::percentage(1.0)), Self::FileCopy { copied_count, total_bytes } => { Some(Progress::generic( - crate::infrastructure::jobs::generic_progress::GenericProgress::new( + crate::infra::jobs::generic_progress::GenericProgress::new( 1.0, "Completed", format!("Copied {} files", copied_count) @@ -105,7 +105,7 @@ impl JobOutput { } Self::Indexed { stats, metrics } => { Some(Progress::generic( - crate::infrastructure::jobs::generic_progress::GenericProgress::new( + crate::infra::jobs::generic_progress::GenericProgress::new( 1.0, "Completed", format!("Indexed {} files, {} directories", stats.files, stats.dirs) @@ -114,7 +114,7 @@ impl JobOutput { } Self::ThumbnailGeneration { generated_count, .. } => { Some(Progress::generic( - crate::infrastructure::jobs::generic_progress::GenericProgress::new( + crate::infra::jobs::generic_progress::GenericProgress::new( 1.0, "Completed", format!("Generated {} thumbnails", generated_count) @@ -160,31 +160,31 @@ impl fmt::Display for JobOutput { write!(f, "Copied {} files ({} bytes)", copied_count, total_bytes) } Self::Indexed { stats, metrics } => { - write!(f, "Indexed {} files, {} directories ({} bytes)", + write!(f, "Indexed {} files, {} directories ({} bytes)", stats.files, stats.dirs, stats.bytes) } Self::ThumbnailsGenerated { generated_count, failed_count } => { - write!(f, "Generated {} thumbnails ({} failed)", + write!(f, "Generated {} thumbnails ({} failed)", generated_count, failed_count) } Self::ThumbnailGeneration { generated_count, skipped_count, error_count, total_size_bytes } => { - write!(f, "Generated {} thumbnails ({} skipped, {} errors, {} bytes)", + write!(f, "Generated {} thumbnails ({} skipped, {} errors, {} bytes)", generated_count, skipped_count, error_count, total_size_bytes) } Self::FileMove { moved_count, failed_count, total_bytes } => { - write!(f, "Moved {} files ({} failed, {} bytes)", + write!(f, "Moved {} files ({} failed, {} bytes)", moved_count, failed_count, total_bytes) } Self::FileDelete { deleted_count, failed_count, total_bytes } => { - write!(f, "Deleted {} files ({} failed, {} bytes)", + write!(f, "Deleted {} files ({} failed, {} bytes)", deleted_count, failed_count, total_bytes) } Self::DuplicateDetection { duplicate_groups, total_duplicates, potential_savings } => { - write!(f, "Found {} duplicate groups ({} duplicates, {} bytes savings)", + write!(f, "Found {} duplicate groups ({} duplicates, {} bytes savings)", duplicate_groups, total_duplicates, potential_savings) } Self::FileValidation { validated_count, issues_found, total_bytes_validated } => { - write!(f, "Validated {} files ({} issues, {} bytes)", + write!(f, "Validated {} files ({} issues, {} bytes)", validated_count, issues_found, total_bytes_validated) } Self::Custom(_) => write!(f, "Custom output"), diff --git a/core/src/infrastructure/jobs/progress.rs b/core/src/infra/job/progress.rs similarity index 96% rename from core/src/infrastructure/jobs/progress.rs rename to core/src/infra/job/progress.rs index ea01b650c..87b33fe76 100644 --- a/core/src/infrastructure/jobs/progress.rs +++ b/core/src/infra/job/progress.rs @@ -1,6 +1,6 @@ //! Progress reporting for jobs -use crate::infrastructure::jobs::generic_progress::GenericProgress; +use crate::infra::jobs::generic_progress::GenericProgress; use serde::{Deserialize, Serialize}; use std::fmt; @@ -10,19 +10,19 @@ use std::fmt; pub enum Progress { /// Simple count-based progress Count { current: usize, total: usize }, - + /// Percentage-based progress (0.0 to 1.0) Percentage(f32), - + /// Indeterminate progress with a message Indeterminate(String), - + /// Bytes-based progress Bytes { current: u64, total: u64 }, - + /// Custom structured progress Structured(serde_json::Value), - + /// Generic progress (recommended for all jobs) Generic(GenericProgress), } @@ -32,32 +32,32 @@ impl Progress { pub fn count(current: usize, total: usize) -> Self { Self::Count { current, total } } - + /// Create percentage progress pub fn percentage(value: f32) -> Self { Self::Percentage(value.clamp(0.0, 1.0)) } - + /// Create indeterminate progress pub fn indeterminate(message: impl Into) -> Self { Self::Indeterminate(message.into()) } - + /// Create bytes-based progress pub fn bytes(current: u64, total: u64) -> Self { Self::Bytes { current, total } } - + /// Create structured progress pub fn structured(data: T) -> Self { Self::Structured(serde_json::to_value(data).unwrap_or(serde_json::Value::Null)) } - + /// Create generic progress pub fn generic(progress: GenericProgress) -> Self { Self::Generic(progress) } - + /// Get progress as a percentage (0.0 to 1.0) pub fn as_percentage(&self) -> Option { match self { @@ -72,7 +72,7 @@ impl Progress { _ => None, } } - + /// Check if progress is determinate pub fn is_determinate(&self) -> bool { !matches!(self, Self::Indeterminate(_)) @@ -107,12 +107,12 @@ fn format_bytes(bytes: u64) -> String { const UNITS: &[&str] = &["B", "KB", "MB", "GB", "TB"]; let mut size = bytes as f64; let mut unit_idx = 0; - + while size >= 1024.0 && unit_idx < UNITS.len() - 1 { size /= 1024.0; unit_idx += 1; } - + if unit_idx == 0 { format!("{} {}", size as u64, UNITS[unit_idx]) } else { diff --git a/core/src/infrastructure/jobs/registry.rs b/core/src/infra/job/registry.rs similarity index 100% rename from core/src/infrastructure/jobs/registry.rs rename to core/src/infra/job/registry.rs diff --git a/core/src/infrastructure/jobs/traits.rs b/core/src/infra/job/traits.rs similarity index 100% rename from core/src/infrastructure/jobs/traits.rs rename to core/src/infra/job/traits.rs diff --git a/core/src/infrastructure/jobs/types.rs b/core/src/infra/job/types.rs similarity index 81% rename from core/src/infrastructure/jobs/types.rs rename to core/src/infra/job/types.rs index ad6f4f610..9ca61628d 100644 --- a/core/src/infrastructure/jobs/types.rs +++ b/core/src/infra/job/types.rs @@ -106,12 +106,8 @@ pub struct JobRegistration { pub schema_fn: fn() -> JobSchema, pub create_fn: fn(serde_json::Value) -> Result, serde_json::Error>, pub deserialize_fn: fn(&[u8]) -> Result, rmp_serde::decode::Error>, - pub deserialize_dyn_fn: fn( - &[u8], - ) -> Result< - Box, - rmp_serde::decode::Error, - >, + pub deserialize_dyn_fn: + fn(&[u8]) -> Result, rmp_serde::decode::Error>, } /// Type-erased job for dynamic dispatch @@ -120,23 +116,17 @@ pub trait ErasedJob: Send + Sync + std::fmt::Debug + 'static { self: Box, job_id: JobId, library: std::sync::Arc, - job_db: std::sync::Arc, + job_db: std::sync::Arc, status_tx: tokio::sync::watch::Sender, - progress_tx: tokio::sync::mpsc::UnboundedSender< - crate::infrastructure::jobs::progress::Progress, - >, - broadcast_tx: tokio::sync::broadcast::Sender< - crate::infrastructure::jobs::progress::Progress, - >, - checkpoint_handler: std::sync::Arc< - dyn crate::infrastructure::jobs::context::CheckpointHandler, - >, + progress_tx: tokio::sync::mpsc::UnboundedSender, + broadcast_tx: tokio::sync::broadcast::Sender, + checkpoint_handler: std::sync::Arc, output_handle: std::sync::Arc< tokio::sync::Mutex< Option< Result< - crate::infrastructure::jobs::output::JobOutput, - crate::infrastructure::jobs::error::JobError, + crate::infra::jobs::output::JobOutput, + crate::infra::jobs::error::JobError, >, >, >, @@ -145,9 +135,9 @@ pub trait ErasedJob: Send + Sync + std::fmt::Debug + 'static { volume_manager: Option>, job_logging_config: Option, job_logs_dir: Option, - ) -> Box>; + ) -> Box>; - fn serialize_state(&self) -> Result, crate::infrastructure::jobs::error::JobError>; + fn serialize_state(&self) -> Result, crate::infra::jobs::error::JobError>; } /// Information about a job (for display/querying) diff --git a/core/src/infrastructure/mod.rs b/core/src/infra/mod.rs similarity index 100% rename from core/src/infrastructure/mod.rs rename to core/src/infra/mod.rs diff --git a/core/src/lib.rs b/core/src/lib.rs index 649adfe4d..c419825b1 100644 --- a/core/src/lib.rs +++ b/core/src/lib.rs @@ -3,36 +3,36 @@ //! //! A unified, simplified architecture for cross-platform file management. +pub mod common; pub mod config; pub mod context; pub mod device; pub mod domain; -pub mod file_type; -pub mod infrastructure; +pub mod filetype; +pub mod infra; pub mod keys; pub mod library; pub mod location; -pub mod operations; -pub mod services; -pub mod shared; -pub mod test_framework; +pub mod ops; +pub mod service; +pub mod testing; pub mod volume; -use services::networking::protocols::PairingProtocolHandler; -use services::networking::utils::logging::NetworkLogger; +use service::networking::protocols::PairingProtocolHandler; +use service::networking::utils::logging::NetworkLogger; // Compatibility module for legacy networking references pub mod networking { - pub use crate::services::networking::*; + pub use crate::service::networking::*; } use crate::config::AppConfig; use crate::context::CoreContext; use crate::device::DeviceManager; -use crate::infrastructure::actions::manager::ActionManager; -use crate::infrastructure::events::{Event, EventBus}; +use crate::infra::actions::manager::ActionManager; +use crate::infra::events::{Event, EventBus}; use crate::library::LibraryManager; -use crate::services::Services; +use crate::service::Services; use crate::volume::{VolumeDetectionConfig, VolumeManager}; use std::path::PathBuf; use std::sync::Arc; @@ -152,7 +152,7 @@ impl Core { // 2. Initialize device manager let device = Arc::new(DeviceManager::init_with_path(&data_dir)?); // Set the global device ID for legacy compatibility - shared::utils::set_current_device_id(device.device_id()?); + common::utils::set_current_device_id(device.device_id()?); // 3. Create event bus let events = Arc::new(EventBus::default()); @@ -179,7 +179,7 @@ impl Core { // 8. Register all job types info!("Registering job types..."); - crate::operations::register_all_jobs(); + crate::ops::register_all_jobs(); info!("Job types registered"); // 9. Create the context that will be shared with services @@ -246,7 +246,7 @@ impl Core { } // 12. Initialize ActionManager and set it in context - let action_manager = Arc::new(crate::infrastructure::actions::manager::ActionManager::new( + let action_manager = Arc::new(crate::infra::actions::manager::ActionManager::new( context.clone(), )); context.set_action_manager(action_manager).await; @@ -341,17 +341,24 @@ impl Core { config.data_dir.clone() }; - let pairing_handler = Arc::new(networking::protocols::PairingProtocolHandler::new_with_persistence( - networking.identity().clone(), - networking.device_registry(), - logger.clone(), - command_sender, - data_dir, - )); + let pairing_handler = Arc::new( + networking::protocols::PairingProtocolHandler::new_with_persistence( + networking.identity().clone(), + networking.device_registry(), + logger.clone(), + command_sender, + data_dir, + ), + ); // Try to load persisted sessions, but don't fail if there's an error if let Err(e) = pairing_handler.load_persisted_sessions().await { - logger.warn(&format!("Failed to load persisted pairing sessions: {}. Starting with empty sessions.", e)).await; + logger + .warn(&format!( + "Failed to load persisted pairing sessions: {}. Starting with empty sessions.", + e + )) + .await; } // Start the state machine task for pairing @@ -424,7 +431,7 @@ impl Core { path: std::path::PathBuf, enabled: bool, ) -> Result<(), Box> { - use crate::services::location_watcher::WatchedLocation; + use crate::service::watcher::WatchedLocation; let watched_location = WatchedLocation { id: location_id, @@ -466,9 +473,7 @@ impl Core { } /// Get all currently watched locations - pub async fn get_watched_locations( - &self, - ) -> Vec { + pub async fn get_watched_locations(&self) -> Vec { self.services.location_watcher.get_watched_locations().await } diff --git a/core/src/library/error.rs b/core/src/library/error.rs index 374de36a3..16983d38c 100644 --- a/core/src/library/error.rs +++ b/core/src/library/error.rs @@ -10,51 +10,51 @@ pub enum LibraryError { /// Library is already open #[error("Library {0} is already open")] AlreadyOpen(Uuid), - + /// Library is already in use by another process #[error("Library is already in use by another process")] AlreadyInUse, - + /// Stale lock file detected #[error("Stale lock file detected - library may have crashed previously")] StaleLock, - + /// Not a valid library directory #[error("Not a valid library directory: {0}")] NotALibrary(PathBuf), - + /// Library not found #[error("Library not found: {0}")] NotFound(String), - + /// Invalid library name #[error("Invalid library name: {0}")] InvalidName(String), - + /// Library already exists #[error("Library already exists at: {0}")] AlreadyExists(PathBuf), - + /// Configuration error #[error("Configuration error: {0}")] ConfigError(String), - + /// Database error #[error("Database error: {0}")] DatabaseError(#[from] sea_orm::DbErr), - + /// IO error #[error("IO error: {0}")] IoError(#[from] std::io::Error), - + /// JSON error #[error("JSON error: {0}")] JsonError(#[from] serde_json::Error), - + /// Job system error #[error("Job system error: {0}")] - JobError(#[from] crate::infrastructure::jobs::error::JobError), - + JobError(#[from] crate::infra::jobs::error::JobError), + /// Generic error #[error("{0}")] Other(String), diff --git a/core/src/library/lock.rs b/core/src/library/lock.rs index f418a9f58..dbd4473f8 100644 --- a/core/src/library/lock.rs +++ b/core/src/library/lock.rs @@ -1,7 +1,7 @@ //! Library lock implementation to prevent concurrent access use super::error::{LibraryError, Result}; -use crate::shared::utils::get_current_device_id; +use crate::common::utils::get_current_device_id; use chrono::{DateTime, Utc}; use serde::{Deserialize, Serialize}; use std::fs::{File, OpenOptions}; @@ -15,13 +15,13 @@ use uuid::Uuid; pub struct LockInfo { /// ID of the device holding the lock pub device_id: Uuid, - + /// Process ID pub process_id: u32, - + /// When the lock was acquired pub acquired_at: DateTime, - + /// Optional description (e.g., "indexing", "backup") pub description: Option, } @@ -30,7 +30,7 @@ pub struct LockInfo { pub struct LibraryLock { /// Path to the lock file path: PathBuf, - + /// The open file handle (keeps the lock active) _file: File, } @@ -39,7 +39,7 @@ impl LibraryLock { /// Attempt to acquire a lock on the library pub fn acquire(library_path: &Path) -> Result { let lock_path = library_path.join(".sdlibrary.lock"); - + // Try to create the lock file exclusively match OpenOptions::new() .write(true) @@ -54,11 +54,11 @@ impl LibraryLock { acquired_at: Utc::now(), description: None, }; - + let json = serde_json::to_string_pretty(&lock_info)?; file.write_all(json.as_bytes())?; file.sync_all()?; - + Ok(Self { path: lock_path, _file: file, @@ -69,7 +69,7 @@ impl LibraryLock { if Self::is_lock_stale(&lock_path)? { // Remove stale lock and try again std::fs::remove_file(&lock_path)?; - + // Recursive call to try again Self::acquire(library_path) } else { @@ -79,7 +79,7 @@ impl LibraryLock { Err(e) => Err(e.into()), } } - + /// Check if a lock file is stale (older than 1 hour or process no longer running) pub fn is_lock_stale(lock_path: &Path) -> Result { let metadata = std::fs::metadata(lock_path)?; @@ -87,12 +87,12 @@ impl LibraryLock { let age = SystemTime::now() .duration_since(modified) .unwrap_or(Duration::ZERO); - + // Consider lock stale if older than 1 hour if age > Duration::from_secs(3600) { return Ok(true); } - + // Also check if the process is still running if let Ok(contents) = std::fs::read_to_string(lock_path) { if let Ok(lock_info) = serde_json::from_str::(&contents) { @@ -102,21 +102,21 @@ impl LibraryLock { } } } - + Ok(false) } - + /// Try to read lock information (for debugging) pub fn read_lock_info(library_path: &Path) -> Result> { let lock_path = library_path.join(".sdlibrary.lock"); - + if !lock_path.exists() { return Ok(None); } - + let contents = std::fs::read_to_string(lock_path)?; let info: LockInfo = serde_json::from_str(&contents)?; - + Ok(Some(info)) } } @@ -125,7 +125,7 @@ impl LibraryLock { #[cfg(unix)] fn is_process_running(pid: u32) -> bool { use std::process::Command; - + match Command::new("ps") .arg("-p") .arg(pid.to_string()) @@ -140,7 +140,7 @@ fn is_process_running(pid: u32) -> bool { #[cfg(windows)] fn is_process_running(pid: u32) -> bool { use std::process::Command; - + match Command::new("tasklist") .arg("/fi") .arg(&format!("pid eq {}", pid)) @@ -167,37 +167,37 @@ impl Drop for LibraryLock { mod tests { use super::*; use tempfile::TempDir; - + #[test] fn test_library_lock() { let temp_dir = TempDir::new().unwrap(); let library_path = temp_dir.path().join("test.sdlibrary"); std::fs::create_dir_all(&library_path).unwrap(); - + // First lock should succeed let _lock1 = LibraryLock::acquire(&library_path).unwrap(); - + // Second lock should fail match LibraryLock::acquire(&library_path) { Err(LibraryError::AlreadyInUse) => {} _ => panic!("Expected AlreadyInUse error"), } - + // Lock file should exist assert!(library_path.join(".sdlibrary.lock").exists()); } - + #[test] fn test_lock_cleanup() { let temp_dir = TempDir::new().unwrap(); let library_path = temp_dir.path().join("test.sdlibrary"); std::fs::create_dir_all(&library_path).unwrap(); - + { let _lock = LibraryLock::acquire(&library_path).unwrap(); assert!(library_path.join(".sdlibrary.lock").exists()); } - + // Lock file should be cleaned up after drop assert!(!library_path.join(".sdlibrary.lock").exists()); } diff --git a/core/src/library/manager.rs b/core/src/library/manager.rs index ebc7cca2d..03312c3bb 100644 --- a/core/src/library/manager.rs +++ b/core/src/library/manager.rs @@ -8,7 +8,7 @@ use super::{ }; use crate::{ context::CoreContext, - infrastructure::{ + infra::{ database::{entities, Database}, events::{Event, EventBus}, jobs::manager::JobManager, diff --git a/core/src/library/mod.rs b/core/src/library/mod.rs index 6e8740e21..18289e590 100644 --- a/core/src/library/mod.rs +++ b/core/src/library/mod.rs @@ -1,5 +1,5 @@ //! Library management system -//! +//! //! This module provides the core library functionality for Spacedrive. //! Each library is a self-contained directory with its own database, //! thumbnails, and other data. @@ -14,7 +14,7 @@ pub use error::{LibraryError, Result}; pub use lock::LibraryLock; pub use manager::{LibraryManager, DiscoveredLibrary}; -use crate::infrastructure::{ +use crate::infra::{ database::Database, jobs::manager::JobManager, }; @@ -27,16 +27,16 @@ use uuid::Uuid; pub struct Library { /// Root directory of the library (the .sdlibrary folder) path: PathBuf, - + /// Library configuration config: RwLock, - + /// Database connection db: Arc, - + /// Job manager for this library jobs: Arc, - + /// Lock preventing concurrent access _lock: LibraryLock, } @@ -50,32 +50,32 @@ impl Library { panic!("Failed to read library config for ID") }) } - + /// Get the library name pub async fn name(&self) -> String { self.config.read().await.name.clone() } - + /// Get the library path pub fn path(&self) -> &Path { &self.path } - + /// Get the database pub fn db(&self) -> &Arc { &self.db } - + /// Get the job manager pub fn jobs(&self) -> &Arc { &self.jobs } - + /// Get a copy of the current configuration pub async fn config(&self) -> LibraryConfig { self.config.read().await.clone() } - + /// Update library configuration pub async fn update_config(&self, f: F) -> Result<()> where @@ -84,15 +84,15 @@ impl Library { let mut config = self.config.write().await; f(&mut config); config.updated_at = chrono::Utc::now(); - + // Save to disk let config_path = self.path.join("library.json"); let json = serde_json::to_string_pretty(&*config)?; tokio::fs::write(config_path, json).await?; - + Ok(()) } - + /// Save library configuration to disk pub async fn save_config(&self, config: &LibraryConfig) -> Result<()> { let config_path = self.path.join("library.json"); @@ -100,68 +100,68 @@ impl Library { tokio::fs::write(config_path, json).await?; Ok(()) } - + /// Get the thumbnail directory for this library pub fn thumbnails_dir(&self) -> PathBuf { self.path.join("thumbnails") } - + /// Get the path for a specific thumbnail with size pub fn thumbnail_path(&self, cas_id: &str, size: u32) -> PathBuf { if cas_id.len() < 4 { // Fallback for short IDs return self.thumbnails_dir().join(format!("{}_{}.webp", cas_id, size)); } - + // Two-level sharding based on first four characters let shard1 = &cas_id[0..2]; let shard2 = &cas_id[2..4]; - + self.thumbnails_dir() .join(shard1) .join(shard2) .join(format!("{}_{}.webp", cas_id, size)) } - + /// Get the path for any thumbnail size (legacy compatibility) pub fn thumbnail_path_legacy(&self, cas_id: &str) -> PathBuf { self.thumbnail_path(cas_id, 256) // Default to 256px } - + /// Save a thumbnail with specific size pub async fn save_thumbnail(&self, cas_id: &str, size: u32, data: &[u8]) -> Result<()> { let path = self.thumbnail_path(cas_id, size); - + // Ensure parent directory exists if let Some(parent) = path.parent() { tokio::fs::create_dir_all(parent).await?; } - + // Write thumbnail tokio::fs::write(path, data).await?; - + Ok(()) } - + /// Check if a thumbnail exists for a specific size pub async fn has_thumbnail(&self, cas_id: &str, size: u32) -> bool { tokio::fs::metadata(self.thumbnail_path(cas_id, size)) .await .is_ok() } - + /// Shutdown the library, gracefully stopping all jobs pub async fn shutdown(&self) -> Result<()> { // Shutdown the job manager, which will pause all running jobs self.jobs.shutdown().await?; - + // Save config to ensure any updates are persisted let config = self.config.read().await; self.save_config(&*config).await?; - + Ok(()) } - + /// Check if thumbnails exist for all specified sizes pub async fn has_all_thumbnails(&self, cas_id: &str, sizes: &[u32]) -> bool { for &size in sizes { @@ -171,33 +171,33 @@ impl Library { } true } - + /// Get thumbnail data for specific size pub async fn get_thumbnail(&self, cas_id: &str, size: u32) -> Result> { let path = self.thumbnail_path(cas_id, size); Ok(tokio::fs::read(path).await?) } - + /// Get the best available thumbnail (largest size available) pub async fn get_best_thumbnail(&self, cas_id: &str, preferred_sizes: &[u32]) -> Result)>> { // Try sizes in descending order let mut sizes = preferred_sizes.to_vec(); sizes.sort_by(|a, b| b.cmp(a)); - + for &size in &sizes { if self.has_thumbnail(cas_id, size).await { let data = self.get_thumbnail(cas_id, size).await?; return Ok(Some((size, data))); } } - + Ok(None) } - + /// Start thumbnail generation job - pub async fn generate_thumbnails(&self, entry_ids: Option>) -> Result { - use crate::operations::media::thumbnail::{ThumbnailJob, ThumbnailJobConfig}; - + pub async fn generate_thumbnails(&self, entry_ids: Option>) -> Result { + use crate::ops::media::thumbnail::{ThumbnailJob, ThumbnailJobConfig}; + let config = ThumbnailJobConfig { sizes: self.config().await.settings.thumbnail_sizes.clone(), quality: self.config().await.settings.thumbnail_quality, @@ -205,17 +205,17 @@ impl Library { batch_size: 50, max_concurrent: 4, }; - + let job = if let Some(ids) = entry_ids { ThumbnailJob::for_entries(ids, config) } else { ThumbnailJob::new(config) }; - + self.jobs().dispatch(job).await .map_err(|e| LibraryError::JobError(e)) } - + /// Update library statistics pub async fn update_statistics(&self, f: F) -> Result<()> where diff --git a/core/src/location/manager.rs b/core/src/location/manager.rs index 1bdbd7636..317b168cd 100644 --- a/core/src/location/manager.rs +++ b/core/src/location/manager.rs @@ -2,13 +2,13 @@ use super::{LocationError, LocationResult, ManagedLocation, IndexMode}; use crate::{ - infrastructure::{ + infra::{ database::entities::{self, entry::EntryKind}, events::{Event, EventBus}, jobs::{manager::JobManager, traits::Job}, }, library::Library, - operations::indexing::{job::{IndexerJob, IndexerJobConfig}, PathResolver}, + ops::indexing::{job::{IndexerJob, IndexerJobConfig}, PathResolver}, domain::addressing::SdPath, }; use sea_orm::{ @@ -119,7 +119,7 @@ impl LocationManager { }; let location_record = location_model.insert(&txn).await?; - + // Commit transaction txn.commit().await?; info!("Created location record with ID: {}", location_record.id); @@ -376,7 +376,7 @@ impl LocationManager { .ok_or_else(|| LocationError::LocationNotFound { id: location_id })?; let path = PathResolver::get_full_path(library.db().conn(), location.entry_id).await?; - + let managed_location = ManagedLocation { id: location.uuid, name: location.name.unwrap_or_else(|| "Unknown".to_string()), diff --git a/core/src/location/mod.rs b/core/src/location/mod.rs index 983d53c72..60461e9b9 100644 --- a/core/src/location/mod.rs +++ b/core/src/location/mod.rs @@ -3,17 +3,21 @@ pub mod manager; use crate::{ - infrastructure::{ + domain::addressing::SdPath, + infra::{ database::entities::{self, entry::EntryKind}, events::{Event, EventBus}, jobs::{handle::JobHandle, output::IndexedOutput, types::JobStatus}, }, library::Library, - operations::indexing::{IndexMode as JobIndexMode, IndexerJob, IndexerJobConfig, PathResolver, rules::RuleToggles}, - domain::addressing::SdPath, + ops::indexing::{ + rules::RuleToggles, IndexMode as JobIndexMode, IndexerJob, IndexerJobConfig, PathResolver, + }, }; -use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait}; +use sea_orm::{ + ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, TransactionTrait, +}; use serde::{Deserialize, Serialize}; use std::{path::PathBuf, sync::Arc}; use tokio::fs; @@ -115,7 +119,7 @@ pub enum LocationError { #[error("Invalid path: {0}")] InvalidPath(String), #[error("Job error: {0}")] - Job(#[from] crate::infrastructure::jobs::error::JobError), + Job(#[from] crate::infra::jobs::error::JobError), #[error("Other error: {0}")] Other(String), } @@ -146,12 +150,17 @@ pub async fn create_location( } // Begin transaction to ensure atomicity - let txn = library.db().conn().begin().await + let txn = library + .db() + .conn() + .begin() + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; // First, check if an entry already exists for this path // We need to create a root entry for the location directory - let directory_name = args.path + let directory_name = args + .path .file_name() .and_then(|n| n.to_str()) .unwrap_or("Unknown") @@ -178,7 +187,9 @@ pub async fn create_location( ..Default::default() }; - let entry_record = entry_model.insert(&txn).await + let entry_record = entry_model + .insert(&txn) + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; let entry_id = entry_record.id; @@ -189,7 +200,9 @@ pub async fn create_location( depth: Set(0), ..Default::default() }; - self_closure.insert(&txn).await + self_closure + .insert(&txn) + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; // Add to directory_paths table @@ -198,7 +211,9 @@ pub async fn create_location( path: Set(path_str.to_string()), ..Default::default() }; - dir_path_entry.insert(&txn).await + dir_path_entry + .insert(&txn) + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; // Check if a location already exists for this entry @@ -210,7 +225,8 @@ pub async fn create_location( if existing.is_some() { // Rollback transaction - txn.rollback().await + txn.rollback() + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; return Err(LocationError::LocationExists { path: args.path }); } @@ -241,12 +257,15 @@ pub async fn create_location( updated_at: Set(chrono::Utc::now()), }; - let location_record = location_model.insert(&txn).await + let location_record = location_model + .insert(&txn) + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; let location_db_id = location_record.id; // Commit transaction - txn.commit().await + txn.commit() + .await .map_err(|e| LocationError::DatabaseError(e.to_string()))?; info!("Created location '{}' with ID: {}", name, location_db_id); @@ -544,10 +563,12 @@ async fn update_location_stats( /// Get device UUID for current device async fn get_device_uuid(_library: Arc) -> LocationResult { // Get the current device ID from the global state - let device_uuid = crate::shared::utils::get_current_device_id(); + let device_uuid = crate::common::utils::get_current_device_id(); if device_uuid.is_nil() { - return Err(LocationError::InvalidPath("Current device ID not initialized".to_string())); + return Err(LocationError::InvalidPath( + "Current device ID not initialized".to_string(), + )); } Ok(device_uuid) diff --git a/core/src/operations/devices/revoke/output.rs b/core/src/operations/devices/revoke/output.rs deleted file mode 100644 index 01e891946..000000000 --- a/core/src/operations/devices/revoke/output.rs +++ /dev/null @@ -1,35 +0,0 @@ -//! Device revoke operation output - -use crate::infrastructure::actions::output::ActionOutputTrait; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct DeviceRevokeOutput { - pub device_id: Uuid, - pub device_name: String, - pub reason: Option, -} - -impl ActionOutputTrait for DeviceRevokeOutput { - fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).unwrap_or(serde_json::Value::Null) - } - - fn display_message(&self) -> String { - match &self.reason { - Some(reason) => format!( - "Revoked device '{}' ({}): {}", - self.device_name, self.device_id, reason - ), - None => format!( - "Revoked device '{}' ({})", - self.device_name, self.device_id - ), - } - } - - fn output_type(&self) -> &'static str { - "device.revoke.output" - } -} \ No newline at end of file diff --git a/core/src/operations/libraries/create/output.rs b/core/src/operations/libraries/create/output.rs deleted file mode 100644 index 607721c17..000000000 --- a/core/src/operations/libraries/create/output.rs +++ /dev/null @@ -1,41 +0,0 @@ -//! Library create operation output types - -use crate::infrastructure::actions::output::ActionOutputTrait; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use uuid::Uuid; - -/// Output from library create action dispatch -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LibraryCreateOutput { - pub library_id: Uuid, - pub name: String, - pub path: PathBuf, -} - -impl LibraryCreateOutput { - pub fn new(library_id: Uuid, name: String, path: PathBuf) -> Self { - Self { - library_id, - name, - path, - } - } -} - -impl ActionOutputTrait for LibraryCreateOutput { - fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).unwrap_or(serde_json::Value::Null) - } - - fn display_message(&self) -> String { - format!( - "Created library '{}' with ID {} at {}", - self.name, self.library_id, self.path.display() - ) - } - - fn output_type(&self) -> &'static str { - "library.create.completed" - } -} \ No newline at end of file diff --git a/core/src/operations/libraries/delete/action.rs b/core/src/operations/libraries/delete/action.rs deleted file mode 100644 index 3d5351270..000000000 --- a/core/src/operations/libraries/delete/action.rs +++ /dev/null @@ -1,54 +0,0 @@ -//! Library deletion action handler - -use crate::{ - context::CoreContext, - infrastructure::actions::{ - Action, error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, - }, - register_action_handler, -}; -use async_trait::async_trait; -use serde::{Deserialize, Serialize}; -use std::sync::Arc; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LibraryDeleteAction { - // Library deletion doesn't need additional fields beyond library_id -} - -pub struct LibraryDeleteHandler; - -impl LibraryDeleteHandler { - pub fn new() -> Self { - Self - } -} - -#[async_trait] -impl ActionHandler for LibraryDeleteHandler { - async fn execute( - &self, - context: Arc, - action: Action, - ) -> ActionResult { - if let Action::LibraryDelete(action) = action { - // For now, library deletion is not implemented in the library manager - // This would need to be implemented as a proper method - Err(ActionError::Internal("Library deletion not yet implemented".to_string())) - } else { - Err(crate::infrastructure::actions::error::ActionError::InvalidActionType) - } - } - - fn can_handle(&self, action: &Action) -> bool { - matches!(action, Action::LibraryDelete(_)) - } - - fn supported_actions() -> &'static [&'static str] { - &["library.delete"] - } -} - -// Register this handler -register_action_handler!(LibraryDeleteHandler, "library.delete"); \ No newline at end of file diff --git a/core/src/operations/libraries/export/output.rs b/core/src/operations/libraries/export/output.rs deleted file mode 100644 index 3e5ab9c4d..000000000 --- a/core/src/operations/libraries/export/output.rs +++ /dev/null @@ -1,33 +0,0 @@ -//! Library export operation output - -use crate::infrastructure::actions::output::ActionOutputTrait; -use serde::{Deserialize, Serialize}; -use std::path::PathBuf; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LibraryExportOutput { - pub library_id: Uuid, - pub library_name: String, - pub export_path: PathBuf, - pub exported_files: Vec, -} - -impl ActionOutputTrait for LibraryExportOutput { - fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).unwrap_or(serde_json::Value::Null) - } - - fn display_message(&self) -> String { - format!( - "Exported library '{}' to {} ({} files)", - self.library_name, - self.export_path.display(), - self.exported_files.len() - ) - } - - fn output_type(&self) -> &'static str { - "library.export.output" - } -} \ No newline at end of file diff --git a/core/src/operations/libraries/rename/output.rs b/core/src/operations/libraries/rename/output.rs deleted file mode 100644 index be5544f2e..000000000 --- a/core/src/operations/libraries/rename/output.rs +++ /dev/null @@ -1,29 +0,0 @@ -//! Library rename operation output - -use crate::infrastructure::actions::output::ActionOutputTrait; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LibraryRenameOutput { - pub library_id: Uuid, - pub old_name: String, - pub new_name: String, -} - -impl ActionOutputTrait for LibraryRenameOutput { - fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).unwrap_or(serde_json::Value::Null) - } - - fn display_message(&self) -> String { - format!( - "Renamed library '{}' to '{}'", - self.old_name, self.new_name - ) - } - - fn output_type(&self) -> &'static str { - "library.rename.output" - } -} \ No newline at end of file diff --git a/core/src/operations/locations/rescan/output.rs b/core/src/operations/locations/rescan/output.rs deleted file mode 100644 index e8990088e..000000000 --- a/core/src/operations/locations/rescan/output.rs +++ /dev/null @@ -1,31 +0,0 @@ -//! Location rescan operation output - -use crate::infrastructure::actions::output::ActionOutputTrait; -use serde::{Deserialize, Serialize}; -use uuid::Uuid; - -#[derive(Debug, Clone, Serialize, Deserialize)] -pub struct LocationRescanOutput { - pub location_id: Uuid, - pub location_path: String, - pub job_id: Uuid, - pub full_rescan: bool, -} - -impl ActionOutputTrait for LocationRescanOutput { - fn to_json(&self) -> serde_json::Value { - serde_json::to_value(self).unwrap_or(serde_json::Value::Null) - } - - fn display_message(&self) -> String { - let scan_type = if self.full_rescan { "Full" } else { "Quick" }; - format!( - "{} rescan started for location {} (job: {})", - scan_type, self.location_path, self.job_id - ) - } - - fn output_type(&self) -> &'static str { - "location.rescan.output" - } -} \ No newline at end of file diff --git a/core/src/operations/addressing.rs b/core/src/ops/addressing.rs similarity index 96% rename from core/src/operations/addressing.rs rename to core/src/ops/addressing.rs index 18471f783..542ea07f8 100644 --- a/core/src/operations/addressing.rs +++ b/core/src/ops/addressing.rs @@ -10,7 +10,7 @@ use uuid::Uuid; use crate::{ context::CoreContext, domain::addressing::{PathResolutionError, SdPath}, - infrastructure::database::entities::{ + infra::database::entities::{ content_identity, device, entry, location, ContentIdentity, Device, Entry, Location, }, }; @@ -106,7 +106,7 @@ impl PathResolver { context: &CoreContext, device_id: Uuid, ) -> Result<(), PathResolutionError> { - let current_device_id = crate::shared::utils::get_current_device_id(); + let current_device_id = crate::common::utils::get_current_device_id(); // Local device is always "online" if device_id == current_device_id { @@ -134,7 +134,7 @@ impl PathResolver { /// Get list of currently online devices async fn get_online_devices(&self, context: &CoreContext) -> Vec { - let mut online = vec![crate::shared::utils::get_current_device_id()]; + let mut online = vec![crate::common::utils::get_current_device_id()]; if let Some(networking) = context.get_networking().await { for device in networking.get_connected_devices().await { @@ -150,7 +150,7 @@ impl PathResolver { let mut metrics = HashMap::new(); // Local device has zero latency - let current_device_id = crate::shared::utils::get_current_device_id(); + let current_device_id = crate::common::utils::get_current_device_id(); metrics.insert( current_device_id, DeviceMetrics { @@ -302,7 +302,7 @@ impl PathResolver { .await? { // Build the full path using PathResolver - let path = crate::operations::indexing::path_resolver::PathResolver::get_full_path( + let path = crate::ops::indexing::path_resolver::PathResolver::get_full_path( db, entry.id, ) .await?; @@ -397,7 +397,7 @@ impl PathResolver { if let Some(location) = location_map.get(&entry.id) { if let Some(device) = device_map.get(&location.device_id) { // Build the full path - let path = crate::operations::indexing::path_resolver::PathResolver::get_full_path( + let path = crate::ops::indexing::path_resolver::PathResolver::get_full_path( db, entry.id, ) @@ -428,7 +428,7 @@ impl PathResolver { online_devices: &[Uuid], device_metrics: &HashMap, ) -> Option { - let current_device_id = crate::shared::utils::get_current_device_id(); + let current_device_id = crate::common::utils::get_current_device_id(); let mut candidates: Vec<(f64, &ContentInstance)> = instances .iter() diff --git a/core/src/operations/content/action.rs b/core/src/ops/content/action.rs similarity index 78% rename from core/src/operations/content/action.rs rename to core/src/ops/content/action.rs index 3007796b3..589f3457a 100644 --- a/core/src/operations/content/action.rs +++ b/core/src/ops/content/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -33,7 +33,7 @@ impl ActionHandler for ContentHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { // TODO: Re-enable when ContentAnalysis variant is added back Err(ActionError::Internal("ContentAnalysis action not yet implemented".to_string())) @@ -42,13 +42,13 @@ impl ActionHandler for ContentHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { // TODO: Re-enable when ContentAnalysis variant is added back Err(ActionError::Internal("ContentAnalysis action not yet implemented".to_string())) } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { // TODO: Re-enable when ContentAnalysis variant is added back false } diff --git a/core/src/operations/content/mod.rs b/core/src/ops/content/mod.rs similarity index 94% rename from core/src/operations/content/mod.rs rename to core/src/ops/content/mod.rs index 4a312b2aa..8270be3d6 100644 --- a/core/src/operations/content/mod.rs +++ b/core/src/ops/content/mod.rs @@ -8,14 +8,14 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; -use crate::infrastructure::database::entities::{ +use crate::infra::database::entities::{ content_identity::{self, Entity as ContentIdentity, Model as ContentIdentityModel}, entry::{self, Entity as Entry, Model as EntryModel}, }; -use crate::operations::indexing::PathResolver; +use crate::ops::indexing::PathResolver; pub use action::ContentAction; -use crate::shared::errors::Result; +use crate::common::errors::Result; #[derive(Clone, Debug, Serialize, Deserialize)] pub struct ContentInstance { @@ -60,7 +60,7 @@ impl ContentService { .one(&*self.library_db) .await? .ok_or_else(|| { - crate::shared::errors::CoreError::NotFound("Content identity not found".to_string()) + crate::common::errors::CoreError::NotFound("Content identity not found".to_string()) })?; // Find all entries that reference this content identity @@ -100,7 +100,7 @@ impl ContentService { .one(&*self.library_db) .await? .ok_or_else(|| { - crate::shared::errors::CoreError::NotFound("Content identity not found".to_string()) + crate::common::errors::CoreError::NotFound("Content identity not found".to_string()) })?; Ok(LibraryContentStats { diff --git a/core/src/operations/devices/mod.rs b/core/src/ops/devices/mod.rs similarity index 100% rename from core/src/operations/devices/mod.rs rename to core/src/ops/devices/mod.rs diff --git a/core/src/operations/devices/revoke/action.rs b/core/src/ops/devices/revoke/action.rs similarity index 94% rename from core/src/operations/devices/revoke/action.rs rename to core/src/ops/devices/revoke/action.rs index 72e7a5f21..2d59ec54c 100644 --- a/core/src/operations/devices/revoke/action.rs +++ b/core/src/ops/devices/revoke/action.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, @@ -40,7 +40,7 @@ impl ActionHandler for DeviceRevokeHandler { // Don't allow revoking self let current_device = context.device_manager.to_device() .map_err(|e| ActionError::Internal(format!("Failed to get current device: {}", e)))?; - + if current_device.id == action.device_id { return Err(ActionError::Validation { field: "device_id".to_string(), @@ -60,7 +60,7 @@ impl ActionHandler for DeviceRevokeHandler { ) -> ActionResult { if let Action::DeviceRevoke { library_id, action } = action { let library_manager = &context.library_manager; - + // Get the specific library let library = library_manager .get_library(library_id) @@ -68,9 +68,9 @@ impl ActionHandler for DeviceRevokeHandler { .ok_or(ActionError::LibraryNotFound(library_id))?; // Remove device from database - use crate::infrastructure::database::entities; + use crate::infra::database::entities; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, ModelTrait}; - + let device = entities::device::Entity::find() .filter(entities::device::Column::Uuid.eq(action.device_id)) .one(library.db().conn()) @@ -79,7 +79,7 @@ impl ActionHandler for DeviceRevokeHandler { .ok_or_else(|| ActionError::Internal(format!("Device not found: {}", action.device_id)))?; let device_name = device.name.clone(); - + // Delete the device device.delete(library.db().conn()) .await diff --git a/core/src/operations/devices/revoke/mod.rs b/core/src/ops/devices/revoke/mod.rs similarity index 100% rename from core/src/operations/devices/revoke/mod.rs rename to core/src/ops/devices/revoke/mod.rs diff --git a/core/src/ops/devices/revoke/output.rs b/core/src/ops/devices/revoke/output.rs new file mode 100644 index 000000000..065dd85d4 --- /dev/null +++ b/core/src/ops/devices/revoke/output.rs @@ -0,0 +1,32 @@ +//! Device revoke operation output + +use crate::infra::actions::output::ActionOutputTrait; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct DeviceRevokeOutput { + pub device_id: Uuid, + pub device_name: String, + pub reason: Option, +} + +impl ActionOutputTrait for DeviceRevokeOutput { + fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).unwrap_or(serde_json::Value::Null) + } + + fn display_message(&self) -> String { + match &self.reason { + Some(reason) => format!( + "Revoked device '{}' ({}): {}", + self.device_name, self.device_id, reason + ), + None => format!("Revoked device '{}' ({})", self.device_name, self.device_id), + } + } + + fn output_type(&self) -> &'static str { + "device.revoke.output" + } +} diff --git a/core/src/operations/entries/mod.rs b/core/src/ops/entries/mod.rs similarity index 100% rename from core/src/operations/entries/mod.rs rename to core/src/ops/entries/mod.rs diff --git a/core/src/operations/entries/state.rs b/core/src/ops/entries/state.rs similarity index 100% rename from core/src/operations/entries/state.rs rename to core/src/ops/entries/state.rs diff --git a/core/src/operations/files/copy/action.rs b/core/src/ops/files/copy/action.rs similarity index 98% rename from core/src/operations/files/copy/action.rs rename to core/src/ops/files/copy/action.rs index 320335569..45c760528 100644 --- a/core/src/operations/files/copy/action.rs +++ b/core/src/ops/files/copy/action.rs @@ -7,7 +7,7 @@ use super::{ }; use crate::{ context::CoreContext, - infrastructure::{ + infra::{ actions::{ builder::{ActionBuildError, ActionBuilder}, error::{ActionError, ActionResult}, @@ -161,13 +161,13 @@ impl ActionBuilder for FileCopyActionBuilder { self.validate()?; let options = self.input.to_copy_options(); - + // Convert PathBuf to SdPath (local paths) let sources = self.input.sources.iter() .map(|p| SdPath::local(p)) .collect(); let destination = SdPath::local(&self.input.destination); - + Ok(FileCopyAction { sources, destination, @@ -181,7 +181,7 @@ impl FileCopyActionBuilder { pub fn from_cli_args(args: FileCopyCliArgs) -> Self { Self::from_input(args.into()) } - + /// Create action directly from URI strings (for CLI/API use) pub fn from_uris( source_uris: Vec, @@ -198,13 +198,13 @@ impl FileCopyActionBuilder { )), } } - + // Parse destination URI let destination = SdPath::from_uri(&destination_uri) .map_err(|e| ActionBuildError::validation( format!("Invalid destination URI '{}': {:?}", destination_uri, e) ))?; - + Ok(FileCopyAction { sources, destination, @@ -336,8 +336,8 @@ register_action_handler!(FileCopyHandler, "file.copy"); mod tests { use super::*; use crate::{ - infrastructure::cli::adapters::{copy::CopyMethodCli, FileCopyCliArgs}, - operations::files::input::CopyMethod, + infra::cli::adapters::{copy::CopyMethodCli, FileCopyCliArgs}, + ops::files::input::CopyMethod, }; use std::path::PathBuf; diff --git a/core/src/operations/files/copy/database.rs b/core/src/ops/files/copy/database.rs similarity index 95% rename from core/src/operations/files/copy/database.rs rename to core/src/ops/files/copy/database.rs index 36ca71295..4614daaab 100644 --- a/core/src/operations/files/copy/database.rs +++ b/core/src/ops/files/copy/database.rs @@ -4,9 +4,9 @@ //! Spacedrive's indexed data, enabling immediate progress feedback. use crate::{ - infrastructure::database::entities::{entry, location, Entry}, - operations::indexing::PathResolver, domain::addressing::SdPath, + infra::database::entities::{entry, location, Entry}, + ops::indexing::PathResolver, }; use anyhow::Result; use sea_orm::{prelude::*, Condition, DatabaseConnection, QuerySelect}; @@ -51,14 +51,13 @@ impl CopyDatabaseQuery { let path_str = path.to_string_lossy().to_string(); // Get all locations to find which one contains this path - let locations = location::Entity::find() - .all(&self.db) - .await?; + let locations = location::Entity::find().all(&self.db).await?; // Check each location to see if it contains this path for location in locations { // Get the full path of the location's root entry - let location_path = match PathResolver::get_full_path(&self.db, location.entry_id).await { + let location_path = match PathResolver::get_full_path(&self.db, location.entry_id).await + { Ok(path) => path, Err(_) => continue, // Skip if we can't get the path }; diff --git a/core/src/operations/files/copy/docs/ANALYSIS.md b/core/src/ops/files/copy/docs/ANALYSIS.md similarity index 100% rename from core/src/operations/files/copy/docs/ANALYSIS.md rename to core/src/ops/files/copy/docs/ANALYSIS.md diff --git a/core/src/operations/files/copy/docs/FILE_SYNC_OVERLAP.md b/core/src/ops/files/copy/docs/FILE_SYNC_OVERLAP.md similarity index 100% rename from core/src/operations/files/copy/docs/FILE_SYNC_OVERLAP.md rename to core/src/ops/files/copy/docs/FILE_SYNC_OVERLAP.md diff --git a/core/src/operations/files/copy/docs/PROGRESSIVE_COPY_DESIGN.md b/core/src/ops/files/copy/docs/PROGRESSIVE_COPY_DESIGN.md similarity index 100% rename from core/src/operations/files/copy/docs/PROGRESSIVE_COPY_DESIGN.md rename to core/src/ops/files/copy/docs/PROGRESSIVE_COPY_DESIGN.md diff --git a/core/src/operations/files/copy/docs/RESUME_VALIDATION_DESIGN.md b/core/src/ops/files/copy/docs/RESUME_VALIDATION_DESIGN.md similarity index 100% rename from core/src/operations/files/copy/docs/RESUME_VALIDATION_DESIGN.md rename to core/src/ops/files/copy/docs/RESUME_VALIDATION_DESIGN.md diff --git a/core/src/operations/files/copy/input.rs b/core/src/ops/files/copy/input.rs similarity index 100% rename from core/src/operations/files/copy/input.rs rename to core/src/ops/files/copy/input.rs diff --git a/core/src/operations/files/copy/job.rs b/core/src/ops/files/copy/job.rs similarity index 99% rename from core/src/operations/files/copy/job.rs rename to core/src/ops/files/copy/job.rs index 9aa626a9e..785c18181 100644 --- a/core/src/operations/files/copy/job.rs +++ b/core/src/ops/files/copy/job.rs @@ -2,8 +2,8 @@ use super::{database::CopyDatabaseQuery, input::CopyMethod, routing::CopyStrategyRouter}; use crate::{ - infrastructure::jobs::generic_progress::{GenericProgress, ToGenericProgress}, - infrastructure::jobs::{prelude::*, traits::Resourceful}, + infra::jobs::generic_progress::{GenericProgress, ToGenericProgress}, + infra::jobs::{prelude::*, traits::Resourceful}, domain::addressing::{SdPath, SdPathBatch}, }; use serde::{Deserialize, Serialize}; @@ -71,7 +71,7 @@ impl Job for FileCopyJob { const DESCRIPTION: Option<&'static str> = Some("Copy or move files to a destination"); } -impl crate::infrastructure::jobs::traits::DynJob for FileCopyJob { +impl crate::infra::jobs::traits::DynJob for FileCopyJob { fn job_name(&self) -> &'static str { Self::NAME } @@ -226,14 +226,14 @@ impl JobHandler for FileCopyJob { // Resolve destination path first if it's content-based let resolved_destination = self.destination.resolve_in_job(&ctx).await .map_err(|e| JobError::execution(format!("Failed to resolve destination path: {}", e)))?; - + // Update destination to the resolved physical path self.destination = resolved_destination; // Process each source using the appropriate strategy for (index, source) in self.sources.paths.iter().enumerate() { ctx.check_interrupt().await?; - + // Resolve source path if it's content-based let resolved_source = source.resolve_in_job(&ctx).await .map_err(|e| JobError::execution(format!("Failed to resolve source path: {}", e)))?; @@ -886,7 +886,7 @@ impl Job for MoveJob { const DESCRIPTION: Option<&'static str> = Some("Move or rename files and directories"); } -impl crate::infrastructure::jobs::traits::DynJob for MoveJob { +impl crate::infra::jobs::traits::DynJob for MoveJob { fn job_name(&self) -> &'static str { Self::NAME } diff --git a/core/src/operations/files/copy/mod.rs b/core/src/ops/files/copy/mod.rs similarity index 100% rename from core/src/operations/files/copy/mod.rs rename to core/src/ops/files/copy/mod.rs diff --git a/core/src/operations/files/copy/output.rs b/core/src/ops/files/copy/output.rs similarity index 92% rename from core/src/operations/files/copy/output.rs rename to core/src/ops/files/copy/output.rs index 8513d02ca..8e59e80d9 100644 --- a/core/src/operations/files/copy/output.rs +++ b/core/src/ops/files/copy/output.rs @@ -1,6 +1,6 @@ //! File copy operation output types -use crate::infrastructure::actions::output::ActionOutputTrait; +use crate::infra::actions::output::ActionOutputTrait; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -26,14 +26,14 @@ impl ActionOutputTrait for FileCopyActionOutput { fn to_json(&self) -> serde_json::Value { serde_json::to_value(self).unwrap_or(serde_json::Value::Null) } - + fn display_message(&self) -> String { format!( "Dispatched file copy job {} for {} source(s) to {}", self.job_id, self.sources_count, self.destination ) } - + fn output_type(&self) -> &'static str { "file.copy.dispatched" } diff --git a/core/src/operations/files/copy/routing.rs b/core/src/ops/files/copy/routing.rs similarity index 100% rename from core/src/operations/files/copy/routing.rs rename to core/src/ops/files/copy/routing.rs diff --git a/core/src/operations/files/copy/strategy.rs b/core/src/ops/files/copy/strategy.rs similarity index 94% rename from core/src/operations/files/copy/strategy.rs rename to core/src/ops/files/copy/strategy.rs index 8442ab791..26b9fd110 100644 --- a/core/src/operations/files/copy/strategy.rs +++ b/core/src/ops/files/copy/strategy.rs @@ -1,10 +1,10 @@ //! Copy strategy implementations for different file operation scenarios use crate::{ - infrastructure::jobs::prelude::*, + infra::jobs::prelude::*, domain::addressing::SdPath, volume::VolumeManager, - operations::files::copy::job::CopyPhase, + ops::files::copy::job::CopyPhase, }; use anyhow::Result; use async_trait::async_trait; @@ -134,7 +134,7 @@ impl CopyStrategy for RemoteTransferStrategy { )); // Create file metadata for transfer - let file_metadata = crate::services::networking::protocols::FileMetadata { + let file_metadata = crate::service::networking::protocols::FileMetadata { name: local_path.file_name() .unwrap_or_default() .to_string_lossy() @@ -155,14 +155,14 @@ impl CopyStrategy for RemoteTransferStrategy { .ok_or_else(|| anyhow::anyhow!("File transfer protocol not registered"))?; let file_transfer_protocol = file_transfer_handler.as_any() - .downcast_ref::() + .downcast_ref::() .ok_or_else(|| anyhow::anyhow!("Invalid file transfer protocol handler"))?; // Initiate transfer let transfer_id = file_transfer_protocol.initiate_transfer( destination.device_id().unwrap_or_default(), local_path.to_path_buf(), - crate::services::networking::protocols::TransferMode::TrustedCopy, + crate::service::networking::protocols::TransferMode::TrustedCopy, ).await?; ctx.log(format!("Transfer initiated with ID: {}", transfer_id)); @@ -171,10 +171,10 @@ impl CopyStrategy for RemoteTransferStrategy { let chunk_size = 64 * 1024u32; let total_chunks = ((file_size + chunk_size as u64 - 1) / chunk_size as u64) as u32; - let transfer_request = crate::services::networking::protocols::file_transfer::FileTransferMessage::TransferRequest { + let transfer_request = crate::service::networking::protocols::file_transfer::FileTransferMessage::TransferRequest { transfer_id, file_metadata: file_metadata.clone(), - transfer_mode: crate::services::networking::protocols::TransferMode::TrustedCopy, + transfer_mode: crate::service::networking::protocols::TransferMode::TrustedCopy, chunk_size, total_chunks, destination_path: destination.path().map(|p| p.to_string_lossy().to_string()).unwrap_or_default(), @@ -242,12 +242,12 @@ async fn copy_single_file<'a>( progress_callback: Option<&ProgressCallback<'a>>, ) -> Result { let result = copy_single_file_with_offset(source, destination, volume_info, ctx, verify_checksum, file_size, progress_callback, 0).await?; - + // For single file copies, send completion signal if let Some(callback) = progress_callback { callback(result, u64::MAX); } - + Ok(result) } @@ -287,7 +287,7 @@ async fn copy_single_file_with_offset<'a>( } else { None }; - + let mut dest_hasher = if verify_checksum { Some(blake3::Hasher::new()) } else { @@ -328,14 +328,14 @@ async fn copy_single_file_with_offset<'a>( // Send bytes copied within current file // The aggregator will add this to the bytes_completed_before_current callback(total_copied, file_size); - + // Debug log every 100MB if total_copied % (100 * 1024 * 1024) < bytes_read as u64 { ctx.log(format!("Strategy progress callback: {} / {} bytes", total_copied, file_size)); } } last_progress_update = std::time::Instant::now(); - + // Explicitly yield to the scheduler to allow other tasks (like progress reporting) to run tokio::task::yield_now().await; } @@ -356,7 +356,7 @@ async fn copy_single_file_with_offset<'a>( if let (Some(source_hasher), Some(dest_hasher)) = (source_hasher, dest_hasher) { let source_hash = source_hasher.finalize(); let dest_hash = dest_hasher.finalize(); - + if source_hash != dest_hash { // Clean up corrupted file let _ = fs::remove_file(destination).await; @@ -365,7 +365,7 @@ async fn copy_single_file_with_offset<'a>( format!("Checksum verification failed: source={}, dest={}", source_hash.to_hex(), dest_hash.to_hex()) )); } - + ctx.log(format!( "Checksum verification passed for {}: {}", destination.display(), @@ -377,7 +377,7 @@ async fn copy_single_file_with_offset<'a>( // Copy file permissions and timestamps if requested let source_metadata = fs::metadata(source).await?; let dest_file = fs::File::open(destination).await?; - + #[cfg(unix)] { use std::os::unix::fs::PermissionsExt; @@ -408,11 +408,11 @@ async fn copy_file_streaming<'a>( // For directories, we need to accumulate progress across multiple files fs::create_dir_all(destination).await?; let mut total_size = 0u64; - + // First, collect all files to copy let mut files_to_copy = Vec::new(); let mut stack = vec![(source.to_path_buf(), destination.to_path_buf())]; - + while let Some((src_path, dest_path)) = stack.pop() { if src_path.is_file() { files_to_copy.push((src_path, dest_path)); @@ -426,7 +426,7 @@ async fn copy_file_streaming<'a>( } } } - + // Now copy all files, tracking cumulative progress let mut cumulative_bytes = 0u64; for (src_path, dest_path) in files_to_copy { @@ -437,22 +437,22 @@ async fn copy_file_streaming<'a>( "Operation cancelled" )); } - + let file_metadata = fs::metadata(&src_path).await?; let file_size = file_metadata.len(); - - + + // Copy the file (offset no longer needed as aggregator tracks it) let bytes_copied = copy_single_file_with_offset(&src_path, &dest_path, volume_info, ctx, verify_checksum, file_size, progress_callback, 0).await?; cumulative_bytes += bytes_copied; total_size += bytes_copied; - + // Signal completion of this file, passing its total size if let Some(callback) = progress_callback { callback(bytes_copied, u64::MAX); // Send file size and MAX signal } } - + return Ok(total_size); } @@ -472,7 +472,7 @@ async fn calculate_file_checksum(path: &Path) -> Result { async fn stream_file_data<'a>( file_path: &Path, transfer_id: uuid::Uuid, - file_transfer_protocol: &crate::services::networking::protocols::FileTransferProtocolHandler, + file_transfer_protocol: &crate::service::networking::protocols::FileTransferProtocolHandler, total_size: u64, destination_device_id: uuid::Uuid, ctx: &JobContext<'a>, @@ -525,7 +525,7 @@ async fn stream_file_data<'a>( )?; // Create encrypted file chunk message - let chunk_message = crate::services::networking::protocols::file_transfer::FileTransferMessage::FileChunk { + let chunk_message = crate::service::networking::protocols::file_transfer::FileTransferMessage::FileChunk { transfer_id, chunk_index, data: encrypted_data, @@ -560,7 +560,7 @@ async fn stream_file_data<'a>( // Send transfer completion message let final_checksum = calculate_file_checksum(file_path).await?; - let completion_message = crate::services::networking::protocols::file_transfer::FileTransferMessage::TransferComplete { + let completion_message = crate::service::networking::protocols::file_transfer::FileTransferMessage::TransferComplete { transfer_id, final_checksum, total_bytes: bytes_transferred, @@ -578,7 +578,7 @@ async fn stream_file_data<'a>( // Mark transfer as completed locally file_transfer_protocol.update_session_state( &transfer_id, - crate::services::networking::protocols::file_transfer::TransferState::Completed, + crate::service::networking::protocols::file_transfer::TransferState::Completed, )?; ctx.log(format!("File streaming completed: {} chunks, {} bytes sent to device {}", diff --git a/core/src/operations/files/delete/action.rs b/core/src/ops/files/delete/action.rs similarity index 98% rename from core/src/operations/files/delete/action.rs rename to core/src/ops/files/delete/action.rs index 3e5425992..4426c0b11 100644 --- a/core/src/operations/files/delete/action.rs +++ b/core/src/ops/files/delete/action.rs @@ -4,7 +4,7 @@ use super::job::{DeleteJob, DeleteMode, DeleteOptions}; use super::output::FileDeleteOutput; use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, diff --git a/core/src/operations/files/delete/job.rs b/core/src/ops/files/delete/job.rs similarity index 98% rename from core/src/operations/files/delete/job.rs rename to core/src/ops/files/delete/job.rs index fe2869d96..61e0379dd 100644 --- a/core/src/operations/files/delete/job.rs +++ b/core/src/ops/files/delete/job.rs @@ -1,6 +1,6 @@ //! Delete job implementation -use crate::{infrastructure::jobs::prelude::*, domain::addressing::SdPathBatch}; +use crate::{infra::jobs::prelude::*, domain::addressing::SdPathBatch}; use serde::{Deserialize, Serialize}; use std::{ path::PathBuf, @@ -69,7 +69,7 @@ impl Job for DeleteJob { const DESCRIPTION: Option<&'static str> = Some("Delete files and directories"); } -impl crate::infrastructure::jobs::traits::DynJob for DeleteJob { +impl crate::infra::jobs::traits::DynJob for DeleteJob { fn job_name(&self) -> &'static str { Self::NAME } @@ -115,7 +115,7 @@ impl JobHandler for DeleteJob { // Process deletions for (index, target) in self.targets.paths.iter().enumerate() { ctx.check_interrupt().await?; - + // Resolve target path if it's content-based let resolved_target = target.resolve_in_job(&ctx).await .map_err(|e| JobError::execution(format!("Failed to resolve target path: {}", e)))?; diff --git a/core/src/operations/files/delete/mod.rs b/core/src/ops/files/delete/mod.rs similarity index 100% rename from core/src/operations/files/delete/mod.rs rename to core/src/ops/files/delete/mod.rs diff --git a/core/src/operations/files/delete/output.rs b/core/src/ops/files/delete/output.rs similarity index 92% rename from core/src/operations/files/delete/output.rs rename to core/src/ops/files/delete/output.rs index aa8b87756..292a2dfeb 100644 --- a/core/src/operations/files/delete/output.rs +++ b/core/src/ops/files/delete/output.rs @@ -1,6 +1,6 @@ //! File delete operation output types -use crate::infrastructure::actions::output::ActionOutputTrait; +use crate::infra::actions::output::ActionOutputTrait; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -24,14 +24,14 @@ impl ActionOutputTrait for FileDeleteOutput { fn to_json(&self) -> serde_json::Value { serde_json::to_value(self).unwrap_or(serde_json::Value::Null) } - + fn display_message(&self) -> String { format!( "Dispatched file delete job {} for {} file(s)", self.job_id, self.targets_count ) } - + fn output_type(&self) -> &'static str { "file.delete.dispatched" } diff --git a/core/src/operations/files/duplicate_detection/action.rs b/core/src/ops/files/duplicate_detection/action.rs similarity index 81% rename from core/src/operations/files/duplicate_detection/action.rs rename to core/src/ops/files/duplicate_detection/action.rs index 5894ab284..1566d0c8e 100644 --- a/core/src/operations/files/duplicate_detection/action.rs +++ b/core/src/ops/files/duplicate_detection/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -35,9 +35,9 @@ impl ActionHandler for DuplicateDetectionHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { - if let crate::infrastructure::actions::Action::DetectDuplicates { action, .. } = action { + if let crate::infra::actions::Action::DetectDuplicates { action, .. } = action { if action.paths.is_empty() { return Err(ActionError::Validation { field: "paths".to_string(), @@ -53,11 +53,11 @@ impl ActionHandler for DuplicateDetectionHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { - if let crate::infrastructure::actions::Action::DetectDuplicates { library_id, action } = action { + if let crate::infra::actions::Action::DetectDuplicates { library_id, action } = action { let library_manager = &context.library_manager; - + let library = library_manager.get_library(library_id).await .ok_or(ActionError::Internal(format!("Library not found: {}", library_id)))?; @@ -91,8 +91,8 @@ impl ActionHandler for DuplicateDetectionHandler { } } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { - matches!(action, crate::infrastructure::actions::Action::DetectDuplicates { .. }) + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { + matches!(action, crate::infra::actions::Action::DetectDuplicates { .. }) } fn supported_actions() -> &'static [&'static str] { diff --git a/core/src/operations/files/duplicate_detection/job.rs b/core/src/ops/files/duplicate_detection/job.rs similarity index 99% rename from core/src/operations/files/duplicate_detection/job.rs rename to core/src/ops/files/duplicate_detection/job.rs index 7e9e504a5..c66978a46 100644 --- a/core/src/operations/files/duplicate_detection/job.rs +++ b/core/src/ops/files/duplicate_detection/job.rs @@ -2,7 +2,7 @@ use crate::{ domain::content_identity::ContentHashGenerator, - infrastructure::jobs::prelude::*, + infra::jobs::prelude::*, domain::addressing::{SdPath, SdPathBatch}, }; use serde::{Deserialize, Serialize}; @@ -82,7 +82,7 @@ impl Job for DuplicateDetectionJob { const DESCRIPTION: Option<&'static str> = Some("Find duplicate files"); } -impl crate::infrastructure::jobs::traits::DynJob for DuplicateDetectionJob { +impl crate::infra::jobs::traits::DynJob for DuplicateDetectionJob { fn job_name(&self) -> &'static str { Self::NAME } diff --git a/core/src/operations/files/duplicate_detection/mod.rs b/core/src/ops/files/duplicate_detection/mod.rs similarity index 100% rename from core/src/operations/files/duplicate_detection/mod.rs rename to core/src/ops/files/duplicate_detection/mod.rs diff --git a/core/src/operations/files/mod.rs b/core/src/ops/files/mod.rs similarity index 100% rename from core/src/operations/files/mod.rs rename to core/src/ops/files/mod.rs diff --git a/core/src/operations/files/validation/action.rs b/core/src/ops/files/validation/action.rs similarity index 79% rename from core/src/operations/files/validation/action.rs rename to core/src/ops/files/validation/action.rs index 42c608a1b..ee9e2e320 100644 --- a/core/src/operations/files/validation/action.rs +++ b/core/src/ops/files/validation/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -35,9 +35,9 @@ impl ActionHandler for ValidationHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { - if let crate::infrastructure::actions::Action::FileValidate { action, .. } = action { + if let crate::infra::actions::Action::FileValidate { action, .. } = action { if action.paths.is_empty() { return Err(ActionError::Validation { field: "paths".to_string(), @@ -53,11 +53,11 @@ impl ActionHandler for ValidationHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { - if let crate::infrastructure::actions::Action::FileValidate { library_id, action } = action { + if let crate::infra::actions::Action::FileValidate { library_id, action } = action { let library_manager = &context.library_manager; - + let library = library_manager.get_library(library_id).await .ok_or(ActionError::Internal(format!("Library not found: {}", library_id)))?; @@ -91,8 +91,8 @@ impl ActionHandler for ValidationHandler { } } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { - matches!(action, crate::infrastructure::actions::Action::FileValidate { .. }) + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { + matches!(action, crate::infra::actions::Action::FileValidate { .. }) } fn supported_actions() -> &'static [&'static str] { diff --git a/core/src/operations/files/validation/job.rs b/core/src/ops/files/validation/job.rs similarity index 99% rename from core/src/operations/files/validation/job.rs rename to core/src/ops/files/validation/job.rs index 358f09f38..f535e08a3 100644 --- a/core/src/operations/files/validation/job.rs +++ b/core/src/ops/files/validation/job.rs @@ -1,9 +1,9 @@ //! File validation and integrity checking job use crate::{ - domain::content_identity::ContentHashGenerator, - infrastructure::jobs::prelude::*, domain::addressing::{SdPath, SdPathBatch}, + domain::content_identity::ContentHashGenerator, + infra::jobs::prelude::*, }; use serde::{Deserialize, Serialize}; use std::{ @@ -80,7 +80,7 @@ impl Job for ValidationJob { const DESCRIPTION: Option<&'static str> = Some("Validate file integrity and accessibility"); } -impl crate::infrastructure::jobs::traits::DynJob for ValidationJob { +impl crate::infra::jobs::traits::DynJob for ValidationJob { fn job_name(&self) -> &'static str { Self::NAME } diff --git a/core/src/operations/files/validation/mod.rs b/core/src/ops/files/validation/mod.rs similarity index 100% rename from core/src/operations/files/validation/mod.rs rename to core/src/ops/files/validation/mod.rs diff --git a/core/src/operations/indexing/action.rs b/core/src/ops/indexing/action.rs similarity index 79% rename from core/src/operations/indexing/action.rs rename to core/src/ops/indexing/action.rs index 99f4ed1b5..2566e5922 100644 --- a/core/src/operations/indexing/action.rs +++ b/core/src/ops/indexing/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -35,9 +35,9 @@ impl ActionHandler for IndexingHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { - if let crate::infrastructure::actions::Action::Index { action, .. } = action { + if let crate::infra::actions::Action::Index { action, .. } = action { if action.paths.is_empty() { return Err(ActionError::Validation { field: "paths".to_string(), @@ -53,11 +53,11 @@ impl ActionHandler for IndexingHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { - if let crate::infrastructure::actions::Action::Index { library_id, action } = action { + if let crate::infra::actions::Action::Index { library_id, action } = action { let library_manager = &context.library_manager; - + let library = library_manager.get_library(library_id).await .ok_or(ActionError::Internal(format!("Library not found: {}", library_id)))?; @@ -73,7 +73,7 @@ impl ActionHandler for IndexingHandler { // TODO: Need location_id - for now using a placeholder let job = IndexerJob::from_location( Uuid::new_v4(), // placeholder location_id - SdPath::local(first_path), + SdPath::local(first_path), IndexMode::Content // default mode ); @@ -90,8 +90,8 @@ impl ActionHandler for IndexingHandler { } } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { - matches!(action, crate::infrastructure::actions::Action::Index { .. }) + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { + matches!(action, crate::infra::actions::Action::Index { .. }) } fn supported_actions() -> &'static [&'static str] { diff --git a/core/src/operations/indexing/change_detection/mod.rs b/core/src/ops/indexing/change_detection/mod.rs similarity index 96% rename from core/src/operations/indexing/change_detection/mod.rs rename to core/src/ops/indexing/change_detection/mod.rs index 8d5bcbffd..168b236e3 100644 --- a/core/src/operations/indexing/change_detection/mod.rs +++ b/core/src/ops/indexing/change_detection/mod.rs @@ -7,7 +7,7 @@ //! - Directory hierarchy tracking use super::state::EntryKind; -use crate::infrastructure::{database::entities, jobs::prelude::JobContext}; +use crate::infra::{database::entities, jobs::prelude::JobContext}; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter, QuerySelect}; use std::{ collections::HashMap, @@ -79,26 +79,26 @@ impl ChangeDetector { ctx: &JobContext<'_>, location_id: i32, indexing_path: &Path, - ) -> Result<(), crate::infrastructure::jobs::prelude::JobError> { - use crate::infrastructure::jobs::prelude::JobError; + ) -> Result<(), crate::infra::jobs::prelude::JobError> { + use crate::infra::jobs::prelude::JobError; use super::persistence::{DatabasePersistence, IndexPersistence}; // For change detection, we need to get the location's root entry ID - use crate::infrastructure::database::entities; + use crate::infra::database::entities; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; - + let location_record = entities::location::Entity::find_by_id(location_id) .one(ctx.library_db()) .await .map_err(|e| JobError::execution(format!("Failed to find location: {}", e)))? .ok_or_else(|| JobError::execution("Location not found".to_string()))?; - + // Create a database persistence instance to leverage the scoped query logic let persistence = DatabasePersistence::new(ctx, 0, Some(location_record.entry_id)); // device_id not needed for query - + // Use the scoped query method let existing_entries = persistence.get_existing_entries(indexing_path).await?; - + // Process the results into our internal data structures for (full_path, (id, inode, modified_time)) in existing_entries { // Determine entry kind from the path (we could query this, but for change detection we mainly care about existence) diff --git a/core/src/operations/indexing/entry.rs b/core/src/ops/indexing/entry.rs similarity index 99% rename from core/src/operations/indexing/entry.rs rename to core/src/ops/indexing/entry.rs index 72c292f00..9f3560f60 100644 --- a/core/src/operations/indexing/entry.rs +++ b/core/src/ops/indexing/entry.rs @@ -3,8 +3,8 @@ use super::path_resolver::PathResolver; use super::state::{DirEntry, EntryKind, IndexerState}; use crate::{ - file_type::FileTypeRegistry, - infrastructure::{ + filetype::FileTypeRegistry, + infra::{ database::entities::{self, directory_paths, entry_closure}, jobs::prelude::{JobContext, JobError}, }, diff --git a/core/src/operations/indexing/hierarchy.rs b/core/src/ops/indexing/hierarchy.rs similarity index 98% rename from core/src/operations/indexing/hierarchy.rs rename to core/src/ops/indexing/hierarchy.rs index de452ff67..b02abc606 100644 --- a/core/src/operations/indexing/hierarchy.rs +++ b/core/src/ops/indexing/hierarchy.rs @@ -1,6 +1,6 @@ //! Hierarchical query helpers using closure table -use crate::infrastructure::database::entities::{entry, entry_closure}; +use crate::infra::database::entities::{entry, entry_closure}; use sea_orm::{ ColumnTrait, Condition, DbConn, EntityTrait, JoinType, PaginatorTrait, QueryFilter, QueryOrder, QuerySelect, RelationTrait, diff --git a/core/src/operations/indexing/job.rs b/core/src/ops/indexing/job.rs similarity index 99% rename from core/src/operations/indexing/job.rs rename to core/src/ops/indexing/job.rs index 62f1b28f0..e066d7d74 100644 --- a/core/src/operations/indexing/job.rs +++ b/core/src/ops/indexing/job.rs @@ -2,8 +2,8 @@ use crate::{ domain::addressing::SdPath, - infrastructure::database::entities, - infrastructure::jobs::{ + infra::database::entities, + infra::jobs::{ prelude::*, traits::{DynJob, Resourceful}, }, diff --git a/core/src/operations/indexing/metrics.rs b/core/src/ops/indexing/metrics.rs similarity index 100% rename from core/src/operations/indexing/metrics.rs rename to core/src/ops/indexing/metrics.rs diff --git a/core/src/operations/indexing/mod.rs b/core/src/ops/indexing/mod.rs similarity index 100% rename from core/src/operations/indexing/mod.rs rename to core/src/ops/indexing/mod.rs diff --git a/core/src/operations/indexing/path_resolver.rs b/core/src/ops/indexing/path_resolver.rs similarity index 95% rename from core/src/operations/indexing/path_resolver.rs rename to core/src/ops/indexing/path_resolver.rs index 9e080424d..78a43de4e 100644 --- a/core/src/operations/indexing/path_resolver.rs +++ b/core/src/ops/indexing/path_resolver.rs @@ -6,7 +6,7 @@ use std::path::PathBuf; use sea_orm::{prelude::*, ConnectionTrait, QuerySelect, Statement}; -use crate::infrastructure::database::entities::{directory_paths, entry, DirectoryPaths, Entry}; +use crate::infra::database::entities::{directory_paths, entry, DirectoryPaths, Entry}; pub struct PathResolver; @@ -23,7 +23,7 @@ impl PathResolver { .ok_or_else(|| DbErr::RecordNotFound(format!("Entry {} not found", entry_id)))?; match entry.entry_kind() { - crate::infrastructure::database::entities::entry::EntryKind::Directory => { + crate::infra::database::entities::entry::EntryKind::Directory => { // For directories, lookup in directory_paths table let dir_path = DirectoryPaths::find_by_id(entry_id) .one(db) @@ -113,7 +113,7 @@ impl PathResolver { for entry in entries { match entry.entry_kind() { - crate::infrastructure::database::entities::entry::EntryKind::Directory => { + crate::infra::database::entities::entry::EntryKind::Directory => { directory_ids.push(entry.id); } _ => { diff --git a/core/src/operations/indexing/persistence.rs b/core/src/ops/indexing/persistence.rs similarity index 89% rename from core/src/operations/indexing/persistence.rs rename to core/src/ops/indexing/persistence.rs index 13f7f9e05..514b6746e 100644 --- a/core/src/operations/indexing/persistence.rs +++ b/core/src/ops/indexing/persistence.rs @@ -4,14 +4,21 @@ //! either persistently in the database or ephemerally in memory. use crate::{ - file_type::FileTypeRegistry, - infrastructure::{ + filetype::FileTypeRegistry, + infra::{ database::entities::{self, entry_closure}, jobs::prelude::{JobContext, JobError, JobResult}, }, }; -use sea_orm::{ActiveModelTrait, ActiveValue::Set, ColumnTrait, EntityTrait, QueryFilter, JoinType, RelationTrait, Condition, DbBackend, Statement, TransactionTrait, QuerySelect, ConnectionTrait}; -use std::{collections::HashMap, path::{Path, PathBuf}, sync::Arc}; +use sea_orm::{ + ActiveModelTrait, ActiveValue::Set, ColumnTrait, Condition, ConnectionTrait, DbBackend, + EntityTrait, JoinType, QueryFilter, QuerySelect, RelationTrait, Statement, TransactionTrait, +}; +use std::{ + collections::HashMap, + path::{Path, PathBuf}, + sync::Arc, +}; use tokio::sync::RwLock; use uuid::Uuid; @@ -62,7 +69,11 @@ pub struct DatabasePersistence<'a> { } impl<'a> DatabasePersistence<'a> { - pub fn new(ctx: &'a JobContext<'a>, device_id: i32, location_root_entry_id: Option) -> Self { + pub fn new( + ctx: &'a JobContext<'a>, + device_id: i32, + location_root_entry_id: Option, + ) -> Self { Self { ctx, device_id, @@ -90,7 +101,6 @@ impl<'a> IndexPersistence for DatabasePersistence<'a> { None }; - // Extract file extension (without dot) for files, None for directories let extension = match entry.kind { EntryKind::File => entry @@ -118,7 +128,7 @@ impl<'a> IndexPersistence for DatabasePersistence<'a> { .map(|n| n.to_string_lossy().to_string()) .unwrap_or_else(|| "unknown".to_string()) }) - }, + } EntryKind::Directory | EntryKind::Symlink => { // For directories and symlinks, use full name entry @@ -173,7 +183,11 @@ impl<'a> IndexPersistence for DatabasePersistence<'a> { }; // Begin transaction for atomic entry creation and closure table population - let txn = self.ctx.library_db().begin().await + let txn = self + .ctx + .library_db() + .begin() + .await .map_err(|e| JobError::execution(format!("Failed to begin transaction: {}", e)))?; // Insert the entry @@ -207,7 +221,9 @@ impl<'a> IndexPersistence for DatabasePersistence<'a> { vec![result.id.into(), parent_id.into()], )) .await - .map_err(|e| JobError::execution(format!("Failed to populate ancestor closures: {}", e)))?; + .map_err(|e| { + JobError::execution(format!("Failed to populate ancestor closures: {}", e)) + })?; } // If this is a directory, populate the directory_paths table @@ -221,14 +237,14 @@ impl<'a> IndexPersistence for DatabasePersistence<'a> { path: Set(absolute_path), ..Default::default() }; - dir_path_entry - .insert(&txn) - .await - .map_err(|e| JobError::execution(format!("Failed to insert directory path: {}", e)))?; + dir_path_entry.insert(&txn).await.map_err(|e| { + JobError::execution(format!("Failed to insert directory path: {}", e)) + })?; } // Commit transaction - txn.commit().await + txn.commit() + .await .map_err(|e| JobError::execution(format!("Failed to commit transaction: {}", e)))?; // Cache the entry ID for potential children @@ -282,17 +298,19 @@ impl<'a> IndexPersistence for DatabasePersistence<'a> { let mut all_entry_ids = vec![location_root_entry_id]; all_entry_ids.extend(descendant_ids); - // Fetch all entries (chunked to avoid SQLite variable limit) - let mut existing_entries: Vec = Vec::new(); - let chunk_size: usize = 900; - for chunk in all_entry_ids.chunks(chunk_size) { - let mut batch = entities::entry::Entity::find() - .filter(entities::entry::Column::Id.is_in(chunk.to_vec())) - .all(self.ctx.library_db()) - .await - .map_err(|e| JobError::execution(format!("Failed to query existing entries: {}", e)))?; - existing_entries.append(&mut batch); - } + // Fetch all entries (chunked to avoid SQLite variable limit) + let mut existing_entries: Vec = Vec::new(); + let chunk_size: usize = 900; + for chunk in all_entry_ids.chunks(chunk_size) { + let mut batch = entities::entry::Entity::find() + .filter(entities::entry::Column::Id.is_in(chunk.to_vec())) + .all(self.ctx.library_db()) + .await + .map_err(|e| { + JobError::execution(format!("Failed to query existing entries: {}", e)) + })?; + existing_entries.append(&mut batch); + } let mut result = HashMap::new(); @@ -457,7 +475,11 @@ impl PersistenceFactory { device_id: i32, location_root_entry_id: Option, ) -> Box { - Box::new(DatabasePersistence::new(ctx, device_id, location_root_entry_id)) + Box::new(DatabasePersistence::new( + ctx, + device_id, + location_root_entry_id, + )) } /// Create an ephemeral persistence instance diff --git a/core/src/operations/indexing/phases/aggregation.rs b/core/src/ops/indexing/phases/aggregation.rs similarity index 98% rename from core/src/operations/indexing/phases/aggregation.rs rename to core/src/ops/indexing/phases/aggregation.rs index 3abd21bf3..c3ee3a196 100644 --- a/core/src/operations/indexing/phases/aggregation.rs +++ b/core/src/ops/indexing/phases/aggregation.rs @@ -1,12 +1,12 @@ //! Directory size aggregation phase use crate::{ - infrastructure::{ + infra::{ database::entities::{self, entry_closure}, jobs::generic_progress::ToGenericProgress, jobs::prelude::{JobContext, JobError, Progress}, }, - operations::indexing::state::{IndexPhase, IndexerProgress, IndexerState, Phase}, + ops::indexing::state::{IndexPhase, IndexerProgress, IndexerState, Phase}, }; use sea_orm::{ ActiveModelTrait, ActiveValue::Set, ColumnTrait, DatabaseConnection, DbBackend, DbErr, diff --git a/core/src/operations/indexing/phases/content.rs b/core/src/ops/indexing/phases/content.rs similarity index 85% rename from core/src/operations/indexing/phases/content.rs rename to core/src/ops/indexing/phases/content.rs index 18c0f48a8..10bfdafd3 100644 --- a/core/src/operations/indexing/phases/content.rs +++ b/core/src/ops/indexing/phases/content.rs @@ -1,9 +1,9 @@ //! Content identification phase - generates CAS IDs and links content use crate::{ - infrastructure::jobs::prelude::{JobContext, JobError, Progress}, - infrastructure::jobs::generic_progress::ToGenericProgress, - operations::indexing::{ + infra::jobs::prelude::{JobContext, JobError, Progress}, + infra::jobs::generic_progress::ToGenericProgress, + ops::indexing::{ state::{IndexerState, IndexPhase, IndexError, IndexerProgress}, entry::EntryProcessor, }, @@ -18,32 +18,32 @@ pub async fn run_content_phase( ) -> Result<(), JobError> { let total = state.entries_for_content.len(); ctx.log(format!("Content identification phase starting with {} files", total)); - + if total == 0 { ctx.log("No files to process for content identification"); - state.phase = crate::operations::indexing::state::Phase::Complete; + state.phase = crate::ops::indexing::state::Phase::Complete; return Ok(()); } - + let mut processed = 0; let mut success_count = 0; let mut error_count = 0; - + // Process in chunks for better performance and memory usage const CHUNK_SIZE: usize = 100; - + while !state.entries_for_content.is_empty() { ctx.check_interrupt().await?; - + let chunk_size = CHUNK_SIZE.min(state.entries_for_content.len()); let chunk: Vec<_> = state.entries_for_content.drain(..chunk_size).collect(); let chunk_len = chunk.len(); - + // Report progress BEFORE processing (using current processed count) let indexer_progress = IndexerProgress { - phase: IndexPhase::ContentIdentification { - current: processed, - total + phase: IndexPhase::ContentIdentification { + current: processed, + total }, current_path: format!("Generating content identities ({}/{})", processed, total), total_found: state.stats, @@ -54,7 +54,7 @@ pub async fn run_content_phase( is_ephemeral: false, }; ctx.progress(Progress::generic(indexer_progress.to_generic_progress())); - + // Process chunk in parallel for better performance let content_hash_futures: Vec<_> = chunk.iter() .map(|(entry_id, path)| async move { @@ -62,10 +62,10 @@ pub async fn run_content_phase( (*entry_id, path.clone(), hash_result) }) .collect(); - + // Wait for all content hash generations to complete let hash_results = futures::future::join_all(content_hash_futures).await; - + // Process results for (entry_id, path, hash_result) in hash_results { match hash_result { @@ -78,9 +78,9 @@ pub async fn run_content_phase( Err(e) => { let error_msg = format!("Failed to create content identity for {}: {}", path.display(), e); ctx.add_non_critical_error(error_msg); - state.add_error(IndexError::ContentId { - path: path.to_string_lossy().to_string(), - error: e.to_string() + state.add_error(IndexError::ContentId { + path: path.to_string_lossy().to_string(), + error: e.to_string() }); error_count += 1; } @@ -89,32 +89,32 @@ pub async fn run_content_phase( Err(e) => { let error_msg = format!("Failed to generate content hash for {}: {}", path.display(), e); ctx.add_non_critical_error(error_msg); - state.add_error(IndexError::ContentId { - path: path.to_string_lossy().to_string(), - error: e.to_string() + state.add_error(IndexError::ContentId { + path: path.to_string_lossy().to_string(), + error: e.to_string() }); error_count += 1; } } } - + // Update processed count AFTER processing chunk processed += chunk_len; - + // Update rate tracking state.items_since_last_update += chunk_len as u64; - + // Periodic checkpoint if processed % 1000 == 0 || processed == total { ctx.checkpoint_with_state(state).await?; } } - + ctx.log(format!( "Content identification complete: {} successful, {} errors out of {} total", success_count, error_count, total )); - - state.phase = crate::operations::indexing::state::Phase::Complete; + + state.phase = crate::ops::indexing::state::Phase::Complete; Ok(()) } \ No newline at end of file diff --git a/core/src/operations/indexing/phases/discovery.rs b/core/src/ops/indexing/phases/discovery.rs similarity index 94% rename from core/src/operations/indexing/phases/discovery.rs rename to core/src/ops/indexing/phases/discovery.rs index 7f907b3db..57cd66adf 100644 --- a/core/src/operations/indexing/phases/discovery.rs +++ b/core/src/ops/indexing/phases/discovery.rs @@ -1,9 +1,9 @@ //! Discovery phase - walks directories and collects entries use crate::{ - infrastructure::jobs::generic_progress::ToGenericProgress, - infrastructure::jobs::prelude::{JobContext, JobError, Progress}, - operations::indexing::{ + infra::jobs::generic_progress::ToGenericProgress, + infra::jobs::prelude::{JobContext, JobError, Progress}, + ops::indexing::{ entry::EntryProcessor, rules::{build_default_ruler, RuleToggles, RulerDecision}, state::{DirEntry, EntryKind, IndexError, IndexPhase, IndexerProgress, IndexerState}, @@ -15,7 +15,7 @@ use std::time::Instant; struct SimpleMetadata { is_dir: bool, } -impl crate::operations::indexing::rules::MetadataForIndexerRules for SimpleMetadata { +impl crate::ops::indexing::rules::MetadataForIndexerRules for SimpleMetadata { fn is_dir(&self) -> bool { self.is_dir } @@ -178,7 +178,7 @@ pub async fn run_discovery_phase( state.entry_batches.len() )); - state.phase = crate::operations::indexing::state::Phase::Processing; + state.phase = crate::ops::indexing::state::Phase::Processing; Ok(()) } diff --git a/core/src/operations/indexing/phases/mod.rs b/core/src/ops/indexing/phases/mod.rs similarity index 100% rename from core/src/operations/indexing/phases/mod.rs rename to core/src/ops/indexing/phases/mod.rs diff --git a/core/src/operations/indexing/phases/processing.rs b/core/src/ops/indexing/phases/processing.rs similarity index 99% rename from core/src/operations/indexing/phases/processing.rs rename to core/src/ops/indexing/phases/processing.rs index 707693bd2..d6cb75599 100644 --- a/core/src/operations/indexing/phases/processing.rs +++ b/core/src/ops/indexing/phases/processing.rs @@ -1,12 +1,12 @@ //! Processing phase - creates/updates database entries use crate::{ - infrastructure::{ + infra::{ database::entities::{self, directory_paths, entry_closure}, jobs::generic_progress::ToGenericProgress, jobs::prelude::{JobContext, JobError, Progress}, }, - operations::indexing::{ + ops::indexing::{ change_detection::{Change, ChangeDetector}, entry::EntryProcessor, state::{DirEntry, EntryKind, IndexError, IndexPhase, IndexerProgress, IndexerState}, @@ -439,6 +439,6 @@ pub async fn run_processing_phase( "Processing phase complete: {} entries processed", total_processed )); - state.phase = crate::operations::indexing::state::Phase::Aggregation; + state.phase = crate::ops::indexing::state::Phase::Aggregation; Ok(()) } diff --git a/core/src/operations/indexing/progress.rs b/core/src/ops/indexing/progress.rs similarity index 97% rename from core/src/operations/indexing/progress.rs rename to core/src/ops/indexing/progress.rs index 41d41763a..a877eb24a 100644 --- a/core/src/operations/indexing/progress.rs +++ b/core/src/ops/indexing/progress.rs @@ -2,7 +2,7 @@ use super::state::{IndexPhase, IndexerProgress}; use crate::{ - infrastructure::jobs::generic_progress::{GenericProgress, ToGenericProgress}, + infra::jobs::generic_progress::{GenericProgress, ToGenericProgress}, domain::addressing::SdPath, }; use std::path::PathBuf; @@ -95,7 +95,7 @@ impl ToGenericProgress for IndexerProgress { #[cfg(test)] mod tests { use super::*; - use crate::operations::indexing::state::{IndexPhase, IndexerStats}; + use crate::ops::indexing::state::{IndexPhase, IndexerStats}; use std::time::Duration; #[test] diff --git a/core/src/operations/indexing/rules.rs b/core/src/ops/indexing/rules.rs similarity index 100% rename from core/src/operations/indexing/rules.rs rename to core/src/ops/indexing/rules.rs diff --git a/core/src/operations/indexing/state.rs b/core/src/ops/indexing/state.rs similarity index 100% rename from core/src/operations/indexing/state.rs rename to core/src/ops/indexing/state.rs diff --git a/core/src/operations/indexing/tests/mod.rs b/core/src/ops/indexing/tests/mod.rs similarity index 100% rename from core/src/operations/indexing/tests/mod.rs rename to core/src/ops/indexing/tests/mod.rs diff --git a/core/src/operations/libraries/create/action.rs b/core/src/ops/libraries/create/action.rs similarity index 77% rename from core/src/operations/libraries/create/action.rs rename to core/src/ops/libraries/create/action.rs index 63a34a698..90531dc32 100644 --- a/core/src/operations/libraries/create/action.rs +++ b/core/src/ops/libraries/create/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -34,9 +34,9 @@ impl ActionHandler for LibraryCreateHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { - if let crate::infrastructure::actions::Action::LibraryCreate(action) = action { + if let crate::infra::actions::Action::LibraryCreate(action) = action { if action.name.trim().is_empty() { return Err(ActionError::Validation { field: "name".to_string(), @@ -52,9 +52,9 @@ impl ActionHandler for LibraryCreateHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { - if let crate::infrastructure::actions::Action::LibraryCreate(action) = action { + if let crate::infra::actions::Action::LibraryCreate(action) = action { let library_manager = &context.library_manager; let new_library = library_manager.create_library(action.name.clone(), action.path.clone(), context.clone()).await?; @@ -70,8 +70,8 @@ impl ActionHandler for LibraryCreateHandler { } } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { - matches!(action, crate::infrastructure::actions::Action::LibraryCreate(_)) + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { + matches!(action, crate::infra::actions::Action::LibraryCreate(_)) } fn supported_actions() -> &'static [&'static str] { diff --git a/core/src/operations/libraries/create/mod.rs b/core/src/ops/libraries/create/mod.rs similarity index 100% rename from core/src/operations/libraries/create/mod.rs rename to core/src/ops/libraries/create/mod.rs diff --git a/core/src/ops/libraries/create/output.rs b/core/src/ops/libraries/create/output.rs new file mode 100644 index 000000000..d09a8f9d8 --- /dev/null +++ b/core/src/ops/libraries/create/output.rs @@ -0,0 +1,43 @@ +//! Library create operation output types + +use crate::infra::actions::output::ActionOutputTrait; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use uuid::Uuid; + +/// Output from library create action dispatch +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryCreateOutput { + pub library_id: Uuid, + pub name: String, + pub path: PathBuf, +} + +impl LibraryCreateOutput { + pub fn new(library_id: Uuid, name: String, path: PathBuf) -> Self { + Self { + library_id, + name, + path, + } + } +} + +impl ActionOutputTrait for LibraryCreateOutput { + fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).unwrap_or(serde_json::Value::Null) + } + + fn display_message(&self) -> String { + format!( + "Created library '{}' with ID {} at {}", + self.name, + self.library_id, + self.path.display() + ) + } + + fn output_type(&self) -> &'static str { + "library.create.completed" + } +} diff --git a/core/src/ops/libraries/delete/action.rs b/core/src/ops/libraries/delete/action.rs new file mode 100644 index 000000000..ad181298e --- /dev/null +++ b/core/src/ops/libraries/delete/action.rs @@ -0,0 +1,59 @@ +//! Library deletion action handler + +use crate::{ + context::CoreContext, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, + output::ActionOutput, + Action, + }, + register_action_handler, +}; +use async_trait::async_trait; +use serde::{Deserialize, Serialize}; +use std::sync::Arc; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryDeleteAction { + // Library deletion doesn't need additional fields beyond library_id +} + +pub struct LibraryDeleteHandler; + +impl LibraryDeleteHandler { + pub fn new() -> Self { + Self + } +} + +#[async_trait] +impl ActionHandler for LibraryDeleteHandler { + async fn execute( + &self, + context: Arc, + action: Action, + ) -> ActionResult { + if let Action::LibraryDelete(action) = action { + // For now, library deletion is not implemented in the library manager + // This would need to be implemented as a proper method + Err(ActionError::Internal( + "Library deletion not yet implemented".to_string(), + )) + } else { + Err(crate::infra::actions::error::ActionError::InvalidActionType) + } + } + + fn can_handle(&self, action: &Action) -> bool { + matches!(action, Action::LibraryDelete(_)) + } + + fn supported_actions() -> &'static [&'static str] { + &["library.delete"] + } +} + +// Register this handler +register_action_handler!(LibraryDeleteHandler, "library.delete"); diff --git a/core/src/operations/libraries/delete/mod.rs b/core/src/ops/libraries/delete/mod.rs similarity index 100% rename from core/src/operations/libraries/delete/mod.rs rename to core/src/ops/libraries/delete/mod.rs diff --git a/core/src/operations/libraries/delete/output.rs b/core/src/ops/libraries/delete/output.rs similarity index 92% rename from core/src/operations/libraries/delete/output.rs rename to core/src/ops/libraries/delete/output.rs index 9795fdec1..06b7a8c52 100644 --- a/core/src/operations/libraries/delete/output.rs +++ b/core/src/ops/libraries/delete/output.rs @@ -1,6 +1,6 @@ //! Library delete operation output types -use crate::infrastructure::actions::output::ActionOutputTrait; +use crate::infra::actions::output::ActionOutputTrait; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -24,14 +24,14 @@ impl ActionOutputTrait for LibraryDeleteOutput { fn to_json(&self) -> serde_json::Value { serde_json::to_value(self).unwrap_or(serde_json::Value::Null) } - + fn display_message(&self) -> String { format!( "Deleted library '{}' with ID {}", self.name, self.library_id ) } - + fn output_type(&self) -> &'static str { "library.delete.completed" } diff --git a/core/src/operations/libraries/export/action.rs b/core/src/ops/libraries/export/action.rs similarity index 99% rename from core/src/operations/libraries/export/action.rs rename to core/src/ops/libraries/export/action.rs index cdb3e32e5..3e45489e6 100644 --- a/core/src/operations/libraries/export/action.rs +++ b/core/src/ops/libraries/export/action.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, @@ -61,7 +61,7 @@ impl ActionHandler for LibraryExportHandler { ) -> ActionResult { if let Action::LibraryExport { library_id, action } = action { let library_manager = &context.library_manager; - + // Get the specific library let library = library_manager .get_library(library_id) diff --git a/core/src/operations/libraries/export/mod.rs b/core/src/ops/libraries/export/mod.rs similarity index 100% rename from core/src/operations/libraries/export/mod.rs rename to core/src/ops/libraries/export/mod.rs diff --git a/core/src/ops/libraries/export/output.rs b/core/src/ops/libraries/export/output.rs new file mode 100644 index 000000000..20d42a023 --- /dev/null +++ b/core/src/ops/libraries/export/output.rs @@ -0,0 +1,33 @@ +//! Library export operation output + +use crate::infra::actions::output::ActionOutputTrait; +use serde::{Deserialize, Serialize}; +use std::path::PathBuf; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryExportOutput { + pub library_id: Uuid, + pub library_name: String, + pub export_path: PathBuf, + pub exported_files: Vec, +} + +impl ActionOutputTrait for LibraryExportOutput { + fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).unwrap_or(serde_json::Value::Null) + } + + fn display_message(&self) -> String { + format!( + "Exported library '{}' to {} ({} files)", + self.library_name, + self.export_path.display(), + self.exported_files.len() + ) + } + + fn output_type(&self) -> &'static str { + "library.export.output" + } +} diff --git a/core/src/operations/libraries/mod.rs b/core/src/ops/libraries/mod.rs similarity index 100% rename from core/src/operations/libraries/mod.rs rename to core/src/ops/libraries/mod.rs diff --git a/core/src/operations/libraries/rename/action.rs b/core/src/ops/libraries/rename/action.rs similarity index 97% rename from core/src/operations/libraries/rename/action.rs rename to core/src/ops/libraries/rename/action.rs index 2ac5c92b6..d504d1274 100644 --- a/core/src/operations/libraries/rename/action.rs +++ b/core/src/ops/libraries/rename/action.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, @@ -57,7 +57,7 @@ impl ActionHandler for LibraryRenameHandler { ) -> ActionResult { if let Action::LibraryRename { library_id, action } = action { let library_manager = &context.library_manager; - + // Get the specific library let library = library_manager .get_library(library_id) @@ -67,7 +67,7 @@ impl ActionHandler for LibraryRenameHandler { // Get current config let old_config = library.config().await; let old_name = old_config.name.clone(); - + // Update the library name using update_config library.update_config(|config| { config.name = action.new_name.clone(); diff --git a/core/src/operations/libraries/rename/mod.rs b/core/src/ops/libraries/rename/mod.rs similarity index 100% rename from core/src/operations/libraries/rename/mod.rs rename to core/src/ops/libraries/rename/mod.rs diff --git a/core/src/ops/libraries/rename/output.rs b/core/src/ops/libraries/rename/output.rs new file mode 100644 index 000000000..beb3bd01b --- /dev/null +++ b/core/src/ops/libraries/rename/output.rs @@ -0,0 +1,26 @@ +//! Library rename operation output + +use crate::infra::actions::output::ActionOutputTrait; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LibraryRenameOutput { + pub library_id: Uuid, + pub old_name: String, + pub new_name: String, +} + +impl ActionOutputTrait for LibraryRenameOutput { + fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).unwrap_or(serde_json::Value::Null) + } + + fn display_message(&self) -> String { + format!("Renamed library '{}' to '{}'", self.old_name, self.new_name) + } + + fn output_type(&self) -> &'static str { + "library.rename.output" + } +} diff --git a/core/src/operations/locations/add/action.rs b/core/src/ops/locations/add/action.rs similarity index 97% rename from core/src/operations/locations/add/action.rs rename to core/src/ops/locations/add/action.rs index 893c7a71a..0acaac89f 100644 --- a/core/src/operations/locations/add/action.rs +++ b/core/src/ops/locations/add/action.rs @@ -3,15 +3,15 @@ use super::output::LocationAddOutput; use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, Action, }, - infrastructure::database::entities, + infra::database::entities, location::manager::LocationManager, - operations::indexing::IndexMode, + ops::indexing::IndexMode, register_action_handler, }; use async_trait::async_trait; diff --git a/core/src/operations/locations/add/mod.rs b/core/src/ops/locations/add/mod.rs similarity index 100% rename from core/src/operations/locations/add/mod.rs rename to core/src/ops/locations/add/mod.rs diff --git a/core/src/operations/locations/add/output.rs b/core/src/ops/locations/add/output.rs similarity index 94% rename from core/src/operations/locations/add/output.rs rename to core/src/ops/locations/add/output.rs index 07ad47d48..35735230a 100644 --- a/core/src/operations/locations/add/output.rs +++ b/core/src/ops/locations/add/output.rs @@ -1,6 +1,6 @@ //! Location add operation output types -use crate::infrastructure::actions::output::ActionOutputTrait; +use crate::infra::actions::output::ActionOutputTrait; use serde::{Deserialize, Serialize}; use std::path::PathBuf; use uuid::Uuid; @@ -34,7 +34,7 @@ impl ActionOutputTrait for LocationAddOutput { fn to_json(&self) -> serde_json::Value { serde_json::to_value(self).unwrap_or(serde_json::Value::Null) } - + fn display_message(&self) -> String { match &self.name { Some(name) => format!( @@ -47,7 +47,7 @@ impl ActionOutputTrait for LocationAddOutput { ), } } - + fn output_type(&self) -> &'static str { "location.add.completed" } diff --git a/core/src/operations/locations/index/action.rs b/core/src/ops/locations/index/action.rs similarity index 94% rename from core/src/operations/locations/index/action.rs rename to core/src/ops/locations/index/action.rs index 3750428a8..85f3ea269 100644 --- a/core/src/operations/locations/index/action.rs +++ b/core/src/ops/locations/index/action.rs @@ -2,12 +2,12 @@ use crate::{ context::CoreContext, - infrastructure::{ + infra::{ actions::{ Action, error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, }, }, - operations::{ + ops::{ indexing::{IndexMode, job::IndexerJob}, }, register_action_handler, @@ -41,7 +41,7 @@ impl ActionHandler for LocationIndexHandler { ) -> ActionResult { if let Action::LocationIndex { library_id, action } = action { let library_manager = &context.library_manager; - + // Get the specific library let library = library_manager .get_library(library_id) @@ -49,9 +49,9 @@ impl ActionHandler for LocationIndexHandler { .ok_or(ActionError::LibraryNotFound(library_id))?; // TODO: In a real implementation, we'd query the database to get the location's actual path - // For now, let's create a placeholder SdPath + // For now, let's create a placeholder SdPath let location_path = SdPath::local("/placeholder"); // This should be the actual location path - + // Create indexer job directly let job = IndexerJob::from_location(action.location_id, location_path, action.mode); diff --git a/core/src/operations/locations/index/mod.rs b/core/src/ops/locations/index/mod.rs similarity index 100% rename from core/src/operations/locations/index/mod.rs rename to core/src/ops/locations/index/mod.rs diff --git a/core/src/operations/locations/mod.rs b/core/src/ops/locations/mod.rs similarity index 100% rename from core/src/operations/locations/mod.rs rename to core/src/ops/locations/mod.rs diff --git a/core/src/operations/locations/remove/action.rs b/core/src/ops/locations/remove/action.rs similarity index 97% rename from core/src/operations/locations/remove/action.rs rename to core/src/ops/locations/remove/action.rs index a4f4044b7..0c096b9a3 100644 --- a/core/src/operations/locations/remove/action.rs +++ b/core/src/ops/locations/remove/action.rs @@ -3,7 +3,7 @@ use crate::{ context::CoreContext, location::manager::LocationManager, - infrastructure::actions::{ + infra::actions::{ Action, error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -36,7 +36,7 @@ impl ActionHandler for LocationRemoveHandler { ) -> ActionResult { if let Action::LocationRemove { library_id, action } = action { let library_manager = &context.library_manager; - + // Get the specific library let library = library_manager .get_library(library_id) diff --git a/core/src/operations/locations/remove/mod.rs b/core/src/ops/locations/remove/mod.rs similarity index 100% rename from core/src/operations/locations/remove/mod.rs rename to core/src/ops/locations/remove/mod.rs diff --git a/core/src/operations/locations/remove/output.rs b/core/src/ops/locations/remove/output.rs similarity index 93% rename from core/src/operations/locations/remove/output.rs rename to core/src/ops/locations/remove/output.rs index a7263c431..bab7b574e 100644 --- a/core/src/operations/locations/remove/output.rs +++ b/core/src/ops/locations/remove/output.rs @@ -1,6 +1,6 @@ //! Location remove operation output types -use crate::infrastructure::actions::output::ActionOutputTrait; +use crate::infra::actions::output::ActionOutputTrait; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -24,7 +24,7 @@ impl ActionOutputTrait for LocationRemoveOutput { fn to_json(&self) -> serde_json::Value { serde_json::to_value(self).unwrap_or(serde_json::Value::Null) } - + fn display_message(&self) -> String { match &self.path { Some(path) => format!( @@ -37,7 +37,7 @@ impl ActionOutputTrait for LocationRemoveOutput { ), } } - + fn output_type(&self) -> &'static str { "location.remove.completed" } diff --git a/core/src/operations/locations/rescan/action.rs b/core/src/ops/locations/rescan/action.rs similarity index 95% rename from core/src/operations/locations/rescan/action.rs rename to core/src/ops/locations/rescan/action.rs index 9b573eed3..16d3a3443 100644 --- a/core/src/operations/locations/rescan/action.rs +++ b/core/src/ops/locations/rescan/action.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::{ + infra::{ actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, @@ -11,7 +11,7 @@ use crate::{ }, database::entities, }, - operations::indexing::{IndexMode, job::IndexerJob, PathResolver}, + ops::indexing::{IndexMode, job::IndexerJob, PathResolver}, register_action_handler, domain::addressing::SdPath, }; @@ -43,7 +43,7 @@ impl ActionHandler for LocationRescanHandler { ) -> ActionResult { if let Action::LocationRescan { library_id, action } = action { let library_manager = &context.library_manager; - + // Get the specific library let library = library_manager .get_library(library_id) @@ -51,9 +51,9 @@ impl ActionHandler for LocationRescanHandler { .ok_or(ActionError::LibraryNotFound(library_id))?; // Get location details from database - use crate::infrastructure::database::entities; + use crate::infra::database::entities; use sea_orm::{ColumnTrait, EntityTrait, QueryFilter}; - + let location = entities::location::Entity::find() .filter(entities::location::Column::Uuid.eq(action.location_id)) .one(library.db().conn()) @@ -67,7 +67,7 @@ impl ActionHandler for LocationRescanHandler { .map_err(|e| ActionError::Internal(format!("Failed to get location path: {}", e)))?; let location_path_str = location_path_buf.to_string_lossy().to_string(); let location_path = SdPath::local(location_path_buf); - + // Determine index mode based on full_rescan flag let mode = if action.full_rescan { IndexMode::Deep diff --git a/core/src/operations/locations/rescan/mod.rs b/core/src/ops/locations/rescan/mod.rs similarity index 100% rename from core/src/operations/locations/rescan/mod.rs rename to core/src/ops/locations/rescan/mod.rs diff --git a/core/src/ops/locations/rescan/output.rs b/core/src/ops/locations/rescan/output.rs new file mode 100644 index 000000000..c90510e0a --- /dev/null +++ b/core/src/ops/locations/rescan/output.rs @@ -0,0 +1,31 @@ +//! Location rescan operation output + +use crate::infra::actions::output::ActionOutputTrait; +use serde::{Deserialize, Serialize}; +use uuid::Uuid; + +#[derive(Debug, Clone, Serialize, Deserialize)] +pub struct LocationRescanOutput { + pub location_id: Uuid, + pub location_path: String, + pub job_id: Uuid, + pub full_rescan: bool, +} + +impl ActionOutputTrait for LocationRescanOutput { + fn to_json(&self) -> serde_json::Value { + serde_json::to_value(self).unwrap_or(serde_json::Value::Null) + } + + fn display_message(&self) -> String { + let scan_type = if self.full_rescan { "Full" } else { "Quick" }; + format!( + "{} rescan started for location {} (job: {})", + scan_type, self.location_path, self.job_id + ) + } + + fn output_type(&self) -> &'static str { + "location.rescan.output" + } +} diff --git a/core/src/operations/media/live_photo.rs b/core/src/ops/media/live_photo.rs similarity index 95% rename from core/src/operations/media/live_photo.rs rename to core/src/ops/media/live_photo.rs index c5e1e2e74..aceead258 100644 --- a/core/src/operations/media/live_photo.rs +++ b/core/src/ops/media/live_photo.rs @@ -12,8 +12,8 @@ use uuid::Uuid; use anyhow::Result; use crate::{ library::Library, - services::sidecar_manager::SidecarManager, - operations::sidecar::{SidecarKind, SidecarVariant, SidecarFormat}, + service::sidecar_manager::SidecarManager, + ops::sidecar::{SidecarKind, SidecarVariant, SidecarFormat}, }; /// Represents a detected Live Photo pair @@ -38,24 +38,24 @@ impl LivePhotoDetector { pub fn detect_pair(path: &Path) -> Option { let file_name = path.file_stem()?.to_str()?; let extension = path.extension()?.to_str()?.to_lowercase(); - + // Check if this is an image file let is_image = matches!(extension.as_str(), "heic" | "heif" | "jpg" | "jpeg"); let is_video = matches!(extension.as_str(), "mov" | "mp4"); - + if !is_image && !is_video { return None; } - + let parent = path.parent()?; - + // Define the counterpart we're looking for let counterpart_extensions = if is_image { vec!["mov", "mp4"] } else { vec!["heic", "heif", "jpg", "jpeg"] }; - + // Look for matching counterpart for ext in counterpart_extensions { let counterpart_path = parent.join(format!("{}.{}", file_name, ext)); @@ -66,7 +66,7 @@ impl LivePhotoDetector { } else { (counterpart_path, path.to_path_buf()) }; - + return Some(LivePhoto { image_path, video_path, @@ -74,39 +74,39 @@ impl LivePhotoDetector { }); } } - + None } - + /// Check if two files form a Live Photo pair pub fn is_live_photo_pair(image_path: &Path, video_path: &Path) -> bool { // Must be in same directory if image_path.parent() != video_path.parent() { return false; } - + // Must have same base name if image_path.file_stem() != video_path.file_stem() { return false; } - + // Check extensions let img_ext = image_path.extension() .and_then(|e| e.to_str()) .map(|e| e.to_lowercase()) .unwrap_or_default(); - + let vid_ext = video_path.extension() .and_then(|e| e.to_str()) .map(|e| e.to_lowercase()) .unwrap_or_default(); - + let valid_image = matches!(img_ext.as_str(), "heic" | "heif" | "jpg" | "jpeg"); let valid_video = matches!(vid_ext.as_str(), "mov" | "mp4"); - + valid_image && valid_video } - + /// Generate a deterministic UUID for a Live Photo pair /// This ensures both components reference the same Live Photo ID pub fn generate_live_photo_id(image_hash: &str, video_hash: &str) -> Uuid { @@ -116,18 +116,18 @@ impl LivePhotoDetector { } else { (video_hash, image_hash) }; - + let combined = format!("{}-{}", first, second); - + // Use a namespace UUID for Live Photos const LIVE_PHOTO_NAMESPACE: Uuid = Uuid::from_bytes([ 0x4c, 0x69, 0x76, 0x65, 0x50, 0x68, 0x6f, 0x74, 0x6f, 0x4e, 0x53, 0x00, 0x00, 0x00, 0x00, 0x01, ]); - + Uuid::new_v5(&LIVE_PHOTO_NAMESPACE, combined.as_bytes()) } - + /// Create a reference sidecar for a Live Photo video /// This is called during indexing when we find a Live Photo pair pub async fn create_live_photo_reference_sidecar( @@ -150,10 +150,10 @@ impl LivePhotoDetector { video_size, video_checksum, ).await?; - + Ok(()) } - + /// Example of how Live Photos would be handled during indexing /// NOTE: This is a demonstration - actual integration would be in the indexer #[allow(dead_code)] @@ -166,19 +166,19 @@ impl LivePhotoDetector { // During indexing, when we process an image file... if let Some(live_photo) = Self::detect_pair(image_path) { // We found a Live Photo pair! - + // The video would normally be indexed as an entry // But instead, we skip indexing it and create a reference sidecar - + // In real implementation, we would: // 1. Get or create the video entry (minimal record) // 2. Get the video's size and checksum // 3. Create the reference sidecar - + let video_entry_id = 12345; // This would come from the database let video_size = 1024 * 1024 * 10; // 10MB, would come from fs::metadata let video_checksum = Some("abc123".to_string()); // Would be computed - + // Create the reference sidecar Self::create_live_photo_reference_sidecar( library, @@ -188,15 +188,15 @@ impl LivePhotoDetector { video_size, video_checksum, ).await?; - + // The video is now tracked as a virtual sidecar of the image // It won't appear in search results or galleries as a separate item // But can be accessed through the image's sidecar API } - + Ok(()) } - + /// Bulk convert reference sidecars to owned sidecars /// This is called when the user wants to take ownership of Live Photo videos pub async fn convert_live_photos_to_owned( @@ -209,7 +209,7 @@ impl LivePhotoDetector { // to the managed sidecar directory structure sidecar_manager.convert_reference_to_owned(library, content_uuid).await?; } - + Ok(()) } } @@ -219,42 +219,42 @@ mod tests { use super::*; use std::fs; use tempfile::tempdir; - + #[test] fn test_live_photo_detection() { let dir = tempdir().unwrap(); let dir_path = dir.path(); - + // Create test files let image_path = dir_path.join("IMG_1234.HEIC"); let video_path = dir_path.join("IMG_1234.MOV"); - + fs::write(&image_path, b"fake image").unwrap(); fs::write(&video_path, b"fake video").unwrap(); - + // Test detection from image let result = LivePhotoDetector::detect_pair(&image_path); assert!(result.is_some()); let live_photo = result.unwrap(); assert_eq!(live_photo.image_path, image_path); assert_eq!(live_photo.video_path, video_path); - + // Test detection from video let result = LivePhotoDetector::detect_pair(&video_path); assert!(result.is_some()); let live_photo = result.unwrap(); assert_eq!(live_photo.image_path, image_path); assert_eq!(live_photo.video_path, video_path); - + // Test pair validation assert!(LivePhotoDetector::is_live_photo_pair(&image_path, &video_path)); } - + #[test] fn test_live_photo_id_generation() { let id1 = LivePhotoDetector::generate_live_photo_id("hash1", "hash2"); let id2 = LivePhotoDetector::generate_live_photo_id("hash2", "hash1"); - + // Should generate same ID regardless of order assert_eq!(id1, id2); } diff --git a/core/src/operations/media/live_photo_query.rs b/core/src/ops/media/live_photo_query.rs similarity index 97% rename from core/src/operations/media/live_photo_query.rs rename to core/src/ops/media/live_photo_query.rs index d15a59654..0358a28c9 100644 --- a/core/src/operations/media/live_photo_query.rs +++ b/core/src/ops/media/live_photo_query.rs @@ -4,7 +4,7 @@ use sea_orm::{ use serde::{Deserialize, Serialize}; use uuid::Uuid; -use crate::infrastructure::database::entities::{sidecar, entry, content_identity}; +use crate::infra::database::entities::{sidecar, entry, content_identity}; /// Represents a Live Photo pair (image + video sidecar) #[derive(Debug, Clone, Serialize, Deserialize)] @@ -13,7 +13,7 @@ pub struct LivePhotoPair { pub image_entry_id: i32, pub image_entry_uuid: Option, pub image_content_uuid: Uuid, - + /// The video sidecar pub video_sidecar_path: String, pub video_sidecar_size: i64, diff --git a/core/src/operations/media/mod.rs b/core/src/ops/media/mod.rs similarity index 100% rename from core/src/operations/media/mod.rs rename to core/src/ops/media/mod.rs diff --git a/core/src/operations/media/thumbnail/action.rs b/core/src/ops/media/thumbnail/action.rs similarity index 77% rename from core/src/operations/media/thumbnail/action.rs rename to core/src/ops/media/thumbnail/action.rs index ce8dff833..8a4b6a833 100644 --- a/core/src/operations/media/thumbnail/action.rs +++ b/core/src/ops/media/thumbnail/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -34,9 +34,9 @@ impl ActionHandler for ThumbnailHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { - if let crate::infrastructure::actions::Action::GenerateThumbnails { action, .. } = action { + if let crate::infra::actions::Action::GenerateThumbnails { action, .. } = action { if action.paths.is_empty() { return Err(ActionError::Validation { field: "paths".to_string(), @@ -52,11 +52,11 @@ impl ActionHandler for ThumbnailHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { - if let crate::infrastructure::actions::Action::GenerateThumbnails { library_id, action } = action { + if let crate::infra::actions::Action::GenerateThumbnails { library_id, action } = action { let library_manager = &context.library_manager; - + let library = library_manager.get_library(library_id).await .ok_or(ActionError::Internal(format!("Library not found: {}", library_id)))?; @@ -85,8 +85,8 @@ impl ActionHandler for ThumbnailHandler { } } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { - matches!(action, crate::infrastructure::actions::Action::GenerateThumbnails { .. }) + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { + matches!(action, crate::infra::actions::Action::GenerateThumbnails { .. }) } fn supported_actions() -> &'static [&'static str] { diff --git a/core/src/operations/media/thumbnail/error.rs b/core/src/ops/media/thumbnail/error.rs similarity index 86% rename from core/src/operations/media/thumbnail/error.rs rename to core/src/ops/media/thumbnail/error.rs index c49aa0604..17f09136f 100644 --- a/core/src/operations/media/thumbnail/error.rs +++ b/core/src/ops/media/thumbnail/error.rs @@ -8,37 +8,37 @@ pub type ThumbnailResult = Result; pub enum ThumbnailError { #[error("I/O error: {0}")] Io(#[from] std::io::Error), - + #[error("Image processing error: {0}")] Image(#[from] image::ImageError), - + #[error("Video processing error: {0}")] VideoProcessing(String), - + #[error("Unsupported format: {0}")] UnsupportedFormat(String), - + #[error("Invalid thumbnail size: {0}")] InvalidSize(u32), - + #[error("Invalid quality setting: {0} (must be 0-100)")] InvalidQuality(u8), - + #[error("File not found: {0}")] FileNotFound(String), - + #[error("Permission denied: {0}")] PermissionDenied(String), - + #[error("Thumbnail already exists: {0}")] AlreadyExists(String), - + #[error("Database error: {0}")] Database(String), - + #[error("Serialization error: {0}")] Serialization(String), - + #[error("Other error: {0}")] Other(String), } @@ -47,22 +47,22 @@ impl ThumbnailError { pub fn video_processing(msg: impl Into) -> Self { Self::VideoProcessing(msg.into()) } - + pub fn unsupported_format(format: impl Into) -> Self { Self::UnsupportedFormat(format.into()) } - + pub fn database(msg: impl Into) -> Self { Self::Database(msg.into()) } - + pub fn other(msg: impl Into) -> Self { Self::Other(msg.into()) } } -impl From for crate::infrastructure::jobs::error::JobError { +impl From for crate::infra::jobs::error::JobError { fn from(err: ThumbnailError) -> Self { - crate::infrastructure::jobs::error::JobError::execution(err.to_string()) + crate::infra::jobs::error::JobError::execution(err.to_string()) } } \ No newline at end of file diff --git a/core/src/operations/media/thumbnail/generator.rs b/core/src/ops/media/thumbnail/generator.rs similarity index 100% rename from core/src/operations/media/thumbnail/generator.rs rename to core/src/ops/media/thumbnail/generator.rs diff --git a/core/src/operations/media/thumbnail/job.rs b/core/src/ops/media/thumbnail/job.rs similarity index 98% rename from core/src/operations/media/thumbnail/job.rs rename to core/src/ops/media/thumbnail/job.rs index 535c9151c..c3db5a244 100644 --- a/core/src/operations/media/thumbnail/job.rs +++ b/core/src/ops/media/thumbnail/job.rs @@ -1,6 +1,6 @@ //! Thumbnail generation job implementation -use crate::infrastructure::jobs::{prelude::*, traits::Resourceful}; +use crate::infra::jobs::{prelude::*, traits::Resourceful}; use serde::{Deserialize, Serialize}; use std::time::Duration; use uuid::Uuid; @@ -76,7 +76,7 @@ impl Job for ThumbnailJob { const DESCRIPTION: Option<&'static str> = Some("Generate thumbnails for media files"); } -impl crate::infrastructure::jobs::traits::DynJob for ThumbnailJob { +impl crate::infra::jobs::traits::DynJob for ThumbnailJob { fn job_name(&self) -> &'static str { Self::NAME } @@ -344,9 +344,9 @@ impl ThumbnailJob { entry.relative_path, e ); state.add_error(error_msg.clone()); - ctx.add_non_critical_error( - crate::infrastructure::jobs::error::JobError::execution(error_msg), - ); + ctx.add_non_critical_error(crate::infra::jobs::error::JobError::execution( + error_msg, + )); } } } diff --git a/core/src/operations/media/thumbnail/mod.rs b/core/src/ops/media/thumbnail/mod.rs similarity index 100% rename from core/src/operations/media/thumbnail/mod.rs rename to core/src/ops/media/thumbnail/mod.rs diff --git a/core/src/operations/media/thumbnail/state.rs b/core/src/ops/media/thumbnail/state.rs similarity index 100% rename from core/src/operations/media/thumbnail/state.rs rename to core/src/ops/media/thumbnail/state.rs diff --git a/core/src/operations/media/thumbnail/utils.rs b/core/src/ops/media/thumbnail/utils.rs similarity index 100% rename from core/src/operations/media/thumbnail/utils.rs rename to core/src/ops/media/thumbnail/utils.rs diff --git a/core/src/operations/metadata/action.rs b/core/src/ops/metadata/action.rs similarity index 74% rename from core/src/operations/metadata/action.rs rename to core/src/ops/metadata/action.rs index a969cb7b3..0545cba32 100644 --- a/core/src/operations/metadata/action.rs +++ b/core/src/ops/metadata/action.rs @@ -2,9 +2,9 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ - error::{ActionError, ActionResult}, - handler::ActionHandler, + infra::actions::{ + error::{ActionError, ActionResult}, + handler::ActionHandler, output::ActionOutput, }, register_action_handler, @@ -33,9 +33,9 @@ impl ActionHandler for MetadataHandler { async fn validate( &self, _context: Arc, - action: &crate::infrastructure::actions::Action, + action: &crate::infra::actions::Action, ) -> ActionResult<()> { - if let crate::infrastructure::actions::Action::MetadataOperation { action, .. } = action { + if let crate::infra::actions::Action::MetadataOperation { action, .. } = action { if action.paths.is_empty() { return Err(ActionError::Validation { field: "paths".to_string(), @@ -51,11 +51,11 @@ impl ActionHandler for MetadataHandler { async fn execute( &self, context: Arc, - action: crate::infrastructure::actions::Action, + action: crate::infra::actions::Action, ) -> ActionResult { - if let crate::infrastructure::actions::Action::MetadataOperation { library_id, action } = action { + if let crate::infra::actions::Action::MetadataOperation { library_id, action } = action { let library_manager = &context.library_manager; - + let job_params = serde_json::json!({ "paths": action.paths, "extract_exif": action.extract_exif, @@ -77,8 +77,8 @@ impl ActionHandler for MetadataHandler { } } - fn can_handle(&self, action: &crate::infrastructure::actions::Action) -> bool { - matches!(action, crate::infrastructure::actions::Action::MetadataOperation { .. }) + fn can_handle(&self, action: &crate::infra::actions::Action) -> bool { + matches!(action, crate::infra::actions::Action::MetadataOperation { .. }) } fn supported_actions() -> &'static [&'static str] { diff --git a/core/src/operations/metadata/mod.rs b/core/src/ops/metadata/mod.rs similarity index 98% rename from core/src/operations/metadata/mod.rs rename to core/src/ops/metadata/mod.rs index ccd041cba..085cac440 100644 --- a/core/src/operations/metadata/mod.rs +++ b/core/src/ops/metadata/mod.rs @@ -8,7 +8,7 @@ use serde::{Deserialize, Serialize}; use std::sync::Arc; use uuid::Uuid; -use crate::infrastructure::database::entities::{ +use crate::infra::database::entities::{ content_identity::{self, Entity as ContentIdentity, Model as ContentIdentityModel}, entry::{self, Entity as Entry, Model as EntryModel}, tag::{self, Entity as Tag, Model as TagModel}, @@ -22,7 +22,7 @@ use crate::infrastructure::database::entities::{ }; pub use action::MetadataAction; -use crate::shared::errors::Result; +use crate::common::errors::Result; #[derive(Clone, Debug, PartialEq, Eq, Serialize, Deserialize)] pub enum MetadataTarget { @@ -253,7 +253,7 @@ impl MetadataService { .one(&*self.library_db) .await? .ok_or_else(|| { - crate::shared::errors::CoreError::NotFound("Metadata not found".to_string()) + crate::common::errors::CoreError::NotFound("Metadata not found".to_string()) })?; // Create new content-level metadata (entry-level remains for hierarchy) diff --git a/core/src/operations/mod.rs b/core/src/ops/mod.rs similarity index 97% rename from core/src/operations/mod.rs rename to core/src/ops/mod.rs index aca83bb2b..5413d93b0 100644 --- a/core/src/operations/mod.rs +++ b/core/src/ops/mod.rs @@ -45,7 +45,7 @@ pub fn register_all_jobs() { /// but for now we call it manually for each job type. fn register_job() where - T: crate::infrastructure::jobs::traits::Job + 'static, + T: crate::infra::jobs::traits::Job + 'static, { // In a real implementation with inventory, this would automatically register the job // For now, this serves as documentation of which jobs should be registered diff --git a/core/src/operations/sidecar/mod.rs b/core/src/ops/sidecar/mod.rs similarity index 100% rename from core/src/operations/sidecar/mod.rs rename to core/src/ops/sidecar/mod.rs diff --git a/core/src/operations/sidecar/path.rs b/core/src/ops/sidecar/path.rs similarity index 100% rename from core/src/operations/sidecar/path.rs rename to core/src/ops/sidecar/path.rs diff --git a/core/src/operations/sidecar/types.rs b/core/src/ops/sidecar/types.rs similarity index 100% rename from core/src/operations/sidecar/types.rs rename to core/src/ops/sidecar/types.rs diff --git a/core/src/operations/volumes/mod.rs b/core/src/ops/volumes/mod.rs similarity index 100% rename from core/src/operations/volumes/mod.rs rename to core/src/ops/volumes/mod.rs diff --git a/core/src/operations/volumes/speed_test/action.rs b/core/src/ops/volumes/speed_test/action.rs similarity index 94% rename from core/src/operations/volumes/speed_test/action.rs rename to core/src/ops/volumes/speed_test/action.rs index 7793f33c6..7a6f26fdd 100644 --- a/core/src/operations/volumes/speed_test/action.rs +++ b/core/src/ops/volumes/speed_test/action.rs @@ -3,7 +3,7 @@ //! This action tests the read/write performance of a volume. use crate::{ - infrastructure::actions::{error::ActionError, output::ActionOutput}, + infra::actions::{error::ActionError, output::ActionOutput}, volume::VolumeFingerprint, }; use serde::{Deserialize, Serialize}; diff --git a/core/src/operations/volumes/speed_test/handler.rs b/core/src/ops/volumes/speed_test/handler.rs similarity index 98% rename from core/src/operations/volumes/speed_test/handler.rs rename to core/src/ops/volumes/speed_test/handler.rs index dee6f7c9c..5673d646d 100644 --- a/core/src/operations/volumes/speed_test/handler.rs +++ b/core/src/ops/volumes/speed_test/handler.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, diff --git a/core/src/operations/volumes/speed_test/mod.rs b/core/src/ops/volumes/speed_test/mod.rs similarity index 100% rename from core/src/operations/volumes/speed_test/mod.rs rename to core/src/ops/volumes/speed_test/mod.rs diff --git a/core/src/operations/volumes/track/action.rs b/core/src/ops/volumes/track/action.rs similarity index 93% rename from core/src/operations/volumes/track/action.rs rename to core/src/ops/volumes/track/action.rs index 4d02c421d..8ffa3a72a 100644 --- a/core/src/operations/volumes/track/action.rs +++ b/core/src/ops/volumes/track/action.rs @@ -4,7 +4,7 @@ //! and index files on the volume. use crate::{ - infrastructure::actions::{error::ActionError, output::ActionOutput}, + infra::actions::{error::ActionError, output::ActionOutput}, volume::VolumeFingerprint, }; use serde::{Deserialize, Serialize}; @@ -15,10 +15,10 @@ use uuid::Uuid; pub struct VolumeTrackAction { /// The fingerprint of the volume to track pub fingerprint: VolumeFingerprint, - + /// The library ID to track the volume in pub library_id: Uuid, - + /// Optional name for the tracked volume pub name: Option, } @@ -35,14 +35,14 @@ impl VolumeTrackAction { .get_library(self.library_id) .await .ok_or_else(|| ActionError::InvalidInput("Library not found".to_string()))?; - + // Check if volume exists let volume = core .volumes .get_volume(&self.fingerprint) .await .ok_or_else(|| ActionError::InvalidInput("Volume not found".to_string()))?; - + // TODO: Implement actual volume tracking in library // For now, just verify the volume exists and is mounted if !volume.is_mounted { @@ -50,7 +50,7 @@ impl VolumeTrackAction { "Cannot track unmounted volume".to_string() )); } - + Ok(ActionOutput::VolumeTracked { fingerprint: self.fingerprint.clone(), library_id: self.library_id, diff --git a/core/src/operations/volumes/track/handler.rs b/core/src/ops/volumes/track/handler.rs similarity index 96% rename from core/src/operations/volumes/track/handler.rs rename to core/src/ops/volumes/track/handler.rs index ce7626694..54852a2d7 100644 --- a/core/src/operations/volumes/track/handler.rs +++ b/core/src/ops/volumes/track/handler.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ Action, error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, }, }; @@ -32,19 +32,19 @@ impl ActionHandler for VolumeTrackHandler { .get_library(action.library_id) .await .ok_or_else(|| ActionError::InvalidInput("Library not found".to_string()))?; - + let volume = context .volume_manager .get_volume(&action.fingerprint) .await .ok_or_else(|| ActionError::InvalidInput("Volume not found".to_string()))?; - + if !volume.is_mounted { return Err(ActionError::InvalidInput( "Cannot track unmounted volume".to_string() )); } - + // Track the volume in the database let tracked = context .volume_manager @@ -62,7 +62,7 @@ impl ActionHandler for VolumeTrackHandler { } _ => ActionError::Internal(e.to_string()), })?; - + Ok(ActionOutput::VolumeTracked { fingerprint: action.fingerprint, library_id: action.library_id, @@ -72,11 +72,11 @@ impl ActionHandler for VolumeTrackHandler { _ => Err(ActionError::InvalidActionType), } } - + fn can_handle(&self, action: &Action) -> bool { matches!(action, Action::VolumeTrack { .. }) } - + fn supported_actions() -> &'static [&'static str] where Self: Sized diff --git a/core/src/operations/volumes/track/mod.rs b/core/src/ops/volumes/track/mod.rs similarity index 100% rename from core/src/operations/volumes/track/mod.rs rename to core/src/ops/volumes/track/mod.rs diff --git a/core/src/operations/volumes/untrack/action.rs b/core/src/ops/volumes/untrack/action.rs similarity index 91% rename from core/src/operations/volumes/untrack/action.rs rename to core/src/ops/volumes/untrack/action.rs index 673828c3f..46ba34c41 100644 --- a/core/src/operations/volumes/untrack/action.rs +++ b/core/src/ops/volumes/untrack/action.rs @@ -3,7 +3,7 @@ //! This action removes volume tracking from a library. use crate::{ - infrastructure::actions::{error::ActionError, output::ActionOutput}, + infra::actions::{error::ActionError, output::ActionOutput}, volume::VolumeFingerprint, }; use serde::{Deserialize, Serialize}; @@ -14,7 +14,7 @@ use uuid::Uuid; pub struct VolumeUntrackAction { /// The fingerprint of the volume to untrack pub fingerprint: VolumeFingerprint, - + /// The library ID to untrack the volume from pub library_id: Uuid, } @@ -31,10 +31,10 @@ impl VolumeUntrackAction { .get_library(self.library_id) .await .ok_or_else(|| ActionError::InvalidInput("Library not found".to_string()))?; - + // TODO: Implement actual volume untracking from library // For now, just verify the library exists - + Ok(ActionOutput::VolumeUntracked { fingerprint: self.fingerprint.clone(), library_id: self.library_id, diff --git a/core/src/operations/volumes/untrack/handler.rs b/core/src/ops/volumes/untrack/handler.rs similarity index 96% rename from core/src/operations/volumes/untrack/handler.rs rename to core/src/ops/volumes/untrack/handler.rs index c17f55dbd..e83575a5b 100644 --- a/core/src/operations/volumes/untrack/handler.rs +++ b/core/src/ops/volumes/untrack/handler.rs @@ -2,7 +2,7 @@ use crate::{ context::CoreContext, - infrastructure::actions::{ + infra::actions::{ Action, error::{ActionError, ActionResult}, handler::ActionHandler, output::ActionOutput, }, }; @@ -32,7 +32,7 @@ impl ActionHandler for VolumeUntrackHandler { .get_library(action.library_id) .await .ok_or_else(|| ActionError::InvalidInput("Library not found".to_string()))?; - + // Untrack the volume from the database context .volume_manager @@ -47,7 +47,7 @@ impl ActionHandler for VolumeUntrackHandler { } _ => ActionError::Internal(e.to_string()), })?; - + Ok(ActionOutput::VolumeUntracked { fingerprint: action.fingerprint, library_id: action.library_id, @@ -56,11 +56,11 @@ impl ActionHandler for VolumeUntrackHandler { _ => Err(ActionError::InvalidActionType), } } - + fn can_handle(&self, action: &Action) -> bool { matches!(action, Action::VolumeUntrack { .. }) } - + fn supported_actions() -> &'static [&'static str] where Self: Sized diff --git a/core/src/operations/volumes/untrack/mod.rs b/core/src/ops/volumes/untrack/mod.rs similarity index 100% rename from core/src/operations/volumes/untrack/mod.rs rename to core/src/ops/volumes/untrack/mod.rs diff --git a/core/src/services/device.rs b/core/src/service/device.rs similarity index 95% rename from core/src/services/device.rs rename to core/src/service/device.rs index deea51be6..7f5fd94a9 100644 --- a/core/src/services/device.rs +++ b/core/src/service/device.rs @@ -1,8 +1,8 @@ //! Device management service -//! +//! //! Provides access to device connection information and networking functionality -use crate::{context::CoreContext, services::networking}; +use crate::{context::CoreContext, service::networking}; use anyhow::Result; use std::sync::Arc; use uuid::Uuid; diff --git a/core/src/services/entry_state_service.rs b/core/src/service/entry_state_service.rs similarity index 92% rename from core/src/services/entry_state_service.rs rename to core/src/service/entry_state_service.rs index b7a8a11cc..6f22c330f 100644 --- a/core/src/services/entry_state_service.rs +++ b/core/src/service/entry_state_service.rs @@ -1,5 +1,5 @@ -use crate::infrastructure::jobs::{manager::JobManager, traits::Resourceful}; -use crate::operations::entries::state::EntryState; +use crate::infra::jobs::{manager::JobManager, traits::Resourceful}; +use crate::ops::entries::state::EntryState; use sea_orm::DbConn; use std::collections::HashMap; use uuid::Uuid; @@ -73,7 +73,7 @@ impl EntryStateService { } // Helper to map a job to a state - fn state_from_job(job: &crate::infrastructure::jobs::types::JobInfo) -> EntryState { + fn state_from_job(job: &crate::infra::jobs::types::JobInfo) -> EntryState { match job.name.as_str() { "indexer" => EntryState::Processing { job_id: job.id }, "file_sync" => EntryState::Syncing { job_id: job.id }, diff --git a/core/src/services/file_sharing.rs b/core/src/service/file_sharing.rs similarity index 93% rename from core/src/services/file_sharing.rs rename to core/src/service/file_sharing.rs index ce6fa865e..a3c918b68 100644 --- a/core/src/services/file_sharing.rs +++ b/core/src/service/file_sharing.rs @@ -2,8 +2,8 @@ use crate::{ context::CoreContext, - operations::files::copy::{CopyOptions, FileCopyJob}, - services::networking::protocols::file_transfer::FileMetadata, + ops::files::copy::{CopyOptions, FileCopyJob}, + service::networking::protocols::file_transfer::FileMetadata, domain::addressing::SdPath, }; use serde::{Deserialize, Serialize}; @@ -214,7 +214,7 @@ impl FileSharingService { preserve_timestamps: options.preserve_timestamps, delete_after_copy: false, move_mode: None, - copy_method: crate::operations::files::copy::input::CopyMethod::Auto, + copy_method: crate::ops::files::copy::input::CopyMethod::Auto, }); // Submit job to job system @@ -337,22 +337,22 @@ impl FileSharingService { if let Some(info) = job_info { let state = match info.status { - crate::infrastructure::jobs::types::JobStatus::Queued => { + crate::infra::jobs::types::JobStatus::Queued => { TransferState::Pending } - crate::infrastructure::jobs::types::JobStatus::Running => { + crate::infra::jobs::types::JobStatus::Running => { TransferState::Active } - crate::infrastructure::jobs::types::JobStatus::Paused => { + crate::infra::jobs::types::JobStatus::Paused => { TransferState::Active } - crate::infrastructure::jobs::types::JobStatus::Completed => { + crate::infra::jobs::types::JobStatus::Completed => { TransferState::Completed } - crate::infrastructure::jobs::types::JobStatus::Failed => { + crate::infra::jobs::types::JobStatus::Failed => { TransferState::Failed } - crate::infrastructure::jobs::types::JobStatus::Cancelled => { + crate::infra::jobs::types::JobStatus::Cancelled => { TransferState::Cancelled } }; @@ -443,14 +443,14 @@ impl FileSharingService { // Only include file copy jobs as transfers if job_info.name == "file_copy" { let state = match job_info.status { - crate::infrastructure::jobs::types::JobStatus::Queued => TransferState::Pending, - crate::infrastructure::jobs::types::JobStatus::Running => TransferState::Active, - crate::infrastructure::jobs::types::JobStatus::Paused => TransferState::Active, - crate::infrastructure::jobs::types::JobStatus::Completed => { + crate::infra::jobs::types::JobStatus::Queued => TransferState::Pending, + crate::infra::jobs::types::JobStatus::Running => TransferState::Active, + crate::infra::jobs::types::JobStatus::Paused => TransferState::Active, + crate::infra::jobs::types::JobStatus::Completed => { TransferState::Completed } - crate::infrastructure::jobs::types::JobStatus::Failed => TransferState::Failed, - crate::infrastructure::jobs::types::JobStatus::Cancelled => { + crate::infra::jobs::types::JobStatus::Failed => TransferState::Failed, + crate::infra::jobs::types::JobStatus::Cancelled => { TransferState::Cancelled } }; @@ -510,7 +510,7 @@ pub struct TransferProgress { mod tests { use super::*; use crate::{ - device::DeviceManager, infrastructure::events::EventBus, + device::DeviceManager, infra::events::EventBus, keys::library_key_manager::LibraryKeyManager, library::LibraryManager, }; use tempfile::tempdir; diff --git a/core/src/services/mod.rs b/core/src/service/mod.rs similarity index 100% rename from core/src/services/mod.rs rename to core/src/service/mod.rs diff --git a/core/src/services/networking/core/event_loop.rs b/core/src/service/network/core/event_loop.rs similarity index 98% rename from core/src/services/networking/core/event_loop.rs rename to core/src/service/network/core/event_loop.rs index 3db52884e..dcb26f6f7 100644 --- a/core/src/services/networking/core/event_loop.rs +++ b/core/src/service/network/core/event_loop.rs @@ -1,6 +1,6 @@ //! Networking event loop for handling Iroh connections and messages -use crate::services::networking::{ +use crate::service::networking::{ core::{NetworkEvent, FILE_TRANSFER_ALPN, MESSAGING_ALPN, PAIRING_ALPN}, device::DeviceRegistry, protocols::ProtocolRegistry, @@ -265,24 +265,24 @@ impl NetworkingEventLoop { match bi_result { Ok((send, recv)) => { logger.info(&format!("Accepted bidirectional stream from {}", remote_node_id)).await; - + // Check if this device is already paired let is_paired = { let registry = device_registry.read().await; if let Some(device_id) = registry.get_device_by_node(remote_node_id) { match registry.get_device_state(device_id) { - Some(crate::services::networking::device::DeviceState::Paired { .. }) | - Some(crate::services::networking::device::DeviceState::Connected { .. }) => true, + Some(crate::service::networking::device::DeviceState::Paired { .. }) | + Some(crate::service::networking::device::DeviceState::Connected { .. }) => true, _ => false, } } else { false } }; - + // Route to appropriate handler based on pairing status let handler_name = if is_paired { "messaging" } else { "pairing" }; - + let registry = protocol_registry.read().await; if let Some(handler) = registry.get_handler(handler_name) { logger.info(&format!("Directing bidirectional stream to {} handler", handler_name)).await; @@ -396,7 +396,7 @@ impl NetworkingEventLoop { // Create NodeAddr for the connection let node_addr = NodeAddr::new(node_id); - + // Attempt to connect with MESSAGING_ALPN match self.endpoint.connect(node_addr, MESSAGING_ALPN).await { Ok(conn) => { @@ -405,14 +405,14 @@ impl NetworkingEventLoop { let mut connections = self.active_connections.write().await; connections.insert(node_id, conn); } - + self.logger .info(&format!( "Successfully established persistent connection to device {} (node: {})", device_id, node_id )) .await; - + // Get the address from the active connection map let connections = self.active_connections.read().await; let addresses = if let Some(conn) = connections.get(&node_id) { @@ -422,7 +422,7 @@ impl NetworkingEventLoop { vec![] }; drop(connections); - + // Update device registry to mark as connected let mut registry = self.device_registry.write().await; if let Err(e) = registry.set_device_connected(device_id, node_id, addresses).await { @@ -430,7 +430,7 @@ impl NetworkingEventLoop { .error(&format!("Failed to update device connection state: {}", e)) .await; } - + // Send connection event let _ = self .event_sender @@ -525,7 +525,7 @@ impl NetworkingEventLoop { node_id, data.len() )) .await; - + // Send message length first let len = data.len() as u32; if let Err(e) = send.write_all(&len.to_be_bytes()).await { @@ -555,7 +555,7 @@ impl NetworkingEventLoop { )) .await; } - + // Flush the stream to ensure data is sent if let Err(e) = send.flush().await { self.logger @@ -565,7 +565,7 @@ impl NetworkingEventLoop { )) .await; } - + let _ = send.finish(); } Err(e) => { diff --git a/core/src/services/networking/core/mod.rs b/core/src/service/network/core/mod.rs similarity index 95% rename from core/src/services/networking/core/mod.rs rename to core/src/service/network/core/mod.rs index d16ed63ca..e2e50983d 100644 --- a/core/src/services/networking/core/mod.rs +++ b/core/src/service/network/core/mod.rs @@ -3,7 +3,7 @@ pub mod event_loop; use crate::device::DeviceManager; -use crate::services::networking::{ +use crate::service::networking::{ device::{DeviceInfo, DeviceRegistry}, protocols::{pairing::PairingProtocolHandler, ProtocolRegistry}, utils::{logging::NetworkLogger, NetworkIdentity}, @@ -272,7 +272,7 @@ impl NetworkingService { &self, auto_reconnect_devices: Vec<( Uuid, - crate::services::networking::device::PersistedPairedDevice, + crate::service::networking::device::PersistedPairedDevice, )>, ) { for (device_id, persisted_device) in auto_reconnect_devices { @@ -297,7 +297,7 @@ impl NetworkingService { /// Attempt to reconnect to a specific device async fn attempt_device_reconnection( device_id: Uuid, - persisted_device: crate::services::networking::device::PersistedPairedDevice, + persisted_device: crate::service::networking::device::PersistedPairedDevice, command_sender: Option>, endpoint: Option, logger: Arc, @@ -420,7 +420,7 @@ impl NetworkingService { let is_disconnected = { let registry = device_registry.read().await; if let Some(device_state) = registry.get_device_state(device_id) { - matches!(device_state, crate::services::networking::device::DeviceState::Disconnected { .. }) + matches!(device_state, crate::service::networking::device::DeviceState::Disconnected { .. }) } else { true // Not in registry, try to reconnect } @@ -638,7 +638,7 @@ impl NetworkingService { // Cast to pairing handler to access pairing-specific methods let pairing_handler = pairing_handler .as_any() - .downcast_ref::() + .downcast_ref::() .ok_or(NetworkingError::Protocol( "Invalid pairing handler type".to_string(), ))?; @@ -646,7 +646,7 @@ impl NetworkingService { // Generate session ID let session_id = uuid::Uuid::new_v4(); let pairing_code = - crate::services::networking::protocols::pairing::PairingCode::from_session_id( + crate::service::networking::protocols::pairing::PairingCode::from_session_id( session_id, ); @@ -672,26 +672,26 @@ impl NetworkingService { self.logger .info("No direct addresses discovered yet, waiting for endpoint to discover addresses...") .await; - + // Wait up to 5 seconds for addresses to be discovered let mut attempts = 0; const MAX_ATTEMPTS: u32 = 10; const WAIT_TIME_MS: u64 = 500; - + while attempts < MAX_ATTEMPTS { tokio::time::sleep(tokio::time::Duration::from_millis(WAIT_TIME_MS)).await; node_addr = self.get_node_addr().await?; - + if node_addr.direct_addresses().count() > 0 { self.logger .info(&format!("Discovered {} direct addresses", node_addr.direct_addresses().count())) .await; break; } - + attempts += 1; } - + if node_addr.direct_addresses().count() == 0 { self.logger .warn("No direct addresses discovered after waiting, proceeding with relay-only address") @@ -713,7 +713,7 @@ impl NetworkingService { .await; // Create pairing advertisement - let node_addr_info = crate::services::networking::protocols::pairing::types::NodeAddrInfo { + let node_addr_info = crate::service::networking::protocols::pairing::types::NodeAddrInfo { node_id: self.node_id().to_string(), direct_addresses: node_addr .direct_addresses() @@ -722,7 +722,7 @@ impl NetworkingService { relay_url: node_addr.relay_url().map(|u| u.to_string()), }; - let advertisement = crate::services::networking::protocols::pairing::PairingAdvertisement { + let advertisement = crate::service::networking::protocols::pairing::PairingAdvertisement { node_id: self.node_id().to_string(), node_addr_info, device_info: pairing_handler.get_device_info().await?, @@ -775,7 +775,7 @@ impl NetworkingService { pub async fn start_pairing_as_joiner(&self, code: &str) -> Result<()> { // Parse BIP39 pairing code let pairing_code = - crate::services::networking::protocols::pairing::PairingCode::from_string(code)?; + crate::service::networking::protocols::pairing::PairingCode::from_string(code)?; let session_id = pairing_code.session_id(); // Get pairing handler @@ -790,7 +790,7 @@ impl NetworkingService { ))?; let pairing_handler = pairing_handler .as_any() - .downcast_ref::() + .downcast_ref::() .ok_or(NetworkingError::Protocol( "Invalid pairing handler type".to_string(), ))?; @@ -819,7 +819,7 @@ impl NetworkingService { let session_file = format!("{}/pairing_session_{}.json", temp_dir, session_id); if let Ok(data) = std::fs::read(&session_file) { if let Ok(advertisement) = serde_json::from_slice::< - crate::services::networking::protocols::pairing::PairingAdvertisement, + crate::service::networking::protocols::pairing::PairingAdvertisement, >(&data) { if let Ok(initiator_node_addr) = advertisement.node_addr() { @@ -944,10 +944,10 @@ impl NetworkingService { let device_registry = self.device_registry(); let registry = device_registry.read().await; registry.get_local_device_info().unwrap_or_else(|_| { - crate::services::networking::device::DeviceInfo { + crate::service::networking::device::DeviceInfo { device_id: self.device_id(), device_name: "Joiner Device".to_string(), - device_type: crate::services::networking::device::DeviceType::Desktop, + device_type: crate::service::networking::device::DeviceType::Desktop, os_version: std::env::consts::OS.to_string(), app_version: env!("CARGO_PKG_VERSION").to_string(), network_fingerprint: self.identity().network_fingerprint(), @@ -957,7 +957,7 @@ impl NetworkingService { }; let pairing_request = - crate::services::networking::protocols::pairing::messages::PairingMessage::PairingRequest { + crate::service::networking::protocols::pairing::messages::PairingMessage::PairingRequest { session_id, device_info: local_device_info, public_key: self.identity().public_key_bytes(), @@ -1016,7 +1016,7 @@ impl NetworkingService { /// Get current pairing status pub async fn get_pairing_status( &self, - ) -> Result> { + ) -> Result> { // Get pairing handler from protocol registry let registry = self.protocol_registry(); let pairing_handler = @@ -1032,7 +1032,7 @@ impl NetworkingService { if let Some(pairing_handler) = pairing_handler .as_any() - .downcast_ref::() + .downcast_ref::() { let sessions = pairing_handler.get_active_sessions().await; Ok(sessions) @@ -1057,13 +1057,13 @@ impl NetworkingService { if let Some(handler) = pairing_handler .as_any() - .downcast_ref::( + .downcast_ref::( ) { let sessions = handler.get_active_sessions().await; if let Some(session) = sessions.iter().find(|s| s.id == session_id) { if !matches!( session.state, - crate::services::networking::protocols::pairing::PairingState::Scanning + crate::service::networking::protocols::pairing::PairingState::Scanning ) { return Ok(()); } @@ -1080,11 +1080,11 @@ impl NetworkingService { let device_registry = self.device_registry(); let registry = device_registry.read().await; registry.get_local_device_info().unwrap_or_else(|_| { - crate::services::networking::device::DeviceInfo { + crate::service::networking::device::DeviceInfo { device_id: self.device_id(), device_name: "Joiner's Test Device".to_string(), device_type: - crate::services::networking::device::DeviceType::Desktop, + crate::service::networking::device::DeviceType::Desktop, os_version: std::env::consts::OS.to_string(), app_version: env!("CARGO_PKG_VERSION").to_string(), network_fingerprint: self.identity().network_fingerprint(), @@ -1094,7 +1094,7 @@ impl NetworkingService { }; let pairing_request = - crate::services::networking::protocols::pairing::messages::PairingMessage::PairingRequest { + crate::service::networking::protocols::pairing::messages::PairingMessage::PairingRequest { session_id, device_info: local_device_info, public_key: self.identity().public_key_bytes(), diff --git a/core/src/services/networking/device/connection.rs b/core/src/service/network/device/connection.rs similarity index 98% rename from core/src/services/networking/device/connection.rs rename to core/src/service/network/device/connection.rs index 770b8cef3..475e9eb3e 100644 --- a/core/src/services/networking/device/connection.rs +++ b/core/src/service/network/device/connection.rs @@ -1,7 +1,7 @@ //! Individual device connection handling use super::{DeviceInfo, SessionKeys}; -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; use chrono::{DateTime, Utc}; use iroh::net::key::NodeId; use std::sync::Arc; diff --git a/core/src/services/networking/device/mod.rs b/core/src/service/network/device/mod.rs similarity index 97% rename from core/src/services/networking/device/mod.rs rename to core/src/service/network/device/mod.rs index 992628beb..ad53a3edb 100644 --- a/core/src/services/networking/device/mod.rs +++ b/core/src/service/network/device/mod.rs @@ -31,7 +31,7 @@ pub struct DeviceInfo { pub device_type: DeviceType, pub os_version: String, pub app_version: String, - pub network_fingerprint: crate::services::networking::utils::identity::NetworkFingerprint, + pub network_fingerprint: crate::service::networking::utils::identity::NetworkFingerprint, pub last_seen: DateTime, } @@ -118,7 +118,7 @@ impl SessionKeys { let mut send_key = [0u8; 32]; let mut receive_key = [0u8; 32]; - // Use the same salt for both keys to ensure initiator's send key + // Use the same salt for both keys to ensure initiator's send key // matches joiner's receive key, enabling successful decryption hk.expand(b"spacedrive-symmetric-key", &mut send_key).unwrap(); hk.expand(b"spacedrive-symmetric-key", &mut receive_key).unwrap(); diff --git a/core/src/services/networking/device/persistence.rs b/core/src/service/network/device/persistence.rs similarity index 99% rename from core/src/services/networking/device/persistence.rs rename to core/src/service/network/device/persistence.rs index 232ca2394..ea4421bbd 100644 --- a/core/src/services/networking/device/persistence.rs +++ b/core/src/service/network/device/persistence.rs @@ -2,7 +2,7 @@ use super::{DeviceInfo, SessionKeys}; use crate::keys::device_key_manager::DeviceKeyManager; -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; use aes_gcm::{ aead::{Aead, AeadCore, KeyInit, OsRng}, Aes256Gcm, Key, Nonce, @@ -386,7 +386,7 @@ impl DevicePersistence { #[cfg(test)] mod tests { use super::*; - use crate::services::networking::utils::identity::NetworkFingerprint; + use crate::service::networking::utils::identity::NetworkFingerprint; use tempfile::TempDir; async fn create_test_persistence() -> (DevicePersistence, TempDir) { diff --git a/core/src/services/networking/device/registry.rs b/core/src/service/network/device/registry.rs similarity index 98% rename from core/src/services/networking/device/registry.rs rename to core/src/service/network/device/registry.rs index 2aadf76aa..2262ba97e 100644 --- a/core/src/services/networking/device/registry.rs +++ b/core/src/service/network/device/registry.rs @@ -2,7 +2,7 @@ use super::{DeviceConnection, DeviceInfo, DeviceState, DevicePersistence, PersistedPairedDevice, SessionKeys, TrustLevel}; use crate::device::DeviceManager; -use crate::services::networking::{NetworkingError, Result, utils::logging::NetworkLogger}; +use crate::service::networking::{NetworkingError, Result, utils::logging::NetworkLogger}; use chrono::{DateTime, Utc}; use iroh::net::NodeAddr; use iroh::net::key::NodeId; @@ -36,7 +36,7 @@ impl DeviceRegistry { /// Create a new device registry pub fn new(device_manager: Arc, data_dir: impl AsRef, logger: Arc) -> Result { let persistence = DevicePersistence::new(data_dir)?; - + Ok(Self { device_manager, devices: HashMap::new(), @@ -126,11 +126,11 @@ impl DeviceRegistry { // Parse node ID from network fingerprint let node_id = info.network_fingerprint.node_id.parse::() .map_err(|e| NetworkingError::Protocol(format!("Invalid node ID in network fingerprint: {}", e)))?; - + // Add node-to-device mapping so device can be found for messaging self.node_to_device.insert(node_id, device_id); self.logger.debug(&format!("Added node-to-device mapping: {} -> {}", node_id, device_id)).await; - + // Get current addresses from any existing state let mut addresses: Vec = match self.devices.get(&device_id) { Some(DeviceState::Discovered { node_addr, .. }) => { @@ -143,7 +143,7 @@ impl DeviceRegistry { } _ => vec![] }; - + // If we still don't have addresses, try to get them from the active connection if addresses.is_empty() { // Check if there's an active connection we can get addresses from @@ -206,7 +206,7 @@ impl DeviceRegistry { // Extract addresses before moving connection let addresses: Vec = connection.addresses.iter().map(|addr| addr.to_string()).collect(); - + let state = DeviceState::Connected { info, connection, @@ -380,7 +380,7 @@ impl DeviceRegistry { os_version: std::env::consts::OS.to_string(), app_version: env!("CARGO_PKG_VERSION").to_string(), network_fingerprint: - crate::services::networking::utils::identity::NetworkFingerprint { + crate::service::networking::utils::identity::NetworkFingerprint { node_id: "placeholder".to_string(), // Will be filled in by caller public_key_hash: "placeholder".to_string(), }, @@ -438,7 +438,7 @@ impl DeviceRegistry { for device_id in to_remove { let _ = self.remove_device(device_id); } - + // Remove expired session mappings for session_id in session_mappings_to_remove { self.session_to_device.remove(&session_id); @@ -451,7 +451,7 @@ impl DeviceRegistry { // Update the node_to_device mapping self.node_to_device.insert(node_id, device_id); - + // Get the current device state to preserve info if let Some(current_state) = self.devices.get(&device_id) { match current_state { @@ -468,7 +468,7 @@ impl DeviceRegistry { }, }; self.devices.insert(device_id, state); - + // Update persisted device with new addresses for future reconnection if !addresses.is_empty() { if let Err(e) = self.persistence.update_device_connection( @@ -489,7 +489,7 @@ impl DeviceRegistry { rx_bytes: connection.rx_bytes, tx_bytes: connection.tx_bytes, }; - + let state = DeviceState::Connected { info: info.clone(), session_keys: session_keys.clone(), @@ -497,7 +497,7 @@ impl DeviceRegistry { connection: updated_connection, }; self.devices.insert(device_id, state); - + // Update persisted device with new addresses if let Err(e) = self.persistence.update_device_connection( device_id, @@ -519,7 +519,7 @@ impl DeviceRegistry { } else { return Err(NetworkingError::DeviceNotFound(device_id)); } - + Ok(()) } } diff --git a/core/src/services/networking/mod.rs b/core/src/service/network/mod.rs similarity index 100% rename from core/src/services/networking/mod.rs rename to core/src/service/network/mod.rs diff --git a/core/src/services/networking/protocols/file_transfer.rs b/core/src/service/network/protocol/file_transfer.rs similarity index 98% rename from core/src/services/networking/protocols/file_transfer.rs rename to core/src/service/network/protocol/file_transfer.rs index c2e6258d6..f50f999d1 100644 --- a/core/src/services/networking/protocols/file_transfer.rs +++ b/core/src/service/network/protocol/file_transfer.rs @@ -1,7 +1,7 @@ //! File transfer protocol for cross-device file operations -use crate::services::networking::utils::logging::NetworkLogger; -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::utils::logging::NetworkLogger; +use crate::service::networking::{NetworkingError, Result}; use async_trait::async_trait; use iroh::net::key::NodeId; use serde::{Deserialize, Serialize}; @@ -37,7 +37,7 @@ pub struct FileTransferProtocolHandler { config: TransferConfig, /// Device registry for session keys device_registry: - Option>>, + Option>>, /// Logger for protocol operations logger: Arc, } @@ -203,31 +203,31 @@ impl FileTransferProtocolHandler { fn truncate_message_for_logging(message: &FileTransferMessage) -> String { match message { FileTransferMessage::TransferRequest { transfer_id, file_metadata, transfer_mode, chunk_size, total_chunks, destination_path } => { - format!("TransferRequest {{ transfer_id: {}, file_metadata: FileMetadata {{ name: \"{}\", size: {}, is_directory: {}, checksum: {:?}, .. }}, transfer_mode: {:?}, chunk_size: {}, total_chunks: {}, destination_path: \"{}\" }}", - transfer_id, file_metadata.name, file_metadata.size, file_metadata.is_directory, - file_metadata.checksum.as_ref().map(|c| &c[..16]).unwrap_or("None"), + format!("TransferRequest {{ transfer_id: {}, file_metadata: FileMetadata {{ name: \"{}\", size: {}, is_directory: {}, checksum: {:?}, .. }}, transfer_mode: {:?}, chunk_size: {}, total_chunks: {}, destination_path: \"{}\" }}", + transfer_id, file_metadata.name, file_metadata.size, file_metadata.is_directory, + file_metadata.checksum.as_ref().map(|c| &c[..16]).unwrap_or("None"), transfer_mode, chunk_size, total_chunks, destination_path) }, FileTransferMessage::FileChunk { transfer_id, chunk_index, data, nonce, chunk_checksum } => { - format!("FileChunk {{ transfer_id: {}, chunk_index: {}, data: [{} bytes], nonce: [{} bytes], chunk_checksum: [{} bytes] }}", + format!("FileChunk {{ transfer_id: {}, chunk_index: {}, data: [{} bytes], nonce: [{} bytes], chunk_checksum: [{} bytes] }}", transfer_id, chunk_index, data.len(), nonce.len(), chunk_checksum.len()) }, FileTransferMessage::TransferComplete { transfer_id, final_checksum, total_bytes } => { - format!("TransferComplete {{ transfer_id: {}, final_checksum: \"{}\", total_bytes: {} }}", - transfer_id, - if final_checksum.len() > 16 { format!("{}...", &final_checksum[..16]) } else { final_checksum.clone() }, + format!("TransferComplete {{ transfer_id: {}, final_checksum: \"{}\", total_bytes: {} }}", + transfer_id, + if final_checksum.len() > 16 { format!("{}...", &final_checksum[..16]) } else { final_checksum.clone() }, total_bytes) }, FileTransferMessage::TransferResponse { transfer_id, accepted, reason, supported_resume } => { - format!("TransferResponse {{ transfer_id: {}, accepted: {}, reason: {:?}, supported_resume: {} }}", + format!("TransferResponse {{ transfer_id: {}, accepted: {}, reason: {:?}, supported_resume: {} }}", transfer_id, accepted, reason, supported_resume) }, FileTransferMessage::ChunkAck { transfer_id, chunk_index, next_expected } => { - format!("ChunkAck {{ transfer_id: {}, chunk_index: {}, next_expected: {} }}", + format!("ChunkAck {{ transfer_id: {}, chunk_index: {}, next_expected: {} }}", transfer_id, chunk_index, next_expected) }, FileTransferMessage::TransferError { transfer_id, error_type, message, recoverable } => { - format!("TransferError {{ transfer_id: {}, error_type: {:?}, message: \"{}\", recoverable: {} }}", + format!("TransferError {{ transfer_id: {}, error_type: {:?}, message: \"{}\", recoverable: {} }}", transfer_id, error_type, message, recoverable) }, FileTransferMessage::TransferFinalAck { transfer_id } => { @@ -240,7 +240,7 @@ impl FileTransferProtocolHandler { pub fn set_device_registry( &mut self, device_registry: Arc< - tokio::sync::RwLock, + tokio::sync::RwLock, >, ) { self.device_registry = Some(device_registry); @@ -1278,8 +1278,8 @@ impl NetworkingError { #[cfg(test)] mod tests { use super::*; - use crate::services::networking::protocols::ProtocolHandler; - use crate::services::networking::utils::logging::SilentLogger; + use crate::service::networking::protocols::ProtocolHandler; + use crate::service::networking::utils::logging::SilentLogger; #[tokio::test] async fn test_file_transfer_handler_creation() { diff --git a/core/src/services/networking/protocols/messaging.rs b/core/src/service/network/protocol/messaging.rs similarity index 98% rename from core/src/services/networking/protocols/messaging.rs rename to core/src/service/network/protocol/messaging.rs index c951af4a5..edcfcd24e 100644 --- a/core/src/services/networking/protocols/messaging.rs +++ b/core/src/service/network/protocol/messaging.rs @@ -1,7 +1,7 @@ //! Basic messaging protocol handler use super::{ProtocolEvent, ProtocolHandler}; -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; use iroh::net::key::NodeId; use async_trait::async_trait; use serde::{Deserialize, Serialize}; @@ -134,7 +134,7 @@ impl ProtocolHandler for MessagingProtocolHandler { remote_node_id: NodeId, ) { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - + // Simple request-response messaging over streams loop { // Read message length (4 bytes) @@ -144,14 +144,14 @@ impl ProtocolHandler for MessagingProtocolHandler { Err(_) => break, // Connection closed } let msg_len = u32::from_be_bytes(len_buf) as usize; - + // Read message let mut msg_buf = vec![0u8; msg_len]; if let Err(e) = recv.read_exact(&mut msg_buf).await { eprintln!("Failed to read message: {}", e); break; } - + // Deserialize and handle match serde_json::from_slice::(&msg_buf) { Ok(message) => { @@ -174,7 +174,7 @@ impl ProtocolHandler for MessagingProtocolHandler { } _ => Vec::new(), // No response for Pong/Ack }; - + // Send response if any if !response.is_empty() { let len = response.len() as u32; diff --git a/core/src/services/networking/protocols/mod.rs b/core/src/service/network/protocol/mod.rs similarity index 84% rename from core/src/services/networking/protocols/mod.rs rename to core/src/service/network/protocol/mod.rs index fa5d2d1fe..b8c7bc4c8 100644 --- a/core/src/services/networking/protocols/mod.rs +++ b/core/src/service/network/protocol/mod.rs @@ -5,13 +5,15 @@ pub mod messaging; pub mod pairing; pub mod registry; -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; use async_trait::async_trait; use iroh::net::key::NodeId; use std::collections::HashMap; use uuid::Uuid; -pub use file_transfer::{FileTransferMessage, FileTransferProtocolHandler, FileMetadata, TransferMode, TransferSession}; +pub use file_transfer::{ + FileMetadata, FileTransferMessage, FileTransferProtocolHandler, TransferMode, TransferSession, +}; pub use messaging::MessagingProtocolHandler; pub use pairing::{PairingMessage, PairingProtocolHandler, PairingSession, PairingState}; pub use registry::ProtocolRegistry; @@ -34,7 +36,12 @@ pub trait ProtocolHandler: Send + Sync { async fn handle_request(&self, from_device: Uuid, request_data: Vec) -> Result>; /// Handle an incoming response (legacy compatibility) - async fn handle_response(&self, from_device: Uuid, from_node: NodeId, response_data: Vec) -> Result<()>; + async fn handle_response( + &self, + from_device: Uuid, + from_node: NodeId, + response_data: Vec, + ) -> Result<()>; /// Handle protocol-specific events async fn handle_event(&self, event: ProtocolEvent) -> Result<()>; @@ -60,4 +67,4 @@ pub enum ProtocolEvent { protocol: String, data: HashMap, }, -} \ No newline at end of file +} diff --git a/core/src/services/networking/protocols/pairing/initiator.rs b/core/src/service/network/protocol/pairing/initiator.rs similarity index 97% rename from core/src/services/networking/protocols/pairing/initiator.rs rename to core/src/service/network/protocol/pairing/initiator.rs index 4dcbf0b94..6bc4cb401 100644 --- a/core/src/services/networking/protocols/pairing/initiator.rs +++ b/core/src/service/network/protocol/pairing/initiator.rs @@ -6,7 +6,7 @@ use super::{ types::{PairingSession, PairingState}, PairingProtocolHandler, }; -use crate::services::networking::{ +use crate::service::networking::{ device::{DeviceInfo, SessionKeys}, NetworkingError, Result, }; @@ -40,11 +40,11 @@ impl PairingProtocolHandler { // Hold the write lock for the entire duration to prevent any scoping issues let mut sessions = self.active_sessions.write().await; self.log_debug(&format!("🔍 INITIATOR_HANDLER_DEBUG: Looking for session {} in {} total sessions", session_id, sessions.len())).await; - + if let Some(existing_session) = sessions.get_mut(&session_id) { self.log_debug(&format!("🔍 INITIATOR_HANDLER_DEBUG: Found existing session {} in state {:?}", session_id, existing_session.state)).await; self.log_debug(&format!("Transitioning existing session {} to ChallengeReceived", session_id)).await; - + // Update the existing session in place existing_session.state = PairingState::ChallengeReceived { challenge: challenge.clone(), @@ -146,14 +146,14 @@ impl PairingProtocolHandler { "Invalid signature for session {} from device {}", session_id, from_device )).await; - + // Mark session as failed if let Some(session) = self.active_sessions.write().await.get_mut(&session_id) { session.state = PairingState::Failed { reason: "Invalid challenge signature".to_string(), }; } - + return Err(NetworkingError::Protocol( "Challenge signature verification failed".to_string(), )); @@ -190,7 +190,7 @@ impl PairingProtocolHandler { }; // Mark device as connected since pairing is successful - let simple_connection = crate::services::networking::device::DeviceConnection { + let simple_connection = crate::service::networking::device::DeviceConnection { addresses: vec![], // Will be filled in later latency_ms: None, rx_bytes: 0, @@ -232,7 +232,7 @@ impl PairingProtocolHandler { )).await; // Send a command to establish a new persistent connection - let command = crate::services::networking::core::event_loop::EventLoopCommand::EstablishPersistentConnection { + let command = crate::service::networking::core::event_loop::EventLoopCommand::EstablishPersistentConnection { device_id: actual_device_id, node_id, }; diff --git a/core/src/services/networking/protocols/pairing/joiner.rs b/core/src/service/network/protocol/pairing/joiner.rs similarity index 94% rename from core/src/services/networking/protocols/pairing/joiner.rs rename to core/src/service/network/protocol/pairing/joiner.rs index 672b54de6..5aeac8312 100644 --- a/core/src/services/networking/protocols/pairing/joiner.rs +++ b/core/src/service/network/protocol/pairing/joiner.rs @@ -5,7 +5,7 @@ use super::{ types::{PairingSession, PairingState}, PairingProtocolHandler, }; -use crate::services::networking::{ +use crate::service::networking::{ device::{DeviceInfo, SessionKeys}, NetworkingError, Result, }; @@ -92,7 +92,7 @@ impl PairingProtocolHandler { // Mark the initiator device as connected immediately after pairing completes // This ensures Bob sees Alice as connected even if the completion message fails { - let simple_connection = crate::services::networking::device::DeviceConnection { + let simple_connection = crate::service::networking::device::DeviceConnection { addresses: vec![], // Will be filled in later latency_ms: None, rx_bytes: 0, @@ -206,13 +206,13 @@ impl PairingProtocolHandler { "No remote device info stored in session, using fallback", ) .await; - crate::services::networking::device::DeviceInfo { + crate::service::networking::device::DeviceInfo { device_id: from_device, device_name: format!("Remote Device {}", &from_device.to_string()[..8]), - device_type: crate::services::networking::device::DeviceType::Desktop, + device_type: crate::service::networking::device::DeviceType::Desktop, os_version: "Unknown".to_string(), app_version: "Unknown".to_string(), - network_fingerprint: crate::services::networking::utils::identity::NetworkFingerprint { + network_fingerprint: crate::service::networking::utils::identity::NetworkFingerprint { node_id: from_node.to_string(), public_key_hash: "unknown".to_string(), }, @@ -220,7 +220,7 @@ impl PairingProtocolHandler { } } } else { - return Err(crate::services::networking::NetworkingError::Protocol( + return Err(crate::service::networking::NetworkingError::Protocol( "Session not found when completing pairing".to_string(), )); } @@ -259,7 +259,7 @@ impl PairingProtocolHandler { if let Some(node_id) = initiator_node_id { let simple_connection = - crate::services::networking::device::DeviceConnection { + crate::service::networking::device::DeviceConnection { addresses: vec![], // Will be filled in later latency_ms: None, rx_bytes: 0, @@ -281,7 +281,7 @@ impl PairingProtocolHandler { )).await; // Send a command to establish a new persistent connection - let command = crate::services::networking::core::event_loop::EventLoopCommand::EstablishPersistentConnection { + let command = crate::service::networking::core::event_loop::EventLoopCommand::EstablishPersistentConnection { device_id: actual_device_id, node_id: from_node, }; diff --git a/core/src/services/networking/protocols/pairing/messages.rs b/core/src/service/network/protocol/pairing/messages.rs similarity index 93% rename from core/src/services/networking/protocols/pairing/messages.rs rename to core/src/service/network/protocol/pairing/messages.rs index 5cc6d2634..dfa657ea7 100644 --- a/core/src/services/networking/protocols/pairing/messages.rs +++ b/core/src/service/network/protocol/pairing/messages.rs @@ -1,6 +1,6 @@ //! Pairing protocol message definitions -use crate::services::networking::device::DeviceInfo; +use crate::service::networking::device::DeviceInfo; use serde::{Deserialize, Serialize}; use uuid::Uuid; diff --git a/core/src/services/networking/protocols/pairing/mod.rs b/core/src/service/network/protocol/pairing/mod.rs similarity index 97% rename from core/src/services/networking/protocols/pairing/mod.rs rename to core/src/service/network/protocol/pairing/mod.rs index b157138f4..8bfd8dcb9 100644 --- a/core/src/services/networking/protocols/pairing/mod.rs +++ b/core/src/service/network/protocol/pairing/mod.rs @@ -12,7 +12,7 @@ pub use messages::PairingMessage; pub use types::{PairingAdvertisement, PairingCode, PairingRole, PairingSession, PairingState}; use super::{ProtocolEvent, ProtocolHandler}; -use crate::services::networking::{ +use crate::service::networking::{ device::{DeviceInfo, DeviceRegistry, SessionKeys}, utils::{identity::NetworkFingerprint, logging::NetworkLogger, NetworkIdentity}, NetworkingError, Result, @@ -47,7 +47,7 @@ pub struct PairingProtocolHandler { logger: Arc, /// Command sender for dispatching commands to the NetworkingEventLoop - command_sender: tokio::sync::mpsc::UnboundedSender, + command_sender: tokio::sync::mpsc::UnboundedSender, /// Current pairing role role: Option, @@ -62,7 +62,7 @@ impl PairingProtocolHandler { identity: NetworkIdentity, device_registry: Arc>, logger: Arc, - command_sender: tokio::sync::mpsc::UnboundedSender, + command_sender: tokio::sync::mpsc::UnboundedSender, ) -> Self { Self { identity, @@ -81,7 +81,7 @@ impl PairingProtocolHandler { identity: NetworkIdentity, device_registry: Arc>, logger: Arc, - command_sender: tokio::sync::mpsc::UnboundedSender, + command_sender: tokio::sync::mpsc::UnboundedSender, data_dir: PathBuf, ) -> Self { let persistence = Arc::new(PairingPersistence::new(data_dir)); @@ -387,7 +387,7 @@ impl PairingProtocolHandler { )).await; // Create the command to send the message - let command = crate::services::networking::core::event_loop::EventLoopCommand::SendMessageToNode { + let command = crate::service::networking::core::event_loop::EventLoopCommand::SendMessageToNode { node_id: *node_id, protocol: "pairing".to_string(), data: response_data.clone(), @@ -403,8 +403,8 @@ impl PairingProtocolHandler { )).await; } else { self.log_error("State Machine: Failed to send command to event loop.").await; - session.state = PairingState::Failed { - reason: "Internal channel closed".to_string() + session.state = PairingState::Failed { + reason: "Internal channel closed".to_string() }; } } else { @@ -412,8 +412,8 @@ impl PairingProtocolHandler { "State Machine: Session {} in ResponsePending but no remote node ID", session.id )).await; - session.state = PairingState::Failed { - reason: "No remote node ID for response".to_string() + session.state = PairingState::Failed { + reason: "No remote node ID for response".to_string() }; } } @@ -426,8 +426,8 @@ impl PairingProtocolHandler { "State Machine: Session {} timed out while scanning, marking as failed", session.id )).await; - session.state = PairingState::Failed { - reason: "Scanning timeout".to_string() + session.state = PairingState::Failed { + reason: "Scanning timeout".to_string() }; } } @@ -457,11 +457,11 @@ impl PairingProtocolHandler { .ok_or_else(|| NetworkingError::Protocol( format!("No pairing code found for session {}", session_id) ))?; - + // Use the pairing code secret as the shared secret Ok(pairing_code.secret().to_vec()) } - + /// Handle a pairing message received over stream async fn handle_pairing_message(&self, message: PairingMessage, remote_node_id: NodeId) -> Result>> { match message { @@ -487,7 +487,7 @@ impl PairingProtocolHandler { } } } - + /// Get or create a device ID for a node async fn get_device_id_for_node(&self, node_id: NodeId) -> Uuid { let registry = self.device_registry.read().await; @@ -503,7 +503,7 @@ impl PairingProtocolHandler { Uuid::from_bytes(uuid_bytes) }) } - + /// Send a pairing message to a specific node using Iroh streams pub async fn send_pairing_message_to_node( &self, @@ -512,48 +512,48 @@ impl PairingProtocolHandler { message: &PairingMessage, ) -> Result> { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - + // Create node address and connect let node_addr = NodeAddr::new(node_id); - let conn = endpoint.connect(node_addr, crate::services::networking::core::PAIRING_ALPN).await + let conn = endpoint.connect(node_addr, crate::service::networking::core::PAIRING_ALPN).await .map_err(|e| NetworkingError::ConnectionFailed(format!("Failed to connect: {}", e)))?; - + // Open a bidirectional stream let (mut send, mut recv) = conn.open_bi().await .map_err(|e| NetworkingError::ConnectionFailed(format!("Failed to open stream: {}", e)))?; - + // Serialize the message let msg_data = serde_json::to_vec(message) .map_err(|e| NetworkingError::Serialization(e))?; - + // Send message length let len = msg_data.len() as u32; send.write_all(&len.to_be_bytes()).await .map_err(|e| NetworkingError::Transport(format!("Failed to write length: {}", e)))?; - + // Send message send.write_all(&msg_data).await .map_err(|e| NetworkingError::Transport(format!("Failed to write message: {}", e)))?; - + // Flush send.flush().await .map_err(|e| NetworkingError::Transport(format!("Failed to flush: {}", e)))?; - + // Read response length let mut len_buf = [0u8; 4]; match recv.read_exact(&mut len_buf).await { Ok(_) => { let resp_len = u32::from_be_bytes(len_buf) as usize; - + // Read response let mut resp_buf = vec![0u8; resp_len]; recv.read_exact(&mut resp_buf).await .map_err(|e| NetworkingError::Transport(format!("Failed to read response: {}", e)))?; - + // Deserialize response let response: PairingMessage = serde_json::from_slice(&resp_buf) .map_err(|e| NetworkingError::Serialization(e))?; - + Ok(Some(response)) } Err(_) => Ok(None), // No response @@ -574,9 +574,9 @@ impl ProtocolHandler for PairingProtocolHandler { remote_node_id: NodeId, ) { use tokio::io::{AsyncReadExt, AsyncWriteExt}; - + self.logger.info(&format!("handle_stream called from node {}", remote_node_id)).await; - + // Keep the stream alive for multiple message exchanges loop { // Read the message length (4 bytes) @@ -591,14 +591,14 @@ impl ProtocolHandler for PairingProtocolHandler { } let msg_len = u32::from_be_bytes(len_buf) as usize; self.logger.info(&format!("Read message length: {} bytes", msg_len)).await; - + // Read the message let mut msg_buf = vec![0u8; msg_len]; if let Err(e) = recv.read_exact(&mut msg_buf).await { self.logger.error(&format!("Failed to read message: {}", e)).await; break; } - + // Deserialize and handle the message let message: PairingMessage = match serde_json::from_slice(&msg_buf) { Ok(msg) => { @@ -617,7 +617,7 @@ impl ProtocolHandler for PairingProtocolHandler { break; } }; - + // Process the message and get response let response = match self.handle_pairing_message(message.clone(), remote_node_id).await { Ok(resp) => resp, @@ -626,7 +626,7 @@ impl ProtocolHandler for PairingProtocolHandler { break; } }; - + // Send response if any if let Some(response_data) = response { // Write message length @@ -635,27 +635,27 @@ impl ProtocolHandler for PairingProtocolHandler { self.logger.error(&format!("Failed to write response length: {}", e)).await; break; } - + // Write message if let Err(e) = send.write_all(&response_data).await { self.logger.error(&format!("Failed to write response: {}", e)).await; break; } - + // Flush the stream if let Err(e) = send.flush().await { self.logger.error(&format!("Failed to flush stream: {}", e)).await; break; } } - + // Check if this was a completion message - if so, we can close the stream if matches!(message, PairingMessage::Complete { .. }) { self.logger.info("Received Complete message, closing pairing stream").await; break; } } - + self.logger.info(&format!("Pairing stream handler completed for node {}", remote_node_id)).await; } @@ -810,19 +810,19 @@ impl ProtocolHandler for PairingProtocolHandler { self.log_error(&format!("ERROR: Session {} not found when trying to update state", session_id)).await; } } - + // Send the response directly via command sender self.log_info(&format!( "Sending challenge response directly to node {}", from_node )).await; - - let command = crate::services::networking::core::event_loop::EventLoopCommand::SendMessageToNode { + + let command = crate::service::networking::core::event_loop::EventLoopCommand::SendMessageToNode { node_id: from_node, protocol: "pairing".to_string(), data: response_data.clone(), }; - + if let Err(e) = self.command_sender.send(command) { self.log_error(&format!( "Failed to send response command: {:?}", diff --git a/core/src/service/network/protocol/pairing/persistence.rs b/core/src/service/network/protocol/pairing/persistence.rs new file mode 100644 index 000000000..cd32ce519 --- /dev/null +++ b/core/src/service/network/protocol/pairing/persistence.rs @@ -0,0 +1,325 @@ +//! Session persistence for pairing protocol + +use super::types::{PairingSession, PairingState}; +use crate::service::networking::{NetworkingError, Result}; +use serde::{Deserialize, Serialize}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use tokio::fs; +use uuid::Uuid; + +/// Serializable version of PairingSession for persistence +#[derive(Debug, Clone, Serialize, Deserialize)] +struct SerializablePairingSession { + pub id: Uuid, + pub state: SerializablePairingState, + pub remote_device_id: Option, + pub remote_public_key: Option>, + pub shared_secret: Option>, + pub created_at: chrono::DateTime, +} + +/// Serializable version of PairingState +#[derive(Debug, Clone, Serialize, Deserialize)] +enum SerializablePairingState { + WaitingForConnection, + Scanning, + ChallengeReceived { challenge: Vec }, + ResponseSent, + Completed, + Failed { reason: String }, +} + +impl From<&PairingSession> for SerializablePairingSession { + fn from(session: &PairingSession) -> Self { + Self { + id: session.id, + state: match &session.state { + PairingState::WaitingForConnection => { + SerializablePairingState::WaitingForConnection + } + PairingState::Scanning => SerializablePairingState::Scanning, + PairingState::ChallengeReceived { challenge } => { + SerializablePairingState::ChallengeReceived { + challenge: challenge.clone(), + } + } + PairingState::ResponseSent => SerializablePairingState::ResponseSent, + PairingState::Completed => SerializablePairingState::Completed, + PairingState::Failed { reason } => SerializablePairingState::Failed { + reason: reason.clone(), + }, + // Skip non-serializable states + _ => SerializablePairingState::Failed { + reason: "State not serializable".to_string(), + }, + }, + remote_device_id: session.remote_device_id, + remote_public_key: session.remote_public_key.clone(), + shared_secret: session.shared_secret.clone(), + created_at: session.created_at, + } + } +} + +impl From for PairingSession { + fn from(serializable: SerializablePairingSession) -> Self { + Self { + id: serializable.id, + state: match serializable.state { + SerializablePairingState::WaitingForConnection => { + PairingState::WaitingForConnection + } + SerializablePairingState::Scanning => PairingState::Scanning, + SerializablePairingState::ChallengeReceived { challenge } => { + PairingState::ChallengeReceived { challenge } + } + SerializablePairingState::ResponseSent => PairingState::ResponseSent, + SerializablePairingState::Completed => PairingState::Completed, + SerializablePairingState::Failed { reason } => PairingState::Failed { reason }, + }, + remote_device_id: serializable.remote_device_id, + remote_device_info: None, // Will be restored from device registry + remote_public_key: serializable.remote_public_key, + shared_secret: serializable.shared_secret, + created_at: serializable.created_at, + } + } +} + +/// Persisted pairing sessions data +#[derive(Debug, Serialize, Deserialize)] +struct PersistedPairingSessions { + sessions: HashMap, + last_saved: chrono::DateTime, +} + +/// Session persistence manager +pub struct PairingPersistence { + data_dir: PathBuf, + sessions_file: PathBuf, +} + +impl PairingPersistence { + /// Create a new persistence manager + pub fn new(data_dir: impl AsRef) -> Self { + let data_dir = data_dir.as_ref().to_path_buf(); + let networking_dir = data_dir.join("networking"); + let sessions_file = networking_dir.join("pairing_sessions.json"); + + Self { + data_dir: networking_dir, + sessions_file, + } + } + + /// Save active sessions to disk + pub async fn save_sessions(&self, sessions: &HashMap) -> Result<()> { + // Ensure data directory exists + if let Some(parent) = self.sessions_file.parent() { + fs::create_dir_all(parent) + .await + .map_err(NetworkingError::Io)?; + } + + // Convert to serializable format, filtering out transient states + let serializable_sessions: HashMap = sessions + .iter() + .filter_map(|(id, session)| { + // Only persist certain states + match &session.state { + PairingState::WaitingForConnection + | PairingState::Scanning + | PairingState::ChallengeReceived { .. } + | PairingState::ResponseSent + | PairingState::Completed => Some((*id, session.into())), + // Don't persist transient or failed states + _ => None, + } + }) + .collect(); + + let persisted = PersistedPairingSessions { + sessions: serializable_sessions, + last_saved: chrono::Utc::now(), + }; + + // Write to temporary file first, then rename for atomic operation + let temp_file = self.sessions_file.with_extension("tmp"); + let json_data = serde_json::to_string_pretty(&persisted) + .map_err(|e| NetworkingError::Serialization(e))?; + + fs::write(&temp_file, json_data) + .await + .map_err(NetworkingError::Io)?; + + fs::rename(&temp_file, &self.sessions_file) + .await + .map_err(NetworkingError::Io)?; + + Ok(()) + } + + /// Load sessions from disk + pub async fn load_sessions(&self) -> Result> { + if !self.sessions_file.exists() { + return Ok(HashMap::new()); + } + + let json_data = match fs::read_to_string(&self.sessions_file).await { + Ok(data) => data, + Err(e) => { + eprintln!("Failed to read pairing sessions file: {}", e); + return Ok(HashMap::new()); + } + }; + + // Handle empty files + if json_data.trim().is_empty() { + eprintln!("Pairing sessions file is empty, returning empty sessions"); + return Ok(HashMap::new()); + } + + let persisted: PersistedPairingSessions = match serde_json::from_str(&json_data) { + Ok(p) => p, + Err(e) => { + eprintln!( + "Failed to parse pairing sessions JSON: {}. File may be corrupted.", + e + ); + // Try to rename the corrupted file for debugging + let backup_path = self.sessions_file.with_extension("json.corrupted"); + let _ = fs::rename(&self.sessions_file, &backup_path).await; + eprintln!("Renamed corrupted file to: {:?}", backup_path); + return Ok(HashMap::new()); + } + }; + + // Filter out expired sessions (older than 1 hour) + let now = chrono::Utc::now(); + let max_age = chrono::Duration::hours(1); + + let sessions: HashMap = persisted + .sessions + .into_iter() + .filter_map(|(id, serializable)| { + let age = now.signed_duration_since(serializable.created_at); + if age <= max_age { + Some((id, serializable.into())) + } else { + None + } + }) + .collect(); + + Ok(sessions) + } + + /// Clean up expired sessions from disk + pub async fn cleanup_expired_sessions(&self) -> Result { + let sessions = self.load_sessions().await?; + let initial_count = sessions.len(); + + // Save the filtered sessions back + self.save_sessions(&sessions).await?; + + Ok(initial_count - sessions.len()) + } + + /// Delete all persisted sessions + pub async fn clear_all_sessions(&self) -> Result<()> { + if self.sessions_file.exists() { + fs::remove_file(&self.sessions_file) + .await + .map_err(NetworkingError::Io)?; + } + Ok(()) + } + + /// Get the path to the sessions file + pub fn sessions_file_path(&self) -> &Path { + &self.sessions_file + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + async fn create_test_persistence() -> (PairingPersistence, TempDir) { + let temp_dir = TempDir::new().expect("Failed to create temp dir"); + let persistence = PairingPersistence::new(temp_dir.path()); + (persistence, temp_dir) + } + + #[tokio::test] + async fn test_save_and_load_sessions() { + let (persistence, _temp_dir) = create_test_persistence().await; + + // Create test sessions + let mut sessions = HashMap::new(); + let session_id = Uuid::new_v4(); + let session = PairingSession { + id: session_id, + state: PairingState::WaitingForConnection, + remote_device_id: Some(Uuid::new_v4()), + remote_device_info: None, + remote_public_key: None, + shared_secret: Some(vec![1, 2, 3, 4]), + created_at: chrono::Utc::now(), + }; + sessions.insert(session_id, session); + + // Save sessions + persistence.save_sessions(&sessions).await.unwrap(); + + // Load sessions + let loaded_sessions = persistence.load_sessions().await.unwrap(); + + assert_eq!(loaded_sessions.len(), 1); + assert!(loaded_sessions.contains_key(&session_id)); + + let loaded_session = &loaded_sessions[&session_id]; + assert_eq!(loaded_session.id, session_id); + assert!(matches!( + loaded_session.state, + PairingState::WaitingForConnection + )); + } + + #[tokio::test] + async fn test_load_nonexistent_file() { + let (persistence, _temp_dir) = create_test_persistence().await; + + let sessions = persistence.load_sessions().await.unwrap(); + assert!(sessions.is_empty()); + } + + #[tokio::test] + async fn test_clear_sessions() { + let (persistence, _temp_dir) = create_test_persistence().await; + + // Create and save sessions + let mut sessions = HashMap::new(); + sessions.insert( + Uuid::new_v4(), + PairingSession { + id: Uuid::new_v4(), + state: PairingState::Completed, + remote_device_id: None, + remote_device_info: None, + remote_public_key: None, + shared_secret: None, + created_at: chrono::Utc::now(), + }, + ); + + persistence.save_sessions(&sessions).await.unwrap(); + assert!(persistence.sessions_file.exists()); + + // Clear sessions + persistence.clear_all_sessions().await.unwrap(); + assert!(!persistence.sessions_file.exists()); + } +} diff --git a/core/src/services/networking/protocols/pairing/security.rs b/core/src/service/network/protocol/pairing/security.rs similarity index 99% rename from core/src/services/networking/protocols/pairing/security.rs rename to core/src/service/network/protocol/pairing/security.rs index abb3d1e18..dec18509d 100644 --- a/core/src/services/networking/protocols/pairing/security.rs +++ b/core/src/service/network/protocol/pairing/security.rs @@ -1,6 +1,6 @@ //! Security utilities for pairing protocol -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; // We'll use our own signature verification /// Security operations for pairing protocol diff --git a/core/src/services/networking/protocols/pairing/types.rs b/core/src/service/network/protocol/pairing/types.rs similarity index 90% rename from core/src/services/networking/protocols/pairing/types.rs rename to core/src/service/network/protocol/pairing/types.rs index 205ca136d..178292fef 100644 --- a/core/src/services/networking/protocols/pairing/types.rs +++ b/core/src/service/network/protocol/pairing/types.rs @@ -1,6 +1,6 @@ //! Pairing protocol types and state definitions -use crate::services::networking::{ +use crate::service::networking::{ device::{DeviceInfo, SessionKeys}, utils::identity::NetworkFingerprint, }; @@ -28,7 +28,7 @@ pub struct PairingCode { impl PairingCode { /// Generate a new pairing code using BIP39 wordlist - pub fn generate() -> crate::services::networking::Result { + pub fn generate() -> crate::service::networking::Result { use rand::RngCore; let mut secret = [0u8; 32]; @@ -59,7 +59,7 @@ impl PairingCode { hasher.update(b"spacedrive-pairing-entropy-extension-v1"); hasher.update(entropy); let derived_bytes = hasher.finalize(); - + let mut secret = [0u8; 32]; secret[..16].copy_from_slice(entropy); secret[16..].copy_from_slice(&derived_bytes.as_bytes()[..16]); @@ -79,11 +79,11 @@ impl PairingCode { } /// Parse a pairing code from a BIP39 mnemonic string - pub fn from_string(code: &str) -> crate::services::networking::Result { + pub fn from_string(code: &str) -> crate::service::networking::Result { // Trim the input and normalize whitespace let trimmed = code.trim(); if trimmed.is_empty() { - return Err(crate::services::networking::NetworkingError::Protocol( + return Err(crate::service::networking::NetworkingError::Protocol( "Pairing code cannot be empty".to_string(), )); } @@ -91,14 +91,14 @@ impl PairingCode { let words: Vec = trimmed.split_whitespace().map(|s| s.to_lowercase()).collect(); if words.len() != 12 { - return Err(crate::services::networking::NetworkingError::Protocol( + return Err(crate::service::networking::NetworkingError::Protocol( format!("Invalid pairing code format - expected 12 words but got {}", words.len()), )); } // Convert Vec to array let words_array: [String; 12] = words.try_into().map_err(|_| { - crate::services::networking::NetworkingError::Protocol( + crate::service::networking::NetworkingError::Protocol( "Failed to convert words to array".to_string(), ) })?; @@ -107,13 +107,13 @@ impl PairingCode { } /// Create pairing code from BIP39 words - pub fn from_words(words: &[String; 12]) -> crate::services::networking::Result { + pub fn from_words(words: &[String; 12]) -> crate::service::networking::Result { // Decode BIP39 words back to secret let secret = Self::decode_from_bip39_words(words)?; // Extract session ID directly from the first 16 bytes (entropy) let session_id = Uuid::from_bytes(secret[..16].try_into().map_err(|_| { - crate::services::networking::NetworkingError::Protocol( + crate::service::networking::NetworkingError::Protocol( "Failed to extract session ID from entropy".to_string(), ) })?); @@ -147,7 +147,7 @@ impl PairingCode { } /// Encode bytes to BIP39 words using proper mnemonic generation - fn encode_to_bip39_words(secret: &[u8; 32]) -> crate::services::networking::Result<[String; 12]> { + fn encode_to_bip39_words(secret: &[u8; 32]) -> crate::service::networking::Result<[String; 12]> { use bip39::{Language, Mnemonic}; // For 12 words, we need 128 bits of entropy (standard BIP39) @@ -156,7 +156,7 @@ impl PairingCode { // Generate mnemonic from entropy let mnemonic = Mnemonic::from_entropy(entropy).map_err(|e| { - crate::services::networking::NetworkingError::Protocol(format!( + crate::service::networking::NetworkingError::Protocol(format!( "BIP39 generation failed: {}", e )) @@ -166,7 +166,7 @@ impl PairingCode { let word_list: Vec<&str> = mnemonic.words().collect(); if word_list.len() != 12 { - return Err(crate::services::networking::NetworkingError::Protocol( + return Err(crate::service::networking::NetworkingError::Protocol( format!("Expected 12 words, got {}", word_list.len()), )); } @@ -188,7 +188,7 @@ impl PairingCode { } /// Decode BIP39 words back to secret - fn decode_from_bip39_words(words: &[String; 12]) -> crate::services::networking::Result<[u8; 32]> { + fn decode_from_bip39_words(words: &[String; 12]) -> crate::service::networking::Result<[u8; 32]> { use bip39::{Language, Mnemonic}; // Join words with spaces to create mnemonic string @@ -196,7 +196,7 @@ impl PairingCode { // Parse the mnemonic let mnemonic = Mnemonic::parse_in(Language::English, &mnemonic_str).map_err(|e| { - crate::services::networking::NetworkingError::Protocol(format!( + crate::service::networking::NetworkingError::Protocol(format!( "Invalid BIP39 mnemonic: {}", e )) @@ -206,7 +206,7 @@ impl PairingCode { let entropy = mnemonic.to_entropy(); if entropy.len() != 16 { - return Err(crate::services::networking::NetworkingError::Protocol( + return Err(crate::service::networking::NetworkingError::Protocol( format!("Expected 16 bytes of entropy, got {}", entropy.len()), )); } @@ -389,9 +389,9 @@ pub struct NodeAddrInfo { impl PairingAdvertisement { /// Convert node ID string back to NodeId - pub fn node_id(&self) -> crate::services::networking::Result { + pub fn node_id(&self) -> crate::service::networking::Result { self.node_id.parse().map_err(|e| { - crate::services::networking::NetworkingError::Protocol(format!( + crate::service::networking::NetworkingError::Protocol(format!( "Invalid node ID: {}", e )) @@ -399,16 +399,16 @@ impl PairingAdvertisement { } /// Convert node address info back to NodeAddr - pub fn node_addr(&self) -> crate::services::networking::Result { + pub fn node_addr(&self) -> crate::service::networking::Result { // Parse node ID let node_id = self.node_addr_info.node_id.parse::() - .map_err(|e| crate::services::networking::NetworkingError::Protocol( + .map_err(|e| crate::service::networking::NetworkingError::Protocol( format!("Invalid node ID in advertisement: {}", e) ))?; - + // Start with base NodeAddr let mut node_addr = NodeAddr::new(node_id); - + // Add direct addresses let mut direct_addrs = Vec::new(); for addr_str in &self.node_addr_info.direct_addresses { @@ -419,14 +419,14 @@ impl PairingAdvertisement { if !direct_addrs.is_empty() { node_addr = node_addr.with_direct_addresses(direct_addrs); } - + // Add relay URL if present if let Some(relay_url) = &self.node_addr_info.relay_url { if let Ok(url) = relay_url.parse() { node_addr = node_addr.with_relay_url(url); } } - + Ok(node_addr) } } \ No newline at end of file diff --git a/core/src/services/networking/protocols/registry.rs b/core/src/service/network/protocol/registry.rs similarity index 97% rename from core/src/services/networking/protocols/registry.rs rename to core/src/service/network/protocol/registry.rs index 61c2657e0..86736a40f 100644 --- a/core/src/services/networking/protocols/registry.rs +++ b/core/src/service/network/protocol/registry.rs @@ -2,7 +2,7 @@ use super::{ProtocolEvent, ProtocolHandler}; use iroh::net::key::NodeId; -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; use std::collections::HashMap; use std::sync::Arc; use uuid::Uuid; diff --git a/core/src/services/networking/utils/identity.rs b/core/src/service/network/utils/identity.rs similarity index 96% rename from core/src/services/networking/utils/identity.rs rename to core/src/service/network/utils/identity.rs index caf9a649f..0b1f26066 100644 --- a/core/src/services/networking/utils/identity.rs +++ b/core/src/service/network/utils/identity.rs @@ -1,6 +1,6 @@ //! Network identity management - node ID and key generation -use crate::services::networking::{NetworkingError, Result}; +use crate::service::networking::{NetworkingError, Result}; use iroh::net::key::{NodeId, SecretKey}; use serde::{Deserialize, Serialize}; use uuid::Uuid; @@ -19,12 +19,12 @@ impl NetworkIdentity { pub async fn new() -> Result { let secret_key = SecretKey::generate(); let node_id = secret_key.public(); - + // Generate Ed25519 seed for backward compatibility let ed25519_seed = rand::random(); - Ok(Self { - secret_key, + Ok(Self { + secret_key, node_id, ed25519_seed, }) @@ -35,18 +35,18 @@ impl NetworkIdentity { // Derive Ed25519 seed from master key using HKDF use hkdf::Hkdf; use sha2::Sha256; - + let hk = Hkdf::::new(None, device_key); let mut ed25519_seed = [0u8; 32]; hk.expand(b"spacedrive-network-identity", &mut ed25519_seed) .map_err(|e| NetworkingError::Protocol(format!("Failed to derive network key: {}", e)))?; - + // Create Iroh secret key from the same seed let secret_key = SecretKey::from_bytes(&ed25519_seed); let node_id = secret_key.public(); - Ok(Self { - secret_key, + Ok(Self { + secret_key, node_id, ed25519_seed, }) @@ -76,7 +76,7 @@ impl NetworkIdentity { pub fn sign(&self, data: &[u8]) -> Result> { // Use Ed25519 signing for backward compatibility use ed25519_dalek::{Signer, SigningKey}; - + let signing_key = SigningKey::from_bytes(&self.ed25519_seed); let signature = signing_key.sign(data); Ok(signature.to_bytes().to_vec()) @@ -86,10 +86,10 @@ impl NetworkIdentity { pub fn verify(&self, data: &[u8], signature: &[u8]) -> bool { // Use Ed25519 verification for backward compatibility use ed25519_dalek::{Signature, Verifier, VerifyingKey, SigningKey}; - + let signing_key = SigningKey::from_bytes(&self.ed25519_seed); let verifying_key = signing_key.verifying_key(); - + if let Ok(sig) = Signature::from_slice(signature) { verifying_key.verify(data, &sig).is_ok() } else { @@ -101,12 +101,12 @@ impl NetworkIdentity { pub fn device_id(&self) -> Uuid { // Create a deterministic UUID from the node ID let node_id_bytes = self.node_id.as_bytes(); - + // Use the first 16 bytes of the node ID hash to create a UUID let mut uuid_bytes = [0u8; 16]; let hash = blake3::hash(node_id_bytes); uuid_bytes.copy_from_slice(&hash.as_bytes()[..16]); - + Uuid::from_bytes(uuid_bytes) } @@ -114,7 +114,7 @@ impl NetworkIdentity { pub fn network_fingerprint(&self) -> NetworkFingerprint { let public_key_bytes = self.public_key_bytes(); let public_key_hash = blake3::hash(&public_key_bytes); - + NetworkFingerprint { node_id: self.node_id.to_string(), public_key_hash: hex::encode(&public_key_hash.as_bytes()[..16]), diff --git a/core/src/services/networking/utils/logging.rs b/core/src/service/network/utils/logging.rs similarity index 100% rename from core/src/services/networking/utils/logging.rs rename to core/src/service/network/utils/logging.rs diff --git a/core/src/services/networking/utils/mod.rs b/core/src/service/network/utils/mod.rs similarity index 100% rename from core/src/services/networking/utils/mod.rs rename to core/src/service/network/utils/mod.rs diff --git a/core/src/services/sidecar_manager.rs b/core/src/service/sidecar_manager.rs similarity index 97% rename from core/src/services/sidecar_manager.rs rename to core/src/service/sidecar_manager.rs index 721ebb517..459a3fc81 100644 --- a/core/src/services/sidecar_manager.rs +++ b/core/src/service/sidecar_manager.rs @@ -15,12 +15,12 @@ use uuid::Uuid; use crate::{ context::CoreContext, - infrastructure::database::entities::{ + infra::database::entities::{ sidecar::{self, Entity as Sidecar}, sidecar_availability::{self, Entity as SidecarAvailability}, }, library::Library, - operations::sidecar::{ + ops::sidecar::{ SidecarFormat, SidecarKind, SidecarPath, SidecarPathBuilder, SidecarStatus, SidecarVariant, }, }; @@ -54,7 +54,7 @@ impl SidecarManager { // Create path builder let builder = Arc::new(SidecarPathBuilder::new(&library_path)); - + let mut builders = self.path_builders.write().await; builders.insert(library.id(), builder); @@ -66,7 +66,7 @@ impl SidecarManager { pub async fn deinit_library(&self, library_id: &Uuid) { let mut builders = self.path_builders.write().await; builders.remove(library_id); - + info!("Deinitialized sidecar manager for library {}", library_id); } @@ -114,7 +114,7 @@ impl SidecarManager { variants: &[SidecarVariant], ) -> Result>> { let db = library.db(); - + // Query database for local sidecars let sidecars = Sidecar::find() .filter(sidecar::Column::ContentUuid.is_in(content_uuids.to_vec())) @@ -125,12 +125,12 @@ impl SidecarManager { // Build presence map let mut presence_map: HashMap> = HashMap::new(); - + for sidecar in sidecars { let entry = presence_map .entry(sidecar.content_uuid) .or_insert_with(HashMap::new); - + let path = self.compute_path( &library.id(), &sidecar.content_uuid, @@ -138,7 +138,7 @@ impl SidecarManager { &SidecarVariant::new(&sidecar.variant), &sidecar.format.as_str().try_into().map_err(|e: String| anyhow::anyhow!(e))?, ).await?; - + entry.insert( sidecar.variant.clone(), SidecarPresence { @@ -171,7 +171,7 @@ impl SidecarManager { status: SidecarStatus::Pending, devices: vec![], }); - + entry.devices.push(avail.device_uuid); } @@ -188,7 +188,7 @@ impl SidecarManager { format: &SidecarFormat, ) -> Result { let path = self.compute_path(&library.id(), content_uuid, kind, variant, format).await?; - + // Check if it exists locally if tokio::fs::try_exists(&path.absolute_path).await? { return Ok(SidecarResult::Ready(path.relative_path)); @@ -278,7 +278,7 @@ impl SidecarManager { checksum: Option, ) -> Result<()> { let db = library.db(); - + // For reference sidecars, we use the source entry's path // The rel_path will be empty as the file is not in our sidecar directory let sidecar = sidecar::ActiveModel { @@ -330,7 +330,7 @@ impl SidecarManager { content_uuid: &Uuid, ) -> Result<()> { let db = library.db(); - + // Find all reference sidecars for this content let reference_sidecars = Sidecar::find() .filter(sidecar::Column::ContentUuid.eq(*content_uuid)) @@ -341,7 +341,7 @@ impl SidecarManager { for sidecar in reference_sidecars { if let Some(source_entry_id) = sidecar.source_entry_id { // Get the source entry to find the file path - use crate::infrastructure::database::entities::entry; + use crate::infra::database::entities::entry; let source_entry = entry::Entity::find_by_id(source_entry_id) .one(db.conn()) .await? @@ -351,7 +351,7 @@ impl SidecarManager { let kind = sidecar.kind.as_str().try_into().map_err(|e: String| anyhow::anyhow!(e))?; let variant = SidecarVariant::new(&sidecar.variant); let format = sidecar.format.as_str().try_into().map_err(|e: String| anyhow::anyhow!(e))?; - + let target_path = self.compute_path( &library.id(), content_uuid, @@ -367,12 +367,12 @@ impl SidecarManager { // Move the file // Get the path from directory_paths for this entry - use crate::infrastructure::database::entities::directory_paths; + use crate::infra::database::entities::directory_paths; let dir_path = directory_paths::Entity::find_by_id(source_entry_id) .one(db.conn()) .await? .ok_or_else(|| anyhow::anyhow!("Directory path not found for entry"))?; - + let source_path = PathBuf::from(&dir_path.path); tokio::fs::rename(&source_path, &target_path.absolute_path).await?; @@ -519,7 +519,7 @@ impl SidecarManager { variant: &SidecarVariant, ) -> Result<()> { let db = library.db(); - + // Delete from database Sidecar::delete_many() .filter(sidecar::Column::ContentUuid.eq(*content_uuid)) @@ -545,11 +545,11 @@ impl SidecarManager { /// Bootstrap scan sidecars directory and sync with database pub async fn bootstrap_scan(&self, library: &Library) -> Result<()> { info!("Starting bootstrap scan for library {}", library.id()); - + let builder = self.get_path_builder(&library.id()).await?; let sidecars_dir = builder.sidecars_dir(); let content_dir = sidecars_dir.join("content"); - + if !content_dir.exists() { info!("No sidecars directory found, skipping bootstrap scan"); return Ok(()); @@ -559,26 +559,26 @@ impl SidecarManager { // Walk through the sharded directory structure let mut shard_dirs = tokio::fs::read_dir(&content_dir).await?; - + while let Some(h0_entry) = shard_dirs.next_entry().await? { if !h0_entry.file_type().await?.is_dir() { continue; } - + let mut h0_dirs = tokio::fs::read_dir(h0_entry.path()).await?; - + while let Some(h1_entry) = h0_dirs.next_entry().await? { if !h1_entry.file_type().await?.is_dir() { continue; } - + let mut content_dirs = tokio::fs::read_dir(h1_entry.path()).await?; - + while let Some(content_entry) = content_dirs.next_entry().await? { if !content_entry.file_type().await?.is_dir() { continue; } - + let content_uuid_str = content_entry.file_name(); let content_uuid = match Uuid::parse_str(&content_uuid_str.to_string_lossy()) { Ok(uuid) => uuid, @@ -587,7 +587,7 @@ impl SidecarManager { continue; } }; - + // Process all sidecars for this content if let Err(e) = self.scan_content_sidecars( library, @@ -606,7 +606,7 @@ impl SidecarManager { "Bootstrap scan completed: scanned {} content directories", scanned_count ); - + Ok(()) } @@ -617,14 +617,14 @@ impl SidecarManager { content_uuid: &Uuid, content_path: &Path, ) -> Result<()> { - + // Scan each sidecar kind directory for kind_str in ["thumbs", "proxies", "embeddings", "ocr", "transcript", "live_photos"] { let kind_path = content_path.join(kind_str); if !kind_path.exists() { continue; } - + let kind = match kind_str { "thumbs" => SidecarKind::Thumb, "proxies" => SidecarKind::Proxy, @@ -634,17 +634,17 @@ impl SidecarManager { "live_photos" => SidecarKind::LivePhotoVideo, _ => continue, }; - + let mut entries = tokio::fs::read_dir(&kind_path).await?; - + while let Some(entry) = entries.next_entry().await? { if !entry.file_type().await?.is_file() { continue; } - + let file_name = entry.file_name(); let file_name_str = file_name.to_string_lossy(); - + // Parse variant and format from filename if let Some((variant_str, format_str)) = file_name_str.rsplit_once('.') { let variant = SidecarVariant::new(variant_str); @@ -655,11 +655,11 @@ impl SidecarManager { continue; } }; - + // Get file metadata let metadata = entry.metadata().await?; let size = metadata.len(); - + // Record in database self.record_sidecar( library, @@ -673,7 +673,7 @@ impl SidecarManager { } } } - + Ok(()) } diff --git a/core/src/services/volume_monitor.rs b/core/src/service/volume_monitor.rs similarity index 97% rename from core/src/services/volume_monitor.rs rename to core/src/service/volume_monitor.rs index 3452f3ab2..37f010b64 100644 --- a/core/src/services/volume_monitor.rs +++ b/core/src/service/volume_monitor.rs @@ -4,9 +4,9 @@ use crate::{ context::CoreContext, - infrastructure::events::EventBus, + infra::events::EventBus, library::LibraryManager, - services::Service, + service::Service, volume::VolumeManager, }; use anyhow::Result; @@ -66,24 +66,24 @@ impl VolumeMonitorService { running: Arc>, ) { let mut interval = tokio::time::interval(Duration::from_secs(config.refresh_interval_secs)); - + while *running.read().await { interval.tick().await; - + // Refresh all volumes if let Err(e) = volume_manager.refresh_volumes().await { error!("Failed to refresh volumes: {}", e); continue; } - + // Update tracked volumes if enabled and library manager is available if config.update_tracked_volumes { if let Some(lib_manager) = library_manager.upgrade() { debug!("Updating tracked volumes across libraries"); - + // Get all open libraries let libraries = lib_manager.get_open_libraries().await; - + for library in &libraries { // Get tracked volumes for this library match volume_manager.get_tracked_volumes(&library).await { @@ -139,7 +139,7 @@ impl VolumeMonitorService { } } } - + // Check for new external volumes to auto-track let all_volumes = volume_manager.get_all_volumes().await; for volume in all_volumes { @@ -184,7 +184,7 @@ impl VolumeMonitorService { } } } - + info!("Volume monitoring stopped"); } } @@ -197,47 +197,47 @@ impl Service for VolumeMonitorService { warn!("Volume monitor service already running"); return Ok(()); } - + *running = true; - + let volume_manager = self.volume_manager.clone(); let library_manager = self.library_manager.clone(); let config = self.config.clone(); let running_flag = Arc::new(RwLock::new(*running)); - + let handle = tokio::spawn(Self::monitor_loop( volume_manager, library_manager, config, running_flag, )); - + *self.handle.write().await = Some(handle); - + info!( "Volume monitor service started (refresh every {}s)", self.config.refresh_interval_secs ); - + Ok(()) } - + async fn stop(&self) -> Result<()> { *self.running.write().await = false; - + if let Some(handle) = self.handle.write().await.take() { handle.abort(); } - + info!("Volume monitor service stopped"); Ok(()) } - + fn is_running(&self) -> bool { // Use blocking read since this is a sync method *self.running.blocking_read() } - + fn name(&self) -> &'static str { "volume_monitor" } diff --git a/core/src/services/location_watcher/event_handler.rs b/core/src/service/watcher/event_handler.rs similarity index 97% rename from core/src/services/location_watcher/event_handler.rs rename to core/src/service/watcher/event_handler.rs index c90a0f617..9fb9cf1e9 100644 --- a/core/src/services/location_watcher/event_handler.rs +++ b/core/src/service/watcher/event_handler.rs @@ -1,6 +1,6 @@ //! Event handling for file system changes -use crate::infrastructure::events::Event; +use crate::infra::events::Event; use notify::{Event as NotifyEvent, EventKind}; use serde::{Deserialize, Serialize}; use std::path::PathBuf; @@ -83,9 +83,9 @@ impl WatcherEvent { pub fn should_process(&self) -> bool { for path in &self.paths { let path_str = path.to_string_lossy(); - + // Skip temporary files - if path_str.contains(".tmp") + if path_str.contains(".tmp") || path_str.contains(".temp") || path_str.contains("~") || path_str.ends_with(".swp") @@ -93,7 +93,7 @@ impl WatcherEvent { || path_str.contains("Thumbs.db") { return false; } - + // Skip hidden files starting with dot (except .gitignore, etc.) if let Some(file_name) = path.file_name() { let name = file_name.to_string_lossy(); @@ -102,7 +102,7 @@ impl WatcherEvent { } } } - + true } @@ -114,7 +114,7 @@ impl WatcherEvent { /// Check if a dotfile is important enough to track fn is_important_dotfile(name: &str) -> bool { - matches!(name, + matches!(name, ".gitignore" | ".gitkeep" | ".gitattributes" | ".editorconfig" | ".env" | ".env.local" | ".nvmrc" | ".node-version" | ".python-version" | @@ -177,7 +177,7 @@ mod tests { timestamp: SystemTime::now(), attrs: vec![], }; - + assert_eq!(event.primary_path(), Some(&PathBuf::from("/test/file1.txt"))); } } \ No newline at end of file diff --git a/core/src/service/watcher/mod.rs b/core/src/service/watcher/mod.rs new file mode 100644 index 000000000..fea06ea2e --- /dev/null +++ b/core/src/service/watcher/mod.rs @@ -0,0 +1,367 @@ +//! Location Watcher Service - Monitors file system changes in indexed locations + +use crate::infra::events::{Event, EventBus}; +use crate::service::Service; +use anyhow::Result; +use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; +use std::collections::HashMap; +use std::path::{Path, PathBuf}; +use std::sync::Arc; +use std::time::Duration; +use tokio::sync::{mpsc, RwLock}; +use tracing::{debug, error, info, warn}; +use uuid::Uuid; + +mod event_handler; +mod platform; +pub mod utils; + +pub use event_handler::WatcherEvent; +pub use platform::PlatformHandler; + +/// Configuration for the location watcher +#[derive(Debug, Clone)] +pub struct LocationWatcherConfig { + /// Debounce duration for file system events + pub debounce_duration: Duration, + /// Maximum number of events to buffer + pub event_buffer_size: usize, + /// Whether to enable detailed debug logging + pub debug_mode: bool, +} + +impl Default for LocationWatcherConfig { + fn default() -> Self { + Self { + debounce_duration: Duration::from_millis(100), + event_buffer_size: 1000, + debug_mode: false, + } + } +} + +/// Location watcher service that monitors file system changes +pub struct LocationWatcher { + /// Watcher configuration + config: LocationWatcherConfig, + /// Event bus for emitting events + events: Arc, + /// Currently watched locations + watched_locations: Arc>>, + /// File system watcher + watcher: Arc>>, + /// Whether the service is running + is_running: Arc>, + /// Platform-specific event handler + platform_handler: Arc, +} + +/// Information about a watched location +#[derive(Debug, Clone)] +pub struct WatchedLocation { + /// Location UUID + pub id: Uuid, + /// Library UUID this location belongs to + pub library_id: Uuid, + /// Path being watched + pub path: PathBuf, + /// Whether watching is enabled for this location + pub enabled: bool, +} + +impl LocationWatcher { + /// Create a new location watcher + pub fn new(config: LocationWatcherConfig, events: Arc) -> Self { + let platform_handler = Arc::new(PlatformHandler::new()); + + Self { + config, + events, + watched_locations: Arc::new(RwLock::new(HashMap::new())), + watcher: Arc::new(RwLock::new(None)), + is_running: Arc::new(RwLock::new(false)), + platform_handler, + } + } + + /// Add a location to watch + pub async fn add_location(&self, location: WatchedLocation) -> Result<()> { + if !location.enabled { + debug!( + "Location {} is disabled, not adding to watcher", + location.id + ); + return Ok(()); + } + + let mut locations = self.watched_locations.write().await; + + if locations.contains_key(&location.id) { + warn!("Location {} is already being watched", location.id); + return Ok(()); + } + + // Add to file system watcher if running + if *self.is_running.read().await { + if let Some(watcher) = self.watcher.write().await.as_mut() { + watcher.watch(&location.path, RecursiveMode::Recursive)?; + info!("Started watching location: {}", location.path.display()); + } + } + + locations.insert(location.id, location); + Ok(()) + } + + /// Remove a location from watching + pub async fn remove_location(&self, location_id: Uuid) -> Result<()> { + let mut locations = self.watched_locations.write().await; + + if let Some(location) = locations.remove(&location_id) { + // Remove from file system watcher if running + if *self.is_running.read().await { + if let Some(watcher) = self.watcher.write().await.as_mut() { + watcher.unwatch(&location.path)?; + info!("Stopped watching location: {}", location.path.display()); + } + } + } + + Ok(()) + } + + /// Update a location's settings + pub async fn update_location(&self, location_id: Uuid, enabled: bool) -> Result<()> { + let mut locations = self.watched_locations.write().await; + + if let Some(location) = locations.get_mut(&location_id) { + let was_enabled = location.enabled; + location.enabled = enabled; + + if *self.is_running.read().await { + if let Some(watcher) = self.watcher.write().await.as_mut() { + match (was_enabled, enabled) { + (false, true) => { + // Enable watching + watcher.watch(&location.path, RecursiveMode::Recursive)?; + info!("Enabled watching for location: {}", location.path.display()); + } + (true, false) => { + // Disable watching + watcher.unwatch(&location.path)?; + info!( + "Disabled watching for location: {}", + location.path.display() + ); + } + _ => {} // No change needed + } + } + } + } + + Ok(()) + } + + /// Get all watched locations + pub async fn get_watched_locations(&self) -> Vec { + self.watched_locations + .read() + .await + .values() + .cloned() + .collect() + } + + /// Start the event processing loop + async fn start_event_loop(&self) -> Result<()> { + let events = self.events.clone(); + let platform_handler = self.platform_handler.clone(); + let watched_locations = self.watched_locations.clone(); + let is_running = self.is_running.clone(); + let debug_mode = self.config.debug_mode; + + let (tx, mut rx) = mpsc::channel(self.config.event_buffer_size); + + // Create file system watcher + let mut watcher = notify::recommended_watcher(move |res| { + match res { + Ok(event) => { + if debug_mode { + debug!("Raw file system event: {:?}", event); + } + + // Convert notify event to our WatcherEvent + let watcher_event = WatcherEvent::from_notify_event(event); + + if let Err(e) = tx.try_send(watcher_event) { + warn!("Failed to send watcher event: {}", e); + } + } + Err(e) => { + error!("File system watcher error: {}", e); + } + } + })?; + + // Configure watcher + watcher.configure(Config::default().with_poll_interval(Duration::from_millis(500)))?; + + // Watch all enabled locations + let locations = watched_locations.read().await; + for location in locations.values() { + if location.enabled { + watcher.watch(&location.path, RecursiveMode::Recursive)?; + info!("Started watching location: {}", location.path.display()); + } + } + drop(locations); + + // Store watcher + *self.watcher.write().await = Some(watcher); + + // Start event processing loop + tokio::spawn(async move { + while *is_running.read().await { + tokio::select! { + Some(event) = rx.recv() => { + // Process the event through platform handler + match platform_handler.process_event(event, &watched_locations).await { + Ok(processed_events) => { + for processed_event in processed_events { + events.emit(processed_event); + } + } + Err(e) => { + error!("Error processing watcher event: {}", e); + } + } + } + _ = tokio::time::sleep(Duration::from_millis(100)) => { + // Periodic tick for debouncing and cleanup + if let Err(e) = platform_handler.tick().await { + error!("Error during platform handler tick: {}", e); + } + + // Handle platform-specific tick events that might generate additional events + #[cfg(target_os = "macos")] + { + if let Ok(tick_events) = platform_handler.inner.tick_with_locations(&watched_locations).await { + for tick_event in tick_events { + events.emit(tick_event); + } + } + } + + #[cfg(target_os = "windows")] + { + if let Ok(tick_events) = platform_handler.inner.tick_with_locations(&watched_locations).await { + for tick_event in tick_events { + events.emit(tick_event); + } + } + } + } + } + } + + info!("Location watcher event loop stopped"); + }); + + Ok(()) + } +} + +#[async_trait::async_trait] +impl Service for LocationWatcher { + async fn start(&self) -> Result<()> { + if *self.is_running.read().await { + warn!("Location watcher is already running"); + return Ok(()); + } + + info!("Starting location watcher service"); + + *self.is_running.write().await = true; + + self.start_event_loop().await?; + + info!("Location watcher service started"); + Ok(()) + } + + async fn stop(&self) -> Result<()> { + if !*self.is_running.read().await { + return Ok(()); + } + + info!("Stopping location watcher service"); + + *self.is_running.write().await = false; + + // Clean up watcher + *self.watcher.write().await = None; + + info!("Location watcher service stopped"); + Ok(()) + } + + fn is_running(&self) -> bool { + // Use try_read to avoid blocking + self.is_running.try_read().map_or(false, |guard| *guard) + } + + fn name(&self) -> &'static str { + "location_watcher" + } +} + +#[cfg(test)] +mod tests { + use super::*; + use tempfile::TempDir; + + fn create_test_events() -> Arc { + Arc::new(EventBus::default()) + } + + #[tokio::test] + async fn test_location_watcher_creation() { + let config = LocationWatcherConfig::default(); + let events = create_test_events(); + let watcher = LocationWatcher::new(config, events); + + assert!(!watcher.is_running()); + assert_eq!(watcher.name(), "location_watcher"); + } + + #[tokio::test] + async fn test_add_remove_location() { + let config = LocationWatcherConfig::default(); + let events = create_test_events(); + let watcher = LocationWatcher::new(config, events); + + let temp_dir = TempDir::new().unwrap(); + let location = WatchedLocation { + id: Uuid::new_v4(), + library_id: Uuid::new_v4(), + path: temp_dir.path().to_path_buf(), + enabled: true, + }; + + let location_id = location.id; + + // Add location + watcher.add_location(location).await.unwrap(); + + let locations = watcher.get_watched_locations().await; + assert_eq!(locations.len(), 1); + assert_eq!(locations[0].id, location_id); + + // Remove location + watcher.remove_location(location_id).await.unwrap(); + + let locations = watcher.get_watched_locations().await; + assert_eq!(locations.len(), 0); + } +} diff --git a/core/src/services/location_watcher/platform/linux.rs b/core/src/service/watcher/platform/linux.rs similarity index 100% rename from core/src/services/location_watcher/platform/linux.rs rename to core/src/service/watcher/platform/linux.rs diff --git a/core/src/services/location_watcher/platform/macos.rs b/core/src/service/watcher/platform/macos.rs similarity index 98% rename from core/src/services/location_watcher/platform/macos.rs rename to core/src/service/watcher/platform/macos.rs index 8063988eb..70e1b4c77 100644 --- a/core/src/services/location_watcher/platform/macos.rs +++ b/core/src/service/watcher/platform/macos.rs @@ -10,9 +10,9 @@ //! When moved from elsewhere to our location, we receive new path rename event (handle as creation). use super::EventHandler; -use crate::infrastructure::events::Event; -use crate::services::location_watcher::event_handler::WatcherEventKind; -use crate::services::location_watcher::{WatchedLocation, WatcherEvent}; +use crate::infra::events::Event; +use crate::service::watcher::event_handler::WatcherEventKind; +use crate::service::watcher::{WatchedLocation, WatcherEvent}; use anyhow::Result; use notify::{ event::{CreateKind, DataChange, MetadataKind, ModifyKind, RenameMode}, diff --git a/core/src/service/watcher/platform/mod.rs b/core/src/service/watcher/platform/mod.rs new file mode 100644 index 000000000..054c35b77 --- /dev/null +++ b/core/src/service/watcher/platform/mod.rs @@ -0,0 +1,129 @@ +//! Platform-specific event handling + +use crate::infra::events::Event; +use crate::service::watcher::{WatchedLocation, WatcherEvent}; +use anyhow::Result; +use std::collections::HashMap; +use std::sync::Arc; +use tokio::sync::RwLock; +use uuid::Uuid; + +#[cfg(target_os = "linux")] +mod linux; +#[cfg(target_os = "macos")] +mod macos; +#[cfg(target_os = "windows")] +mod windows; + +/// Platform-specific event handler +pub struct PlatformHandler { + #[cfg(target_os = "linux")] + pub inner: linux::LinuxHandler, + #[cfg(target_os = "macos")] + pub inner: macos::MacOSHandler, + #[cfg(target_os = "windows")] + pub inner: windows::WindowsHandler, + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + pub inner: DefaultHandler, +} + +impl PlatformHandler { + /// Create a new platform handler + pub fn new() -> Self { + Self { + #[cfg(target_os = "linux")] + inner: linux::LinuxHandler::new(), + #[cfg(target_os = "macos")] + inner: macos::MacOSHandler::new(), + #[cfg(target_os = "windows")] + inner: windows::WindowsHandler::new(), + #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] + inner: DefaultHandler::new(), + } + } + + /// Process a file system event + pub async fn process_event( + &self, + event: WatcherEvent, + watched_locations: &Arc>>, + ) -> Result> { + self.inner.process_event(event, watched_locations).await + } + + /// Periodic tick for cleanup and debouncing + pub async fn tick(&self) -> Result<()> { + self.inner.tick().await + } +} + +/// Trait for platform-specific handlers +#[async_trait::async_trait] +pub trait EventHandler: Send + Sync { + /// Process a file system event and return core events + async fn process_event( + &self, + event: WatcherEvent, + watched_locations: &Arc>>, + ) -> Result>; + + /// Periodic cleanup and processing + async fn tick(&self) -> Result<()>; +} + +/// Default handler for unsupported platforms +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +pub struct DefaultHandler; + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +impl DefaultHandler { + pub fn new() -> Self { + Self + } +} + +#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] +#[async_trait::async_trait] +impl EventHandler for DefaultHandler { + async fn process_event( + &self, + event: WatcherEvent, + watched_locations: &Arc>>, + ) -> Result> { + // Basic event processing without platform-specific optimizations + if !event.should_process() { + return Ok(vec![]); + } + + let locations = watched_locations.read().await; + let mut events = Vec::new(); + + for location in locations.values() { + if !location.enabled { + continue; + } + + for path in &event.paths { + if path.starts_with(&location.path) { + // Generate a placeholder entry ID for now + // In a real implementation, this would look up or create an entry + let entry_id = Uuid::new_v4(); + + if let Some(core_event) = + event.to_core_event(location.library_id, Some(entry_id)) + { + events.push(core_event); + } + break; + } + } + } + + Ok(events) + } + + async fn tick(&self) -> Result<()> { + // Nothing to do for default handler + Ok(()) + } +} diff --git a/core/src/services/location_watcher/platform/windows.rs b/core/src/service/watcher/platform/windows.rs similarity index 100% rename from core/src/services/location_watcher/platform/windows.rs rename to core/src/service/watcher/platform/windows.rs diff --git a/core/src/services/location_watcher/utils.rs b/core/src/service/watcher/utils.rs similarity index 100% rename from core/src/services/location_watcher/utils.rs rename to core/src/service/watcher/utils.rs diff --git a/core/src/services/location_watcher/mod.rs b/core/src/services/location_watcher/mod.rs deleted file mode 100644 index 9e41e19a5..000000000 --- a/core/src/services/location_watcher/mod.rs +++ /dev/null @@ -1,356 +0,0 @@ -//! Location Watcher Service - Monitors file system changes in indexed locations - -use crate::infrastructure::events::{Event, EventBus}; -use crate::services::Service; -use anyhow::Result; -use notify::{Config, RecommendedWatcher, RecursiveMode, Watcher as NotifyWatcher}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use std::sync::Arc; -use std::time::Duration; -use tokio::sync::{mpsc, RwLock}; -use tracing::{debug, error, info, warn}; -use uuid::Uuid; - -mod event_handler; -mod platform; -pub mod utils; - -pub use event_handler::WatcherEvent; -pub use platform::PlatformHandler; - -/// Configuration for the location watcher -#[derive(Debug, Clone)] -pub struct LocationWatcherConfig { - /// Debounce duration for file system events - pub debounce_duration: Duration, - /// Maximum number of events to buffer - pub event_buffer_size: usize, - /// Whether to enable detailed debug logging - pub debug_mode: bool, -} - -impl Default for LocationWatcherConfig { - fn default() -> Self { - Self { - debounce_duration: Duration::from_millis(100), - event_buffer_size: 1000, - debug_mode: false, - } - } -} - -/// Location watcher service that monitors file system changes -pub struct LocationWatcher { - /// Watcher configuration - config: LocationWatcherConfig, - /// Event bus for emitting events - events: Arc, - /// Currently watched locations - watched_locations: Arc>>, - /// File system watcher - watcher: Arc>>, - /// Whether the service is running - is_running: Arc>, - /// Platform-specific event handler - platform_handler: Arc, -} - -/// Information about a watched location -#[derive(Debug, Clone)] -pub struct WatchedLocation { - /// Location UUID - pub id: Uuid, - /// Library UUID this location belongs to - pub library_id: Uuid, - /// Path being watched - pub path: PathBuf, - /// Whether watching is enabled for this location - pub enabled: bool, -} - -impl LocationWatcher { - /// Create a new location watcher - pub fn new(config: LocationWatcherConfig, events: Arc) -> Self { - let platform_handler = Arc::new(PlatformHandler::new()); - - Self { - config, - events, - watched_locations: Arc::new(RwLock::new(HashMap::new())), - watcher: Arc::new(RwLock::new(None)), - is_running: Arc::new(RwLock::new(false)), - platform_handler, - } - } - - /// Add a location to watch - pub async fn add_location(&self, location: WatchedLocation) -> Result<()> { - if !location.enabled { - debug!("Location {} is disabled, not adding to watcher", location.id); - return Ok(()); - } - - let mut locations = self.watched_locations.write().await; - - if locations.contains_key(&location.id) { - warn!("Location {} is already being watched", location.id); - return Ok(()); - } - - // Add to file system watcher if running - if *self.is_running.read().await { - if let Some(watcher) = self.watcher.write().await.as_mut() { - watcher.watch(&location.path, RecursiveMode::Recursive)?; - info!("Started watching location: {}", location.path.display()); - } - } - - locations.insert(location.id, location); - Ok(()) - } - - /// Remove a location from watching - pub async fn remove_location(&self, location_id: Uuid) -> Result<()> { - let mut locations = self.watched_locations.write().await; - - if let Some(location) = locations.remove(&location_id) { - // Remove from file system watcher if running - if *self.is_running.read().await { - if let Some(watcher) = self.watcher.write().await.as_mut() { - watcher.unwatch(&location.path)?; - info!("Stopped watching location: {}", location.path.display()); - } - } - } - - Ok(()) - } - - /// Update a location's settings - pub async fn update_location(&self, location_id: Uuid, enabled: bool) -> Result<()> { - let mut locations = self.watched_locations.write().await; - - if let Some(location) = locations.get_mut(&location_id) { - let was_enabled = location.enabled; - location.enabled = enabled; - - if *self.is_running.read().await { - if let Some(watcher) = self.watcher.write().await.as_mut() { - match (was_enabled, enabled) { - (false, true) => { - // Enable watching - watcher.watch(&location.path, RecursiveMode::Recursive)?; - info!("Enabled watching for location: {}", location.path.display()); - } - (true, false) => { - // Disable watching - watcher.unwatch(&location.path)?; - info!("Disabled watching for location: {}", location.path.display()); - } - _ => {} // No change needed - } - } - } - } - - Ok(()) - } - - /// Get all watched locations - pub async fn get_watched_locations(&self) -> Vec { - self.watched_locations.read().await.values().cloned().collect() - } - - /// Start the event processing loop - async fn start_event_loop(&self) -> Result<()> { - let events = self.events.clone(); - let platform_handler = self.platform_handler.clone(); - let watched_locations = self.watched_locations.clone(); - let is_running = self.is_running.clone(); - let debug_mode = self.config.debug_mode; - - let (tx, mut rx) = mpsc::channel(self.config.event_buffer_size); - - // Create file system watcher - let mut watcher = notify::recommended_watcher(move |res| { - match res { - Ok(event) => { - if debug_mode { - debug!("Raw file system event: {:?}", event); - } - - // Convert notify event to our WatcherEvent - let watcher_event = WatcherEvent::from_notify_event(event); - - if let Err(e) = tx.try_send(watcher_event) { - warn!("Failed to send watcher event: {}", e); - } - } - Err(e) => { - error!("File system watcher error: {}", e); - } - } - })?; - - // Configure watcher - watcher.configure(Config::default().with_poll_interval(Duration::from_millis(500)))?; - - // Watch all enabled locations - let locations = watched_locations.read().await; - for location in locations.values() { - if location.enabled { - watcher.watch(&location.path, RecursiveMode::Recursive)?; - info!("Started watching location: {}", location.path.display()); - } - } - drop(locations); - - // Store watcher - *self.watcher.write().await = Some(watcher); - - // Start event processing loop - tokio::spawn(async move { - while *is_running.read().await { - tokio::select! { - Some(event) = rx.recv() => { - // Process the event through platform handler - match platform_handler.process_event(event, &watched_locations).await { - Ok(processed_events) => { - for processed_event in processed_events { - events.emit(processed_event); - } - } - Err(e) => { - error!("Error processing watcher event: {}", e); - } - } - } - _ = tokio::time::sleep(Duration::from_millis(100)) => { - // Periodic tick for debouncing and cleanup - if let Err(e) = platform_handler.tick().await { - error!("Error during platform handler tick: {}", e); - } - - // Handle platform-specific tick events that might generate additional events - #[cfg(target_os = "macos")] - { - if let Ok(tick_events) = platform_handler.inner.tick_with_locations(&watched_locations).await { - for tick_event in tick_events { - events.emit(tick_event); - } - } - } - - #[cfg(target_os = "windows")] - { - if let Ok(tick_events) = platform_handler.inner.tick_with_locations(&watched_locations).await { - for tick_event in tick_events { - events.emit(tick_event); - } - } - } - } - } - } - - info!("Location watcher event loop stopped"); - }); - - Ok(()) - } -} - -#[async_trait::async_trait] -impl Service for LocationWatcher { - async fn start(&self) -> Result<()> { - if *self.is_running.read().await { - warn!("Location watcher is already running"); - return Ok(()); - } - - info!("Starting location watcher service"); - - *self.is_running.write().await = true; - - self.start_event_loop().await?; - - info!("Location watcher service started"); - Ok(()) - } - - async fn stop(&self) -> Result<()> { - if !*self.is_running.read().await { - return Ok(()); - } - - info!("Stopping location watcher service"); - - *self.is_running.write().await = false; - - // Clean up watcher - *self.watcher.write().await = None; - - info!("Location watcher service stopped"); - Ok(()) - } - - fn is_running(&self) -> bool { - // Use try_read to avoid blocking - self.is_running.try_read().map_or(false, |guard| *guard) - } - - fn name(&self) -> &'static str { - "location_watcher" - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - fn create_test_events() -> Arc { - Arc::new(EventBus::default()) - } - - #[tokio::test] - async fn test_location_watcher_creation() { - let config = LocationWatcherConfig::default(); - let events = create_test_events(); - let watcher = LocationWatcher::new(config, events); - - assert!(!watcher.is_running()); - assert_eq!(watcher.name(), "location_watcher"); - } - - #[tokio::test] - async fn test_add_remove_location() { - let config = LocationWatcherConfig::default(); - let events = create_test_events(); - let watcher = LocationWatcher::new(config, events); - - let temp_dir = TempDir::new().unwrap(); - let location = WatchedLocation { - id: Uuid::new_v4(), - library_id: Uuid::new_v4(), - path: temp_dir.path().to_path_buf(), - enabled: true, - }; - - let location_id = location.id; - - // Add location - watcher.add_location(location).await.unwrap(); - - let locations = watcher.get_watched_locations().await; - assert_eq!(locations.len(), 1); - assert_eq!(locations[0].id, location_id); - - // Remove location - watcher.remove_location(location_id).await.unwrap(); - - let locations = watcher.get_watched_locations().await; - assert_eq!(locations.len(), 0); - } -} \ No newline at end of file diff --git a/core/src/services/location_watcher/platform/mod.rs b/core/src/services/location_watcher/platform/mod.rs deleted file mode 100644 index 3f132fa92..000000000 --- a/core/src/services/location_watcher/platform/mod.rs +++ /dev/null @@ -1,127 +0,0 @@ -//! Platform-specific event handling - -use crate::infrastructure::events::Event; -use crate::services::location_watcher::{WatchedLocation, WatcherEvent}; -use anyhow::Result; -use std::collections::HashMap; -use std::sync::Arc; -use tokio::sync::RwLock; -use uuid::Uuid; - -#[cfg(target_os = "linux")] -mod linux; -#[cfg(target_os = "macos")] -mod macos; -#[cfg(target_os = "windows")] -mod windows; - -/// Platform-specific event handler -pub struct PlatformHandler { - #[cfg(target_os = "linux")] - pub inner: linux::LinuxHandler, - #[cfg(target_os = "macos")] - pub inner: macos::MacOSHandler, - #[cfg(target_os = "windows")] - pub inner: windows::WindowsHandler, - #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] - pub inner: DefaultHandler, -} - -impl PlatformHandler { - /// Create a new platform handler - pub fn new() -> Self { - Self { - #[cfg(target_os = "linux")] - inner: linux::LinuxHandler::new(), - #[cfg(target_os = "macos")] - inner: macos::MacOSHandler::new(), - #[cfg(target_os = "windows")] - inner: windows::WindowsHandler::new(), - #[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] - inner: DefaultHandler::new(), - } - } - - /// Process a file system event - pub async fn process_event( - &self, - event: WatcherEvent, - watched_locations: &Arc>>, - ) -> Result> { - self.inner.process_event(event, watched_locations).await - } - - /// Periodic tick for cleanup and debouncing - pub async fn tick(&self) -> Result<()> { - self.inner.tick().await - } -} - -/// Trait for platform-specific handlers -#[async_trait::async_trait] -pub trait EventHandler: Send + Sync { - /// Process a file system event and return core events - async fn process_event( - &self, - event: WatcherEvent, - watched_locations: &Arc>>, - ) -> Result>; - - /// Periodic cleanup and processing - async fn tick(&self) -> Result<()>; -} - -/// Default handler for unsupported platforms -#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] -pub struct DefaultHandler; - -#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] -impl DefaultHandler { - pub fn new() -> Self { - Self - } -} - -#[cfg(not(any(target_os = "linux", target_os = "macos", target_os = "windows")))] -#[async_trait::async_trait] -impl EventHandler for DefaultHandler { - async fn process_event( - &self, - event: WatcherEvent, - watched_locations: &Arc>>, - ) -> Result> { - // Basic event processing without platform-specific optimizations - if !event.should_process() { - return Ok(vec![]); - } - - let locations = watched_locations.read().await; - let mut events = Vec::new(); - - for location in locations.values() { - if !location.enabled { - continue; - } - - for path in &event.paths { - if path.starts_with(&location.path) { - // Generate a placeholder entry ID for now - // In a real implementation, this would look up or create an entry - let entry_id = Uuid::new_v4(); - - if let Some(core_event) = event.to_core_event(location.library_id, Some(entry_id)) { - events.push(core_event); - } - break; - } - } - } - - Ok(events) - } - - async fn tick(&self) -> Result<()> { - // Nothing to do for default handler - Ok(()) - } -} \ No newline at end of file diff --git a/core/src/services/networking/protocols/pairing/persistence.rs b/core/src/services/networking/protocols/pairing/persistence.rs deleted file mode 100644 index 30d9c2ae6..000000000 --- a/core/src/services/networking/protocols/pairing/persistence.rs +++ /dev/null @@ -1,305 +0,0 @@ -//! Session persistence for pairing protocol - -use super::types::{PairingSession, PairingState}; -use crate::services::networking::{NetworkingError, Result}; -use serde::{Deserialize, Serialize}; -use std::collections::HashMap; -use std::path::{Path, PathBuf}; -use tokio::fs; -use uuid::Uuid; - -/// Serializable version of PairingSession for persistence -#[derive(Debug, Clone, Serialize, Deserialize)] -struct SerializablePairingSession { - pub id: Uuid, - pub state: SerializablePairingState, - pub remote_device_id: Option, - pub remote_public_key: Option>, - pub shared_secret: Option>, - pub created_at: chrono::DateTime, -} - -/// Serializable version of PairingState -#[derive(Debug, Clone, Serialize, Deserialize)] -enum SerializablePairingState { - WaitingForConnection, - Scanning, - ChallengeReceived { challenge: Vec }, - ResponseSent, - Completed, - Failed { reason: String }, -} - -impl From<&PairingSession> for SerializablePairingSession { - fn from(session: &PairingSession) -> Self { - Self { - id: session.id, - state: match &session.state { - PairingState::WaitingForConnection => SerializablePairingState::WaitingForConnection, - PairingState::Scanning => SerializablePairingState::Scanning, - PairingState::ChallengeReceived { challenge } => { - SerializablePairingState::ChallengeReceived { - challenge: challenge.clone(), - } - } - PairingState::ResponseSent => SerializablePairingState::ResponseSent, - PairingState::Completed => SerializablePairingState::Completed, - PairingState::Failed { reason } => SerializablePairingState::Failed { - reason: reason.clone(), - }, - // Skip non-serializable states - _ => SerializablePairingState::Failed { - reason: "State not serializable".to_string(), - }, - }, - remote_device_id: session.remote_device_id, - remote_public_key: session.remote_public_key.clone(), - shared_secret: session.shared_secret.clone(), - created_at: session.created_at, - } - } -} - -impl From for PairingSession { - fn from(serializable: SerializablePairingSession) -> Self { - Self { - id: serializable.id, - state: match serializable.state { - SerializablePairingState::WaitingForConnection => PairingState::WaitingForConnection, - SerializablePairingState::Scanning => PairingState::Scanning, - SerializablePairingState::ChallengeReceived { challenge } => { - PairingState::ChallengeReceived { challenge } - } - SerializablePairingState::ResponseSent => PairingState::ResponseSent, - SerializablePairingState::Completed => PairingState::Completed, - SerializablePairingState::Failed { reason } => PairingState::Failed { reason }, - }, - remote_device_id: serializable.remote_device_id, - remote_device_info: None, // Will be restored from device registry - remote_public_key: serializable.remote_public_key, - shared_secret: serializable.shared_secret, - created_at: serializable.created_at, - } - } -} - -/// Persisted pairing sessions data -#[derive(Debug, Serialize, Deserialize)] -struct PersistedPairingSessions { - sessions: HashMap, - last_saved: chrono::DateTime, -} - -/// Session persistence manager -pub struct PairingPersistence { - data_dir: PathBuf, - sessions_file: PathBuf, -} - -impl PairingPersistence { - /// Create a new persistence manager - pub fn new(data_dir: impl AsRef) -> Self { - let data_dir = data_dir.as_ref().to_path_buf(); - let networking_dir = data_dir.join("networking"); - let sessions_file = networking_dir.join("pairing_sessions.json"); - - Self { - data_dir: networking_dir, - sessions_file, - } - } - - /// Save active sessions to disk - pub async fn save_sessions(&self, sessions: &HashMap) -> Result<()> { - // Ensure data directory exists - if let Some(parent) = self.sessions_file.parent() { - fs::create_dir_all(parent).await.map_err(NetworkingError::Io)?; - } - - // Convert to serializable format, filtering out transient states - let serializable_sessions: HashMap = sessions - .iter() - .filter_map(|(id, session)| { - // Only persist certain states - match &session.state { - PairingState::WaitingForConnection - | PairingState::Scanning - | PairingState::ChallengeReceived { .. } - | PairingState::ResponseSent - | PairingState::Completed => Some((*id, session.into())), - // Don't persist transient or failed states - _ => None, - } - }) - .collect(); - - let persisted = PersistedPairingSessions { - sessions: serializable_sessions, - last_saved: chrono::Utc::now(), - }; - - // Write to temporary file first, then rename for atomic operation - let temp_file = self.sessions_file.with_extension("tmp"); - let json_data = serde_json::to_string_pretty(&persisted).map_err(|e| { - NetworkingError::Serialization(e) - })?; - - fs::write(&temp_file, json_data).await.map_err(NetworkingError::Io)?; - - fs::rename(&temp_file, &self.sessions_file).await.map_err(NetworkingError::Io)?; - - Ok(()) - } - - /// Load sessions from disk - pub async fn load_sessions(&self) -> Result> { - if !self.sessions_file.exists() { - return Ok(HashMap::new()); - } - - let json_data = match fs::read_to_string(&self.sessions_file).await { - Ok(data) => data, - Err(e) => { - eprintln!("Failed to read pairing sessions file: {}", e); - return Ok(HashMap::new()); - } - }; - - // Handle empty files - if json_data.trim().is_empty() { - eprintln!("Pairing sessions file is empty, returning empty sessions"); - return Ok(HashMap::new()); - } - - let persisted: PersistedPairingSessions = match serde_json::from_str(&json_data) { - Ok(p) => p, - Err(e) => { - eprintln!("Failed to parse pairing sessions JSON: {}. File may be corrupted.", e); - // Try to rename the corrupted file for debugging - let backup_path = self.sessions_file.with_extension("json.corrupted"); - let _ = fs::rename(&self.sessions_file, &backup_path).await; - eprintln!("Renamed corrupted file to: {:?}", backup_path); - return Ok(HashMap::new()); - } - }; - - // Filter out expired sessions (older than 1 hour) - let now = chrono::Utc::now(); - let max_age = chrono::Duration::hours(1); - - let sessions: HashMap = persisted - .sessions - .into_iter() - .filter_map(|(id, serializable)| { - let age = now.signed_duration_since(serializable.created_at); - if age <= max_age { - Some((id, serializable.into())) - } else { - None - } - }) - .collect(); - - Ok(sessions) - } - - /// Clean up expired sessions from disk - pub async fn cleanup_expired_sessions(&self) -> Result { - let sessions = self.load_sessions().await?; - let initial_count = sessions.len(); - - // Save the filtered sessions back - self.save_sessions(&sessions).await?; - - Ok(initial_count - sessions.len()) - } - - /// Delete all persisted sessions - pub async fn clear_all_sessions(&self) -> Result<()> { - if self.sessions_file.exists() { - fs::remove_file(&self.sessions_file).await.map_err(NetworkingError::Io)?; - } - Ok(()) - } - - /// Get the path to the sessions file - pub fn sessions_file_path(&self) -> &Path { - &self.sessions_file - } -} - -#[cfg(test)] -mod tests { - use super::*; - use tempfile::TempDir; - - async fn create_test_persistence() -> (PairingPersistence, TempDir) { - let temp_dir = TempDir::new().expect("Failed to create temp dir"); - let persistence = PairingPersistence::new(temp_dir.path()); - (persistence, temp_dir) - } - - #[tokio::test] - async fn test_save_and_load_sessions() { - let (persistence, _temp_dir) = create_test_persistence().await; - - // Create test sessions - let mut sessions = HashMap::new(); - let session_id = Uuid::new_v4(); - let session = PairingSession { - id: session_id, - state: PairingState::WaitingForConnection, - remote_device_id: Some(Uuid::new_v4()), - remote_device_info: None, - remote_public_key: None, - shared_secret: Some(vec![1, 2, 3, 4]), - created_at: chrono::Utc::now(), - }; - sessions.insert(session_id, session); - - // Save sessions - persistence.save_sessions(&sessions).await.unwrap(); - - // Load sessions - let loaded_sessions = persistence.load_sessions().await.unwrap(); - - assert_eq!(loaded_sessions.len(), 1); - assert!(loaded_sessions.contains_key(&session_id)); - - let loaded_session = &loaded_sessions[&session_id]; - assert_eq!(loaded_session.id, session_id); - assert!(matches!(loaded_session.state, PairingState::WaitingForConnection)); - } - - #[tokio::test] - async fn test_load_nonexistent_file() { - let (persistence, _temp_dir) = create_test_persistence().await; - - let sessions = persistence.load_sessions().await.unwrap(); - assert!(sessions.is_empty()); - } - - #[tokio::test] - async fn test_clear_sessions() { - let (persistence, _temp_dir) = create_test_persistence().await; - - // Create and save sessions - let mut sessions = HashMap::new(); - sessions.insert(Uuid::new_v4(), PairingSession { - id: Uuid::new_v4(), - state: PairingState::Completed, - remote_device_id: None, - remote_device_info: None, - remote_public_key: None, - shared_secret: None, - created_at: chrono::Utc::now(), - }); - - persistence.save_sessions(&sessions).await.unwrap(); - assert!(persistence.sessions_file.exists()); - - // Clear sessions - persistence.clear_all_sessions().await.unwrap(); - assert!(!persistence.sessions_file.exists()); - } -} \ No newline at end of file diff --git a/core/src/test_framework/mod.rs b/core/src/testing/mod.rs similarity index 100% rename from core/src/test_framework/mod.rs rename to core/src/testing/mod.rs diff --git a/core/src/test_framework/runner.rs b/core/src/testing/runner.rs similarity index 100% rename from core/src/test_framework/runner.rs rename to core/src/testing/runner.rs diff --git a/core/src/volume/manager.rs b/core/src/volume/manager.rs index 061ff98f5..59aa05283 100644 --- a/core/src/volume/manager.rs +++ b/core/src/volume/manager.rs @@ -1,7 +1,7 @@ //! Volume Manager - Central management for all volume operations -use crate::infrastructure::database::entities; -use crate::infrastructure::events::{Event, EventBus}; +use crate::infra::database::entities; +use crate::infra::events::{Event, EventBus}; use crate::library::LibraryManager; use crate::volume::{ error::{VolumeError, VolumeResult}, diff --git a/crates/ffmpeg/Cargo.toml b/crates/ffmpeg/Cargo.toml index dda5c3a3c..aae036fc8 100644 --- a/crates/ffmpeg/Cargo.toml +++ b/crates/ffmpeg/Cargo.toml @@ -19,7 +19,7 @@ chrono = { workspace = true, features = ["serde"] } image = { workspace = true } libc = { workspace = true } thiserror = { workspace = true } -tokio = { workspace = true, features = ["fs", "rt"] } +tokio = { workspace = true, features = ["fs", "rt", "io-util"] } tracing = { workspace = true } webp = { workspace = true } diff --git a/crates/ffmpeg/src/thumbnailer.rs b/crates/ffmpeg/src/thumbnailer.rs index 6333e34c4..4fc1b4dcd 100644 --- a/crates/ffmpeg/src/thumbnailer.rs +++ b/crates/ffmpeg/src/thumbnailer.rs @@ -24,31 +24,31 @@ impl Thumbnailer { ) -> Result<(), Error> { let output_thumbnail_path = output_thumbnail_path.as_ref(); let path = output_thumbnail_path.parent().ok_or_else(|| { - FileIOError::from(( + FileIOError::from_std_io_err( output_thumbnail_path, io::Error::new( io::ErrorKind::InvalidInput, "Cannot determine parent directory", ), - )) + ) })?; fs::create_dir_all(path) .await - .map_err(|e| FileIOError::from((path, e)))?; + .map_err(|e| FileIOError::from_std_io_err(path, e))?; let webp = self.process_to_webp_bytes(video_file_path).await?; let mut file = fs::File::create(output_thumbnail_path) .await - .map_err(|e: io::Error| FileIOError::from((output_thumbnail_path, e)))?; + .map_err(|e: io::Error| FileIOError::from_std_io_err(output_thumbnail_path, e))?; file.write_all(&webp) .await - .map_err(|e| FileIOError::from((output_thumbnail_path, e)))?; + .map_err(|e| FileIOError::from_std_io_err(output_thumbnail_path, e))?; file.sync_all() .await - .map_err(|e| FileIOError::from((output_thumbnail_path, e)).into()) + .map_err(|e| FileIOError::from_std_io_err(output_thumbnail_path, e).into()) } /// Processes an video input file and returns a webp encoded thumbnail as bytes diff --git a/crates/media-metadata/Cargo.toml b/crates/media-metadata/Cargo.toml index 0c93f57ed..0633a3eb5 100644 --- a/crates/media-metadata/Cargo.toml +++ b/crates/media-metadata/Cargo.toml @@ -22,9 +22,9 @@ chrono = { workspace = true, features = ["serde"] } image = { workspace = true } serde = { workspace = true, features = ["derive"] } serde_json = { workspace = true } -specta = { workspace = true, features = ["chrono"] } +specta = { workspace = true, features = ["chrono", "derive"] } thiserror = { workspace = true } -tokio = { workspace = true } +tokio = { workspace = true, features = ["rt", "rt-multi-thread"] } # Specific Media Metadata dependencies kamadak-exif = "0.5.5" diff --git a/crates/media-metadata/src/exif/mod.rs b/crates/media-metadata/src/exif/mod.rs index 739c9c41e..ea9b8f201 100644 --- a/crates/media-metadata/src/exif/mod.rs +++ b/crates/media-metadata/src/exif/mod.rs @@ -52,7 +52,7 @@ impl ExifMetadata { | exif::Error::NotSupported(_) | exif::Error::BlankValue(_), )) => Ok(None), - Err(Error::Exif(exif::Error::Io(e))) => Err(FileIOError::from((path, e)).into()), + Err(Error::Exif(exif::Error::Io(e))) => Err(FileIOError::from_std_io_err(path, e).into()), Err(e) => Err(e), } } diff --git a/crates/media-metadata/src/exif/reader.rs b/crates/media-metadata/src/exif/reader.rs index 98a13e5ea..5374b779c 100644 --- a/crates/media-metadata/src/exif/reader.rs +++ b/crates/media-metadata/src/exif/reader.rs @@ -17,7 +17,7 @@ impl ExifReader { pub fn from_path(path: impl AsRef) -> Result { exif::Reader::new() .read_from_container(&mut BufReader::new( - File::open(&path).map_err(|e| FileIOError::from((path, e)))?, + File::open(&path).map_err(|e| FileIOError::from_std_io_err(&path, e))?, )) .map(Self) .map_err(Into::into) diff --git a/crates/utils/Cargo.toml b/crates/utils/Cargo.toml new file mode 100644 index 000000000..0296672d6 --- /dev/null +++ b/crates/utils/Cargo.toml @@ -0,0 +1,11 @@ +[package] +name = "sd-utils" +version = "0.1.0" +edition = "2021" + +[dependencies] +thiserror = { workspace = true } +tracing = { workspace = true } + +[features] +default = [] \ No newline at end of file diff --git a/crates/utils/src/error.rs b/crates/utils/src/error.rs new file mode 100644 index 000000000..a09696ee9 --- /dev/null +++ b/crates/utils/src/error.rs @@ -0,0 +1,62 @@ +use std::{fmt::Display, path::Path}; + +use thiserror::Error; +use tracing::error; + +/// Report an error with tracing +pub fn report_error(res: &Result<(), impl Display>) { + if let Err(e) = res { + error!("{e:#}"); + } +} + +/// File I/O error that includes the path that caused the error +#[derive(Error, Debug)] +pub struct FileIOError { + pub path: Box, + #[source] + pub source: std::io::Error, + pub maybe_context: Option, +} + +impl Display for FileIOError { + fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { + write!( + f, + "file I/O error{}: {}; path: '{}'", + self.maybe_context + .as_ref() + .map(|ctx| format!(" ({ctx})")) + .unwrap_or_default(), + self.source, + self.path.display() + ) + } +} + +impl FileIOError { + pub fn from_std_io_err(path: impl AsRef, source: std::io::Error) -> Self { + Self { + path: path.as_ref().into(), + source, + maybe_context: None, + } + } + + pub fn from_std_io_err_with_msg( + path: impl AsRef, + source: std::io::Error, + msg: impl Into, + ) -> Self { + Self { + path: path.as_ref().into(), + source, + maybe_context: Some(msg.into()), + } + } +} + +/// Error for paths that contain non-UTF8 characters +#[derive(Error, Debug)] +#[error("Received a non UTF-8 path: ")] +pub struct NonUtf8PathError(pub Box); \ No newline at end of file diff --git a/crates/utils/src/lib.rs b/crates/utils/src/lib.rs new file mode 100644 index 000000000..f9c8fcb6f --- /dev/null +++ b/crates/utils/src/lib.rs @@ -0,0 +1,69 @@ +pub mod error; + +// Re-export commonly used error types +pub use error::{FileIOError, NonUtf8PathError}; + +// Chain optional iterators utility +pub fn chain_optional_iter( + required: impl IntoIterator, + optional: Option>, +) -> impl Iterator { + required + .into_iter() + .chain(optional.into_iter().flatten()) +} + +// Frontend compatibility utilities for large integers +// JavaScript can't handle i64/u64 properly, so we convert to tuples + +/// Convert i64 to a tuple for frontend compatibility +pub fn i64_to_frontend(value: i64) -> (i32, u32) { + #[allow(clippy::cast_possible_truncation, clippy::cast_sign_loss)] + { + // Split into (high, low) parts + ((value >> 32) as i32, value as u32) + } +} + +/// Convert u64 to a tuple for frontend compatibility +pub fn u64_to_frontend(value: u64) -> (u32, u32) { + #[allow(clippy::cast_possible_truncation)] + { + // Split into (high, low) parts + ((value >> 32) as u32, value as u32) + } +} + +#[cfg(test)] +mod tests { + use super::*; + + #[test] + fn test_chain_optional_iter() { + let required = vec![1, 2, 3]; + let optional = Some(vec![4, 5, 6]); + let result: Vec<_> = chain_optional_iter(required, optional).collect(); + assert_eq!(result, vec![1, 2, 3, 4, 5, 6]); + + let required = vec![1, 2, 3]; + let optional: Option> = None; + let result: Vec<_> = chain_optional_iter(required, optional).collect(); + assert_eq!(result, vec![1, 2, 3]); + } + + #[test] + fn test_i64_to_frontend() { + let value: i64 = 0x123456789ABCDEF0_u64 as i64; + let (high, low) = i64_to_frontend(value); + assert_eq!(high, 0x12345678_i32); + assert_eq!(low, 0x9ABCDEF0_u32); + } + + #[test] + fn test_u64_to_frontend() { + let value: u64 = 0x123456789ABCDEF0; + let (high, low) = u64_to_frontend(value); + assert_eq!(high, 0x12345678_u32); + assert_eq!(low, 0x9ABCDEF0_u32); + } +} \ No newline at end of file diff --git a/rust-toolchain.toml b/rust-toolchain.toml index 4cef0b738..292fe499e 100644 --- a/rust-toolchain.toml +++ b/rust-toolchain.toml @@ -1,2 +1,2 @@ [toolchain] -channel = "1.81" +channel = "stable"