mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-16 12:26:42 -04:00
feat: add cloud volume handling
This commit is contained in:
@@ -1,3 +1,4 @@
|
||||
pub mod cloud;
|
||||
pub mod devices;
|
||||
pub mod file;
|
||||
pub mod index;
|
||||
|
||||
@@ -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<CoreContext>) -> 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)?;
|
||||
|
||||
@@ -104,6 +104,7 @@ impl LibraryAction for VolumeAddCloudAction {
|
||||
let backend_arc: Arc<dyn crate::volume::VolumeBackend> = 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()))
|
||||
|
||||
@@ -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<crate::library::Library>],
|
||||
library_key_manager: std::sync::Arc<crate::crypto::library_key_manager::LibraryKeyManager>,
|
||||
) -> 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<Option<Volume>> {
|
||||
// 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<Option<Volume>> {
|
||||
// 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<Uuid> {
|
||||
// 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
|
||||
|
||||
Reference in New Issue
Block a user