diff --git a/Cargo.lock b/Cargo.lock index ba7a17b..6b9b1ac 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -299,6 +299,8 @@ dependencies = [ "gpui_platform", "image", "menu", + "notify 6.1.1", + "open", "portable-pty", "rfd", "russh", @@ -1414,6 +1416,15 @@ dependencies = [ "cfg-if", ] +[[package]] +name = "crossbeam-channel" +version = "0.5.15" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "82b8f8f868b36967f9606790d1903570de9ceaf870a7bf9fbbd3016d636a2cb2" +dependencies = [ + "crossbeam-utils", +] + [[package]] name = "crossbeam-deque" version = "0.8.6" @@ -2696,7 +2707,7 @@ dependencies = [ "lsp-types", "markdown", "markup5ever_rcdom", - "notify", + "notify 7.0.0", "num-traits", "once_cell", "paste", @@ -3458,6 +3469,17 @@ dependencies = [ "serde_core", ] +[[package]] +name = "inotify" +version = "0.9.6" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "f8069d3ec154eb856955c1c0fbffefbf5f3c40a104ec912d4797314c1801abff" +dependencies = [ + "bitflags 1.3.2", + "inotify-sys", + "libc", +] + [[package]] name = "inotify" version = "0.10.2" @@ -3547,6 +3569,25 @@ version = "2.12.0" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "d98f6fed1fde3f8c21bc40a1abb88dd75e67924f9cffc3ef95607bad8017f8e2" +[[package]] +name = "is-docker" +version = "0.2.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "928bae27f42bc99b60d9ac7334e3a21d10ad8f1835a4e12ec3ec0464765ed1b3" +dependencies = [ + "once_cell", +] + +[[package]] +name = "is-wsl" +version = "0.4.0" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "173609498df190136aa7dea1a91db051746d339e18476eed5ca40521f02d7aa5" +dependencies = [ + "is-docker", + "once_cell", +] + [[package]] name = "itertools" version = "0.11.0" @@ -4109,6 +4150,18 @@ dependencies = [ "simd-adler32", ] +[[package]] +name = "mio" +version = "0.8.11" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "a4a650543ca06a924e8b371db273b2756685faae30f8487da1b56505a8f78b0c" +dependencies = [ + "libc", + "log", + "wasi", + "windows-sys 0.48.0", +] + [[package]] name = "mio" version = "1.2.1" @@ -4270,6 +4323,25 @@ dependencies = [ "windows-sys 0.61.2", ] +[[package]] +name = "notify" +version = "6.1.1" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "6205bd8bb1e454ad2e27422015fb5e4f2bcc7e08fa8f27058670d208324a4d2d" +dependencies = [ + "bitflags 2.12.1", + "crossbeam-channel", + "filetime", + "fsevent-sys", + "inotify 0.9.6", + "kqueue", + "libc", + "log", + "mio 0.8.11", + "walkdir", + "windows-sys 0.48.0", +] + [[package]] name = "notify" version = "7.0.0" @@ -4279,11 +4351,11 @@ dependencies = [ "bitflags 2.12.1", "filetime", "fsevent-sys", - "inotify", + "inotify 0.10.2", "kqueue", "libc", "log", - "mio", + "mio 1.2.1", "notify-types", "walkdir", "windows-sys 0.52.0", @@ -4707,6 +4779,17 @@ version = "0.3.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c08d65885ee38876c4f86fa503fb49d7b507c2b62552df7c70b2fce627e06381" +[[package]] +name = "open" +version = "5.3.5" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "2fbaa89d2ddc8473c78a3adf69eea8cffa28c483b8e02a971ef31527cd0fc92c" +dependencies = [ + "is-wsl", + "libc", + "pathdiff", +] + [[package]] name = "openssl-probe" version = "0.2.1" @@ -4832,6 +4915,12 @@ version = "0.1.1" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "35fb2e5f958ec131621fdd531e9fc186ed768cbe395337403ae56c17a74c68ec" +[[package]] +name = "pathdiff" +version = "0.2.3" +source = "registry+https://github.com/rust-lang/crates.io-index" +checksum = "df94ce210e5bc13cb6651479fa48d14f601d9858cfe0467f43ae157023b938d3" + [[package]] name = "pathfinder_geometry" version = "0.5.1" @@ -7418,7 +7507,7 @@ checksum = "8fc7f01b389ac15039e4dc9531aa973a135d7a4135281b12d7c1bc79fd57fffe" dependencies = [ "bytes", "libc", - "mio", + "mio 1.2.1", "pin-project-lite", "socket2", "tokio-macros", diff --git a/Cargo.toml b/Cargo.toml index 78e66d4..27817d5 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -40,6 +40,8 @@ walkdir = "2" zip = { version = "2", default-features = false, features = ["deflate"] } image = { version = "0.25.9", default-features = false, features = ["png"] } futures = "0.3.32" +notify = "6.1.1" +open = "5.1" [profile.release] opt-level = 3 diff --git a/locales/en.yml b/locales/en.yml index 3bb49a6..c87ab27 100644 --- a/locales/en.yml +++ b/locales/en.yml @@ -6,6 +6,7 @@ upload_file: "Upload File" upload_folder: "Upload Folder" hidden: "Hidden" download: "Download" +download_folder: "Download Folder" download_count: "Download (%{count})" name: "NAME" size: "SIZE" @@ -94,4 +95,7 @@ session: "Session" software_builtin: "Built-in" retry: "Retry" right_click_copy_paste: "Right-click Copy/Paste" -copy_paste_hint: "Tip: Regardless of this switch, you can always use %{key} + C/V to copy/paste." \ No newline at end of file +copy_paste_hint: "Tip: Regardless of this switch, you can always use %{key} + C/V to copy/paste." +edit_file: "Edit File" +edit_file_tooltip: "Download to temporary directory, open in default editor, and auto upload on save." +auto_saved_and_uploaded: "Auto-saved & uploaded: %{base}" diff --git a/locales/zh-CN.yml b/locales/zh-CN.yml index 001cde5..67b355a 100644 --- a/locales/zh-CN.yml +++ b/locales/zh-CN.yml @@ -6,6 +6,7 @@ upload_file: "上传文件" upload_folder: "上传文件夹" hidden: "显示隐藏文件" download: "下载" +download_folder: "下载文件夹" download_count: "下载 (%{count})" name: "名称" size: "大小" @@ -94,4 +95,7 @@ session: "会话" software_builtin: "软件内置" retry: "重试" right_click_copy_paste: "右键复制/粘贴" -copy_paste_hint: "提示:不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。" \ No newline at end of file +copy_paste_hint: "提示:不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。" +edit_file: "编辑文件" +edit_file_tooltip: "下载到临时目录,使用默认编辑器打开,并在保存后自动上传。" +auto_saved_and_uploaded: "已自动保存并上传: %{base}" diff --git a/src/config.rs b/src/config.rs index d26a71f..20b0f1d 100644 --- a/src/config.rs +++ b/src/config.rs @@ -160,6 +160,9 @@ impl ConfigStore { if let Some(parent) = path.parent() { fs::create_dir_all(parent) .with_context(|| format!("failed to create config dir {}", parent.display()))?; + let tmp_dir = parent.join("tmp"); + let _ = fs::remove_dir_all(&tmp_dir); + let _ = fs::create_dir_all(&tmp_dir); } let cache = if path.exists() { @@ -193,6 +196,10 @@ impl ConfigStore { &self.cache.sessions } + pub fn tmp_dir(&self) -> Option { + self.path.parent().map(|p| p.join("tmp")) + } + pub fn follow_system_theme(&self) -> bool { self.cache.follow_system_theme } diff --git a/src/main.rs b/src/main.rs index 51bc487..abdabe8 100644 --- a/src/main.rs +++ b/src/main.rs @@ -2154,6 +2154,18 @@ impl Ashell { cx.notify(); } + fn trigger_sftp_context_edit(&mut self, cx: &mut Context) { + let Some(menu) = self.sftp_context_menu.take() else { + return; + }; + if let Some(id) = self.active_tab.clone() { + if let Some(handle) = self.sftp_handles.get(&id) { + handle.edit_file(menu.remote_path); + } + } + cx.notify(); + } + fn download_sftp_entry( &mut self, remote_path: String, @@ -4118,9 +4130,9 @@ impl Render for Ashell { .children(Root::render_sheet_layer(window, cx)) .when_some(self.sftp_context_menu.clone(), |this, menu| { let label = if menu.is_dir { - "Download Folder" + t!("download_folder").to_string() } else { - "Download" + t!("download").to_string() }; this.child( div() @@ -4156,14 +4168,31 @@ impl Render for Ashell { cx.stop_propagation(); }) .child( - Button::new("sftp-context-download") - .ghost() + v_flex() .w_full() - .justify_start() - .label(label) - .on_click(cx.listener(|this, _, window, cx| { - this.trigger_sftp_context_download(window, cx); - })), + .child( + Button::new("sftp-context-download") + .ghost() + .w_full() + .justify_start() + .label(label) + .on_click(cx.listener(|this, _, window, cx| { + this.trigger_sftp_context_download(window, cx); + })), + ) + .when(!menu.is_dir && is_editable_text_file(&menu.remote_path), |this| { + this.child( + Button::new("sftp-context-edit") + .ghost() + .w_full() + .justify_start() + .label(t!("edit_file")) + .tooltip(t!("edit_file_tooltip").to_string()) + .on_click(cx.listener(|this, _, _, cx| { + this.trigger_sftp_context_edit(cx); + })) + ) + }), ), ), ) @@ -4471,3 +4500,17 @@ fn main() { open_main_window(cx); }); } + +fn is_editable_text_file(filename: &str) -> bool { + let lower = filename.to_lowercase(); + let ext = std::path::Path::new(&lower).extension().and_then(|s| s.to_str()).unwrap_or(""); + let known_exts = ["txt", "conf", "json", "yaml", "yml", "xml", "ini", "sh", "py", "rs", "js", "ts", "html", "css", "md", "toml", "csv", "log", "cfg"]; + if known_exts.contains(&ext) { + return true; + } + let known_names = ["dockerfile", "makefile", ".gitignore", ".env"]; + if known_names.contains(&lower.as_str()) { + return true; + } + false +} diff --git a/src/sftp.rs b/src/sftp.rs index ca52347..aa9a566 100644 --- a/src/sftp.rs +++ b/src/sftp.rs @@ -58,6 +58,13 @@ pub enum SftpCommand { remote: String, local_dir: String, }, + EditFile { + remote_path: String, + }, + UploadEditedFile { + local_path: String, + remote_path: String, + }, UploadPaths { locals: Vec, remote_dir: String, @@ -77,11 +84,17 @@ impl TransferStateFlag { pub fn new() -> Self { Self(Arc::new(AtomicU8::new(0))) } - - pub fn pause(&self) { self.0.store(1, Ordering::SeqCst); } - pub fn resume(&self) { self.0.store(0, Ordering::SeqCst); } - pub fn cancel(&self) { self.0.store(2, Ordering::SeqCst); } - + + pub fn pause(&self) { + self.0.store(1, Ordering::SeqCst); + } + pub fn resume(&self) { + self.0.store(0, Ordering::SeqCst); + } + pub fn cancel(&self) { + self.0.store(2, Ordering::SeqCst); + } + pub async fn yield_if_paused( &self, events: &std::sync::mpsc::Sender, @@ -161,6 +174,10 @@ impl SftpHandle { .send(SftpCommand::UploadPaths { locals, remote_dir }); } + pub fn edit_file(&self, remote_path: String) { + let _ = self.commands.send(SftpCommand::EditFile { remote_path }); + } + pub fn close(&self) { let _ = self.commands.send(SftpCommand::Close); } @@ -187,7 +204,15 @@ pub fn spawn_sftp( let (cmd_tx, cmd_rx) = mpsc::unbounded_channel(); let cmd_tx_clone = cmd_tx.clone(); let join = runtime.spawn(async move { - if let Err(err) = run_sftp(tab_id.clone(), session, cmd_rx, cmd_tx_clone, events.clone()).await { + if let Err(err) = run_sftp( + tab_id.clone(), + session, + cmd_rx, + cmd_tx_clone, + events.clone(), + ) + .await + { let _ = events.send(BackendEvent::SftpStatus { tab_id, text: format!("sftp error: {err:#}"), @@ -231,7 +256,8 @@ async fn run_sftp( .unwrap_or_else(|_| "/".to_string()); emit_entries(&events, &tab_id, &sftp, &home).await?; - let mut active_transfers: std::collections::HashMap = std::collections::HashMap::new(); + let mut active_transfers: std::collections::HashMap = + std::collections::HashMap::new(); while let Some(command) = commands.recv().await { match command { @@ -308,16 +334,33 @@ async fn run_sftp( let commands_tx_clone = commands_tx.clone(); tokio::spawn(async move { - let Ok(channel) = handle_clone.channel_open_session().await else { return }; - let Ok(_) = channel.request_subsystem(true, "sftp").await else { return }; - let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { return }; - + let Ok(channel) = handle_clone.channel_open_session().await else { + return; + }; + let Ok(_) = channel.request_subsystem(true, "sftp").await else { + return; + }; + let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { + return; + }; + let _ = events_clone.send(BackendEvent::SftpStatus { tab_id: tab_id_clone.clone(), text: t!("downloading_file", base = base_name(&remote)).to_string(), }); - match download_path_impl(&handle_clone, &sftp_session, &remote, Path::new(&local_dir), flag, &events_clone, &tab_id_clone, &id).await { + match download_path_impl( + &handle_clone, + &sftp_session, + &remote, + Path::new(&local_dir), + flag, + &events_clone, + &tab_id_clone, + &id, + ) + .await + { Ok(summary) => { let _ = events_clone.send(BackendEvent::SftpStatus { tab_id: tab_id_clone, @@ -364,7 +407,12 @@ async fn run_sftp( } else if file_count == 0 && folder_count > 0 { t!("n_folders", folders = folder_count).to_string() } else { - t!("n_files_and_folders", files = file_count, folders = folder_count).to_string() + t!( + "n_files_and_folders", + files = file_count, + folders = folder_count + ) + .to_string() } }; @@ -387,16 +435,32 @@ async fn run_sftp( let commands_tx_clone = commands_tx.clone(); tokio::spawn(async move { - let Ok(channel) = handle_clone.channel_open_session().await else { return }; - let Ok(_) = channel.request_subsystem(true, "sftp").await else { return }; - let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { return }; - + let Ok(channel) = handle_clone.channel_open_session().await else { + return; + }; + let Ok(_) = channel.request_subsystem(true, "sftp").await else { + return; + }; + let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { + return; + }; + let _ = events_clone.send(BackendEvent::SftpStatus { tab_id: tab_id_clone.clone(), text: t!("uploading").to_string(), }); - match upload_paths_impl(&sftp_session, &locals, &remote_dir, flag, &events_clone, &tab_id_clone, &id).await { + match upload_paths_impl( + &sftp_session, + &locals, + &remote_dir, + flag, + &events_clone, + &tab_id_clone, + &id, + ) + .await + { Ok(summary) => { let _ = events_clone.send(BackendEvent::SftpStatus { tab_id: tab_id_clone.clone(), @@ -422,6 +486,144 @@ async fn run_sftp( let _ = commands_tx_clone.send(SftpCommand::TransferFinished(id)); }); } + SftpCommand::EditFile { remote_path } => { + let id = uuid::Uuid::new_v4().to_string(); + let config = crate::config::ConfigStore::load().unwrap(); + let tmp_dir = config.tmp_dir().unwrap_or_else(|| PathBuf::from("/tmp")); + let base = base_name(&remote_path); + let local_path = tmp_dir.join(format!("{}-{}", id, base)); + + let handle_clone = handle.clone(); + let commands_tx_clone = commands_tx.clone(); + let events_clone = events.clone(); + let tab_id_clone = tab_id.clone(); + + tokio::spawn(async move { + let flag = TransferStateFlag::new(); + let Ok(channel) = handle_clone.channel_open_session().await else { + return; + }; + let Ok(_) = channel.request_subsystem(true, "sftp").await else { + return; + }; + let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { + return; + }; + + let _ = events_clone.send(BackendEvent::SftpStatus { + tab_id: tab_id_clone.clone(), + text: t!("downloading_file", base = base).to_string(), + }); + + if let Err(err) = download_file_impl( + &sftp_session, + &remote_path, + &local_path, + &flag, + &events_clone, + &tab_id_clone, + "edit-download", + ) + .await + { + let _ = events_clone.send(BackendEvent::SftpStatus { + tab_id: tab_id_clone.clone(), + text: format!("Edit download failed: {err:#}"), + }); + return; + } + + if let Err(err) = open::that(&local_path) { + let _ = events_clone.send(BackendEvent::SftpStatus { + tab_id: tab_id_clone.clone(), + text: format!("Failed to open editor: {err:#}"), + }); + return; + } + + use notify::Watcher; + let (tx, mut rx) = tokio::sync::mpsc::unbounded_channel(); + let mut watcher = match notify::recommended_watcher( + move |res: notify::Result| { + if let Ok(event) = res { + if event.kind.is_modify() { + let _ = tx.send(()); + } + } + }, + ) { + Ok(w) => w, + Err(_) => return, + }; + + if let Err(_) = watcher.watch(&local_path, notify::RecursiveMode::NonRecursive) + { + return; + } + + while let Some(_) = rx.recv().await { + tokio::time::sleep(std::time::Duration::from_millis(500)).await; + while let Ok(_) = rx.try_recv() {} // drain pending + + if commands_tx_clone.send(SftpCommand::UploadEditedFile { + local_path: local_path.to_string_lossy().to_string(), + remote_path: remote_path.clone(), + }).is_err() { + break; + } + } + }); + } + SftpCommand::UploadEditedFile { + local_path, + remote_path, + } => { + let handle_clone = handle.clone(); + let events_clone = events.clone(); + let tab_id_clone = tab_id.clone(); + + tokio::spawn(async move { + let flag = TransferStateFlag::new(); + let Ok(channel) = handle_clone.channel_open_session().await else { + return; + }; + let Ok(_) = channel.request_subsystem(true, "sftp").await else { + return; + }; + let Ok(sftp_session) = SftpSession::new(channel.into_stream()).await else { + return; + }; + + let transferred = Arc::new(AtomicU64::new(0)); + match upload_file_impl( + &sftp_session, + Path::new(&local_path), + &remote_path, + &flag, + &events_clone, + &tab_id_clone, + "edit-upload", + transferred, + None, + ) + .await + { + Ok(_) => { + let now = chrono::Local::now().format("%H:%M:%S"); + let _ = events_clone.send(BackendEvent::SftpStatus { + tab_id: tab_id_clone.clone(), + text: format!("{} ({})", t!("auto_saved_and_uploaded", base = base_name(&remote_path)), now), + }); + } + Err(err) => { + let _ = events_clone.send(BackendEvent::SftpStatus { + tab_id: tab_id_clone.clone(), + text: format!("Auto-upload failed: {err:#}"), + }); + } + } + }); + } } } @@ -736,8 +938,17 @@ async fn download_path_impl( base_name(remote), Uuid::new_v4() )); - let extracted_to = - download_remote_directory_archive(handle, sftp, remote, &local_archive, &flag, events, tab_id, id).await?; + let extracted_to = download_remote_directory_archive( + handle, + sftp, + remote, + &local_archive, + &flag, + events, + tab_id, + id, + ) + .await?; return Ok(t!("downloaded_folder", path = extracted_to.display()).to_string()); } @@ -763,9 +974,27 @@ async fn download_dir_recursive( for entry in entries { let local_path = local_dir.join(&entry.name); if entry.is_dir { - Box::pin(download_dir_recursive(sftp, &entry.full_path, &local_path, flag, events, tab_id, id)).await?; + Box::pin(download_dir_recursive( + sftp, + &entry.full_path, + &local_path, + flag, + events, + tab_id, + id, + )) + .await?; } else { - download_file_impl(sftp, &entry.full_path, &local_path, flag, events, tab_id, id).await?; + download_file_impl( + sftp, + &entry.full_path, + &local_path, + flag, + events, + tab_id, + id, + ) + .await?; let _ = maybe_extract_archive(&local_path).await; } } @@ -794,7 +1023,16 @@ async fn download_remote_directory_archive( .join(base_name(remote_dir)); let archive_download = async { - download_file_impl(sftp, &remote_archive, local_archive, flag, events, tab_id, id).await?; + download_file_impl( + sftp, + &remote_archive, + local_archive, + flag, + events, + tab_id, + id, + ) + .await?; extract_archive_to( local_archive, local_archive.parent().unwrap_or_else(|| Path::new(".")), @@ -839,7 +1077,8 @@ async fn download_file_impl( let mut buffer = vec![0u8; 128 * 1024]; loop { - flag.yield_if_paused(events, tab_id, id, transferred, total).await?; + flag.yield_if_paused(events, tab_id, id, transferred, total) + .await?; let read = remote_file .read(&mut buffer) .await @@ -851,7 +1090,7 @@ async fn download_file_impl( .write_all(&buffer[..read]) .await .with_context(|| format!("write {}", local.display()))?; - + transferred += read as u64; let _ = events.send(BackendEvent::TransferProgress { tab_id: tab_id.to_string(), @@ -862,7 +1101,7 @@ async fn download_file_impl( }); } local_file.flush().await.context("flush local file")?; - + let _ = events.send(BackendEvent::TransferProgress { tab_id: tab_id.to_string(), id: id.to_string(), @@ -870,7 +1109,7 @@ async fn download_file_impl( total, state: crate::terminal::TransferState::Completed, }); - + Ok(()) } @@ -886,7 +1125,7 @@ async fn upload_paths_impl( create_remote_dir_all(sftp, remote_dir).await?; let mut file_count = 0usize; let mut folder_count = 0usize; - + let mut total_bytes = 0u64; let mut files_to_upload = Vec::new(); let mut dirs_to_create = Vec::new(); @@ -902,14 +1141,20 @@ async fn upload_paths_impl( for entry in WalkDir::new(&p) { let entry = entry?; let path = entry.path(); - if path == p { continue; } + if path == p { + continue; + } if let Ok(meta) = tokio::fs::metadata(&path).await { let relative = path.strip_prefix(&p)?; let remote_path = if relative.as_os_str().is_empty() { remote_root.clone() } else { - let rel = relative.components().map(|c| c.as_os_str().to_string_lossy().to_string()).collect::>().join("/"); + let rel = relative + .components() + .map(|c| c.as_os_str().to_string_lossy().to_string()) + .collect::>() + .join("/"); join_remote(&remote_root, &rel) }; @@ -955,7 +1200,8 @@ async fn upload_paths_impl( &id_clone, transferred_clone, Some(total_bytes), - ).await + ) + .await }); } @@ -964,7 +1210,7 @@ async fn upload_paths_impl( while let Some(res) = stream.next().await { res?; } - + let _ = events.send(BackendEvent::TransferProgress { tab_id: tab_id.to_string(), id: id.to_string(), @@ -972,7 +1218,7 @@ async fn upload_paths_impl( total: Some(total_bytes), state: crate::terminal::TransferState::Completed, }); - + let summary = if file_count == 1 && folder_count == 0 { t!("uploaded_file").to_string() } else if file_count == 0 && folder_count == 1 { @@ -982,14 +1228,19 @@ async fn upload_paths_impl( } else if file_count == 0 && folder_count > 0 { t!("uploaded_n_folders", folders = folder_count).to_string() } else { - t!("uploaded_files_and_folders", files = file_count, folders = folder_count).to_string() + t!( + "uploaded_files_and_folders", + files = file_count, + folders = folder_count + ) + .to_string() }; Ok(summary) } async fn upload_file_impl( - sftp: &SftpSession, - local_file: &Path, + sftp: &SftpSession, + local_file: &Path, remote_path: &str, flag: &TransferStateFlag, events: &std::sync::mpsc::Sender, @@ -1018,7 +1269,7 @@ async fn upload_file_impl( .write_all(&buffer[..read]) .await .with_context(|| format!("write remote {remote_path}"))?; - + let new_cur = transferred.fetch_add(read as u64, Ordering::Relaxed) + read as u64; let _ = events.send(BackendEvent::TransferProgress { tab_id: tab_id.to_string(),