Enhance volume management and filesystem handling

- Updated `volume_for_path` method to canonicalize paths, improving cache key accuracy and handling of relative paths.
- Enhanced logging for path canonicalization failures, ensuring better debugging information.
- Improved filesystem detection logic to skip autofs and other special filesystems, preventing unnecessary processing.
- Added validation to skip invalid size strings in volume parsing, enhancing robustness.

These changes improve the reliability and accuracy of volume management operations, ensuring better handling of filesystem interactions.
This commit is contained in:
Jamie Pine
2025-07-06 18:53:34 -07:00
parent 5aa2eecad5
commit 733e46c4e4
5 changed files with 396 additions and 337 deletions

View File

@@ -212,10 +212,20 @@ impl VolumeManager {
/// Get volume information for a specific path
#[instrument(skip(self))]
pub async fn volume_for_path(&self, path: &Path) -> Option<Volume> {
// Check cache first
// Canonicalize the path to handle relative paths properly
let canonical_path = match path.canonicalize() {
Ok(p) => p,
Err(e) => {
debug!("Failed to canonicalize path {}: {}", path.display(), e);
// If canonicalization fails, try with the original path
path.to_path_buf()
}
};
// Check cache first (use canonical path for cache key)
{
let cache = self.path_cache.read().await;
if let Some(fingerprint) = cache.get(path) {
if let Some(fingerprint) = cache.get(&canonical_path) {
let volumes = self.volumes.read().await;
if let Some(volume) = volumes.get(fingerprint) {
return Some(volume.clone());
@@ -223,18 +233,18 @@ impl VolumeManager {
}
}
// Search through all volumes
// Search through all volumes using canonical path
let volumes = self.volumes.read().await;
for volume in volumes.values() {
if volume.contains_path(&path.to_path_buf()) {
// Cache the result
if volume.contains_path(&canonical_path) {
// Cache the result using canonical path
let mut cache = self.path_cache.write().await;
cache.insert(path.to_path_buf(), volume.fingerprint.clone());
cache.insert(canonical_path.clone(), volume.fingerprint.clone());
return Some(volume.clone());
}
}
debug!("No volume found for path: {}", path.display());
debug!("No volume found for path: {}", canonical_path.display());
None
}

View File

@@ -79,11 +79,24 @@ mod macos {
}
let filesystem = parts[0];
// Handle special case where autofs filesystem has name and target split across columns
if filesystem == "map" && parts.len() > 1 && parts[1].contains("auto") {
debug!("Skipping autofs filesystem: map {}", parts[1]);
return Ok(None);
}
let size_str = parts[1];
let used_str = parts[2];
let available_str = parts[3];
let mount_point = parts[8];
// Skip autofs and other special filesystems
if filesystem.starts_with("map") || filesystem.contains("auto_") {
debug!("Skipping autofs filesystem: {}", filesystem);
return Ok(None);
}
// Skip system filesystems unless requested
if !config.include_system && is_system_filesystem(filesystem) {
return Ok(None);
@@ -350,10 +363,11 @@ fn is_system_filesystem(filesystem: &str) -> bool {
}
fn is_virtual_filesystem(filesystem: &str) -> bool {
let fs_lower = filesystem.to_lowercase();
matches!(
filesystem.to_lowercase().as_str(),
fs_lower.as_str(),
"devfs" | "sysfs" | "proc" | "tmpfs" | "ramfs" | "devtmpfs" | "overlay" | "fuse"
)
) || fs_lower.starts_with("map ") || fs_lower.contains("auto_")
}
fn parse_size_string(size_str: &str) -> VolumeResult<u64> {
@@ -361,6 +375,11 @@ fn parse_size_string(size_str: &str) -> VolumeResult<u64> {
return Ok(0);
}
// Skip invalid size strings that don't look like numbers
if size_str.is_empty() || size_str.chars().all(char::is_alphabetic) {
return Ok(0);
}
let size_str = size_str.replace(",", ""); // Remove commas
let (number_part, unit) = if let Some(pos) = size_str.find(char::is_alphabetic) {
(&size_str[..pos], &size_str[pos..])

View File

@@ -4,358 +4,388 @@
//! file system operations, and audit logging.
use sd_core_new::{
Core,
infrastructure::{
actions::{
Action,
manager::ActionManager,
},
database::entities::{audit_log, AuditLog},
jobs::types::{JobId, JobStatus},
},
operations::files::copy::{
action::FileCopyAction,
job::{CopyOptions, MoveMode},
},
infrastructure::{
actions::{manager::ActionManager, Action},
database::entities::{audit_log, AuditLog},
jobs::types::{JobId, JobStatus},
},
operations::files::copy::{
action::FileCopyAction,
job::{CopyOptions, MoveMode},
},
Core,
};
use sea_orm::{EntityTrait, QuerySelect};
use std::{
path::PathBuf,
sync::Arc,
time::Duration,
};
use std::{path::PathBuf, sync::Arc, time::Duration};
use tempfile::TempDir;
use tokio::{fs, time::timeout};
use uuid::Uuid;
/// Helper to create test files with content
async fn create_test_file(path: &std::path::Path, content: &str) -> Result<(), std::io::Error> {
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(path, content).await
if let Some(parent) = path.parent() {
fs::create_dir_all(parent).await?;
}
fs::write(path, content).await
}
/// Helper to verify file content matches expected
async fn verify_file_content(path: &std::path::Path, expected: &str) -> Result<bool, std::io::Error> {
let content = fs::read_to_string(path).await?;
Ok(content == expected)
async fn verify_file_content(
path: &std::path::Path,
expected: &str,
) -> Result<bool, std::io::Error> {
let content = fs::read_to_string(path).await?;
Ok(content == expected)
}
#[tokio::test]
async fn test_copy_action_full_integration() {
// Setup test environment
let temp_dir = TempDir::new().unwrap();
let test_root = temp_dir.path();
// Create source and destination directories
let source_dir = test_root.join("source");
let dest_dir = test_root.join("destination");
fs::create_dir_all(&source_dir).await.unwrap();
fs::create_dir_all(&dest_dir).await.unwrap();
// Create test files
let source_file1 = source_dir.join("test1.txt");
let source_file2 = source_dir.join("test2.txt");
let dest_file1 = dest_dir.join("test1.txt");
let dest_file2 = dest_dir.join("test2.txt");
create_test_file(&source_file1, "Hello, World! This is test file 1.").await.unwrap();
create_test_file(&source_file2, "This is the content of test file 2.").await.unwrap();
// Initialize core with custom data directory
let core_data_dir = test_root.join("core_data");
let core = Core::new_with_config(core_data_dir)
.await
.unwrap();
// Create a test library
let library = core
.libraries
.create_library("Copy Test Library", None)
.await
.unwrap();
let library_id = library.id();
// Create ActionManager
let context = core.context.clone();
let action_manager = ActionManager::new(context);
// Build the copy action
let copy_action = FileCopyAction {
sources: vec![source_file1.clone(), source_file2.clone()],
destination: dest_dir.clone(),
options: CopyOptions {
overwrite: false,
verify_checksum: true,
preserve_timestamps: true,
delete_after_copy: false,
move_mode: None,
},
};
// Create the Action enum with library context
let action = Action::FileCopy {
library_id,
action: copy_action,
};
// Record initial state
let initial_audit_count = count_audit_entries(&library, library_id).await;
// Verify source files exist and destination files don't
assert!(source_file1.exists());
assert!(source_file2.exists());
assert!(!dest_file1.exists());
assert!(!dest_file2.exists());
// ===== Execute the action =====
let action_output = action_manager
.dispatch(action)
.await
.expect("Action dispatch should succeed");
// Verify action output
assert_eq!(action_output.output_type, "file.copy.dispatched");
assert!(action_output.data.get("job_id").is_some());
assert!(action_output.message.contains("Dispatched file copy job"));
// Extract job ID from output
let job_id_value = action_output.data.get("job_id").unwrap();
let job_id_str = job_id_value.as_str().expect("job_id should be a string");
let job_id = Uuid::parse_str(job_id_str).expect("job_id should be valid UUID");
// ===== Wait for job completion =====
// Poll job status until completion (with timeout)
let job_completion = timeout(Duration::from_secs(30), async {
loop {
if let Some(job_handle) = library.jobs().get_job(JobId::from(job_id)).await {
let status = job_handle.status();
if matches!(status, JobStatus::Completed | JobStatus::Failed) {
return status;
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await
.expect("Job should complete within timeout");
// Verify job completed successfully
assert!(matches!(job_completion, JobStatus::Completed), "Job should complete successfully");
// ===== Verify file system changes =====
// Check that files were copied successfully
assert!(dest_file1.exists(), "Destination file 1 should exist");
assert!(dest_file2.exists(), "Destination file 2 should exist");
// Verify file contents match
assert!(
verify_file_content(&dest_file1, "Hello, World! This is test file 1.")
.await
.unwrap(),
"Destination file 1 content should match source"
);
assert!(
verify_file_content(&dest_file2, "This is the content of test file 2.")
.await
.unwrap(),
"Destination file 2 content should match source"
);
// Verify source files still exist (copy, not move)
assert!(source_file1.exists(), "Source file 1 should still exist");
assert!(source_file2.exists(), "Source file 2 should still exist");
// ===== Verify audit log =====
let final_audit_count = count_audit_entries(&library, library_id).await;
assert_eq!(
final_audit_count,
initial_audit_count + 1,
"Should have one new audit log entry"
);
// Get the audit log entry
let audit_entries = get_recent_audit_entries(&library, library_id, 1).await;
assert_eq!(audit_entries.len(), 1, "Should have exactly one audit entry");
let audit_entry = &audit_entries[0];
assert_eq!(audit_entry.action_type, "file.copy");
assert_eq!(audit_entry.status, audit_log::ActionStatus::Completed);
assert!(audit_entry.job_id.is_some(), "Audit entry should have job_id");
assert_eq!(audit_entry.job_id.as_ref().unwrap(), &job_id.to_string());
assert!(audit_entry.completed_at.is_some(), "Audit entry should have completion time");
assert!(audit_entry.error_message.is_none(), "Audit entry should not have error message");
// Verify audit entry targets contain source and destination info (now stored as JSON string)
let targets_json: serde_json::Value = serde_json::from_str(&audit_entry.targets).unwrap();
assert!(targets_json.get("sources").is_some(), "Audit should contain sources");
assert!(targets_json.get("destination").is_some(), "Audit should contain destination");
let sources = targets_json.get("sources").unwrap().as_array().unwrap();
assert_eq!(sources.len(), 2, "Should have 2 source files in audit");
println!("✅ Copy action integration test passed!");
println!(" - Action dispatched successfully");
println!(" - Job executed and completed");
println!(" - Files copied correctly");
println!(" - Audit log entry created");
// Setup test environment
let temp_dir = TempDir::new().unwrap();
let test_root = temp_dir.path();
// Create source and destination directories
let source_dir = test_root.join("source");
let dest_dir = test_root.join("destination");
fs::create_dir_all(&source_dir).await.unwrap();
fs::create_dir_all(&dest_dir).await.unwrap();
// Create test files
let source_file1 = source_dir.join("test1.txt");
let source_file2 = source_dir.join("test2.txt");
let dest_file1 = dest_dir.join("test1.txt");
let dest_file2 = dest_dir.join("test2.txt");
create_test_file(&source_file1, "Hello, World! This is test file 1.")
.await
.unwrap();
create_test_file(&source_file2, "This is the content of test file 2.")
.await
.unwrap();
// Initialize core with custom data directory
let core_data_dir = test_root.join("core_data");
let core = Core::new_with_config(core_data_dir).await.unwrap();
// Create a test library
let library = core
.libraries
.create_library("Copy Test Library", None, core.context.clone())
.await
.unwrap();
let library_id = library.id();
// Create ActionManager
let context = core.context.clone();
let action_manager = ActionManager::new(context);
// Build the copy action
let copy_action = FileCopyAction {
sources: vec![source_file1.clone(), source_file2.clone()],
destination: dest_dir.clone(),
options: CopyOptions {
overwrite: false,
verify_checksum: true,
preserve_timestamps: true,
delete_after_copy: false,
move_mode: None,
},
};
// Create the Action enum with library context
let action = Action::FileCopy {
library_id,
action: copy_action,
};
// Record initial state
let initial_audit_count = count_audit_entries(&library, library_id).await;
// Verify source files exist and destination files don't
assert!(source_file1.exists());
assert!(source_file2.exists());
assert!(!dest_file1.exists());
assert!(!dest_file2.exists());
// ===== Execute the action =====
let action_output = action_manager
.dispatch(action)
.await
.expect("Action dispatch should succeed");
// Verify action output
assert_eq!(action_output.output_type, "file.copy.dispatched");
assert!(action_output.data.get("job_id").is_some());
assert!(action_output.message.contains("Dispatched file copy job"));
// Extract job ID from output
let job_id_value = action_output.data.get("job_id").unwrap();
let job_id_str = job_id_value.as_str().expect("job_id should be a string");
let job_id = Uuid::parse_str(job_id_str).expect("job_id should be valid UUID");
// ===== Wait for job completion =====
// Poll job status until completion (with timeout)
let job_completion = timeout(Duration::from_secs(30), async {
loop {
if let Some(job_handle) = library.jobs().get_job(JobId::from(job_id)).await {
let status = job_handle.status();
if matches!(status, JobStatus::Completed | JobStatus::Failed) {
return status;
}
}
tokio::time::sleep(Duration::from_millis(100)).await;
}
})
.await
.expect("Job should complete within timeout");
// Verify job completed successfully
assert!(
matches!(job_completion, JobStatus::Completed),
"Job should complete successfully"
);
// ===== Verify file system changes =====
// Check that files were copied successfully
assert!(dest_file1.exists(), "Destination file 1 should exist");
assert!(dest_file2.exists(), "Destination file 2 should exist");
// Verify file contents match
assert!(
verify_file_content(&dest_file1, "Hello, World! This is test file 1.")
.await
.unwrap(),
"Destination file 1 content should match source"
);
assert!(
verify_file_content(&dest_file2, "This is the content of test file 2.")
.await
.unwrap(),
"Destination file 2 content should match source"
);
// Verify source files still exist (copy, not move)
assert!(source_file1.exists(), "Source file 1 should still exist");
assert!(source_file2.exists(), "Source file 2 should still exist");
// ===== Verify audit log =====
let final_audit_count = count_audit_entries(&library, library_id).await;
assert_eq!(
final_audit_count,
initial_audit_count + 1,
"Should have one new audit log entry"
);
// Get the audit log entry
let audit_entries = get_recent_audit_entries(&library, library_id, 1).await;
assert_eq!(
audit_entries.len(),
1,
"Should have exactly one audit entry"
);
let audit_entry = &audit_entries[0];
assert_eq!(audit_entry.action_type, "file.copy");
assert_eq!(audit_entry.status, audit_log::ActionStatus::Completed);
assert!(
audit_entry.job_id.is_some(),
"Audit entry should have job_id"
);
assert_eq!(audit_entry.job_id.as_ref().unwrap(), &job_id.to_string());
assert!(
audit_entry.completed_at.is_some(),
"Audit entry should have completion time"
);
assert!(
audit_entry.error_message.is_none(),
"Audit entry should not have error message"
);
// Verify audit entry targets contain source and destination info (now stored as JSON string)
let targets_json: serde_json::Value = serde_json::from_str(&audit_entry.targets).unwrap();
assert!(
targets_json.get("sources").is_some(),
"Audit should contain sources"
);
assert!(
targets_json.get("destination").is_some(),
"Audit should contain destination"
);
let sources = targets_json.get("sources").unwrap().as_array().unwrap();
assert_eq!(sources.len(), 2, "Should have 2 source files in audit");
println!("✅ Copy action integration test passed!");
println!(" - Action dispatched successfully");
println!(" - Job executed and completed");
println!(" - Files copied correctly");
println!(" - Audit log entry created");
}
#[tokio::test]
async fn test_copy_action_with_move_operation() {
// Setup test environment
let temp_dir = TempDir::new().unwrap();
let test_root = temp_dir.path();
let source_dir = test_root.join("source");
let dest_dir = test_root.join("destination");
fs::create_dir_all(&source_dir).await.unwrap();
fs::create_dir_all(&dest_dir).await.unwrap();
// Create test file
let source_file = source_dir.join("move_test.txt");
let dest_file = dest_dir.join("move_test.txt");
create_test_file(&source_file, "This file will be moved.").await.unwrap();
// Initialize core and library
let core_data_dir = test_root.join("core_data");
let core = Core::new_with_config(core_data_dir).await.unwrap();
let library = core
.libraries
.create_library("Move Test Library", None)
.await
.unwrap();
let library_id = library.id();
// Create ActionManager
let context = core.context.clone();
let action_manager = ActionManager::new(context);
// Build move action (copy with delete_after_copy)
let copy_action = FileCopyAction {
sources: vec![source_file.clone()],
destination: dest_file.clone(),
options: CopyOptions {
overwrite: false,
verify_checksum: false,
preserve_timestamps: true,
delete_after_copy: true, // This makes it a move operation
move_mode: Some(MoveMode::Move),
},
};
let action = Action::FileCopy {
library_id,
action: copy_action,
};
// Verify initial state
assert!(source_file.exists());
assert!(!dest_file.exists());
// Execute the move action
let action_output = action_manager
.dispatch(action)
.await
.expect("Move action should succeed");
// Extract and wait for job completion
let job_id_str = action_output.data.get("job_id").unwrap().as_str().unwrap();
let job_id = Uuid::parse_str(job_id_str).unwrap();
// Wait for job completion
timeout(Duration::from_secs(15), async {
loop {
if let Some(job_handle) = library.jobs().get_job(JobId::from(job_id)).await {
let status = job_handle.status();
if matches!(status, JobStatus::Completed | JobStatus::Failed) {
break;
}
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("Move job should complete");
// Verify file was moved (destination exists, source doesn't)
assert!(dest_file.exists(), "Destination file should exist after move");
assert!(!source_file.exists(), "Source file should not exist after move");
// Verify content
assert!(
verify_file_content(&dest_file, "This file will be moved.")
.await
.unwrap(),
"Moved file content should match"
);
println!("✅ Move operation test passed!");
// Setup test environment
let temp_dir = TempDir::new().unwrap();
let test_root = temp_dir.path();
let source_dir = test_root.join("source");
let dest_dir = test_root.join("destination");
fs::create_dir_all(&source_dir).await.unwrap();
fs::create_dir_all(&dest_dir).await.unwrap();
// Create test file
let source_file = source_dir.join("move_test.txt");
let dest_file = dest_dir.join("move_test.txt");
create_test_file(&source_file, "This file will be moved.")
.await
.unwrap();
// Initialize core and library
let core_data_dir = test_root.join("core_data");
let core = Core::new_with_config(core_data_dir).await.unwrap();
let library = core
.libraries
.create_library("Move Test Library", None, core.context.clone())
.await
.unwrap();
let library_id = library.id();
// Create ActionManager
let context = core.context.clone();
let action_manager = ActionManager::new(context);
// Build move action (copy with delete_after_copy)
let copy_action = FileCopyAction {
sources: vec![source_file.clone()],
destination: dest_file.clone(),
options: CopyOptions {
overwrite: false,
verify_checksum: false,
preserve_timestamps: true,
delete_after_copy: true, // This makes it a move operation
move_mode: Some(MoveMode::Move),
},
};
let action = Action::FileCopy {
library_id,
action: copy_action,
};
// Verify initial state
assert!(source_file.exists());
assert!(!dest_file.exists());
// Execute the move action
let action_output = action_manager
.dispatch(action)
.await
.expect("Move action should succeed");
// Extract and wait for job completion
let job_id_str = action_output.data.get("job_id").unwrap().as_str().unwrap();
let job_id = Uuid::parse_str(job_id_str).unwrap();
// Wait for job completion
timeout(Duration::from_secs(15), async {
loop {
if let Some(job_handle) = library.jobs().get_job(JobId::from(job_id)).await {
let status = job_handle.status();
if matches!(status, JobStatus::Completed | JobStatus::Failed) {
break;
}
}
tokio::time::sleep(Duration::from_millis(50)).await;
}
})
.await
.expect("Move job should complete");
// Verify file was moved (destination exists, source doesn't)
assert!(
dest_file.exists(),
"Destination file should exist after move"
);
assert!(
!source_file.exists(),
"Source file should not exist after move"
);
// Verify content
assert!(
verify_file_content(&dest_file, "This file will be moved.")
.await
.unwrap(),
"Moved file content should match"
);
println!("✅ Move operation test passed!");
}
#[tokio::test]
async fn test_copy_action_validation_errors() {
let temp_dir = TempDir::new().unwrap();
let core_data_dir = temp_dir.path().join("core_data");
let core = Core::new_with_config(core_data_dir).await.unwrap();
let library = core
.libraries
.create_library("Validation Test Library", None)
.await
.unwrap();
let library_id = library.id();
let context = core.context.clone();
let action_manager = ActionManager::new(context);
// Test 1: Empty sources should fail validation
let invalid_action = Action::FileCopy {
library_id,
action: FileCopyAction {
sources: vec![], // Empty sources
destination: PathBuf::from("/tmp/dest"),
options: CopyOptions::default(),
},
};
let result = action_manager.dispatch(invalid_action).await;
assert!(result.is_err(), "Empty sources should cause validation error");
let error = result.unwrap_err();
assert!(error.to_string().contains("At least one source"), "Error should mention source requirement");
println!("✅ Validation error test passed!");
let temp_dir = TempDir::new().unwrap();
let core_data_dir = temp_dir.path().join("core_data");
let core = Core::new_with_config(core_data_dir).await.unwrap();
let library = core
.libraries
.create_library("Validation Test Library", None, core.context.clone())
.await
.unwrap();
let library_id = library.id();
let context = core.context.clone();
let action_manager = ActionManager::new(context);
// Test 1: Empty sources should fail validation
let invalid_action = Action::FileCopy {
library_id,
action: FileCopyAction {
sources: vec![], // Empty sources
destination: PathBuf::from("/tmp/dest"),
options: CopyOptions::default(),
},
};
let result = action_manager.dispatch(invalid_action).await;
assert!(
result.is_err(),
"Empty sources should cause validation error"
);
let error = result.unwrap_err();
assert!(
error.to_string().contains("At least one source"),
"Error should mention source requirement"
);
println!("✅ Validation error test passed!");
}
/// Helper function to count audit log entries for a library
async fn count_audit_entries(
library: &Arc<sd_core_new::library::Library>,
_library_id: Uuid,
library: &Arc<sd_core_new::library::Library>,
_library_id: Uuid,
) -> usize {
let db = library.db().conn();
AuditLog::find()
.all(db)
.await
.unwrap_or_default()
.len()
let db = library.db().conn();
AuditLog::find().all(db).await.unwrap_or_default().len()
}
/// Helper function to get recent audit log entries
async fn get_recent_audit_entries(
library: &Arc<sd_core_new::library::Library>,
_library_id: Uuid,
limit: u64,
library: &Arc<sd_core_new::library::Library>,
_library_id: Uuid,
limit: u64,
) -> Vec<audit_log::Model> {
let db = library.db().conn();
AuditLog::find()
.limit(limit)
.all(db)
.await
.unwrap_or_default()
}
let db = library.db().conn();
AuditLog::find()
.limit(limit)
.all(db)
.await
.unwrap_or_default()
}

View File

@@ -16,7 +16,7 @@ async fn test_library_lifecycle() {
// Create library (will be created in the libraries directory)
let library = core
.libraries
.create_library("Test Library", None)
.create_library("Test Library", None, core.context.clone())
.await
.unwrap();
@@ -85,7 +85,7 @@ async fn test_library_locking() {
// Create library
let library = core
.libraries
.create_library("Lock Test", None)
.create_library("Lock Test", None, core.context.clone())
.await
.unwrap();
@@ -117,13 +117,13 @@ async fn test_library_discovery() {
// Create multiple libraries
let lib1 = core
.libraries
.create_library("Library 1", None)
.create_library("Library 1", None, core.context.clone())
.await
.unwrap();
let lib2 = core
.libraries
.create_library("Library 2", None)
.create_library("Library 2", None, core.context.clone())
.await
.unwrap();
@@ -160,7 +160,7 @@ async fn test_library_name_sanitization() {
// Create library with problematic name
let library = core
.libraries
.create_library("My/Library:Name*", None)
.create_library("My/Library:Name*", None, core.context.clone())
.await
.unwrap();

View File

@@ -365,7 +365,7 @@ async fn bob_file_transfer_scenario() {
println!("📚 Bob: Creating library for file transfer jobs...");
let _library = core
.libraries
.create_library("Bob Transfer Library", None)
.create_library("Bob Transfer Library", None, core.context.clone())
.await
.unwrap();
println!("✅ Bob: Library created successfully");