Skip to content

fix: handle deleting files from smb shares#284

Open
BrianLeishman wants to merge 3 commits into
masterfrom
fix/smb-share-delete
Open

fix: handle deleting files from smb shares#284
BrianLeishman wants to merge 3 commits into
masterfrom
fix/smb-share-delete

Conversation

@BrianLeishman
Copy link
Copy Markdown
Member

Summary

  • route trash and permanent delete through the provider layer instead of assuming local filesystem paths
  • fall back from Trash to permanent delete for SMB/SFTP-style paths, and keep provider trash behavior for Google Drive
  • recursively delete SMB directories and avoid showing Finder restore hints for non-system trash responses

Verification

  • cargo check
  • cargo test locations::smb::tests --lib
  • npm run test:run -- src/tests/unit/store/useAppStore.test.ts

@gemini-code-assist
Copy link
Copy Markdown

Summary of Changes

Hello, I'm Gemini Code Assist1! I'm currently reviewing this pull request and will post my feedback shortly. In the meantime, here's a summary to help you and other reviewers quickly get up to speed!

This pull request significantly enhances file deletion capabilities by introducing a more robust and provider-agnostic approach. It centralizes deletion logic through a new provider layer, allowing for differentiated handling of various file systems like local, SMB, and Google Drive. This change improves reliability for network shares by implementing recursive deletion and intelligent fallback strategies, while also refining UI feedback for trash operations.

Highlights

  • Flexible Deletion Strategy: Implemented a flexible trash/delete strategy that routes operations through a provider layer, moving away from assumptions about local filesystem paths.
  • SMB/SFTP Fallback: Introduced a fallback mechanism for SMB/SFTP paths, where trash operations will default to permanent deletion, while Google Drive operations will continue to use provider-specific trash.
  • Recursive SMB Directory Deletion: Enabled recursive deletion for SMB directories, ensuring that directories and their contents can be properly removed.
  • Refined UI Feedback for Trash: Refined the user experience by preventing the display of macOS Finder restore hints when items are not moved to the system trash.
Changelog
  • src-tauri/src/commands.rs
    • Defined TrashStrategy enum to categorize deletion methods (SystemTrash, ProviderTrash, PermanentDeleteFallback).
    • Added trash_strategy_for_scheme function to determine the appropriate deletion strategy based on the location scheme.
    • Introduced DeleteTarget struct to encapsulate provider, location, and display path for deletion targets.
    • Implemented resolve_delete_targets to process input paths and identify their respective providers and locations.
    • Modified trash_paths to use the new deletion strategies, handle multiple provider types, and return used_system_trash status.
    • Updated delete_paths_permanently to utilize resolve_delete_targets and perform provider-specific permanent deletions.
    • Added used_system_trash field to TrashPathsResponse struct.
  • src-tauri/src/smb_sidecar/operations.rs
    • Added join_smb_path helper function for constructing SMB paths.
    • Implemented delete_directory_recursive to handle recursive deletion of SMB directories.
    • Updated the delete function to use delete_directory_recursive when deleting directories on SMB shares.
  • src/tests/unit/store/useAppStore.test.ts
    • Updated mock responses for trash_paths and delete_paths_permanently to include the new usedSystemTrash field.
  • src/store/useAppStore.ts
    • Adjusted the logic for displaying the "Restore via Finder if needed" message, now conditionally showing it only when the system trash was actually used.
  • src/types/index.ts
    • Added usedSystemTrash property to the TrashPathsResponse interface.
Activity
  • No specific activity (comments, reviews, progress updates) was found in the provided context.
Using Gemini Code Assist

The full guide for Gemini Code Assist can be found on our documentation page, here are some quick tips.

Invoking Gemini

You can request assistance from Gemini at any point by creating a comment using either /gemini <command> or @gemini-code-assist <command>. Below is a summary of the supported commands on the current page.

Feature Command Description
Code Review /gemini review Performs a code review for the current pull request in its current state.
Pull Request Summary /gemini summary Provides a summary of the current pull request in its current state.
Comment @gemini-code-assist Responds in comments when explicitly tagged, both in pull request comments and review comments.
Help /gemini help Displays a list of available commands.

Customization

To customize Gemini Code Assist for GitHub experience, repository maintainers can create a configuration file and/or provide a custom code review style guide (such as PEP-8 for Python) by creating and adding files to a .gemini/ folder in the base of the repository. Detailed instructions can be found here.

Limitations & Feedback

Gemini Code Assist may make mistakes. Please leave feedback on any instances where its feedback is incorrect or counter productive. You can react with 👍 and 👎 on @gemini-code-assist comments. If you're interested in giving your feedback about your experience with Gemini Code Assist for GitHub and other Google products, sign up here.

You can also get AI-powered code generation, chat, as well as code reviews directly in the IDE at no cost with the Gemini Code Assist IDE Extension.

Footnotes

  1. Review the Privacy Notices, Generative AI Prohibited Use Policy, Terms of Service, and learn how to configure Gemini Code Assist in GitHub here. Gemini can make mistakes, so double check it and use code with caution.

