feat: support editing remote text files with local system editor

- Add right-click 'Edit' context menu option for common text formats.

- Automatically download file to a temporary directory and open in default editor.

- Background daemon listens for save events and auto-uploads changes quietly.

- Isolate auto-sync from transfer history UI.

- Clear legacy session config tmp files on application startup.
This commit is contained in:
TomZz
2026-06-12 07:14:55 +08:00
parent f1186e4c84
commit 68439bc599
7 changed files with 452 additions and 52 deletions
Generated
+93 -4
View File
@@ -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",
+2
View File
@@ -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
+5 -1
View File
@@ -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."
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}"
+5 -1
View File
@@ -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 快捷键进行复制和粘贴。"
copy_paste_hint: "提示:不管是否开启此开关,都可以使用 %{key} + C/V 快捷键进行复制和粘贴。"
edit_file: "编辑文件"
edit_file_tooltip: "下载到临时目录,使用默认编辑器打开,并在保存后自动上传。"
auto_saved_and_uploaded: "已自动保存并上传: %{base}"
+7
View File
@@ -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<PathBuf> {
self.path.parent().map(|p| p.join("tmp"))
}
pub fn follow_system_theme(&self) -> bool {
self.cache.follow_system_theme
}
+52 -9
View File
@@ -2154,6 +2154,18 @@ impl Ashell {
cx.notify();
}
fn trigger_sftp_context_edit(&mut self, cx: &mut Context<Self>) {
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
}
+288 -37
View File
@@ -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<String>,
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<crate::terminal::BackendEvent>,
@@ -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<String, TransferStateFlag> = std::collections::HashMap::new();
let mut active_transfers: std::collections::HashMap<String, TransferStateFlag> =
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<notify::Event>| {
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::<Vec<_>>().join("/");
let rel = relative
.components()
.map(|c| c.as_os_str().to_string_lossy().to_string())
.collect::<Vec<_>>()
.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<BackendEvent>,
@@ -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(),