Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
104 changes: 88 additions & 16 deletions src/apps/desktop/src/api/commands.rs
Original file line number Diff line number Diff line change
Expand Up @@ -810,6 +810,8 @@ async fn search_remote_file_names_with_progress(
pub struct RenameFileRequest {
pub old_path: String,
pub new_path: String,
#[serde(default)]
pub remote_connection_id: Option<String>,
}

#[derive(Debug, Deserialize)]
Expand All @@ -822,22 +824,30 @@ pub struct ExportLocalFileRequest {
#[derive(Debug, Deserialize)]
pub struct DeleteFileRequest {
pub path: String,
#[serde(default, rename = "remoteConnectionId")]
pub remote_connection_id: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct DeleteDirectoryRequest {
pub path: String,
pub recursive: Option<bool>,
#[serde(default, rename = "remoteConnectionId")]
pub remote_connection_id: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct CreateFileRequest {
pub path: String,
#[serde(default, rename = "remoteConnectionId")]
pub remote_connection_id: Option<String>,
}

#[derive(Debug, Deserialize)]
pub struct CreateDirectoryRequest {
pub path: String,
#[serde(default, rename = "remoteConnectionId")]
pub remote_connection_id: Option<String>,
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -2952,7 +2962,13 @@ pub async fn rename_file(
state: State<'_, AppState>,
request: RenameFileRequest,
) -> Result<(), String> {
rename_path(&state, &request.old_path, &request.new_path).await
rename_path(
&state,
&request.old_path,
&request.new_path,
request.remote_connection_id.as_deref(),
)
.await
}

/// Copy a local file to another local path (binary-safe). Used for export and drag-upload into local workspaces.
Expand All @@ -2979,7 +2995,12 @@ pub async fn delete_file(
state: State<'_, AppState>,
request: DeleteFileRequest,
) -> Result<(), String> {
delete_desktop_file(&state, &request.path).await
delete_desktop_file(
&state,
&request.path,
request.remote_connection_id.as_deref(),
)
.await
}

#[tauri::command]
Expand All @@ -2988,23 +3009,39 @@ pub async fn delete_directory(
request: DeleteDirectoryRequest,
) -> Result<(), String> {
let recursive = request.recursive.unwrap_or(false);
delete_desktop_directory(&state, &request.path, recursive).await
delete_desktop_directory(
&state,
&request.path,
recursive,
request.remote_connection_id.as_deref(),
)
.await
}

#[tauri::command]
pub async fn create_file(
state: State<'_, AppState>,
request: CreateFileRequest,
) -> Result<(), String> {
create_empty_file(&state, &request.path).await
create_empty_file(
&state,
&request.path,
request.remote_connection_id.as_deref(),
)
.await
}

#[tauri::command]
pub async fn create_directory(
state: State<'_, AppState>,
request: CreateDirectoryRequest,
) -> Result<(), String> {
create_desktop_directory(&state, &request.path).await
create_desktop_directory(
&state,
&request.path,
request.remote_connection_id.as_deref(),
)
.await
}

#[derive(Debug, Deserialize)]
Expand Down Expand Up @@ -3123,7 +3160,7 @@ pub async fn reveal_in_explorer(
} else {
let normalized_path = path_str.replace("/", "\\");
bitfun_core::util::process_manager::create_command("explorer")
.args(["/select,", &normalized_path])
.arg(format!("/select,{}", normalized_path))
.spawn()
.map_err(|e| format!("Failed to open explorer: {}", e))?;
}
Expand All @@ -3146,17 +3183,52 @@ pub async fn reveal_in_explorer(

#[cfg(target_os = "linux")]
{
let target = if is_directory {
path.to_path_buf()
if is_directory {
bitfun_core::util::process_manager::create_command("xdg-open")
.arg(&path_str)
.spawn()
.map_err(|e| format!("Failed to open file manager: {}", e))?;
} else {
path.parent()
.ok_or_else(|| "Failed to get parent directory".to_string())?
.to_path_buf()
};
bitfun_core::util::process_manager::create_command("xdg-open")
.arg(target)
.spawn()
.map_err(|e| format!("Failed to open file manager: {}", e))?;
// On Linux there is no cross-desktop standard to select a specific
// file in the file manager. Try the freedesktop FileManager1 D-Bus
// interface (supported by Nautilus, Dolphin, Nemo) to highlight the
// file; fall back to opening the parent directory with xdg-open.
// Encode each path segment so spaces and other special characters
// do not break the dbus-send array:string: syntax (which splits on
// spaces) and produce a valid file:// URI.
let encoded_path: String = path
.to_string_lossy()
.split('/')
.map(|s| urlencoding::encode(s).to_string())
.collect::<Vec<_>>()
.join("/");
let file_uri = format!("file://{}", encoded_path);
let dbus_ok = match bitfun_core::util::process_manager::create_command("dbus-send")
.args([
"--session",
"--print-reply",
"--dest=org.freedesktop.FileManager1",
"/org/freedesktop/FileManager1",
"org.freedesktop.FileManager1.ShowItems",
&format!("array:string:{}", file_uri),
"string:",
])
.spawn()
{
Ok(mut child) => child.wait().map(|s| s.success()).unwrap_or(false),
Err(_) => false,
};

if !dbus_ok {
let parent = path
.parent()
.ok_or_else(|| "Failed to get parent directory".to_string())?;
bitfun_core::util::process_manager::create_command("xdg-open")
.arg(parent)
.spawn()
.map_err(|e| format!("Failed to open file manager: {}", e))?;
}
}
}

Ok(())
Expand Down
63 changes: 42 additions & 21 deletions src/apps/desktop/src/api/path_target.rs
Original file line number Diff line number Diff line change
Expand Up @@ -218,7 +218,9 @@ pub async fn read_text_file(
raw_path: &str,
preferred_remote_connection_id: Option<&str>,
) -> Result<String, String> {
match resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await? {
let target =
resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await?;
match &target {
DesktopPathTarget::Local { resolved_path, .. } => app_state
.filesystem_service
.read_file(&resolved_path.to_string_lossy())
Expand All @@ -234,7 +236,7 @@ pub async fn read_text_file(
.await
.map_err(|e| format!("Remote file service not available: {}", e))?;
let bytes = remote_fs
.read_file(&entry.connection_id, &requested_path)
.read_file(&entry.connection_id, requested_path)
.await
.map_err(|e| format!("Failed to read remote file: {}", e))?;
String::from_utf8(bytes).map_err(|e| format!("File is not valid UTF-8: {}", e))
Expand Down Expand Up @@ -352,22 +354,28 @@ pub async fn rename_path(
app_state: &AppState,
old_path: &str,
new_path: &str,
preferred_remote_connection_id: Option<&str>,
) -> Result<(), String> {
match resolve_desktop_path_target(app_state, old_path, None).await? {
match resolve_desktop_path_target(app_state, old_path, preferred_remote_connection_id).await? {
DesktopPathTarget::Local {
resolved_path: old_resolved_path,
..
} => {
let new_resolved_path =
match resolve_desktop_path_target(app_state, new_path, None).await? {
DesktopPathTarget::Local { resolved_path, .. } => resolved_path,
DesktopPathTarget::Remote { .. } => {
return Err(format!(
"Cannot rename local path '{}' to remote destination '{}'",
old_path, new_path
))
}
};
let new_resolved_path = match resolve_desktop_path_target(
app_state,
new_path,
preferred_remote_connection_id,
)
.await?
{
DesktopPathTarget::Local { resolved_path, .. } => resolved_path,
DesktopPathTarget::Remote { .. } => {
return Err(format!(
"Cannot rename local path '{}' to remote destination '{}'",
old_path, new_path
))
}
};

app_state
.filesystem_service
Expand All @@ -391,8 +399,12 @@ pub async fn rename_path(
}
}

pub async fn delete_file(app_state: &AppState, raw_path: &str) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, None).await? {
pub async fn delete_file(
app_state: &AppState,
raw_path: &str,
preferred_remote_connection_id: Option<&str>,
) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await? {
DesktopPathTarget::Local { resolved_path, .. } => app_state
.filesystem_service
.delete_file(&resolved_path.to_string_lossy())
Expand All @@ -418,8 +430,9 @@ pub async fn delete_directory(
app_state: &AppState,
raw_path: &str,
recursive: bool,
preferred_remote_connection_id: Option<&str>,
) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, None).await? {
match resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await? {
DesktopPathTarget::Local { resolved_path, .. } => app_state
.filesystem_service
.delete_directory(&resolved_path.to_string_lossy(), recursive)
Expand All @@ -440,16 +453,20 @@ pub async fn delete_directory(
.map_err(|e| format!("Failed to delete remote directory: {}", e))
} else {
remote_fs
.remove_dir_all(&entry.connection_id, &requested_path)
.remove_dir(&entry.connection_id, &requested_path)
.await
.map_err(|e| format!("Failed to delete remote directory: {}", e))
}
}
}
}

pub async fn create_empty_file(app_state: &AppState, raw_path: &str) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, None).await? {
pub async fn create_empty_file(
app_state: &AppState,
raw_path: &str,
preferred_remote_connection_id: Option<&str>,
) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await? {
DesktopPathTarget::Local { resolved_path, .. } => {
let options = FileOperationOptions::default();
app_state
Expand All @@ -475,8 +492,12 @@ pub async fn create_empty_file(app_state: &AppState, raw_path: &str) -> Result<(
}
}

pub async fn create_directory(app_state: &AppState, raw_path: &str) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, None).await? {
pub async fn create_directory(
app_state: &AppState,
raw_path: &str,
preferred_remote_connection_id: Option<&str>,
) -> Result<(), String> {
match resolve_desktop_path_target(app_state, raw_path, preferred_remote_connection_id).await? {
DesktopPathTarget::Local { resolved_path, .. } => app_state
.filesystem_service
.create_directory(&resolved_path.to_string_lossy())
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -599,6 +599,10 @@ impl RemoteFileService {
Err(unsupported())
}

pub async fn remove_dir(&self, _connection_id: &str, _path: &str) -> anyhow::Result<()> {
Err(unsupported())
}

pub async fn rename(
&self,
_connection_id: &str,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -327,6 +327,12 @@ impl RemoteFileService {
manager.sftp_rmdir(connection_id, path).await
}

/// Remove an empty directory via SFTP (non-recursive; fails if not empty)
pub async fn remove_dir(&self, connection_id: &str, path: &str) -> anyhow::Result<()> {
let manager = self.get_manager(connection_id).await?;
manager.sftp_rmdir(connection_id, path).await
}

/// Rename/move a remote file or directory via SFTP
pub async fn rename(
&self,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -128,8 +128,13 @@ impl RemoteWorkspaceRegistry {
.filter(|r| registration_matches_path(r, &path_norm))
.collect();

if let Some(pref) = preferred_connection_id {
candidates.retain(|r| r.connection_id == pref);
// Only use preferred_connection_id to disambiguate when multiple
// path-matching candidates exist. When there is exactly one match,
// the path is unambiguous and the preferred hint must not override it.
if candidates.len() > 1 {
if let Some(pref) = preferred_connection_id {
candidates.retain(|r| r.connection_id == pref);
}
}

let best_len = candidates.iter().map(|r| r.remote_root.len()).max()?;
Expand Down
Loading
Loading