@BrianLeishman
Copy link
Copy Markdown
Member Author

/gemini review

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request effectively refactors the file deletion logic to support various providers like local filesystem, Google Drive, and SMB shares. The introduction of TrashStrategy and the provider-based approach in trash_paths and delete_paths_permanently are excellent architectural improvements. The backend changes are correctly reflected in the frontend, including the conditional display of the 'Restore via Finder' message. I have a couple of suggestions to make the Rust code in src-tauri/src/commands.rs more idiomatic, but overall, this is a solid contribution.

Comment thread src-tauri/src/commands.rs
Comment on lines +291 to +315
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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(), and collect(), you can transform the vector of paths into a Result containing a vector of DeleteTargets, while still handling errors gracefully as the collect() over an iterator of Results will short-circuit on the first Err.

    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()

Comment thread src-tauri/src/commands.rs
Comment on lines +2033 to +2036
if targets
.iter()
.any(|target| trash_strategy_for_scheme(target.location.scheme()) != strategy)
{
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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
if targets
.iter()
.any(|target| trash_strategy_for_scheme(target.location.scheme()) != strategy)
{
if targets
.iter()
.skip(1)
.any(|target| trash_strategy_for_scheme(target.location.scheme()) != strategy)
{

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a great architectural improvement, introducing a provider layer for deletion operations to correctly handle file deletions from SMB shares, including recursive directory deletion. The changes are well-structured, and the fallback from trash to permanent delete for providers that don't support it is a thoughtful UX decision. I have one suggestion to improve the performance of permanent deletion for multiple files.

Comment thread src-tauri/src/commands.rs
Comment on lines +2286 to +2314
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);
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

The current implementation for delete_paths_permanently deletes files sequentially. This can be slow when deleting multiple files from a remote source like SMB or GDrive. You could improve performance by parallelizing the delete operations.

Using futures::stream::try_for_each_concurrent would allow you to process multiple deletions in parallel while still stopping on the first error, preserving the current error handling behavior.

You'll need to add use futures::stream::{self, TryStreamExt}; at the top of the file.

    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);
    }

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request is a solid improvement, refactoring the file deletion logic to support various providers like local filesystem, SMB, and Google Drive. The introduction of TrashStrategy is a clean way to handle different deletion behaviors. The recursive deletion for SMB directories and the UI enhancements for clipboard shortcuts and trash notifications are well-executed. However, I've identified a critical performance regression in the permanent deletion logic for local files. A blocking I/O operation is now performed on the async runtime, which could freeze the UI. My review includes a detailed comment and a suggested fix for this issue.

Comment thread src-tauri/src/commands.rs
Comment on lines +2300 to 2313
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);
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

critical

The refactoring of delete_paths_permanently removed the tauri::async_runtime::spawn_blocking call. The new implementation awaits target.provider.delete() directly in an async loop.

This is a critical issue because for the local filesystem provider, the delete method performs blocking I/O. Executing this on the async runtime will freeze the UI when deleting large directories.

To fix this, blocking operations for the file scheme should be moved to a blocking thread. You can achieve this by wrapping the delete future in spawn_blocking and using futures::executor::block_on to run it to completion on the blocking thread.

Note: You may need to add use futures::executor; at the top of the file.

        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);
        }

Copy link
Copy Markdown

@gemini-code-assist gemini-code-assist Bot left a comment

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

Code Review

This pull request refactors file deletion to support different providers like SMB, which is a great improvement. The core logic is moved to a provider layer, and recursive deletion for SMB directories is added. The changes look solid. I've added a few suggestions: one to fix a potential path joining bug in the SMB implementation, another to improve performance by parallelizing delete operations for providers that support it, and a third to refactor a function for better readability and more helpful error messages.

Comment on lines +67 to +74
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)
}
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

high

The current implementation of join_smb_path incorrectly handles an empty parent path. If parent is an empty string, it will produce /{name} which is likely an incorrect path. An empty parent path should result in just {name}. This can be fixed by handling the empty parent path case correctly. I've also added an explicit check for the root path / for clarity.

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)
    }
}

Comment thread src-tauri/src/commands.rs
Comment on lines +291 to +315
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)
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

This function can be refactored to use a more idiomatic iterator-based approach with map and collect. This makes the code more concise. I've also taken the opportunity to improve the error message to include the path that caused the issue, which is helpful for debugging.

    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()

Comment thread src-tauri/src/commands.rs
Comment on lines +2055 to 2057
for target in &targets {
target.provider.delete(&target.location).await?;
}
Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

medium

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 futures::future::try_join_all. This will execute all delete futures concurrently and fail fast if any of them error, preserving the current error handling logic.

Suggested change
for target in &targets {
target.provider.delete(&target.location).await?;
}
let delete_futures = targets.iter().map(|target| target.provider.delete(&target.location));
futures::future::try_join_all(delete_futures).await?;

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment

Labels

None yet

Projects

None yet

Development

Successfully merging this pull request may close these issues.

1 participant