-
Notifications
You must be signed in to change notification settings - Fork 2
fix: handle deleting files from smb shares #284
New issue
Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.
By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.
Already on GitHub? Sign in to your account
base: master
Are you sure you want to change the base?
Changes from all commits
File filter
Filter by extension
Conversations
Jump to
Diff view
Diff view
There are no files selected for viewing
| Original file line number | Diff line number | Diff line change | ||||||||||||||||||
|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|---|
|
|
@@ -266,6 +266,55 @@ fn normalize_path_for_compare(path: &Path) -> String { | |||||||||||||||||||
| value | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Clone, Copy, PartialEq, Eq)] | ||||||||||||||||||||
| enum TrashStrategy { | ||||||||||||||||||||
| SystemTrash, | ||||||||||||||||||||
| ProviderTrash, | ||||||||||||||||||||
| PermanentDeleteFallback, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn trash_strategy_for_scheme(scheme: &str) -> TrashStrategy { | ||||||||||||||||||||
| match scheme { | ||||||||||||||||||||
| "file" => TrashStrategy::SystemTrash, | ||||||||||||||||||||
| "gdrive" => TrashStrategy::ProviderTrash, | ||||||||||||||||||||
| _ => TrashStrategy::PermanentDeleteFallback, | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| struct DeleteTarget { | ||||||||||||||||||||
| provider: crate::locations::ProviderRef, | ||||||||||||||||||||
| location: Location, | ||||||||||||||||||||
| display_path: String, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn resolve_delete_targets(paths: Vec<String>) -> Result<Vec<DeleteTarget>, String> { | ||||||||||||||||||||
| let mut targets = Vec::with_capacity(paths.len()); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| for original in paths { | ||||||||||||||||||||
| let (provider, location) = resolve_location(LocationInput::Raw(original))?; | ||||||||||||||||||||
| let capabilities = provider.capabilities(&location); | ||||||||||||||||||||
| if !capabilities.can_delete { | ||||||||||||||||||||
| return Err("Provider does not support deleting items".to_string()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let display_path = if location.scheme() == "file" { | ||||||||||||||||||||
| expand_path(&location.to_path_string())? | ||||||||||||||||||||
| .to_string_lossy() | ||||||||||||||||||||
| .to_string() | ||||||||||||||||||||
| } else { | ||||||||||||||||||||
| location.raw().to_string() | ||||||||||||||||||||
| }; | ||||||||||||||||||||
|
|
||||||||||||||||||||
| targets.push(DeleteTarget { | ||||||||||||||||||||
| provider, | ||||||||||||||||||||
| location, | ||||||||||||||||||||
| display_path, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Ok(targets) | ||||||||||||||||||||
|
Comment on lines
+291
to
+315
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. This function can be refactored to use a more idiomatic iterator-based approach with paths
.into_iter()
.map(|original| {
let (provider, location) = resolve_location(LocationInput::Raw(original))?;
let capabilities = provider.capabilities(&location);
if !capabilities.can_delete {
return Err(format!(
"Provider for path '{}' does not support deleting items",
location.raw()
));
}
let display_path = if location.scheme() == "file" {
expand_path(&location.to_path_string())?
.to_string_lossy()
.to_string()
} else {
location.raw().to_string()
};
Ok(DeleteTarget {
provider,
location,
display_path,
})
})
.collect() |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| fn cleanup_trash_undo_records(state: &TrashUndoState) { | ||||||||||||||||||||
| const TTL: Duration = Duration::from_secs(300); | ||||||||||||||||||||
| const MAX_RECORDS: usize = 10; | ||||||||||||||||||||
|
|
@@ -714,6 +763,7 @@ pub struct TrashPathsResponse { | |||||||||||||||||||
| trashed: Vec<String>, | ||||||||||||||||||||
| undo_token: Option<String>, | ||||||||||||||||||||
| fallback_to_permanent: bool, | ||||||||||||||||||||
| used_system_trash: bool, | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| #[derive(Debug, Serialize, Clone)] | ||||||||||||||||||||
|
|
@@ -1974,22 +2024,55 @@ pub async fn trash_paths(app: AppHandle, paths: Vec<String>) -> Result<TrashPath | |||||||||||||||||||
| trashed: Vec::new(), | ||||||||||||||||||||
| undo_token: None, | ||||||||||||||||||||
| fallback_to_permanent: false, | ||||||||||||||||||||
| used_system_trash: false, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let mut expanded: Vec<PathBuf> = Vec::with_capacity(paths.len()); | ||||||||||||||||||||
| for original in paths { | ||||||||||||||||||||
| let path_buf = expand_path(&original)?; | ||||||||||||||||||||
| if !path_buf.exists() { | ||||||||||||||||||||
| return Err(format!("Path does not exist: {}", original)); | ||||||||||||||||||||
| let targets = resolve_delete_targets(paths)?; | ||||||||||||||||||||
| let strategy = trash_strategy_for_scheme(targets[0].location.scheme()); | ||||||||||||||||||||
| if targets | ||||||||||||||||||||
| .iter() | ||||||||||||||||||||
| .any(|target| trash_strategy_for_scheme(target.location.scheme()) != strategy) | ||||||||||||||||||||
| { | ||||||||||||||||||||
|
Comment on lines
+2033
to
+2036
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current check iterates over all targets to ensure they use the same trash strategy. Since the strategy is determined from the first target, you can make this slightly more efficient by skipping the first element in the iterator. This avoids an unnecessary check for the first element against itself.
Suggested change
|
||||||||||||||||||||
| return Err("Delete selection must come from a single provider type".to_string()); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let original_paths: Vec<String> = targets | ||||||||||||||||||||
| .iter() | ||||||||||||||||||||
| .map(|target| target.display_path.clone()) | ||||||||||||||||||||
| .collect(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if strategy == TrashStrategy::PermanentDeleteFallback { | ||||||||||||||||||||
| return Ok(TrashPathsResponse { | ||||||||||||||||||||
| trashed: Vec::new(), | ||||||||||||||||||||
| undo_token: None, | ||||||||||||||||||||
| fallback_to_permanent: true, | ||||||||||||||||||||
| used_system_trash: false, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if strategy == TrashStrategy::ProviderTrash { | ||||||||||||||||||||
| for target in &targets { | ||||||||||||||||||||
| target.provider.delete(&target.location).await?; | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+2055
to
2057
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation deletes files from the provider sequentially. When deleting multiple files from a remote provider (like Google Drive), this can be slow. You can improve performance by running the delete operations in parallel using
Suggested change
|
||||||||||||||||||||
| expanded.push(path_buf); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| return Ok(TrashPathsResponse { | ||||||||||||||||||||
| trashed: original_paths, | ||||||||||||||||||||
| undo_token: None, | ||||||||||||||||||||
| fallback_to_permanent: false, | ||||||||||||||||||||
| used_system_trash: false, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let original_paths: Vec<String> = expanded | ||||||||||||||||||||
| let expanded: Vec<PathBuf> = targets | ||||||||||||||||||||
| .iter() | ||||||||||||||||||||
| .map(|path| path.to_string_lossy().to_string()) | ||||||||||||||||||||
| .map(|target| PathBuf::from(&target.display_path)) | ||||||||||||||||||||
| .collect(); | ||||||||||||||||||||
| for path_buf in &expanded { | ||||||||||||||||||||
| if !path_buf.exists() { | ||||||||||||||||||||
| return Err(format!("Path does not exist: {}", path_buf.to_string_lossy())); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let delete_targets: Vec<PathBuf> = expanded.clone(); | ||||||||||||||||||||
| let state = app.state::<TrashUndoState>(); | ||||||||||||||||||||
|
|
@@ -2018,6 +2101,7 @@ pub async fn trash_paths(app: AppHandle, paths: Vec<String>) -> Result<TrashPath | |||||||||||||||||||
| trashed: Vec::new(), | ||||||||||||||||||||
| undo_token: None, | ||||||||||||||||||||
| fallback_to_permanent: true, | ||||||||||||||||||||
| used_system_trash: false, | ||||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return Err(err); | ||||||||||||||||||||
|
|
@@ -2103,6 +2187,7 @@ pub async fn trash_paths(app: AppHandle, paths: Vec<String>) -> Result<TrashPath | |||||||||||||||||||
| trashed: original_paths, | ||||||||||||||||||||
| undo_token, | ||||||||||||||||||||
| fallback_to_permanent: false, | ||||||||||||||||||||
| used_system_trash: true, | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
|
|
@@ -2191,72 +2276,54 @@ pub async fn delete_paths_permanently( | |||||||||||||||||||
| }); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let mut expanded: Vec<PathBuf> = Vec::with_capacity(paths.len()); | ||||||||||||||||||||
| for original in paths { | ||||||||||||||||||||
| let path_buf = expand_path(&original)?; | ||||||||||||||||||||
| if !path_buf.exists() { | ||||||||||||||||||||
| return Err(format!("Path does not exist: {}", original)); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| expanded.push(path_buf); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let total = expanded.len(); | ||||||||||||||||||||
| let original_paths: Vec<String> = expanded | ||||||||||||||||||||
| let targets = resolve_delete_targets(paths)?; | ||||||||||||||||||||
| let total = targets.len(); | ||||||||||||||||||||
| let original_paths: Vec<String> = targets | ||||||||||||||||||||
| .iter() | ||||||||||||||||||||
| .map(|path| path.to_string_lossy().to_string()) | ||||||||||||||||||||
| .map(|target| target.display_path.clone()) | ||||||||||||||||||||
| .collect(); | ||||||||||||||||||||
| let targets: Vec<PathBuf> = expanded.clone(); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| let app_handle = app.clone(); | ||||||||||||||||||||
| let request_id_clone = request_id.clone(); | ||||||||||||||||||||
| for (idx, target) in targets.iter().enumerate() { | ||||||||||||||||||||
| let current_path = target.display_path.clone(); | ||||||||||||||||||||
| emit_delete_progress_update( | ||||||||||||||||||||
| &app, | ||||||||||||||||||||
| DeleteProgressUpdatePayload { | ||||||||||||||||||||
| request_id: request_id.clone(), | ||||||||||||||||||||
| current_path: Some(current_path.clone()), | ||||||||||||||||||||
| completed: idx, | ||||||||||||||||||||
| total, | ||||||||||||||||||||
| finished: false, | ||||||||||||||||||||
| error: None, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| tauri::async_runtime::spawn_blocking(move || -> Result<(), String> { | ||||||||||||||||||||
| for (idx, target) in targets.iter().enumerate() { | ||||||||||||||||||||
| let current_path = target.to_string_lossy().to_string(); | ||||||||||||||||||||
| if let Err(err) = target.provider.delete(&target.location).await { | ||||||||||||||||||||
| emit_delete_progress_update( | ||||||||||||||||||||
| &app_handle, | ||||||||||||||||||||
| &app, | ||||||||||||||||||||
| DeleteProgressUpdatePayload { | ||||||||||||||||||||
| request_id: request_id_clone.clone(), | ||||||||||||||||||||
| current_path: Some(current_path.clone()), | ||||||||||||||||||||
| request_id: request_id.clone(), | ||||||||||||||||||||
| current_path: Some(current_path), | ||||||||||||||||||||
| completed: idx, | ||||||||||||||||||||
| total, | ||||||||||||||||||||
| finished: false, | ||||||||||||||||||||
| error: None, | ||||||||||||||||||||
| finished: true, | ||||||||||||||||||||
| error: Some(err.clone()), | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| if let Err(err) = delete_file_or_directory(target) { | ||||||||||||||||||||
| emit_delete_progress_update( | ||||||||||||||||||||
| &app_handle, | ||||||||||||||||||||
| DeleteProgressUpdatePayload { | ||||||||||||||||||||
| request_id: request_id_clone.clone(), | ||||||||||||||||||||
| current_path: Some(current_path), | ||||||||||||||||||||
| completed: idx, | ||||||||||||||||||||
| total, | ||||||||||||||||||||
| finished: true, | ||||||||||||||||||||
| error: Some(err.clone()), | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
| return Err(err); | ||||||||||||||||||||
| } | ||||||||||||||||||||
| return Err(err); | ||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+2300
to
2313
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The refactoring of This is a critical issue because for the local filesystem provider, the To fix this, blocking operations for the Note: You may need to add let delete_result = if target.location.scheme() == "file" {
let provider = Arc::clone(&target.provider);
let location = target.location.clone();
tauri::async_runtime::spawn_blocking(move || {
futures::executor::block_on(provider.delete(&location))
})
.await
.map_err(|e| e.to_string())?
} else {
target.provider.delete(&target.location).await
};
if let Err(err) = delete_result {
emit_delete_progress_update(
&app,
DeleteProgressUpdatePayload {
request_id: request_id.clone(),
current_path: Some(current_path),
completed: idx,
total,
finished: true,
error: Some(err.clone()),
},
);
return Err(err);
} |
||||||||||||||||||||
| } | ||||||||||||||||||||
|
Comment on lines
+2286
to
+2314
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation for Using You'll need to add let result = stream::iter(targets.into_iter().enumerate())
.try_for_each_concurrent(4, |(idx, target)| async {
let current_path = target.display_path.clone();
emit_delete_progress_update(
&app,
DeleteProgressUpdatePayload {
request_id: request_id.clone(),
current_path: Some(current_path.clone()),
completed: idx,
total,
finished: false,
error: None,
},
);
target.provider.delete(&target.location).await.map_err(|err| {
emit_delete_progress_update(
&app,
DeleteProgressUpdatePayload {
request_id: request_id.clone(),
current_path: Some(current_path),
completed: idx,
total,
finished: true,
error: Some(err.clone()),
},
);
err
})
})
.await;
if let Err(err) = result {
return Err(err);
} |
||||||||||||||||||||
|
|
||||||||||||||||||||
| emit_delete_progress_update( | ||||||||||||||||||||
| &app_handle, | ||||||||||||||||||||
| DeleteProgressUpdatePayload { | ||||||||||||||||||||
| request_id: request_id_clone, | ||||||||||||||||||||
| current_path: None, | ||||||||||||||||||||
| completed: total, | ||||||||||||||||||||
| total, | ||||||||||||||||||||
| finished: true, | ||||||||||||||||||||
| error: None, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Ok(()) | ||||||||||||||||||||
| }) | ||||||||||||||||||||
| .await | ||||||||||||||||||||
| .map_err(|err| format!("Failed to join delete task: {err}"))??; | ||||||||||||||||||||
| emit_delete_progress_update( | ||||||||||||||||||||
| &app, | ||||||||||||||||||||
| DeleteProgressUpdatePayload { | ||||||||||||||||||||
| request_id, | ||||||||||||||||||||
| current_path: None, | ||||||||||||||||||||
| completed: total, | ||||||||||||||||||||
| total, | ||||||||||||||||||||
| finished: true, | ||||||||||||||||||||
| error: None, | ||||||||||||||||||||
| }, | ||||||||||||||||||||
| ); | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Ok(DeletePathsResponse { | ||||||||||||||||||||
| deleted: original_paths, | ||||||||||||||||||||
|
|
||||||||||||||||||||
| Original file line number | Diff line number | Diff line change |
|---|---|---|
|
|
@@ -64,6 +64,50 @@ fn is_hidden_file(name: &str) -> bool { | |
| name.starts_with('.') | ||
| } | ||
|
|
||
| fn join_smb_path(parent: &str, name: &str) -> String { | ||
| let trimmed = parent.trim_end_matches('/'); | ||
| if trimmed.is_empty() { | ||
| format!("/{}", name) | ||
| } else { | ||
| format!("{}/{}", trimmed, name) | ||
| } | ||
| } | ||
|
Comment on lines
+67
to
+74
There was a problem hiding this comment. Choose a reason for hiding this commentThe reason will be displayed to describe this comment to others. Learn more. The current implementation of fn join_smb_path(parent: &str, name: &str) -> String {
if parent == "/" {
return format!("/{}", name);
}
let trimmed = parent.trim_end_matches('/');
if trimmed.is_empty() {
name.to_string()
} else {
format!("{}/{}", trimmed, name)
}
} |
||
|
|
||
| fn delete_directory_recursive(client: &SmbClient, path: &str) -> Result<(), (i32, String)> { | ||
| let entries = client | ||
| .list_dirplus(path) | ||
| .map_err(|e| { | ||
| let (code, msg) = map_smb_error(&e); | ||
| (code, format!("Failed to list directory for deletion: {}", msg)) | ||
| })?; | ||
|
|
||
| for entry in entries { | ||
| let name = entry.name(); | ||
| if name == "." || name == ".." { | ||
| continue; | ||
| } | ||
|
|
||
| let child_path = join_smb_path(path, name); | ||
| if matches!(entry.get_type(), pavao::SmbDirentType::Dir) { | ||
| delete_directory_recursive(client, &child_path)?; | ||
| } else { | ||
| client | ||
| .unlink(&child_path) | ||
| .map_err(|e| { | ||
| let (code, msg) = map_smb_error(&e); | ||
| (code, format!("Failed to delete file {}: {}", child_path, msg)) | ||
| })?; | ||
| } | ||
| } | ||
|
|
||
| client | ||
| .rmdir(path) | ||
| .map_err(|e| { | ||
| let (code, msg) = map_smb_error(&e); | ||
| (code, format!("Failed to delete directory {}: {}", path, msg)) | ||
| }) | ||
| } | ||
|
|
||
| /// Read directory contents. | ||
| pub fn read_directory(params: ReadDirectoryParams) -> Result<ReadDirectoryResult, (i32, String)> { | ||
| let _guard = SMB_MUTEX | ||
|
|
@@ -215,12 +259,7 @@ pub fn delete(params: DeleteParams) -> Result<(), (i32, String)> { | |
| })?; | ||
|
|
||
| if stat.mode.is_dir() { | ||
| client | ||
| .rmdir(¶ms.path) | ||
| .map_err(|e| { | ||
| let (code, msg) = map_smb_error(&e); | ||
| (code, format!("Failed to delete directory: {}", msg)) | ||
| }) | ||
| delete_directory_recursive(&client, ¶ms.path) | ||
| } else { | ||
| client | ||
| .unlink(¶ms.path) | ||
|
|
||
There was a problem hiding this comment.
Choose a reason for hiding this comment
The reason will be displayed to describe this comment to others. Learn more.
This function can be written more idiomatically using Rust's iterator methods, which can make the code more concise and expressive. By using
into_iter(),map(), andcollect(), you can transform the vector of paths into aResultcontaining a vector ofDeleteTargets, while still handling errors gracefully as thecollect()over an iterator ofResults will short-circuit on the firstErr.paths .into_iter() .map(|original| { let (provider, location) = resolve_location(LocationInput::Raw(original))?; let capabilities = provider.capabilities(&location); if !capabilities.can_delete { return Err("Provider does not support deleting items".to_string()); } let display_path = if location.scheme() == "file" { expand_path(&location.to_path_string())? .to_string_lossy() .to_string() } else { location.raw().to_string() }; Ok(DeleteTarget { provider, location, display_path, }) }) .collect()