diff --git a/apps/cli/src/domains/mod.rs b/apps/cli/src/domains/mod.rs index 0a653ec5b..3291d5e5d 100644 --- a/apps/cli/src/domains/mod.rs +++ b/apps/cli/src/domains/mod.rs @@ -1,3 +1,4 @@ +pub mod cloud; pub mod devices; pub mod file; pub mod index; diff --git a/core/src/library/manager.rs b/core/src/library/manager.rs index 07742b846..41136cd35 100644 --- a/core/src/library/manager.rs +++ b/core/src/library/manager.rs @@ -173,7 +173,7 @@ impl LibraryManager { tokio::fs::create_dir_all(&library_path).await?; // Initialize library - self.initialize_library(&library_path, name).await?; + self.initialize_library(&library_path, name, context.clone()).await?; // Open the newly created library let library = self.open_library(&library_path, context.clone()).await?; @@ -454,7 +454,7 @@ impl LibraryManager { } /// Initialize a new library directory - async fn initialize_library(&self, path: &Path, name: String) -> Result<()> { + async fn initialize_library(&self, path: &Path, name: String, context: Arc) -> Result<()> { // Create subdirectories tokio::fs::create_dir_all(path.join("thumbnails")).await?; tokio::fs::create_dir_all(path.join("previews")).await?; @@ -475,6 +475,13 @@ impl LibraryManager { statistics: LibraryStatistics::default(), }; + // Initialize library encryption key + context.library_key_manager + .get_or_create_library_key(config.id) + .map_err(|e| LibraryError::Other(format!("Failed to initialize library encryption key: {}", e)))?; + + info!("Initialized encryption key for library '{}'", config.name); + // Save configuration let config_path = path.join("library.json"); let json = serde_json::to_string_pretty(&config)?; diff --git a/core/src/ops/volumes/add_cloud/action.rs b/core/src/ops/volumes/add_cloud/action.rs index ef0dfee0b..3ed2b2a86 100644 --- a/core/src/ops/volumes/add_cloud/action.rs +++ b/core/src/ops/volumes/add_cloud/action.rs @@ -104,6 +104,7 @@ impl LibraryAction for VolumeAddCloudAction { let backend_arc: Arc = Arc::new(backend); let volume = Volume { + uuid: Uuid::new_v4(), // Generate UUID for cloud volume fingerprint: fingerprint.clone(), device_id, name: self.input.display_name.clone(), @@ -137,6 +138,15 @@ impl LibraryAction for VolumeAddCloudAction { ActionError::InvalidInput(format!("Failed to store credentials: {}", e)) })?; + tracing::info!("Successfully stored credentials for cloud volume {} in keyring (library: {}, fingerprint: {})", + self.input.display_name, library_id, fingerprint.0); + + // Register the cloud volume with the volume manager so it can be tracked + context + .volume_manager + .register_cloud_volume(volume.clone()) + .await; + let tracked = context .volume_manager .track_volume(&library, &fingerprint, Some(self.input.display_name.clone())) diff --git a/core/src/volume/manager.rs b/core/src/volume/manager.rs index f94145958..a83187bdb 100644 --- a/core/src/volume/manager.rs +++ b/core/src/volume/manager.rs @@ -109,7 +109,7 @@ impl VolumeManager { pub async fn initialize(&self) -> VolumeResult<()> { info!("Initializing volume manager"); - // Perform initial volume detection + // Perform initial volume detection (for local volumes) self.refresh_volumes().await?; // Start monitoring if configured @@ -125,6 +125,130 @@ impl VolumeManager { Ok(()) } + /// Load cloud volumes from the database and restore them to the in-memory HashMap + /// This should be called after libraries are loaded + pub async fn load_cloud_volumes_from_db( + &self, + libraries: &[std::sync::Arc], + library_key_manager: std::sync::Arc, + ) -> VolumeResult<()> { + use crate::crypto::cloud_credentials::CloudCredentialManager; + + let mut loaded_count = 0; + + for library in libraries { + let db = library.db().conn(); + + // Query all network volumes (cloud volumes) for this library + let cloud_volumes = entities::volume::Entity::find() + .filter(entities::volume::Column::IsNetworkDrive.eq(true)) + .all(db) + .await + .map_err(|e| VolumeError::Database(e.to_string()))?; + + info!("Found {} cloud volumes in database for library {}", cloud_volumes.len(), library.id()); + + for db_volume in cloud_volumes { + let fingerprint = VolumeFingerprint(db_volume.fingerprint.clone()); + + // Skip if already loaded + if self.get_volume(&fingerprint).await.is_some() { + continue; + } + + // Try to load credentials and recreate the backend + let credential_manager = CloudCredentialManager::new(library_key_manager.clone()); + + match credential_manager.get_credential(library.id(), &db_volume.fingerprint) { + Ok(credential) => { + // Recreate the cloud backend from stored credentials + match credential.service { + crate::volume::CloudServiceType::S3 => { + if let crate::crypto::cloud_credentials::CredentialData::AccessKey { + access_key_id, + secret_access_key, + .. + } = &credential.data + { + // Parse mount point to extract bucket info + let mount_point_str = db_volume.mount_point.as_ref() + .ok_or_else(|| VolumeError::InvalidData("No mount point for cloud volume".to_string()))?; + + // Extract bucket name from mount_point like "cloud://s3/bucket-name" + let bucket = mount_point_str + .strip_prefix("cloud://s3/") + .ok_or_else(|| VolumeError::InvalidData(format!("Invalid S3 mount point: {}", mount_point_str)))?; + + // Try to recreate the backend (we don't have region/endpoint stored, use defaults) + match crate::volume::CloudBackend::new_s3( + bucket, + "us-east-1", // Default region - TODO: store this in DB + access_key_id, + secret_access_key, + None, + ).await { + Ok(backend) => { + // Reconstruct the Volume struct + let volume = Volume { + uuid: db_volume.uuid, // Use UUID from database + fingerprint: fingerprint.clone(), + device_id: db_volume.device_id, + name: db_volume.display_name.clone().unwrap_or_else(|| bucket.to_string()), + mount_type: crate::volume::types::MountType::Network, + volume_type: crate::volume::types::VolumeType::Network, + mount_point: std::path::PathBuf::from(mount_point_str), + mount_points: vec![std::path::PathBuf::from(mount_point_str)], + is_mounted: true, + disk_type: crate::volume::types::DiskType::Unknown, + file_system: crate::volume::types::FileSystem::Other("S3".to_string()), + total_bytes_capacity: db_volume.total_capacity.unwrap_or(0) as u64, + total_bytes_available: db_volume.available_capacity.unwrap_or(0) as u64, + read_only: false, + hardware_id: None, + error_status: None, + apfs_container: None, + container_volume_id: None, + path_mappings: Vec::new(), + backend: Some(Arc::new(backend)), + read_speed_mbps: db_volume.read_speed_mbps.map(|s| s as u64), + write_speed_mbps: db_volume.write_speed_mbps.map(|s| s as u64), + auto_track_eligible: db_volume.auto_track_eligible.unwrap_or(false), + is_user_visible: db_volume.is_user_visible.unwrap_or(true), + last_updated: chrono::Utc::now(), + }; + + // Register in memory + let mut volumes = self.volumes.write().await; + volumes.insert(fingerprint.clone(), volume); + loaded_count += 1; + info!("Loaded cloud volume {} from database", db_volume.display_name.as_ref().unwrap_or(&bucket.to_string())); + } + Err(e) => { + warn!("Failed to recreate cloud backend for volume {}: {}", fingerprint.0, e); + } + } + } + } + _ => { + warn!("Unsupported cloud service type for volume {}", fingerprint.0); + } + } + } + Err(e) => { + warn!("Failed to load credentials for cloud volume {} ({}): {}. The volume will not be available until credentials are re-entered by removing and re-adding the volume.", + db_volume.display_name.as_ref().unwrap_or(&"Unknown".to_string()), + fingerprint.0, + e + ); + } + } + } + } + + info!("Loaded {} cloud volumes from database", loaded_count); + Ok(()) + } + /// Start background monitoring of volume changes pub async fn start_monitoring(&self) { if *self.is_monitoring.read().await { @@ -499,6 +623,51 @@ impl VolumeManager { self.volumes.read().await.get(fingerprint).cloned() } + /// Get a volume by its database UUID (for cloud volumes) + /// This queries all libraries to find the volume record and then returns the in-memory Volume + pub async fn get_volume_by_uuid( + &self, + volume_uuid: Uuid, + library: &crate::library::Library, + ) -> VolumeResult> { + // Query the database to find the volume record by UUID + let db = library.db().conn(); + let volume_record = entities::volume::Entity::find() + .filter(entities::volume::Column::Uuid.eq(volume_uuid)) + .one(db) + .await + .map_err(|e| VolumeError::Database(e.to_string()))?; + + if let Some(record) = volume_record { + // Get the volume from our in-memory store by fingerprint + let fingerprint = VolumeFingerprint(record.fingerprint); + Ok(self.get_volume(&fingerprint).await) + } else { + Ok(None) + } + } + + /// Resolve a volume for an SdPath (unified method for cloud and local paths) + /// This abstracts away the cloud/local path distinction + pub async fn resolve_volume_for_sdpath( + &self, + sdpath: &crate::domain::addressing::SdPath, + library: &crate::library::Library, + ) -> VolumeResult> { + // Check if this is a cloud path (has volume_id) + if let Some(volume_uuid) = sdpath.volume_id() { + // Cloud path - look up by UUID + self.get_volume_by_uuid(volume_uuid, library).await + } else { + // Local path - resolve by filesystem path + if let Some(local_path) = sdpath.as_local_path() { + Ok(self.volume_for_path(local_path).await) + } else { + Ok(None) + } + } + } + /// Check if two paths are on the same volume pub async fn same_volume(&self, path1: &Path, path2: &Path) -> bool { let vol1 = self.volume_for_path(path1).await; @@ -663,6 +832,16 @@ impl VolumeManager { debug!("Volume path cache cleared"); } + /// Register a cloud volume with the volume manager + /// This adds the volume to the internal volumes map so it can be tracked + pub async fn register_cloud_volume(&self, volume: Volume) { + let fingerprint = volume.fingerprint.clone(); + let mut volumes = self.volumes.write().await; + + info!("Registering cloud volume '{}' with fingerprint {}", volume.name, fingerprint); + volumes.insert(fingerprint, volume); + } + /// Track a volume in the specified library pub async fn track_volume( &self, @@ -721,7 +900,7 @@ impl VolumeManager { // Create tracking record let active_model = entities::volume::ActiveModel { - uuid: Set(Uuid::new_v4()), + uuid: Set(volume.uuid), // Use the volume's UUID device_id: Set(volume.device_id), // Use Uuid directly fingerprint: Set(fingerprint.0.clone()), display_name: Set(display_name.clone()), @@ -1084,6 +1263,13 @@ impl VolumeManager { /// Create or read Spacedrive identifier file for a volume /// Returns the UUID from the identifier file if successfully created/read async fn manage_spacedrive_identifier(&self, volume: &Volume) -> Option { + // Skip cloud volumes - they don't have filesystem mount points + if matches!(volume.volume_type, crate::volume::types::VolumeType::Network) + && matches!(volume.mount_type, crate::volume::types::MountType::Network) { + debug!("Skipping Spacedrive identifier management for cloud volume: {}", volume.name); + return None; + } + let id_file_path = volume.mount_point.join(SPACEDRIVE_VOLUME_ID_FILE); // Try to read existing identifier file