mirror of
https://github.com/spacedriveapp/spacedrive.git
synced 2025-12-11 20:15:30 +01:00
Enhance file copy operations with new copy method options and CLI support
- Updated `CopyMethod` enum to include `Atomic` and `Streaming` variants, replacing the previous `AtomicMove` and `StreamingCopy` options for clarity. - Refactored the `select_strategy` method to respect user preferences for copy methods, improving the logic for same-volume operations. - Added CLI support for the new copy methods in `args.rs`, allowing users to specify their preferred method during file copy operations. - Updated relevant tests to reflect changes in copy method naming and functionality. - Enhanced documentation to include new copy method options and their usage. Co-authored-by: ijamespine <ijamespine@me.com>
This commit is contained in:
parent
ecdcf04066
commit
aa1a8d8c00
@ -11,7 +11,7 @@ clap = { version = "4", features = ["derive"] }
|
||||
crossterm = "0.27"
|
||||
indicatif = "0.17"
|
||||
ratatui = "0.26"
|
||||
sd-core = { path = "../../core" }
|
||||
sd-core = { path = "../../core", features = ["cli"] }
|
||||
serde = { version = "1", features = ["derive"] }
|
||||
serde_json = "1"
|
||||
tokio = { version = "1", features = ["full"] }
|
||||
|
||||
@ -30,6 +30,10 @@ pub struct FileCopyArgs {
|
||||
/// Delete source files after copy (move)
|
||||
#[arg(long, default_value_t = false)]
|
||||
pub move_files: bool,
|
||||
|
||||
/// Copy method to use
|
||||
#[arg(long, default_value_t = CopyMethod::Auto)]
|
||||
pub method: CopyMethod,
|
||||
}
|
||||
|
||||
impl From<FileCopyArgs> for FileCopyInput {
|
||||
@ -47,8 +51,7 @@ impl From<FileCopyArgs> for FileCopyInput {
|
||||
verify_checksum: args.verify_checksum,
|
||||
preserve_timestamps: args.preserve_timestamps,
|
||||
move_files: args.move_files,
|
||||
copy_method: CopyMethod::Auto,
|
||||
copy_method: args.method,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@ -58,3 +58,4 @@ pub const SPINNER_CHARS: &[char] = &['⠋', '⠙', '⠹', '⠸', '⠼', '⠴', '
|
||||
pub fn spinner_char(frame: usize) -> char {
|
||||
SPINNER_CHARS[frame % SPINNER_CHARS.len()]
|
||||
}
|
||||
|
||||
|
||||
@ -2,25 +2,25 @@
|
||||
|
||||
/// Display the Spacedrive logo
|
||||
pub fn print_logo() {
|
||||
println!(r#" [48;2;240;230;255m [48;2;235;220;255m [48;2;240;230;255m [0m
|
||||
[48;2;240;230;255m [48;2;220;180;255m [48;2;240;230;255m [0m
|
||||
[48;2;235;220;255m [48;2;180;120;255m [48;2;160;80;255m [48;2;235;220;255m [0m
|
||||
[48;2;235;220;255m [48;2;160;80;255m [48;2;140;40;255m [48;2;160;80;255m [48;2;235;220;255m [0m
|
||||
[48;2;240;230;255m [48;2;140;40;255m [48;2;120;20;255m [48;2;140;40;255m [48;2;240;230;255m [0m
|
||||
[48;2;220;180;255m [48;2;120;20;255m [48;2;100;0;255m [48;2;120;20;255m [48;2;220;180;255m [0m
|
||||
[48;2;180;120;255m [48;2;100;0;255m [48;2;120;20;255m [48;2;100;0;255m [48;2;180;120;255m [0m
|
||||
[48;2;160;80;255m [48;2;120;20;255m [48;2;140;40;255m [48;2;120;20;255m [48;2;160;80;255m [0m
|
||||
[48;2;160;80;255m [48;2;140;40;255m [48;2;160;80;255m [48;2;140;40;255m [48;2;160;80;255m [0m
|
||||
[48;2;180;120;255m [48;2;160;80;255m [48;2;180;120;255m [48;2;160;80;255m [0m
|
||||
[48;2;220;180;255m [48;2;180;120;255m [48;2;220;180;255m [0m
|
||||
[48;2;235;220;255m [48;2;220;180;255m [48;2;235;220;255m [0m"#);
|
||||
println!();
|
||||
println!(" 🚀 Spacedrive CLI v2");
|
||||
println!(" Cross-platform file management");
|
||||
println!();
|
||||
// println!(r#" [48;2;240;230;255m [48;2;235;220;255m [48;2;240;230;255m [0m
|
||||
// [48;2;240;230;255m [48;2;220;180;255m [48;2;240;230;255m [0m
|
||||
// [48;2;235;220;255m [48;2;180;120;255m [48;2;160;80;255m [48;2;235;220;255m [0m
|
||||
// [48;2;235;220;255m [48;2;160;80;255m [48;2;140;40;255m [48;2;160;80;255m [48;2;235;220;255m [0m
|
||||
// [48;2;240;230;255m [48;2;140;40;255m [48;2;120;20;255m [48;2;140;40;255m [48;2;240;230;255m [0m
|
||||
// [48;2;220;180;255m [48;2;120;20;255m [48;2;100;0;255m [48;2;120;20;255m [48;2;220;180;255m [0m
|
||||
// [48;2;180;120;255m [48;2;100;0;255m [48;2;120;20;255m [48;2;100;0;255m [48;2;180;120;255m [0m
|
||||
// [48;2;160;80;255m [48;2;120;20;255m [48;2;140;40;255m [48;2;120;20;255m [48;2;160;80;255m [0m
|
||||
// [48;2;160;80;255m [48;2;140;40;255m [48;2;160;80;255m [48;2;140;40;255m [48;2;160;80;255m [0m
|
||||
// [48;2;180;120;255m [48;2;160;80;255m [48;2;180;120;255m [48;2;160;80;255m [0m
|
||||
// [48;2;220;180;255m [48;2;180;120;255m [48;2;220;180;255m [0m
|
||||
// [48;2;235;220;255m [48;2;220;180;255m [48;2;235;220;255m [0m"#);
|
||||
println!();
|
||||
println!(" 🚀 Spacedrive CLI v2");
|
||||
println!(" Cross-platform file management");
|
||||
println!();
|
||||
}
|
||||
|
||||
/// Display a compact version of the logo
|
||||
pub fn print_compact_logo() {
|
||||
println!("🚀 Spacedrive CLI v2");
|
||||
println!("🚀 Spacedrive CLI v2");
|
||||
}
|
||||
|
||||
@ -464,3 +464,4 @@ where
|
||||
|
||||
Ok(())
|
||||
}
|
||||
|
||||
|
||||
@ -97,3 +97,4 @@ crate::register_core_action!(LibraryCreateAction, "libraries.create");
|
||||
## Debug Instructions
|
||||
|
||||
- You can view the logs of a job in the job_logs directory in the root of the data folder
|
||||
- When testing the CLI, after compiling you must `stop` then `start` the Spacedrive daemon.
|
||||
|
||||
@ -13,6 +13,8 @@ ai = []
|
||||
heif = []
|
||||
# Mobile platform support
|
||||
mobile = []
|
||||
# CLI support
|
||||
cli = ["dep:clap"]
|
||||
|
||||
|
||||
[dependencies]
|
||||
@ -45,6 +47,7 @@ serde_json = "1.0"
|
||||
strum = { version = "0.26", features = ["derive"] }
|
||||
toml = "0.8"
|
||||
|
||||
|
||||
# Error handling
|
||||
anyhow = "1.0"
|
||||
thiserror = "1.0"
|
||||
@ -139,7 +142,7 @@ whoami = "1.5"
|
||||
keyring = "3.6"
|
||||
|
||||
# CLI dependencies
|
||||
clap = { version = "4.5", features = ["derive", "env"] }
|
||||
clap = { version = "4.5", features = ["derive", "env"], optional = true }
|
||||
colored = "2.1"
|
||||
comfy-table = "7.1"
|
||||
console = "0.15"
|
||||
|
||||
@ -8,13 +8,14 @@ use std::path::PathBuf;
|
||||
|
||||
/// Copy method preference for file operations
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
#[cfg_attr(feature = "cli", derive(clap::ValueEnum))]
|
||||
pub enum CopyMethod {
|
||||
/// Automatically select the best method based on source and destination
|
||||
Auto,
|
||||
/// Use atomic move (rename) for same-volume operations
|
||||
AtomicMove,
|
||||
/// Use streaming copy for cross-volume operations
|
||||
StreamingCopy,
|
||||
/// Use atomic operations (rename for moves, APFS clone for copies, etc.)
|
||||
Atomic,
|
||||
/// Use streaming copy/move (works across all scenarios)
|
||||
Streaming,
|
||||
}
|
||||
|
||||
impl Default for CopyMethod {
|
||||
@ -23,6 +24,17 @@ impl Default for CopyMethod {
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(feature = "cli")]
|
||||
impl std::fmt::Display for CopyMethod {
|
||||
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
||||
match self {
|
||||
CopyMethod::Auto => write!(f, "auto"),
|
||||
CopyMethod::Atomic => write!(f, "atomic"),
|
||||
CopyMethod::Streaming => write!(f, "streaming"),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Core input structure for file copy operations
|
||||
/// This is the canonical interface that all external APIs (CLI, GraphQL, REST) convert to
|
||||
#[derive(Debug, Clone, PartialEq, Eq, Serialize, Deserialize)]
|
||||
|
||||
@ -1,8 +1,11 @@
|
||||
//! Strategy router for selecting the optimal copy method
|
||||
|
||||
use super::{
|
||||
input::CopyMethod,
|
||||
strategy::{CopyStrategy, LocalMoveStrategy, LocalStreamCopyStrategy, RemoteTransferStrategy},
|
||||
input::CopyMethod,
|
||||
strategy::{
|
||||
CopyStrategy, FastCopyStrategy, LocalMoveStrategy, LocalStreamCopyStrategy,
|
||||
RemoteTransferStrategy,
|
||||
},
|
||||
};
|
||||
use crate::{domain::addressing::SdPath, volume::VolumeManager};
|
||||
use std::sync::Arc;
|
||||
@ -10,276 +13,309 @@ use std::sync::Arc;
|
||||
pub struct CopyStrategyRouter;
|
||||
|
||||
impl CopyStrategyRouter {
|
||||
/// Selects the optimal copy strategy based on source, destination, and volume info
|
||||
pub async fn select_strategy(
|
||||
source: &SdPath,
|
||||
destination: &SdPath,
|
||||
is_move: bool,
|
||||
copy_method: &CopyMethod,
|
||||
volume_manager: Option<&VolumeManager>,
|
||||
) -> Box<dyn CopyStrategy> {
|
||||
// Cross-device transfer - always use network strategy
|
||||
if source.device_id() != destination.device_id() {
|
||||
return Box::new(RemoteTransferStrategy);
|
||||
}
|
||||
/// Selects the optimal copy strategy based on source, destination, and volume info
|
||||
pub async fn select_strategy(
|
||||
source: &SdPath,
|
||||
destination: &SdPath,
|
||||
is_move: bool,
|
||||
copy_method: &CopyMethod,
|
||||
volume_manager: Option<&VolumeManager>,
|
||||
) -> Box<dyn CopyStrategy> {
|
||||
// Cross-device transfer - always use network strategy
|
||||
if source.device_id() != destination.device_id() {
|
||||
return Box::new(RemoteTransferStrategy);
|
||||
}
|
||||
|
||||
// For same-device operations, respect user's method preference
|
||||
match copy_method {
|
||||
CopyMethod::AtomicMove => {
|
||||
// User explicitly wants atomic move - validate it's possible
|
||||
if is_move {
|
||||
return Box::new(LocalMoveStrategy);
|
||||
} else {
|
||||
// Cannot do atomic move for copy operations, fall back to streaming
|
||||
return Box::new(LocalStreamCopyStrategy);
|
||||
}
|
||||
}
|
||||
CopyMethod::StreamingCopy => {
|
||||
// User explicitly wants streaming copy
|
||||
return Box::new(LocalStreamCopyStrategy);
|
||||
}
|
||||
CopyMethod::Auto => {
|
||||
// Auto-select based on optimal strategy (original logic)
|
||||
// Same-device operation - get local paths for volume analysis
|
||||
let (source_path, dest_path) = match (source.as_local_path(), destination.as_local_path()) {
|
||||
(Some(s), Some(d)) => (s, d),
|
||||
_ => {
|
||||
// Fallback to streaming copy if paths aren't local
|
||||
return Box::new(LocalStreamCopyStrategy);
|
||||
}
|
||||
};
|
||||
// For same-device operations, respect user's method preference
|
||||
match copy_method {
|
||||
CopyMethod::Atomic => {
|
||||
// User explicitly wants atomic operations
|
||||
if is_move {
|
||||
return Box::new(LocalMoveStrategy);
|
||||
} else {
|
||||
// For atomic copy, use fast copy strategy (std::fs::copy handles optimizations)
|
||||
return Box::new(FastCopyStrategy);
|
||||
}
|
||||
}
|
||||
CopyMethod::Streaming => {
|
||||
// User explicitly wants streaming copy
|
||||
return Box::new(LocalStreamCopyStrategy);
|
||||
}
|
||||
CopyMethod::Auto => {
|
||||
// Auto-select based on optimal strategy (original logic)
|
||||
// Same-device operation - get local paths for volume analysis
|
||||
let (source_path, dest_path) =
|
||||
match (source.as_local_path(), destination.as_local_path()) {
|
||||
(Some(s), Some(d)) => (s, d),
|
||||
_ => {
|
||||
// Fallback to streaming copy if paths aren't local
|
||||
return Box::new(LocalStreamCopyStrategy);
|
||||
}
|
||||
};
|
||||
|
||||
// Check if paths are on the same volume
|
||||
if let Some(vm) = volume_manager {
|
||||
if vm.same_volume(source_path, dest_path).await {
|
||||
// Same volume
|
||||
if is_move {
|
||||
// Use atomic move for same-volume moves
|
||||
return Box::new(LocalMoveStrategy);
|
||||
}
|
||||
// For same-volume copies, we could add optimized copy strategies here
|
||||
// (e.g., reflink on filesystems that support it)
|
||||
// For now, fall through to streaming copy
|
||||
}
|
||||
} else {
|
||||
// No volume manager available - make best guess
|
||||
// If it's a move operation on the same device, try atomic move
|
||||
if is_move {
|
||||
return Box::new(LocalMoveStrategy);
|
||||
}
|
||||
}
|
||||
// Check if paths are on the same volume
|
||||
let same_volume = if let Some(vm) = volume_manager {
|
||||
vm.same_volume(source_path, dest_path).await
|
||||
} else {
|
||||
// Fallback: if no volume manager, assume same-device local paths are same-volume
|
||||
Self::paths_likely_same_volume(source_path, dest_path)
|
||||
};
|
||||
|
||||
// Default to streaming copy for cross-volume or non-move same-volume
|
||||
Box::new(LocalStreamCopyStrategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
if same_volume {
|
||||
// Same volume
|
||||
if is_move {
|
||||
// Use atomic move for same-volume moves
|
||||
return Box::new(LocalMoveStrategy);
|
||||
} else {
|
||||
// Same-volume copy - use fast copy strategy (std::fs::copy handles optimizations)
|
||||
return Box::new(FastCopyStrategy);
|
||||
}
|
||||
} else {
|
||||
// Cross-volume operation - use streaming copy
|
||||
return Box::new(LocalStreamCopyStrategy);
|
||||
}
|
||||
|
||||
/// Provides a human-readable description of the selected strategy
|
||||
pub async fn describe_strategy(
|
||||
source: &SdPath,
|
||||
destination: &SdPath,
|
||||
is_move: bool,
|
||||
copy_method: &CopyMethod,
|
||||
volume_manager: Option<&VolumeManager>,
|
||||
) -> String {
|
||||
if source.device_id() != destination.device_id() {
|
||||
return if is_move {
|
||||
"Cross-device move".to_string()
|
||||
} else {
|
||||
"Cross-device transfer".to_string()
|
||||
};
|
||||
}
|
||||
// Default to streaming copy for same-volume non-move operations
|
||||
Box::new(LocalStreamCopyStrategy)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// For same-device operations, include user preference info
|
||||
let method_prefix = match copy_method {
|
||||
CopyMethod::Auto => "",
|
||||
CopyMethod::AtomicMove => "User-requested atomic ",
|
||||
CopyMethod::StreamingCopy => "User-requested streaming ",
|
||||
};
|
||||
/// Heuristic to determine if two local paths are likely on the same volume
|
||||
/// Used as fallback when VolumeManager is unavailable or incomplete
|
||||
fn paths_likely_same_volume(path1: &std::path::Path, path2: &std::path::Path) -> bool {
|
||||
// On macOS, paths under the same root are typically same volume
|
||||
#[cfg(target_os = "macos")]
|
||||
{
|
||||
// Both under /Users, /Applications, /System, etc. are likely same volume
|
||||
let common_roots = ["/Users", "/Applications", "/System", "/Library", "/private"];
|
||||
for root in &common_roots {
|
||||
if path1.starts_with(root) && path2.starts_with(root) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
// Both directly under / (like /tmp, /var) are likely same volume
|
||||
if path1.parent() == Some(std::path::Path::new("/"))
|
||||
&& path2.parent() == Some(std::path::Path::new("/"))
|
||||
{
|
||||
return true;
|
||||
}
|
||||
}
|
||||
|
||||
match copy_method {
|
||||
CopyMethod::AtomicMove => {
|
||||
if is_move {
|
||||
format!("{}move", method_prefix)
|
||||
} else {
|
||||
format!("{}copy (fallback to streaming)", method_prefix)
|
||||
}
|
||||
}
|
||||
CopyMethod::StreamingCopy => {
|
||||
if is_move {
|
||||
format!("{}move", method_prefix)
|
||||
} else {
|
||||
format!("{}copy", method_prefix)
|
||||
}
|
||||
}
|
||||
CopyMethod::Auto => {
|
||||
// Auto-select - use original logic for description
|
||||
let (source_path, dest_path) = match (source.as_local_path(), destination.as_local_path()) {
|
||||
(Some(s), Some(d)) => (s, d),
|
||||
_ => {
|
||||
return "Streaming copy".to_string();
|
||||
}
|
||||
};
|
||||
// On Linux, similar heuristics
|
||||
#[cfg(target_os = "linux")]
|
||||
{
|
||||
let common_roots = ["/home", "/usr", "/var", "/opt", "/tmp"];
|
||||
for root in &common_roots {
|
||||
if path1.starts_with(root) && path2.starts_with(root) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
if let Some(vm) = volume_manager {
|
||||
if vm.same_volume(source_path, dest_path).await {
|
||||
if is_move {
|
||||
return "Atomic move".to_string();
|
||||
} else {
|
||||
return "Same-volume copy".to_string();
|
||||
}
|
||||
} else {
|
||||
return if is_move {
|
||||
"Cross-volume move".to_string()
|
||||
} else {
|
||||
"Cross-volume streaming copy".to_string()
|
||||
};
|
||||
}
|
||||
}
|
||||
// On Windows, same drive letter
|
||||
#[cfg(target_os = "windows")]
|
||||
{
|
||||
if let (Some(s1), Some(s2)) = (path1.to_str(), path2.to_str()) {
|
||||
if s1.len() >= 2 && s2.len() >= 2 {
|
||||
return s1.chars().nth(0) == s2.chars().nth(0)
|
||||
&& s1.chars().nth(1) == Some(':')
|
||||
&& s2.chars().nth(1) == Some(':');
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// Fallback description
|
||||
if is_move {
|
||||
"Local move".to_string()
|
||||
} else {
|
||||
"Local copy".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
false
|
||||
}
|
||||
|
||||
/// Estimates the performance characteristics of the selected strategy
|
||||
pub async fn estimate_performance(
|
||||
source: &SdPath,
|
||||
destination: &SdPath,
|
||||
is_move: bool,
|
||||
copy_method: &CopyMethod,
|
||||
volume_manager: Option<&VolumeManager>,
|
||||
) -> PerformanceEstimate {
|
||||
// Cross-device transfers always use network
|
||||
if source.device_id() != destination.device_id() {
|
||||
return PerformanceEstimate {
|
||||
speed_category: SpeedCategory::Network,
|
||||
supports_resume: true,
|
||||
requires_network: true,
|
||||
is_atomic: false,
|
||||
};
|
||||
}
|
||||
/// Provides a human-readable description of the selected strategy
|
||||
pub async fn describe_strategy(
|
||||
source: &SdPath,
|
||||
destination: &SdPath,
|
||||
is_move: bool,
|
||||
copy_method: &CopyMethod,
|
||||
volume_manager: Option<&VolumeManager>,
|
||||
) -> String {
|
||||
if source.device_id() != destination.device_id() {
|
||||
return if is_move {
|
||||
"Cross-device move".to_string()
|
||||
} else {
|
||||
"Cross-device transfer".to_string()
|
||||
};
|
||||
}
|
||||
|
||||
// For same-device operations, consider user's method preference
|
||||
match copy_method {
|
||||
CopyMethod::AtomicMove => {
|
||||
if is_move {
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::Instant,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: true,
|
||||
}
|
||||
} else {
|
||||
// Fallback to streaming for copy operations
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
CopyMethod::StreamingCopy => {
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
}
|
||||
}
|
||||
CopyMethod::Auto => {
|
||||
// Auto-select - use original performance estimation logic
|
||||
let (source_path, dest_path) = match (source.as_local_path(), destination.as_local_path()) {
|
||||
(Some(s), Some(d)) => (s, d),
|
||||
_ => {
|
||||
return PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
// For same-device operations, include user preference info
|
||||
let method_prefix = match copy_method {
|
||||
CopyMethod::Auto => "",
|
||||
CopyMethod::Atomic => "User-requested atomic ",
|
||||
CopyMethod::Streaming => "User-requested streaming ",
|
||||
};
|
||||
|
||||
if let Some(vm) = volume_manager {
|
||||
if vm.same_volume(source_path, dest_path).await {
|
||||
if is_move {
|
||||
return PerformanceEstimate {
|
||||
speed_category: SpeedCategory::Instant,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: true,
|
||||
};
|
||||
} else {
|
||||
// Could be optimized with filesystem features
|
||||
let source_vol = vm.volume_for_path(source_path).await;
|
||||
let supports_fast_copy = source_vol
|
||||
.map(|v| v.supports_fast_copy())
|
||||
.unwrap_or(false);
|
||||
match copy_method {
|
||||
CopyMethod::Atomic => {
|
||||
if is_move {
|
||||
format!("{}move", method_prefix)
|
||||
} else {
|
||||
format!("{}fast copy", method_prefix)
|
||||
}
|
||||
}
|
||||
CopyMethod::Streaming => {
|
||||
if is_move {
|
||||
format!("{}move", method_prefix)
|
||||
} else {
|
||||
format!("{}copy", method_prefix)
|
||||
}
|
||||
}
|
||||
CopyMethod::Auto => {
|
||||
// Auto-select - use same logic as strategy selection
|
||||
let (source_path, dest_path) =
|
||||
match (source.as_local_path(), destination.as_local_path()) {
|
||||
(Some(s), Some(d)) => (s, d),
|
||||
_ => {
|
||||
return "Streaming copy".to_string();
|
||||
}
|
||||
};
|
||||
|
||||
return PerformanceEstimate {
|
||||
speed_category: if supports_fast_copy {
|
||||
SpeedCategory::FastLocal
|
||||
} else {
|
||||
SpeedCategory::LocalDisk
|
||||
},
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: supports_fast_copy,
|
||||
};
|
||||
}
|
||||
} else {
|
||||
// Cross-volume on same device
|
||||
return PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: true,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
};
|
||||
}
|
||||
}
|
||||
// Check if paths are on the same volume (same logic as select_strategy)
|
||||
let same_volume = if let Some(vm) = volume_manager {
|
||||
vm.same_volume(source_path, dest_path).await
|
||||
} else {
|
||||
Self::paths_likely_same_volume(source_path, dest_path)
|
||||
};
|
||||
|
||||
// Fallback estimate
|
||||
PerformanceEstimate {
|
||||
speed_category: if is_move {
|
||||
SpeedCategory::FastLocal
|
||||
} else {
|
||||
SpeedCategory::LocalDisk
|
||||
},
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: is_move,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
if same_volume {
|
||||
if is_move {
|
||||
"Atomic move".to_string()
|
||||
} else {
|
||||
// Same-volume copy - use fast copy
|
||||
"Fast copy".to_string()
|
||||
}
|
||||
} else {
|
||||
if is_move {
|
||||
"Cross-volume move".to_string()
|
||||
} else {
|
||||
"Cross-volume streaming copy".to_string()
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Estimates the performance characteristics of the selected strategy
|
||||
pub async fn estimate_performance(
|
||||
source: &SdPath,
|
||||
destination: &SdPath,
|
||||
is_move: bool,
|
||||
copy_method: &CopyMethod,
|
||||
volume_manager: Option<&VolumeManager>,
|
||||
) -> PerformanceEstimate {
|
||||
// Cross-device transfers always use network
|
||||
if source.device_id() != destination.device_id() {
|
||||
return PerformanceEstimate {
|
||||
speed_category: SpeedCategory::Network,
|
||||
supports_resume: true,
|
||||
requires_network: true,
|
||||
is_atomic: false,
|
||||
};
|
||||
}
|
||||
|
||||
// For same-device operations, consider user's method preference
|
||||
match copy_method {
|
||||
CopyMethod::Atomic => {
|
||||
if is_move {
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::Instant,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: true,
|
||||
}
|
||||
} else {
|
||||
// Fast copy operations (std::fs::copy with filesystem optimizations)
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::FastLocal,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: true,
|
||||
}
|
||||
}
|
||||
}
|
||||
CopyMethod::Streaming => PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
},
|
||||
CopyMethod::Auto => {
|
||||
// Auto-select - use same logic as strategy selection
|
||||
let (source_path, dest_path) =
|
||||
match (source.as_local_path(), destination.as_local_path()) {
|
||||
(Some(s), Some(d)) => (s, d),
|
||||
_ => {
|
||||
return PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
};
|
||||
}
|
||||
};
|
||||
|
||||
// Check if paths are on the same volume (same logic as select_strategy)
|
||||
let same_volume = if let Some(vm) = volume_manager {
|
||||
vm.same_volume(source_path, dest_path).await
|
||||
} else {
|
||||
Self::paths_likely_same_volume(source_path, dest_path)
|
||||
};
|
||||
|
||||
if same_volume {
|
||||
if is_move {
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::Instant,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: true,
|
||||
}
|
||||
} else {
|
||||
// Same-volume copy - use fast copy
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::FastLocal,
|
||||
supports_resume: false,
|
||||
requires_network: false,
|
||||
is_atomic: true,
|
||||
}
|
||||
}
|
||||
} else {
|
||||
// Cross-volume on same device
|
||||
PerformanceEstimate {
|
||||
speed_category: SpeedCategory::LocalDisk,
|
||||
supports_resume: true,
|
||||
requires_network: false,
|
||||
is_atomic: false,
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/// Performance characteristics of a copy strategy
|
||||
#[derive(Debug, Clone)]
|
||||
pub struct PerformanceEstimate {
|
||||
pub speed_category: SpeedCategory,
|
||||
pub supports_resume: bool,
|
||||
pub requires_network: bool,
|
||||
pub is_atomic: bool,
|
||||
pub speed_category: SpeedCategory,
|
||||
pub supports_resume: bool,
|
||||
pub requires_network: bool,
|
||||
pub is_atomic: bool,
|
||||
}
|
||||
|
||||
/// Categories of copy operation speed
|
||||
#[derive(Debug, Clone, PartialEq)]
|
||||
pub enum SpeedCategory {
|
||||
/// Instant operations (like atomic moves)
|
||||
Instant,
|
||||
/// Fast local operations (reflinks, same-volume copies)
|
||||
FastLocal,
|
||||
/// Regular disk-to-disk operations
|
||||
LocalDisk,
|
||||
/// Network transfers
|
||||
Network,
|
||||
}
|
||||
/// Instant operations (like atomic moves)
|
||||
Instant,
|
||||
/// Fast local operations (reflinks, same-volume copies)
|
||||
FastLocal,
|
||||
/// Regular disk-to-disk operations
|
||||
LocalDisk,
|
||||
/// Network transfers
|
||||
Network,
|
||||
}
|
||||
|
||||
File diff suppressed because it is too large
Load Diff
@ -5,3 +5,4 @@ pub mod cancel;
|
||||
pub use pause::*;
|
||||
pub use resume::*;
|
||||
pub use cancel::*;
|
||||
|
||||
|
||||
@ -122,7 +122,7 @@ async fn test_copy_progress_monitoring_large_file() {
|
||||
preserve_timestamps: true, // --preserve-timestamps
|
||||
delete_after_copy: false,
|
||||
move_mode: None,
|
||||
copy_method: CopyMethod::StreamingCopy, // --method streaming
|
||||
copy_method: CopyMethod::Streaming, // --method streaming
|
||||
},
|
||||
};
|
||||
|
||||
@ -418,7 +418,7 @@ async fn test_copy_progress_multiple_files() {
|
||||
preserve_timestamps: true,
|
||||
delete_after_copy: false,
|
||||
move_mode: None,
|
||||
copy_method: CopyMethod::StreamingCopy,
|
||||
copy_method: CopyMethod::Streaming,
|
||||
},
|
||||
};
|
||||
|
||||
|
||||
BIN
debug_paths
Executable file
BIN
debug_paths
Executable file
Binary file not shown.
@ -315,3 +315,4 @@ nc -U ~/.local/share/spacedrive/daemon/daemon.sock
|
||||
- **Structured logging**: Use `tracing` fields for filtering
|
||||
- **Log levels**: DEBUG for development, INFO for production
|
||||
- **Event correlation**: Track operations across client-daemon boundary
|
||||
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user