mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2026-05-01 20:03:51 -04:00
feat(volumes): Implement persistent volume tracking with automatic system volume detection
- Add database migration for volumes table with comprehensive schema - Create SeaORM entity for volume persistence - Implement VolumeManager database operations: - track_volume() - persists volume tracking in library database - untrack_volume() - removes volume tracking - get_tracked_volumes() - retrieves all tracked volumes - auto_track_system_volumes() - automatically tracks system/boot volumes - Update volume action handlers to use database operations - Add VolumeMonitorService for background volume state updates - Add automatic system volume tracking on library creation/open - Add configuration options: - auto_track_system_volumes (default: true) - tracks system drives automatically - auto_track_external_volumes (default: false) - tracks external drives when connected - Add comprehensive integration tests for volume tracking lifecycle System volumes like "Macintosh HD" are now automatically tracked when libraries are created or opened, providing a seamless user experience. 🤖 Generated with [Claude Code](https://claude.ai/code) Co-Authored-By: Claude <noreply@anthropic.com>
This commit is contained in:
@@ -15,6 +15,7 @@ pub mod metadata_tag;
|
||||
pub use metadata_tag as user_metadata_tag; // Alias for hierarchical metadata operations
|
||||
pub mod metadata_label;
|
||||
pub mod audit_log;
|
||||
pub mod volume;
|
||||
|
||||
// Re-export all entities
|
||||
pub use device::Entity as Device;
|
||||
@@ -26,6 +27,7 @@ pub use tag::Entity as Tag;
|
||||
pub use label::Entity as Label;
|
||||
pub use metadata_tag::Entity as UserMetadataTag;
|
||||
pub use audit_log::Entity as AuditLog;
|
||||
pub use volume::Entity as Volume;
|
||||
|
||||
// Re-export active models for easy access
|
||||
pub use device::ActiveModel as DeviceActive;
|
||||
@@ -36,4 +38,5 @@ pub use content_identity::ActiveModel as ContentIdentityActive;
|
||||
pub use tag::ActiveModel as TagActive;
|
||||
pub use label::ActiveModel as LabelActive;
|
||||
pub use metadata_tag::ActiveModel as UserMetadataTagActive;
|
||||
pub use audit_log::ActiveModel as AuditLogActive;
|
||||
pub use audit_log::ActiveModel as AuditLogActive;
|
||||
pub use volume::ActiveModel as VolumeActive;
|
||||
58
core-new/src/infrastructure/database/entities/volume.rs
Normal file
58
core-new/src/infrastructure/database/entities/volume.rs
Normal file
@@ -0,0 +1,58 @@
|
||||
//! Volume entity
|
||||
|
||||
use crate::volume::types::TrackedVolume;
|
||||
use sea_orm::entity::prelude::*;
|
||||
use serde::{Deserialize, Serialize};
|
||||
|
||||
#[derive(Clone, Debug, PartialEq, Eq, DeriveEntityModel, Serialize, Deserialize)]
|
||||
#[sea_orm(table_name = "volumes")]
|
||||
pub struct Model {
|
||||
#[sea_orm(primary_key)]
|
||||
pub id: i32,
|
||||
pub uuid: Uuid,
|
||||
pub fingerprint: String,
|
||||
pub display_name: Option<String>,
|
||||
pub tracked_at: DateTimeUtc,
|
||||
pub last_seen_at: DateTimeUtc,
|
||||
pub is_online: bool,
|
||||
pub total_capacity: Option<i64>,
|
||||
pub available_capacity: Option<i64>,
|
||||
pub read_speed_mbps: Option<i32>,
|
||||
pub write_speed_mbps: Option<i32>,
|
||||
pub last_speed_test_at: Option<DateTimeUtc>,
|
||||
pub file_system: Option<String>,
|
||||
pub mount_point: Option<String>,
|
||||
pub is_removable: Option<bool>,
|
||||
pub is_network_drive: Option<bool>,
|
||||
pub device_model: Option<String>,
|
||||
}
|
||||
|
||||
#[derive(Copy, Clone, Debug, EnumIter, DeriveRelation)]
|
||||
pub enum Relation {}
|
||||
|
||||
impl ActiveModelBehavior for ActiveModel {}
|
||||
|
||||
impl Model {
|
||||
/// Convert database model to tracked volume
|
||||
pub fn to_tracked_volume(&self) -> TrackedVolume {
|
||||
TrackedVolume {
|
||||
id: self.id,
|
||||
uuid: self.uuid,
|
||||
fingerprint: crate::volume::VolumeFingerprint(self.fingerprint.clone()),
|
||||
display_name: self.display_name.clone(),
|
||||
tracked_at: self.tracked_at,
|
||||
last_seen_at: self.last_seen_at,
|
||||
is_online: self.is_online,
|
||||
total_capacity: self.total_capacity.map(|c| c as u64),
|
||||
available_capacity: self.available_capacity.map(|c| c as u64),
|
||||
read_speed_mbps: self.read_speed_mbps.map(|s| s as u32),
|
||||
write_speed_mbps: self.write_speed_mbps.map(|s| s as u32),
|
||||
last_speed_test_at: self.last_speed_test_at,
|
||||
file_system: self.file_system.clone(),
|
||||
mount_point: self.mount_point.clone(),
|
||||
is_removable: self.is_removable,
|
||||
is_network_drive: self.is_network_drive,
|
||||
device_model: self.device_model.clone(),
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,121 @@
|
||||
//! Create volumes table for tracking mounted volumes in each library
|
||||
|
||||
use sea_orm_migration::prelude::*;
|
||||
|
||||
#[derive(DeriveMigrationName)]
|
||||
pub struct Migration;
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl MigrationTrait for Migration {
|
||||
async fn up(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
// Create volumes table
|
||||
manager
|
||||
.create_table(
|
||||
Table::create()
|
||||
.table(Volume::Table)
|
||||
.if_not_exists()
|
||||
.col(
|
||||
ColumnDef::new(Volume::Id)
|
||||
.integer()
|
||||
.not_null()
|
||||
.auto_increment()
|
||||
.primary_key(),
|
||||
)
|
||||
.col(ColumnDef::new(Volume::Uuid).text().not_null().unique_key())
|
||||
.col(ColumnDef::new(Volume::Fingerprint).text().not_null())
|
||||
.col(ColumnDef::new(Volume::DisplayName).text())
|
||||
.col(
|
||||
ColumnDef::new(Volume::TrackedAt)
|
||||
.timestamp()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Volume::LastSeenAt)
|
||||
.timestamp()
|
||||
.not_null(),
|
||||
)
|
||||
.col(
|
||||
ColumnDef::new(Volume::IsOnline)
|
||||
.boolean()
|
||||
.not_null()
|
||||
.default(true),
|
||||
)
|
||||
.col(ColumnDef::new(Volume::TotalCapacity).big_integer())
|
||||
.col(ColumnDef::new(Volume::AvailableCapacity).big_integer())
|
||||
.col(ColumnDef::new(Volume::ReadSpeedMbps).integer())
|
||||
.col(ColumnDef::new(Volume::WriteSpeedMbps).integer())
|
||||
.col(ColumnDef::new(Volume::LastSpeedTestAt).timestamp())
|
||||
.col(ColumnDef::new(Volume::FileSystem).text())
|
||||
.col(ColumnDef::new(Volume::MountPoint).text())
|
||||
.col(ColumnDef::new(Volume::IsRemovable).boolean())
|
||||
.col(ColumnDef::new(Volume::IsNetworkDrive).boolean())
|
||||
.col(ColumnDef::new(Volume::DeviceModel).text())
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create unique index on fingerprint (since each library tracks volumes independently)
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_volume_fingerprint_unique")
|
||||
.table(Volume::Table)
|
||||
.col(Volume::Fingerprint)
|
||||
.unique()
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create index on last_seen_at for efficient queries
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_volume_last_seen_at")
|
||||
.table(Volume::Table)
|
||||
.col(Volume::LastSeenAt)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
// Create index on is_online for filtering
|
||||
manager
|
||||
.create_index(
|
||||
Index::create()
|
||||
.name("idx_volume_is_online")
|
||||
.table(Volume::Table)
|
||||
.col(Volume::IsOnline)
|
||||
.to_owned(),
|
||||
)
|
||||
.await?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
async fn down(&self, manager: &SchemaManager) -> Result<(), DbErr> {
|
||||
manager
|
||||
.drop_table(Table::drop().table(Volume::Table).to_owned())
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
#[derive(DeriveIden)]
|
||||
enum Volume {
|
||||
Table,
|
||||
Id,
|
||||
Uuid,
|
||||
Fingerprint,
|
||||
DisplayName,
|
||||
TrackedAt,
|
||||
LastSeenAt,
|
||||
IsOnline,
|
||||
TotalCapacity,
|
||||
AvailableCapacity,
|
||||
ReadSpeedMbps,
|
||||
WriteSpeedMbps,
|
||||
LastSpeedTestAt,
|
||||
FileSystem,
|
||||
MountPoint,
|
||||
IsRemovable,
|
||||
IsNetworkDrive,
|
||||
DeviceModel,
|
||||
}
|
||||
@@ -10,9 +10,11 @@ impl MigratorTrait for Migrator {
|
||||
vec![
|
||||
Box::new(m20240101_000001_create_initial_tables::Migration),
|
||||
Box::new(m20240102_000001_add_audit_log::Migration),
|
||||
Box::new(m20240103_000001_create_volumes::Migration),
|
||||
]
|
||||
}
|
||||
}
|
||||
|
||||
mod m20240101_000001_create_initial_tables;
|
||||
mod m20240102_000001_add_audit_log;
|
||||
mod m20240102_000001_add_audit_log;
|
||||
mod m20240103_000001_create_volumes;
|
||||
@@ -58,6 +58,12 @@ pub struct LibrarySettings {
|
||||
|
||||
/// Maximum file size to index (in bytes)
|
||||
pub max_file_size: Option<u64>,
|
||||
|
||||
/// Whether to automatically track system volumes
|
||||
pub auto_track_system_volumes: bool,
|
||||
|
||||
/// Whether to automatically track external volumes when connected
|
||||
pub auto_track_external_volumes: bool,
|
||||
}
|
||||
|
||||
impl LibraryConfig {
|
||||
@@ -87,6 +93,8 @@ impl Default for LibrarySettings {
|
||||
".part".to_string(),
|
||||
],
|
||||
max_file_size: Some(100 * 1024 * 1024 * 1024), // 100GB
|
||||
auto_track_system_volumes: true, // Default to true for user convenience
|
||||
auto_track_external_volumes: false, // Default to false for privacy
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -245,6 +245,17 @@ impl LibraryManager {
|
||||
let mut libraries = self.libraries.write().await;
|
||||
libraries.insert(config.id, library.clone());
|
||||
}
|
||||
|
||||
// Auto-track system volumes if enabled
|
||||
if config.settings.auto_track_system_volumes {
|
||||
info!("Auto-tracking system volumes for library {}", config.name);
|
||||
if let Err(e) = context.volume_manager
|
||||
.auto_track_system_volumes(&library)
|
||||
.await
|
||||
{
|
||||
warn!("Failed to auto-track system volumes: {}", e);
|
||||
}
|
||||
}
|
||||
|
||||
// Emit event
|
||||
self.event_bus.emit(Event::LibraryOpened {
|
||||
|
||||
@@ -45,12 +45,28 @@ impl ActionHandler for VolumeTrackHandler {
|
||||
));
|
||||
}
|
||||
|
||||
// TODO: Implement actual volume tracking in library
|
||||
// Track the volume in the database
|
||||
let tracked = context
|
||||
.volume_manager
|
||||
.track_volume(&library, &action.fingerprint, action.name.clone())
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
crate::volume::VolumeError::AlreadyTracked(_) => {
|
||||
ActionError::InvalidInput("Volume is already tracked in this library".to_string())
|
||||
}
|
||||
crate::volume::VolumeError::NotFound(_) => {
|
||||
ActionError::InvalidInput("Volume not found".to_string())
|
||||
}
|
||||
crate::volume::VolumeError::Database(msg) => {
|
||||
ActionError::Internal(format!("Database error: {}", msg))
|
||||
}
|
||||
_ => ActionError::Internal(e.to_string()),
|
||||
})?;
|
||||
|
||||
Ok(ActionOutput::VolumeTracked {
|
||||
fingerprint: action.fingerprint,
|
||||
library_id: action.library_id,
|
||||
volume_name: volume.name,
|
||||
volume_name: tracked.display_name.unwrap_or(volume.name),
|
||||
})
|
||||
}
|
||||
_ => Err(ActionError::InvalidActionType),
|
||||
|
||||
@@ -27,13 +27,26 @@ impl ActionHandler for VolumeUntrackHandler {
|
||||
match action {
|
||||
Action::VolumeUntrack { action } => {
|
||||
// Verify library exists
|
||||
let _library = context
|
||||
let library = context
|
||||
.library_manager
|
||||
.get_library(action.library_id)
|
||||
.await
|
||||
.ok_or_else(|| ActionError::InvalidInput("Library not found".to_string()))?;
|
||||
|
||||
// TODO: Implement actual volume untracking from library
|
||||
// Untrack the volume from the database
|
||||
context
|
||||
.volume_manager
|
||||
.untrack_volume(&library, &action.fingerprint)
|
||||
.await
|
||||
.map_err(|e| match e {
|
||||
crate::volume::VolumeError::NotTracked(_) => {
|
||||
ActionError::InvalidInput("Volume is not tracked in this library".to_string())
|
||||
}
|
||||
crate::volume::VolumeError::Database(msg) => {
|
||||
ActionError::Internal(format!("Database error: {}", msg))
|
||||
}
|
||||
_ => ActionError::Internal(e.to_string()),
|
||||
})?;
|
||||
|
||||
Ok(ActionOutput::VolumeUntracked {
|
||||
fingerprint: action.fingerprint,
|
||||
|
||||
@@ -13,11 +13,13 @@ pub mod device;
|
||||
pub mod file_sharing;
|
||||
pub mod location_watcher;
|
||||
pub mod networking;
|
||||
pub mod volume_monitor;
|
||||
|
||||
use device::DeviceService;
|
||||
use file_sharing::FileSharingService;
|
||||
use location_watcher::{LocationWatcher, LocationWatcherConfig};
|
||||
use networking::NetworkingService;
|
||||
use volume_monitor::{VolumeMonitorService, VolumeMonitorConfig};
|
||||
|
||||
/// Container for all background services
|
||||
pub struct Services {
|
||||
@@ -29,6 +31,8 @@ pub struct Services {
|
||||
pub device: Arc<DeviceService>,
|
||||
/// Networking service for device connections
|
||||
pub networking: Option<Arc<NetworkingService>>,
|
||||
/// Volume monitoring service
|
||||
pub volume_monitor: Option<Arc<VolumeMonitorService>>,
|
||||
/// Library key manager
|
||||
pub library_key_manager: Arc<LibraryKeyManager>,
|
||||
/// Shared context for all services
|
||||
@@ -54,6 +58,7 @@ impl Services {
|
||||
file_sharing,
|
||||
device,
|
||||
networking: None, // Initialized separately when needed
|
||||
volume_monitor: None, // Initialized after library manager is available
|
||||
library_key_manager,
|
||||
context,
|
||||
}
|
||||
@@ -69,6 +74,11 @@ impl Services {
|
||||
info!("Starting all background services");
|
||||
|
||||
self.location_watcher.start().await?;
|
||||
|
||||
// Start volume monitor if initialized
|
||||
if let Some(monitor) = &self.volume_monitor {
|
||||
monitor.start().await?;
|
||||
}
|
||||
|
||||
// Networking service is already started during initialization
|
||||
|
||||
@@ -84,6 +94,11 @@ impl Services {
|
||||
info!("Stopping all background services");
|
||||
|
||||
self.location_watcher.stop().await?;
|
||||
|
||||
// Stop volume monitor if initialized
|
||||
if let Some(monitor) = &self.volume_monitor {
|
||||
monitor.stop().await?;
|
||||
}
|
||||
|
||||
// Stop networking service if initialized
|
||||
if let Some(networking) = &self.networking {
|
||||
@@ -137,6 +152,40 @@ impl Services {
|
||||
pub fn networking(&self) -> Option<Arc<NetworkingService>> {
|
||||
self.networking.clone()
|
||||
}
|
||||
|
||||
/// Initialize volume monitor service
|
||||
pub fn init_volume_monitor(
|
||||
&mut self,
|
||||
volume_manager: Arc<crate::volume::VolumeManager>,
|
||||
library_manager: std::sync::Weak<crate::library::LibraryManager>,
|
||||
) {
|
||||
info!("Initializing volume monitor service");
|
||||
|
||||
let config = VolumeMonitorConfig::default();
|
||||
let volume_monitor = Arc::new(VolumeMonitorService::new(
|
||||
volume_manager,
|
||||
library_manager,
|
||||
config,
|
||||
));
|
||||
|
||||
self.volume_monitor = Some(volume_monitor);
|
||||
}
|
||||
|
||||
/// Start volume monitor service
|
||||
pub async fn start_volume_monitor(&self) -> Result<()> {
|
||||
if let Some(monitor) = &self.volume_monitor {
|
||||
monitor.start().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Stop volume monitor service
|
||||
pub async fn stop_volume_monitor(&self) -> Result<()> {
|
||||
if let Some(monitor) = &self.volume_monitor {
|
||||
monitor.stop().await?;
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
/// Trait for background services
|
||||
|
||||
244
core-new/src/services/volume_monitor.rs
Normal file
244
core-new/src/services/volume_monitor.rs
Normal file
@@ -0,0 +1,244 @@
|
||||
//! Volume monitoring service
|
||||
//!
|
||||
//! Periodically refreshes volume information and updates tracked volumes in the database.
|
||||
|
||||
use crate::{
|
||||
context::CoreContext,
|
||||
infrastructure::events::EventBus,
|
||||
library::LibraryManager,
|
||||
services::Service,
|
||||
volume::VolumeManager,
|
||||
};
|
||||
use anyhow::Result;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info, warn};
|
||||
|
||||
/// Configuration for volume monitoring
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct VolumeMonitorConfig {
|
||||
/// How often to refresh volume information (in seconds)
|
||||
pub refresh_interval_secs: u64,
|
||||
/// Whether to update tracked volumes in the database
|
||||
pub update_tracked_volumes: bool,
|
||||
}
|
||||
|
||||
impl Default for VolumeMonitorConfig {
|
||||
fn default() -> Self {
|
||||
Self {
|
||||
refresh_interval_secs: 30,
|
||||
update_tracked_volumes: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Background service that monitors volume state changes
|
||||
pub struct VolumeMonitorService {
|
||||
volume_manager: Arc<VolumeManager>,
|
||||
library_manager: Weak<LibraryManager>,
|
||||
config: VolumeMonitorConfig,
|
||||
running: RwLock<bool>,
|
||||
handle: RwLock<Option<tokio::task::JoinHandle<()>>>,
|
||||
}
|
||||
|
||||
impl VolumeMonitorService {
|
||||
/// Create a new volume monitor service
|
||||
pub fn new(
|
||||
volume_manager: Arc<VolumeManager>,
|
||||
library_manager: Weak<LibraryManager>,
|
||||
config: VolumeMonitorConfig,
|
||||
) -> Self {
|
||||
Self {
|
||||
volume_manager,
|
||||
library_manager,
|
||||
config,
|
||||
running: RwLock::new(false),
|
||||
handle: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Monitor volumes and update tracked volumes in libraries
|
||||
async fn monitor_loop(
|
||||
volume_manager: Arc<VolumeManager>,
|
||||
library_manager: Weak<LibraryManager>,
|
||||
config: VolumeMonitorConfig,
|
||||
running: Arc<RwLock<bool>>,
|
||||
) {
|
||||
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 {
|
||||
Ok(tracked_volumes) => {
|
||||
for tracked in tracked_volumes {
|
||||
// Check if volume is still present
|
||||
if let Some(current_volume) = volume_manager
|
||||
.get_volume(&tracked.fingerprint)
|
||||
.await
|
||||
{
|
||||
// Update volume state if changed
|
||||
if tracked.is_online != current_volume.is_mounted {
|
||||
if let Err(e) = volume_manager
|
||||
.update_tracked_volume_state(
|
||||
&library,
|
||||
&tracked.fingerprint,
|
||||
¤t_volume,
|
||||
)
|
||||
.await
|
||||
{
|
||||
error!(
|
||||
"Failed to update tracked volume {} in library {}: {}",
|
||||
tracked.fingerprint,
|
||||
library.id(),
|
||||
e
|
||||
);
|
||||
} else {
|
||||
debug!(
|
||||
"Updated tracked volume {} in library {} (online: {} -> {})",
|
||||
tracked.fingerprint,
|
||||
library.id(),
|
||||
tracked.is_online,
|
||||
current_volume.is_mounted
|
||||
);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Volume no longer detected but still tracked
|
||||
debug!(
|
||||
"Tracked volume {} not detected in library {}",
|
||||
tracked.fingerprint,
|
||||
library.id()
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
error!(
|
||||
"Failed to get tracked volumes for library {}: {}",
|
||||
library.id(),
|
||||
e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Check for new external volumes to auto-track
|
||||
let all_volumes = volume_manager.get_all_volumes().await;
|
||||
for volume in all_volumes {
|
||||
// Only consider external volumes
|
||||
if matches!(volume.mount_type, crate::volume::types::MountType::External) {
|
||||
for library in &libraries {
|
||||
// Check if auto-tracking is enabled
|
||||
let config = library.config().await;
|
||||
if config.settings.auto_track_external_volumes {
|
||||
// Check if not already tracked
|
||||
if !volume_manager
|
||||
.is_volume_tracked(&library, &volume.fingerprint)
|
||||
.await
|
||||
.unwrap_or(false)
|
||||
{
|
||||
// Auto-track the external volume
|
||||
match volume_manager
|
||||
.track_volume(&library, &volume.fingerprint, None)
|
||||
.await
|
||||
{
|
||||
Ok(_) => {
|
||||
info!(
|
||||
"Auto-tracked external volume '{}' in library '{}'",
|
||||
volume.name,
|
||||
library.name().await
|
||||
);
|
||||
}
|
||||
Err(e) => {
|
||||
debug!(
|
||||
"Failed to auto-track external volume '{}': {}",
|
||||
volume.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
} else {
|
||||
debug!("Library manager not available, skipping tracked volume updates");
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
info!("Volume monitoring stopped");
|
||||
}
|
||||
}
|
||||
|
||||
#[async_trait::async_trait]
|
||||
impl Service for VolumeMonitorService {
|
||||
async fn start(&self) -> Result<()> {
|
||||
let mut running = self.running.write().await;
|
||||
if *running {
|
||||
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"
|
||||
}
|
||||
}
|
||||
@@ -48,6 +48,18 @@ pub enum VolumeError {
|
||||
/// Invalid volume data
|
||||
#[error("Invalid volume data: {0}")]
|
||||
InvalidData(String),
|
||||
|
||||
/// Database operation failed
|
||||
#[error("Database error: {0}")]
|
||||
Database(String),
|
||||
|
||||
/// Volume is already tracked
|
||||
#[error("Volume is already tracked: {0}")]
|
||||
AlreadyTracked(String),
|
||||
|
||||
/// Volume is not tracked
|
||||
#[error("Volume is not tracked: {0}")]
|
||||
NotTracked(String),
|
||||
}
|
||||
|
||||
impl VolumeError {
|
||||
|
||||
@@ -1,17 +1,21 @@
|
||||
//! Volume Manager - Central management for all volume operations
|
||||
|
||||
use crate::infrastructure::database::entities;
|
||||
use crate::infrastructure::events::{Event, EventBus};
|
||||
use crate::library::LibraryManager;
|
||||
use crate::volume::{
|
||||
error::{VolumeError, VolumeResult},
|
||||
os_detection,
|
||||
types::{Volume, VolumeDetectionConfig, VolumeFingerprint, VolumeInfo},
|
||||
types::{TrackedVolume, Volume, VolumeDetectionConfig, VolumeFingerprint, VolumeInfo},
|
||||
};
|
||||
use sea_orm::{ActiveModelTrait, ColumnTrait, EntityTrait, PaginatorTrait, QueryFilter, Set};
|
||||
use std::collections::HashMap;
|
||||
use std::path::{Path, PathBuf};
|
||||
use std::sync::Arc;
|
||||
use std::sync::{Arc, Weak};
|
||||
use std::time::Duration;
|
||||
use tokio::sync::RwLock;
|
||||
use tracing::{debug, error, info, instrument, warn};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Central manager for volume detection, monitoring, and operations
|
||||
pub struct VolumeManager {
|
||||
@@ -29,6 +33,9 @@ pub struct VolumeManager {
|
||||
|
||||
/// Whether the manager is currently running monitoring
|
||||
is_monitoring: Arc<RwLock<bool>>,
|
||||
|
||||
/// Weak reference to library manager for database operations
|
||||
library_manager: RwLock<Option<Weak<LibraryManager>>>,
|
||||
}
|
||||
|
||||
impl VolumeManager {
|
||||
@@ -40,9 +47,15 @@ impl VolumeManager {
|
||||
config,
|
||||
events,
|
||||
is_monitoring: Arc::new(RwLock::new(false)),
|
||||
library_manager: RwLock::new(None),
|
||||
}
|
||||
}
|
||||
|
||||
/// Set the library manager reference
|
||||
pub async fn set_library_manager(&self, library_manager: Weak<LibraryManager>) {
|
||||
*self.library_manager.write().await = Some(library_manager);
|
||||
}
|
||||
|
||||
/// Initialize the volume manager and perform initial detection
|
||||
#[instrument(skip(self))]
|
||||
pub async fn initialize(&self) -> VolumeResult<()> {
|
||||
@@ -360,77 +373,110 @@ impl VolumeManager {
|
||||
/// Track a volume in the database
|
||||
pub async fn track_volume(
|
||||
&self,
|
||||
fingerprint: &VolumeFingerprint,
|
||||
library: &crate::library::Library,
|
||||
fingerprint: &VolumeFingerprint,
|
||||
display_name: Option<String>,
|
||||
) -> VolumeResult<()> {
|
||||
let volumes = self.volumes.read().await;
|
||||
|
||||
if let Some(runtime_volume) = volumes.get(fingerprint) {
|
||||
// Convert runtime volume to domain volume
|
||||
let device_id = crate::shared::types::get_current_device_id();
|
||||
let mut domain_volume =
|
||||
crate::domain::volume::Volume::from_runtime_volume(runtime_volume, device_id);
|
||||
|
||||
// Track the volume for this library
|
||||
domain_volume.track(Some(library.id()));
|
||||
|
||||
// Set custom display name if provided
|
||||
if let Some(name) = display_name {
|
||||
domain_volume.set_display_preferences(Some(name), None, None);
|
||||
}
|
||||
|
||||
// TODO: Save to database via library context
|
||||
// library_ctx.db.volume().create(domain_volume).await?;
|
||||
|
||||
info!(
|
||||
"Tracked volume '{}' for library '{}'",
|
||||
domain_volume.display_name(),
|
||||
library.name().await
|
||||
);
|
||||
|
||||
// Emit tracking event
|
||||
self.events
|
||||
.emit(crate::infrastructure::events::Event::Custom {
|
||||
event_type: "VolumeTracked".to_string(),
|
||||
data: serde_json::json!({
|
||||
"fingerprint": fingerprint.to_string(),
|
||||
"library_id": library.id(),
|
||||
"volume_name": domain_volume.display_name(),
|
||||
}),
|
||||
});
|
||||
|
||||
Ok(())
|
||||
} else {
|
||||
Err(VolumeError::NotFound(fingerprint.to_string()))
|
||||
) -> VolumeResult<entities::volume::Model> {
|
||||
let db = library.db().conn();
|
||||
|
||||
// Check if already tracked
|
||||
if let Some(existing) = entities::volume::Entity::find()
|
||||
.filter(entities::volume::Column::Fingerprint.eq(fingerprint.0.clone()))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?
|
||||
{
|
||||
return Err(VolumeError::AlreadyTracked(fingerprint.to_string()));
|
||||
}
|
||||
|
||||
// Get current volume info
|
||||
let volume = self.get_volume(fingerprint).await
|
||||
.ok_or_else(|| VolumeError::NotFound(fingerprint.to_string()))?;
|
||||
|
||||
// Determine removability and network status
|
||||
let is_removable = matches!(volume.mount_type, crate::volume::types::MountType::External);
|
||||
let is_network_drive = matches!(volume.mount_type, crate::volume::types::MountType::Network);
|
||||
|
||||
// Create tracking record
|
||||
let active_model = entities::volume::ActiveModel {
|
||||
uuid: Set(Uuid::new_v4()),
|
||||
fingerprint: Set(fingerprint.0.clone()),
|
||||
display_name: Set(display_name.clone()),
|
||||
tracked_at: Set(chrono::Utc::now()),
|
||||
last_seen_at: Set(chrono::Utc::now()),
|
||||
is_online: Set(volume.is_mounted),
|
||||
total_capacity: Set(Some(volume.total_bytes_capacity as i64)),
|
||||
available_capacity: Set(Some(volume.total_bytes_available as i64)),
|
||||
read_speed_mbps: Set(volume.read_speed_mbps.map(|s| s as i32)),
|
||||
write_speed_mbps: Set(volume.write_speed_mbps.map(|s| s as i32)),
|
||||
last_speed_test_at: Set(None),
|
||||
file_system: Set(Some(volume.file_system.to_string())),
|
||||
mount_point: Set(Some(volume.mount_point.to_string_lossy().to_string())),
|
||||
is_removable: Set(Some(is_removable)),
|
||||
is_network_drive: Set(Some(is_network_drive)),
|
||||
device_model: Set(volume.hardware_id.clone()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
let model = active_model
|
||||
.insert(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?;
|
||||
|
||||
info!(
|
||||
"Tracked volume '{}' for library '{}'",
|
||||
display_name.as_ref().unwrap_or(&volume.name),
|
||||
library.name().await
|
||||
);
|
||||
|
||||
// Emit tracking event
|
||||
self.events
|
||||
.emit(Event::Custom {
|
||||
event_type: "VolumeTracked".to_string(),
|
||||
data: serde_json::json!({
|
||||
"library_id": library.id(),
|
||||
"volume_fingerprint": fingerprint.to_string(),
|
||||
"display_name": display_name,
|
||||
}),
|
||||
});
|
||||
|
||||
Ok(model)
|
||||
}
|
||||
|
||||
/// Untrack a volume from the database
|
||||
pub async fn untrack_volume(
|
||||
&self,
|
||||
fingerprint: &VolumeFingerprint,
|
||||
library: &crate::library::Library,
|
||||
fingerprint: &VolumeFingerprint,
|
||||
) -> VolumeResult<()> {
|
||||
// TODO: Update database to mark as untracked
|
||||
// library_ctx.db.volume().untrack(fingerprint).await?;
|
||||
|
||||
let db = library.db().conn();
|
||||
|
||||
let result = entities::volume::Entity::delete_many()
|
||||
.filter(entities::volume::Column::Fingerprint.eq(fingerprint.0.clone()))
|
||||
.exec(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?;
|
||||
|
||||
if result.rows_affected == 0 {
|
||||
return Err(VolumeError::NotTracked(fingerprint.to_string()));
|
||||
}
|
||||
|
||||
info!(
|
||||
"Untracked volume '{}' from library '{}'",
|
||||
fingerprint.to_string(),
|
||||
library.name().await
|
||||
);
|
||||
|
||||
|
||||
// Emit untracking event
|
||||
self.events
|
||||
.emit(crate::infrastructure::events::Event::Custom {
|
||||
.emit(Event::Custom {
|
||||
event_type: "VolumeUntracked".to_string(),
|
||||
data: serde_json::json!({
|
||||
"fingerprint": fingerprint.to_string(),
|
||||
"library_id": library.id(),
|
||||
"volume_fingerprint": fingerprint.to_string(),
|
||||
}),
|
||||
});
|
||||
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
@@ -438,26 +484,130 @@ impl VolumeManager {
|
||||
pub async fn get_tracked_volumes(
|
||||
&self,
|
||||
library: &crate::library::Library,
|
||||
) -> VolumeResult<Vec<crate::domain::volume::Volume>> {
|
||||
// TODO: Query database for tracked volumes
|
||||
// library_ctx.db.volume().find_by_library(library.id()).await
|
||||
|
||||
) -> VolumeResult<Vec<TrackedVolume>> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let volumes = entities::volume::Entity::find()
|
||||
.all(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?;
|
||||
|
||||
let tracked_volumes: Vec<TrackedVolume> = volumes
|
||||
.into_iter()
|
||||
.map(|model| model.to_tracked_volume())
|
||||
.collect();
|
||||
|
||||
debug!(
|
||||
"Getting tracked volumes for library '{}'",
|
||||
"Found {} tracked volumes for library '{}'",
|
||||
tracked_volumes.len(),
|
||||
library.name().await
|
||||
);
|
||||
Ok(Vec::new())
|
||||
|
||||
Ok(tracked_volumes)
|
||||
}
|
||||
|
||||
/// Check if a volume is tracked in any library
|
||||
pub async fn is_volume_tracked(&self, fingerprint: &VolumeFingerprint) -> VolumeResult<bool> {
|
||||
// TODO: Query database to check if volume is tracked
|
||||
// This would check across all libraries on this device
|
||||
debug!(
|
||||
"Checking if volume '{}' is tracked",
|
||||
fingerprint.to_string()
|
||||
/// Check if a volume is tracked in a specific library
|
||||
pub async fn is_volume_tracked(
|
||||
&self,
|
||||
library: &crate::library::Library,
|
||||
fingerprint: &VolumeFingerprint,
|
||||
) -> VolumeResult<bool> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let count = entities::volume::Entity::find()
|
||||
.filter(entities::volume::Column::Fingerprint.eq(fingerprint.0.clone()))
|
||||
.count(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?;
|
||||
|
||||
Ok(count > 0)
|
||||
}
|
||||
|
||||
/// Update tracked volume state during refresh
|
||||
pub async fn update_tracked_volume_state(
|
||||
&self,
|
||||
library: &crate::library::Library,
|
||||
fingerprint: &VolumeFingerprint,
|
||||
volume: &Volume,
|
||||
) -> VolumeResult<()> {
|
||||
let db = library.db().conn();
|
||||
|
||||
let mut active_model: entities::volume::ActiveModel = entities::volume::Entity::find()
|
||||
.filter(entities::volume::Column::Fingerprint.eq(fingerprint.0.clone()))
|
||||
.one(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?
|
||||
.ok_or_else(|| VolumeError::NotTracked(fingerprint.to_string()))?
|
||||
.into();
|
||||
|
||||
active_model.last_seen_at = Set(chrono::Utc::now());
|
||||
active_model.is_online = Set(volume.is_mounted);
|
||||
active_model.total_capacity = Set(Some(volume.total_bytes_capacity as i64));
|
||||
active_model.available_capacity = Set(Some(volume.total_bytes_available as i64));
|
||||
|
||||
active_model
|
||||
.update(db)
|
||||
.await
|
||||
.map_err(|e| VolumeError::Database(e.to_string()))?;
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
/// Get all system volumes (boot/OS volumes)
|
||||
pub async fn get_system_volumes(&self) -> Vec<Volume> {
|
||||
self.volumes
|
||||
.read()
|
||||
.await
|
||||
.values()
|
||||
.filter(|v| matches!(v.mount_type, crate::volume::types::MountType::System))
|
||||
.cloned()
|
||||
.collect()
|
||||
}
|
||||
|
||||
/// Automatically track system volumes for a library
|
||||
pub async fn auto_track_system_volumes(
|
||||
&self,
|
||||
library: &crate::library::Library,
|
||||
) -> VolumeResult<Vec<entities::volume::Model>> {
|
||||
let system_volumes = self.get_system_volumes().await;
|
||||
let mut tracked_volumes = Vec::new();
|
||||
|
||||
info!(
|
||||
"Auto-tracking {} system volumes for library '{}'",
|
||||
system_volumes.len(),
|
||||
library.name().await
|
||||
);
|
||||
Ok(false)
|
||||
|
||||
for volume in system_volumes {
|
||||
// Skip if already tracked
|
||||
if self.is_volume_tracked(library, &volume.fingerprint).await? {
|
||||
debug!(
|
||||
"System volume '{}' already tracked in library",
|
||||
volume.name
|
||||
);
|
||||
continue;
|
||||
}
|
||||
|
||||
// Track the system volume
|
||||
match self.track_volume(library, &volume.fingerprint, None).await {
|
||||
Ok(tracked) => {
|
||||
info!(
|
||||
"Auto-tracked system volume '{}' in library '{}'",
|
||||
volume.name,
|
||||
library.name().await
|
||||
);
|
||||
tracked_volumes.push(tracked);
|
||||
}
|
||||
Err(e) => {
|
||||
warn!(
|
||||
"Failed to auto-track system volume '{}': {}",
|
||||
volume.name, e
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
Ok(tracked_volumes)
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -128,6 +128,28 @@ pub struct VolumeInfo {
|
||||
pub error_status: Option<String>,
|
||||
}
|
||||
|
||||
/// Information about a tracked volume in the database
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct TrackedVolume {
|
||||
pub id: i32,
|
||||
pub uuid: uuid::Uuid,
|
||||
pub fingerprint: VolumeFingerprint,
|
||||
pub display_name: Option<String>,
|
||||
pub tracked_at: chrono::DateTime<chrono::Utc>,
|
||||
pub last_seen_at: chrono::DateTime<chrono::Utc>,
|
||||
pub is_online: bool,
|
||||
pub total_capacity: Option<u64>,
|
||||
pub available_capacity: Option<u64>,
|
||||
pub read_speed_mbps: Option<u32>,
|
||||
pub write_speed_mbps: Option<u32>,
|
||||
pub last_speed_test_at: Option<chrono::DateTime<chrono::Utc>>,
|
||||
pub file_system: Option<String>,
|
||||
pub mount_point: Option<String>,
|
||||
pub is_removable: Option<bool>,
|
||||
pub is_network_drive: Option<bool>,
|
||||
pub device_model: Option<String>,
|
||||
}
|
||||
|
||||
impl From<&Volume> for VolumeInfo {
|
||||
fn from(volume: &Volume) -> Self {
|
||||
Self {
|
||||
|
||||
197
core-new/tests/volume_auto_track_test.rs
Normal file
197
core-new/tests/volume_auto_track_test.rs
Normal file
@@ -0,0 +1,197 @@
|
||||
//! Test automatic volume tracking functionality
|
||||
|
||||
use sd_core_new::{
|
||||
context::CoreContext,
|
||||
infrastructure::events::EventBus,
|
||||
library::{LibraryConfig, LibraryManager, LibrarySettings},
|
||||
volume::{VolumeDetectionConfig, VolumeManager},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_auto_track_system_volumes_on_library_open() {
|
||||
// Setup test environment
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path().join("data");
|
||||
std::fs::create_dir_all(&data_dir).expect("Failed to create data dir");
|
||||
|
||||
// Initialize volume manager
|
||||
let events = Arc::new(EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize volume manager to detect volumes
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Get system volumes before library creation
|
||||
let system_volumes = volume_manager.get_system_volumes().await;
|
||||
println!("Found {} system volumes", system_volumes.len());
|
||||
|
||||
// Create library manager
|
||||
let library_manager = Arc::new(LibraryManager::new_with_dir(
|
||||
temp_dir.path().join("libraries"),
|
||||
events.clone(),
|
||||
));
|
||||
|
||||
// Create core context
|
||||
let context = CoreContext::test_with_volume_manager(
|
||||
data_dir,
|
||||
volume_manager.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create test context");
|
||||
let context = Arc::new(context);
|
||||
|
||||
// Create a library with auto-tracking enabled (default)
|
||||
let library = library_manager
|
||||
.create_library("Test Library", None, context.clone())
|
||||
.await
|
||||
.expect("Failed to create library");
|
||||
|
||||
// Verify system volumes were auto-tracked
|
||||
let tracked_volumes = volume_manager
|
||||
.get_tracked_volumes(&library)
|
||||
.await
|
||||
.expect("Failed to get tracked volumes");
|
||||
|
||||
// Should have tracked all system volumes
|
||||
assert_eq!(
|
||||
tracked_volumes.len(),
|
||||
system_volumes.len(),
|
||||
"Should have auto-tracked all system volumes"
|
||||
);
|
||||
|
||||
// Verify each system volume is tracked
|
||||
for sys_vol in &system_volumes {
|
||||
let is_tracked = tracked_volumes
|
||||
.iter()
|
||||
.any(|tv| tv.fingerprint.0 == sys_vol.fingerprint.0);
|
||||
assert!(
|
||||
is_tracked,
|
||||
"System volume '{}' should be tracked",
|
||||
sys_vol.name
|
||||
);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_auto_track_disabled() {
|
||||
// Setup test environment
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path().join("data");
|
||||
std::fs::create_dir_all(&data_dir).expect("Failed to create data dir");
|
||||
|
||||
// Initialize volume manager
|
||||
let events = Arc::new(EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize volume manager
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Create library manager
|
||||
let library_manager = Arc::new(LibraryManager::new_with_dir(
|
||||
temp_dir.path().join("libraries"),
|
||||
events.clone(),
|
||||
));
|
||||
|
||||
// Create library path manually
|
||||
let library_path = temp_dir.path().join("libraries").join("test.sdlibrary");
|
||||
std::fs::create_dir_all(&library_path).expect("Failed to create library dir");
|
||||
|
||||
// Create config with auto-tracking disabled
|
||||
let mut settings = LibrarySettings::default();
|
||||
settings.auto_track_system_volumes = false;
|
||||
|
||||
let config = LibraryConfig {
|
||||
version: 1,
|
||||
id: Uuid::new_v4(),
|
||||
name: "Test Library".to_string(),
|
||||
description: None,
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
settings,
|
||||
statistics: sd_core_new::library::LibraryStatistics::default(),
|
||||
};
|
||||
|
||||
// Save config
|
||||
let config_json = serde_json::to_string_pretty(&config).expect("Failed to serialize config");
|
||||
std::fs::write(library_path.join("library.json"), config_json).expect("Failed to write config");
|
||||
|
||||
// Create database
|
||||
let db_path = library_path.join("database.db");
|
||||
let db = sd_core_new::infrastructure::database::Database::create(&db_path)
|
||||
.await
|
||||
.expect("Failed to create database");
|
||||
db.migrate().await.expect("Failed to run migrations");
|
||||
|
||||
// Create context
|
||||
let context = CoreContext::test_with_volume_manager(
|
||||
data_dir,
|
||||
volume_manager.clone(),
|
||||
)
|
||||
.await
|
||||
.expect("Failed to create test context");
|
||||
let context = Arc::new(context);
|
||||
|
||||
// Open library with auto-tracking disabled
|
||||
let library = library_manager
|
||||
.open_library_with_context(&library_path, context.clone())
|
||||
.await
|
||||
.expect("Failed to open library");
|
||||
|
||||
// Verify no volumes were auto-tracked
|
||||
let tracked_volumes = volume_manager
|
||||
.get_tracked_volumes(&library)
|
||||
.await
|
||||
.expect("Failed to get tracked volumes");
|
||||
|
||||
assert_eq!(
|
||||
tracked_volumes.len(),
|
||||
0,
|
||||
"Should not have auto-tracked any volumes"
|
||||
);
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_system_volume_properties() {
|
||||
// Initialize volume manager
|
||||
let events = Arc::new(EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize to detect volumes
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Get system volumes
|
||||
let system_volumes = volume_manager.get_system_volumes().await;
|
||||
|
||||
// Verify system volume properties
|
||||
for volume in system_volumes {
|
||||
// System volumes should be mounted
|
||||
assert!(volume.is_mounted, "System volume should be mounted");
|
||||
|
||||
// System volumes should have capacity info
|
||||
assert!(volume.total_bytes_capacity > 0, "System volume should have capacity");
|
||||
|
||||
// System volumes typically have specific names
|
||||
println!(
|
||||
"System volume: {} ({}), FS: {}, Capacity: {} GB",
|
||||
volume.name,
|
||||
volume.mount_point.display(),
|
||||
volume.file_system,
|
||||
volume.total_bytes_capacity / (1024 * 1024 * 1024)
|
||||
);
|
||||
}
|
||||
}
|
||||
@@ -1,185 +1,151 @@
|
||||
//! Simple volume test that works with the existing system
|
||||
//!
|
||||
//! This test verifies basic volume functionality without requiring
|
||||
//! the full action system integration.
|
||||
//! Simple integration test for volume tracking
|
||||
|
||||
use sd_core_new::{
|
||||
Core,
|
||||
volume::VolumeExt,
|
||||
infrastructure::database::entities,
|
||||
library::LibraryConfig,
|
||||
volume::{VolumeDetectionConfig, VolumeFingerprint, VolumeManager},
|
||||
};
|
||||
use std::sync::Arc;
|
||||
use tempfile::tempdir;
|
||||
use tracing::{info, warn};
|
||||
|
||||
const TEST_VOLUME_NAME: &str = "TestVolume";
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_volume_detection_and_tracking() {
|
||||
// Initialize logging
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
// Create test data directory
|
||||
let data_dir = tempdir().unwrap();
|
||||
let data_path = data_dir.path().to_path_buf();
|
||||
|
||||
// Initialize core
|
||||
let core = Arc::new(
|
||||
Core::new_with_config(data_path.clone())
|
||||
.await
|
||||
.expect("Failed to create core"),
|
||||
);
|
||||
|
||||
// Get volume manager
|
||||
let volume_manager = core.volumes.clone();
|
||||
|
||||
// Refresh volumes to ensure we have the latest
|
||||
volume_manager
|
||||
.refresh_volumes()
|
||||
.await
|
||||
.expect("Failed to refresh volumes");
|
||||
|
||||
// Get all volumes
|
||||
let all_volumes = volume_manager.get_all_volumes().await;
|
||||
async fn test_volume_tracking_basic() {
|
||||
// Create temp directory for library
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let library_path = temp_dir.path().join("test.sdlibrary");
|
||||
|
||||
info!("Detected {} volumes:", all_volumes.len());
|
||||
for volume in &all_volumes {
|
||||
info!(
|
||||
" - {} ({}) at {} - {} {} [{}]",
|
||||
// Create library config
|
||||
let config = LibraryConfig {
|
||||
id: Uuid::new_v4(),
|
||||
name: "Test Library".to_string(),
|
||||
description: Some("Test library for volume tracking".to_string()),
|
||||
created_at: chrono::Utc::now(),
|
||||
updated_at: chrono::Utc::now(),
|
||||
settings: sd_core_new::library::LibrarySettings::default(),
|
||||
statistics: sd_core_new::library::LibraryStatistics::default(),
|
||||
};
|
||||
|
||||
// Save config
|
||||
std::fs::create_dir_all(&library_path).expect("Failed to create library dir");
|
||||
let config_path = library_path.join("library.json");
|
||||
let config_json = serde_json::to_string_pretty(&config).expect("Failed to serialize config");
|
||||
std::fs::write(config_path, config_json).expect("Failed to write config");
|
||||
|
||||
// Create events and volume manager
|
||||
let events = Arc::new(sd_core_new::infrastructure::events::EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize volume manager to detect volumes
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Stop monitoring to avoid background tasks
|
||||
volume_manager.stop_monitoring().await;
|
||||
|
||||
// Get all volumes
|
||||
let volumes = volume_manager.get_all_volumes().await;
|
||||
println!("Detected {} volumes", volumes.len());
|
||||
|
||||
// Print volume information
|
||||
for volume in &volumes {
|
||||
println!(
|
||||
"Volume: {} ({}), Mounted: {}, Capacity: {} GB",
|
||||
volume.name,
|
||||
volume.fingerprint,
|
||||
volume.mount_point.display(),
|
||||
volume.file_system,
|
||||
volume.disk_type,
|
||||
if volume.is_mounted { "mounted" } else { "unmounted" }
|
||||
volume.is_mounted,
|
||||
volume.total_bytes_capacity / (1024 * 1024 * 1024)
|
||||
);
|
||||
}
|
||||
|
||||
// Find TestVolume if it exists
|
||||
let test_volume = all_volumes
|
||||
.iter()
|
||||
.find(|v| v.name == TEST_VOLUME_NAME)
|
||||
.cloned();
|
||||
|
||||
if let Some(test_volume) = test_volume {
|
||||
info!("Found TestVolume!");
|
||||
|
||||
// Test volume properties
|
||||
assert!(test_volume.is_mounted, "TestVolume should be mounted");
|
||||
assert!(
|
||||
test_volume.is_available().await,
|
||||
"TestVolume should be accessible"
|
||||
);
|
||||
|
||||
// Test volume fingerprint
|
||||
let fingerprint = &test_volume.fingerprint;
|
||||
info!("TestVolume fingerprint: {}", fingerprint);
|
||||
|
||||
// Test volume retrieval by fingerprint
|
||||
let retrieved_volume = volume_manager
|
||||
.get_volume(&fingerprint)
|
||||
.await
|
||||
.expect("Should be able to retrieve volume by fingerprint");
|
||||
|
||||
assert_eq!(retrieved_volume.name, TEST_VOLUME_NAME);
|
||||
assert_eq!(retrieved_volume.fingerprint, test_volume.fingerprint);
|
||||
|
||||
// Test path containment
|
||||
let test_path = test_volume.mount_point.join("test_file.txt");
|
||||
assert!(
|
||||
test_volume.contains_path(&test_path),
|
||||
"Volume should contain paths under its mount point"
|
||||
);
|
||||
|
||||
// Test volume for path
|
||||
let volume_for_path = volume_manager
|
||||
.volume_for_path(&test_volume.mount_point)
|
||||
.await
|
||||
.expect("Should find volume for its mount point");
|
||||
|
||||
assert_eq!(volume_for_path.fingerprint, test_volume.fingerprint);
|
||||
|
||||
info!("All TestVolume tests passed!");
|
||||
} else {
|
||||
warn!(
|
||||
"TestVolume not found. Available volumes: {:?}",
|
||||
all_volumes.iter().map(|v| &v.name).collect::<Vec<_>>()
|
||||
);
|
||||
println!("SKIPPING TEST: TestVolume not mounted on system");
|
||||
|
||||
// If we have at least one volume, test fingerprint parsing
|
||||
if let Some(first_volume) = volumes.first() {
|
||||
let fingerprint_str = first_volume.fingerprint.to_string();
|
||||
let parsed = VolumeFingerprint::from_string(&fingerprint_str)
|
||||
.expect("Failed to parse fingerprint");
|
||||
assert_eq!(parsed.0, fingerprint_str);
|
||||
}
|
||||
|
||||
// Test volume statistics
|
||||
let stats = volume_manager.get_statistics().await;
|
||||
|
||||
info!("Volume statistics:");
|
||||
info!(" Total volumes: {}", stats.total_volumes);
|
||||
info!(" Mounted volumes: {}", stats.mounted_volumes);
|
||||
info!(" Total capacity: {} TB", stats.total_capacity / (1024 * 1024 * 1024 * 1024));
|
||||
info!(" Total available: {} TB", stats.total_available / (1024 * 1024 * 1024 * 1024));
|
||||
|
||||
assert_eq!(stats.total_volumes, all_volumes.len());
|
||||
assert!(stats.mounted_volumes <= stats.total_volumes);
|
||||
assert!(stats.total_available <= stats.total_capacity);
|
||||
|
||||
// Clean up
|
||||
let _ = core.services.stop_all().await;
|
||||
|
||||
info!("Volume test completed successfully");
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_volume_speed_test() {
|
||||
// Initialize logging
|
||||
let _ = tracing_subscriber::fmt::try_init();
|
||||
|
||||
// Create test data directory
|
||||
let data_dir = tempdir().unwrap();
|
||||
let data_path = data_dir.path().to_path_buf();
|
||||
|
||||
// Initialize core
|
||||
let core = Arc::new(
|
||||
Core::new_with_config(data_path)
|
||||
.await
|
||||
.expect("Failed to create core"),
|
||||
);
|
||||
|
||||
let volume_manager = core.volumes.clone();
|
||||
async fn test_volume_fingerprint_operations() {
|
||||
// Test fingerprint creation from hex
|
||||
let hex_string = "abcdef1234567890";
|
||||
let fingerprint = VolumeFingerprint::from_hex(hex_string);
|
||||
assert_eq!(fingerprint.0, hex_string);
|
||||
assert_eq!(fingerprint.to_string(), hex_string);
|
||||
|
||||
// Find a writable volume
|
||||
let all_volumes = volume_manager.get_all_volumes().await;
|
||||
let writable_volume = all_volumes
|
||||
.into_iter()
|
||||
.find(|v| v.is_mounted && !v.read_only);
|
||||
// Test fingerprint equality
|
||||
let fp1 = VolumeFingerprint::from_hex("test123");
|
||||
let fp2 = VolumeFingerprint::from_hex("test123");
|
||||
let fp3 = VolumeFingerprint::from_hex("test456");
|
||||
|
||||
if let Some(volume) = writable_volume {
|
||||
info!("Testing speed on volume: {}", volume.name);
|
||||
|
||||
let fingerprint = volume.fingerprint.clone();
|
||||
|
||||
// Run speed test
|
||||
match volume_manager.run_speed_test(&fingerprint).await {
|
||||
Ok(()) => {
|
||||
// Get updated volume to check results
|
||||
let updated_volume = volume_manager
|
||||
.get_volume(&fingerprint)
|
||||
.await
|
||||
.expect("Volume should still exist");
|
||||
|
||||
if let (Some(read_speed), Some(write_speed)) =
|
||||
(updated_volume.read_speed_mbps, updated_volume.write_speed_mbps) {
|
||||
info!("Speed test results: {} MB/s read, {} MB/s write", read_speed, write_speed);
|
||||
assert!(read_speed > 0, "Read speed should be positive");
|
||||
assert!(write_speed > 0, "Write speed should be positive");
|
||||
} else {
|
||||
warn!("Speed test completed but no results stored");
|
||||
}
|
||||
}
|
||||
Err(e) => {
|
||||
warn!("Speed test failed (this is okay in CI): {}", e);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
warn!("No writable volume found for speed test");
|
||||
}
|
||||
assert_eq!(fp1, fp2);
|
||||
assert_ne!(fp1, fp3);
|
||||
}
|
||||
|
||||
let _ = core.services.stop_all().await;
|
||||
#[tokio::test]
|
||||
async fn test_volume_entity_conversion() {
|
||||
use chrono::Utc;
|
||||
|
||||
// Create a volume entity model
|
||||
let model = entities::volume::Model {
|
||||
id: 1,
|
||||
uuid: Uuid::new_v4(),
|
||||
fingerprint: "test_fingerprint_123".to_string(),
|
||||
display_name: Some("Test Volume".to_string()),
|
||||
tracked_at: Utc::now(),
|
||||
last_seen_at: Utc::now(),
|
||||
is_online: true,
|
||||
total_capacity: Some(1000000000),
|
||||
available_capacity: Some(500000000),
|
||||
read_speed_mbps: Some(100),
|
||||
write_speed_mbps: Some(80),
|
||||
last_speed_test_at: None,
|
||||
file_system: Some("APFS".to_string()),
|
||||
mount_point: Some("/Volumes/Test".to_string()),
|
||||
is_removable: Some(false),
|
||||
is_network_drive: Some(false),
|
||||
device_model: Some("Samsung SSD".to_string()),
|
||||
};
|
||||
|
||||
// Convert to tracked volume
|
||||
let tracked = model.to_tracked_volume();
|
||||
|
||||
// Verify conversion
|
||||
assert_eq!(tracked.id, model.id);
|
||||
assert_eq!(tracked.uuid, model.uuid);
|
||||
assert_eq!(tracked.fingerprint.0, model.fingerprint);
|
||||
assert_eq!(tracked.display_name, model.display_name);
|
||||
assert_eq!(tracked.is_online, model.is_online);
|
||||
assert_eq!(tracked.total_capacity, Some(1000000000));
|
||||
assert_eq!(tracked.available_capacity, Some(500000000));
|
||||
assert_eq!(tracked.read_speed_mbps, Some(100));
|
||||
assert_eq!(tracked.write_speed_mbps, Some(80));
|
||||
assert_eq!(tracked.file_system, model.file_system);
|
||||
assert_eq!(tracked.mount_point, model.mount_point);
|
||||
assert_eq!(tracked.is_removable, model.is_removable);
|
||||
assert_eq!(tracked.is_network_drive, model.is_network_drive);
|
||||
assert_eq!(tracked.device_model, model.device_model);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_volume_error_types() {
|
||||
use sd_core_new::volume::VolumeError;
|
||||
|
||||
// Test error creation and display
|
||||
let db_error = VolumeError::Database("Connection failed".to_string());
|
||||
assert_eq!(db_error.to_string(), "Database error: Connection failed");
|
||||
|
||||
let already_tracked = VolumeError::AlreadyTracked("vol123".to_string());
|
||||
assert_eq!(already_tracked.to_string(), "Volume is already tracked: vol123");
|
||||
|
||||
let not_tracked = VolumeError::NotTracked("vol456".to_string());
|
||||
assert_eq!(not_tracked.to_string(), "Volume is not tracked: vol456");
|
||||
|
||||
let not_found = VolumeError::NotFound("vol789".to_string());
|
||||
assert_eq!(not_found.to_string(), "Volume not found: vol789");
|
||||
}
|
||||
335
core-new/tests/volume_tracking_test.rs
Normal file
335
core-new/tests/volume_tracking_test.rs
Normal file
@@ -0,0 +1,335 @@
|
||||
//! Integration tests for volume tracking functionality
|
||||
|
||||
use core_new::{
|
||||
context::CoreContext,
|
||||
infrastructure::{
|
||||
actions::{Action, manager::ActionManager},
|
||||
database::Database,
|
||||
events::EventBus,
|
||||
},
|
||||
library::{Library, LibraryConfig, LibraryManager},
|
||||
operations::volumes::{
|
||||
track::action::VolumeTrackAction,
|
||||
untrack::action::VolumeUntrackAction,
|
||||
},
|
||||
volume::{VolumeDetectionConfig, VolumeFingerprint, VolumeManager},
|
||||
};
|
||||
use std::path::PathBuf;
|
||||
use std::sync::Arc;
|
||||
use tempfile::TempDir;
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Helper to create a test library
|
||||
async fn create_test_library(
|
||||
context: Arc<CoreContext>,
|
||||
temp_dir: &TempDir,
|
||||
) -> Arc<Library> {
|
||||
let library_path = temp_dir.path().join("test_library.sdlibrary");
|
||||
let config = LibraryConfig {
|
||||
id: Uuid::new_v4(),
|
||||
name: "Test Library".to_string(),
|
||||
description: Some("Test library for volume tracking".to_string()),
|
||||
..Default::default()
|
||||
};
|
||||
|
||||
context
|
||||
.library_manager
|
||||
.create_library(library_path, config)
|
||||
.await
|
||||
.expect("Failed to create test library")
|
||||
}
|
||||
|
||||
/// Helper to get first available volume for testing
|
||||
async fn get_test_volume(volume_manager: &Arc<VolumeManager>) -> Option<(VolumeFingerprint, String)> {
|
||||
let volumes = volume_manager.get_all_volumes().await;
|
||||
volumes.first().map(|v| (v.fingerprint.clone(), v.name.clone()))
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_volume_tracking_lifecycle() {
|
||||
// Setup test environment
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path().join("data");
|
||||
std::fs::create_dir_all(&data_dir).expect("Failed to create data dir");
|
||||
|
||||
// Initialize core context
|
||||
let events = Arc::new(EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize volume manager
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Create test context with volume manager
|
||||
let context = CoreContext::test_with_volume_manager(data_dir, volume_manager.clone())
|
||||
.await
|
||||
.expect("Failed to create test context");
|
||||
let context = Arc::new(context);
|
||||
|
||||
// Create test library
|
||||
let library = create_test_library(context.clone(), &temp_dir).await;
|
||||
|
||||
// Get a test volume
|
||||
let (fingerprint, volume_name) = get_test_volume(&volume_manager)
|
||||
.await
|
||||
.expect("No volumes available for testing");
|
||||
|
||||
// Test 1: Track volume
|
||||
{
|
||||
let track_action = VolumeTrackAction {
|
||||
library_id: library.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
name: Some("My Test Volume".to_string()),
|
||||
};
|
||||
|
||||
let action = Action::VolumeTrack { action: track_action };
|
||||
let result = context.action_manager.execute(action).await;
|
||||
|
||||
assert!(result.is_ok(), "Failed to track volume: {:?}", result);
|
||||
|
||||
// Verify volume is tracked
|
||||
let is_tracked = volume_manager
|
||||
.is_volume_tracked(&library, &fingerprint)
|
||||
.await
|
||||
.expect("Failed to check tracking status");
|
||||
assert!(is_tracked, "Volume should be tracked");
|
||||
|
||||
// Get tracked volumes
|
||||
let tracked_volumes = volume_manager
|
||||
.get_tracked_volumes(&library)
|
||||
.await
|
||||
.expect("Failed to get tracked volumes");
|
||||
assert_eq!(tracked_volumes.len(), 1, "Should have one tracked volume");
|
||||
assert_eq!(tracked_volumes[0].fingerprint, fingerprint);
|
||||
assert_eq!(
|
||||
tracked_volumes[0].display_name,
|
||||
Some("My Test Volume".to_string())
|
||||
);
|
||||
}
|
||||
|
||||
// Test 2: Try to track same volume again (should fail)
|
||||
{
|
||||
let track_action = VolumeTrackAction {
|
||||
library_id: library.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
name: Some("Another Name".to_string()),
|
||||
};
|
||||
|
||||
let action = Action::VolumeTrack { action: track_action };
|
||||
let result = context.action_manager.execute(action).await;
|
||||
|
||||
assert!(result.is_err(), "Should not be able to track volume twice");
|
||||
}
|
||||
|
||||
// Test 3: Untrack volume
|
||||
{
|
||||
let untrack_action = VolumeUntrackAction {
|
||||
library_id: library.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
};
|
||||
|
||||
let action = Action::VolumeUntrack { action: untrack_action };
|
||||
let result = context.action_manager.execute(action).await;
|
||||
|
||||
assert!(result.is_ok(), "Failed to untrack volume: {:?}", result);
|
||||
|
||||
// Verify volume is no longer tracked
|
||||
let is_tracked = volume_manager
|
||||
.is_volume_tracked(&library, &fingerprint)
|
||||
.await
|
||||
.expect("Failed to check tracking status");
|
||||
assert!(!is_tracked, "Volume should not be tracked");
|
||||
|
||||
// Get tracked volumes (should be empty)
|
||||
let tracked_volumes = volume_manager
|
||||
.get_tracked_volumes(&library)
|
||||
.await
|
||||
.expect("Failed to get tracked volumes");
|
||||
assert_eq!(tracked_volumes.len(), 0, "Should have no tracked volumes");
|
||||
}
|
||||
|
||||
// Test 4: Try to untrack non-tracked volume (should fail)
|
||||
{
|
||||
let untrack_action = VolumeUntrackAction {
|
||||
library_id: library.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
};
|
||||
|
||||
let action = Action::VolumeUntrack { action: untrack_action };
|
||||
let result = context.action_manager.execute(action).await;
|
||||
|
||||
assert!(result.is_err(), "Should not be able to untrack non-tracked volume");
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_volume_state_updates() {
|
||||
// Setup test environment
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path().join("data");
|
||||
std::fs::create_dir_all(&data_dir).expect("Failed to create data dir");
|
||||
|
||||
// Initialize core context
|
||||
let events = Arc::new(EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize volume manager
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Create test context
|
||||
let context = CoreContext::test_with_volume_manager(data_dir, volume_manager.clone())
|
||||
.await
|
||||
.expect("Failed to create test context");
|
||||
let context = Arc::new(context);
|
||||
|
||||
// Create test library
|
||||
let library = create_test_library(context.clone(), &temp_dir).await;
|
||||
|
||||
// Get a test volume
|
||||
let (fingerprint, _) = get_test_volume(&volume_manager)
|
||||
.await
|
||||
.expect("No volumes available for testing");
|
||||
|
||||
// Track volume
|
||||
let track_action = VolumeTrackAction {
|
||||
library_id: library.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
name: None,
|
||||
};
|
||||
|
||||
let action = Action::VolumeTrack { action: track_action };
|
||||
context
|
||||
.action_manager
|
||||
.execute(action)
|
||||
.await
|
||||
.expect("Failed to track volume");
|
||||
|
||||
// Get initial state
|
||||
let tracked_volumes = volume_manager
|
||||
.get_tracked_volumes(&library)
|
||||
.await
|
||||
.expect("Failed to get tracked volumes");
|
||||
let initial_state = tracked_volumes[0].clone();
|
||||
|
||||
// Update volume state (simulate volume change)
|
||||
if let Some(current_volume) = volume_manager.get_volume(&fingerprint).await {
|
||||
volume_manager
|
||||
.update_tracked_volume_state(&library, &fingerprint, ¤t_volume)
|
||||
.await
|
||||
.expect("Failed to update volume state");
|
||||
|
||||
// Get updated state
|
||||
let tracked_volumes = volume_manager
|
||||
.get_tracked_volumes(&library)
|
||||
.await
|
||||
.expect("Failed to get tracked volumes");
|
||||
let updated_state = tracked_volumes[0].clone();
|
||||
|
||||
// Verify state was updated
|
||||
assert!(updated_state.last_seen_at > initial_state.last_seen_at);
|
||||
}
|
||||
}
|
||||
|
||||
#[tokio::test]
|
||||
async fn test_multiple_libraries_tracking_same_volume() {
|
||||
// Setup test environment
|
||||
let temp_dir = TempDir::new().expect("Failed to create temp dir");
|
||||
let data_dir = temp_dir.path().join("data");
|
||||
std::fs::create_dir_all(&data_dir).expect("Failed to create data dir");
|
||||
|
||||
// Initialize core context
|
||||
let events = Arc::new(EventBus::default());
|
||||
let volume_config = VolumeDetectionConfig::default();
|
||||
let volume_manager = Arc::new(VolumeManager::new(volume_config, events.clone()));
|
||||
|
||||
// Initialize volume manager
|
||||
volume_manager
|
||||
.initialize()
|
||||
.await
|
||||
.expect("Failed to initialize volume manager");
|
||||
|
||||
// Create test context
|
||||
let context = CoreContext::test_with_volume_manager(data_dir, volume_manager.clone())
|
||||
.await
|
||||
.expect("Failed to create test context");
|
||||
let context = Arc::new(context);
|
||||
|
||||
// Create two test libraries
|
||||
let library1 = create_test_library(context.clone(), &temp_dir).await;
|
||||
let library2 = create_test_library(context.clone(), &temp_dir).await;
|
||||
|
||||
// Get a test volume
|
||||
let (fingerprint, _) = get_test_volume(&volume_manager)
|
||||
.await
|
||||
.expect("No volumes available for testing");
|
||||
|
||||
// Track volume in library 1
|
||||
let track_action1 = VolumeTrackAction {
|
||||
library_id: library1.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
name: Some("Library 1 Volume".to_string()),
|
||||
};
|
||||
|
||||
let action1 = Action::VolumeTrack { action: track_action1 };
|
||||
context
|
||||
.action_manager
|
||||
.execute(action1)
|
||||
.await
|
||||
.expect("Failed to track volume in library 1");
|
||||
|
||||
// Track same volume in library 2 (should work)
|
||||
let track_action2 = VolumeTrackAction {
|
||||
library_id: library2.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
name: Some("Library 2 Volume".to_string()),
|
||||
};
|
||||
|
||||
let action2 = Action::VolumeTrack { action: track_action2 };
|
||||
context
|
||||
.action_manager
|
||||
.execute(action2)
|
||||
.await
|
||||
.expect("Failed to track volume in library 2");
|
||||
|
||||
// Verify both libraries have the volume tracked
|
||||
let lib1_volumes = volume_manager
|
||||
.get_tracked_volumes(&library1)
|
||||
.await
|
||||
.expect("Failed to get library 1 volumes");
|
||||
assert_eq!(lib1_volumes.len(), 1);
|
||||
assert_eq!(lib1_volumes[0].display_name, Some("Library 1 Volume".to_string()));
|
||||
|
||||
let lib2_volumes = volume_manager
|
||||
.get_tracked_volumes(&library2)
|
||||
.await
|
||||
.expect("Failed to get library 2 volumes");
|
||||
assert_eq!(lib2_volumes.len(), 1);
|
||||
assert_eq!(lib2_volumes[0].display_name, Some("Library 2 Volume".to_string()));
|
||||
|
||||
// Untrack from library 1
|
||||
let untrack_action = VolumeUntrackAction {
|
||||
library_id: library1.id(),
|
||||
fingerprint: fingerprint.clone(),
|
||||
};
|
||||
|
||||
let action = Action::VolumeUntrack { action: untrack_action };
|
||||
context
|
||||
.action_manager
|
||||
.execute(action)
|
||||
.await
|
||||
.expect("Failed to untrack from library 1");
|
||||
|
||||
// Verify library 2 still has it tracked
|
||||
let lib2_volumes = volume_manager
|
||||
.get_tracked_volumes(&library2)
|
||||
.await
|
||||
.expect("Failed to get library 2 volumes");
|
||||
assert_eq!(lib2_volumes.len(), 1);
|
||||
}
|
||||
Reference in New Issue
Block a user