tlc: Sprint 5 G-series — error dialog retry wiring + SFTP open_write
G1: Error dialog Retry now re-invokes the stored file operation (copy/move/
delete) using the PendingErrorOp parameters. Previously Retry just logged
'Retry not yet wired'. Copy/move/delete error paths now create a
PendingErrorOp + ErrorDialog instead of silently setting a status message.
Skip/Ignore/Abort outcomes produce appropriate status messages.
G2: SftpVfs::open_write now returns a working SftpWriter that buffers writes
and flushes to the remote SFTP server via session.write(). Previously
returned VfsError::Unsupported. The SftpWriter buffers in memory and
writes on flush/drop, matching the existing open_read buffer pattern.
1369 tests pass (default and --features sftp).
This commit is contained in:
@@ -1002,7 +1002,19 @@ impl FileManager {
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
self.status.set_message(format!("copy: {e}"));
|
||||
self.pending_error_op = Some(PendingErrorOp {
|
||||
kind: "copy".to_string(),
|
||||
sources: sources.clone(),
|
||||
dst: Some(dst.clone()),
|
||||
preserve_attributes,
|
||||
follow_links,
|
||||
});
|
||||
self.dialog = Some(DialogState::Error(Box::new(
|
||||
error_dialog::ErrorDialog::new(
|
||||
sources.first().map(|p| p.display().to_string()).unwrap_or_default(),
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1099,7 +1111,19 @@ impl FileManager {
|
||||
)));
|
||||
}
|
||||
Err(e) => {
|
||||
self.status.set_message(format!("move: {e}"));
|
||||
self.pending_error_op = Some(PendingErrorOp {
|
||||
kind: "move".to_string(),
|
||||
sources: sources.clone(),
|
||||
dst: Some(dst.clone()),
|
||||
preserve_attributes,
|
||||
follow_links,
|
||||
});
|
||||
self.dialog = Some(DialogState::Error(Box::new(
|
||||
error_dialog::ErrorDialog::new(
|
||||
sources.first().map(|p| p.display().to_string()).unwrap_or_default(),
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1120,7 +1144,19 @@ impl FileManager {
|
||||
let _ = self.active_panel_mut().refresh();
|
||||
}
|
||||
Err(e) => {
|
||||
self.status.set_message(format!("delete: {e}"));
|
||||
self.pending_error_op = Some(PendingErrorOp {
|
||||
kind: "delete".to_string(),
|
||||
sources: paths.clone(),
|
||||
dst: None,
|
||||
preserve_attributes: false,
|
||||
follow_links: false,
|
||||
});
|
||||
self.dialog = Some(DialogState::Error(Box::new(
|
||||
error_dialog::ErrorDialog::new(
|
||||
paths.first().map(|p| p.display().to_string()).unwrap_or_default(),
|
||||
e.to_string(),
|
||||
),
|
||||
)));
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -1219,28 +1255,62 @@ impl FileManager {
|
||||
}
|
||||
Some(DialogState::Error(d)) => {
|
||||
use error_dialog::ErrorOutcome;
|
||||
if let Some(_op) = self.pending_error_op.take() {
|
||||
if let Some(op) = self.pending_error_op.take() {
|
||||
if d.is_finished() {
|
||||
match d.outcome {
|
||||
ErrorOutcome::Retry => {
|
||||
self.status.set_message(
|
||||
"Retry not yet wired (requires batch step resumption)".to_string(),
|
||||
let handle = self.ops_manager.begin(
|
||||
match op.kind.as_str() {
|
||||
"move" => crate::ops::OpKind::Move,
|
||||
"delete" => crate::ops::OpKind::Delete,
|
||||
_ => crate::ops::OpKind::Copy,
|
||||
},
|
||||
op.sources.clone(),
|
||||
op.dst.clone(),
|
||||
);
|
||||
let result = match op.kind.as_str() {
|
||||
"delete" => crate::ops::delete::delete_many(&op.sources, &handle),
|
||||
"move" => crate::ops::move_op::move_many(
|
||||
&op.sources,
|
||||
op.dst.as_deref().unwrap_or_else(|| std::path::Path::new("/")),
|
||||
&handle,
|
||||
false,
|
||||
op.preserve_attributes,
|
||||
op.follow_links,
|
||||
),
|
||||
_ => crate::ops::copy::copy_many(
|
||||
&op.sources,
|
||||
op.dst.as_deref().unwrap_or_else(|| std::path::Path::new("/")),
|
||||
&handle,
|
||||
false,
|
||||
op.preserve_attributes,
|
||||
op.follow_links,
|
||||
),
|
||||
};
|
||||
match result {
|
||||
Ok(()) => {
|
||||
self.ops_manager.finish();
|
||||
self.status.set_message(format!("{}: retried successfully", op.kind));
|
||||
let _ = self.active_panel_mut().refresh();
|
||||
}
|
||||
Err(e) => {
|
||||
self.status.set_message(format!("{} retry: {e}", op.kind));
|
||||
}
|
||||
}
|
||||
}
|
||||
ErrorOutcome::Skip
|
||||
| ErrorOutcome::Ignore
|
||||
| ErrorOutcome::Abort => {
|
||||
self.status.set_message(format!(
|
||||
"{}: aborted (see pending error op)",
|
||||
d.failed_path
|
||||
));
|
||||
| ErrorOutcome::Ignore => {
|
||||
self.status.set_message(format!("{}: skipped ({})", op.kind, d.failed_path));
|
||||
}
|
||||
ErrorOutcome::Abort => {
|
||||
self.status.set_message(format!("{}: aborted", op.kind));
|
||||
}
|
||||
ErrorOutcome::Running => {
|
||||
self.pending_error_op = Some(_op);
|
||||
self.pending_error_op = Some(op);
|
||||
}
|
||||
}
|
||||
} else {
|
||||
self.pending_error_op = Some(_op);
|
||||
self.pending_error_op = Some(op);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -408,20 +408,67 @@ impl Vfs for SftpVfs {
|
||||
}
|
||||
|
||||
fn open_write(&self, p: &VfsPath) -> Result<Box<dyn Write + Send>, VfsError> {
|
||||
let _ = sftp_path(p)?;
|
||||
Err(VfsError::Unsupported("open_write"))
|
||||
let path = sftp_path(p)?;
|
||||
Ok(Box::new(SftpWriter {
|
||||
session: Arc::clone(&self.session),
|
||||
handle: self.runtime.handle().clone(),
|
||||
path,
|
||||
buffer: Vec::new(),
|
||||
flushed: false,
|
||||
}))
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SftpVfs {
|
||||
fn drop(&mut self) {
|
||||
// Best-effort: close the SftpSession before dropping the
|
||||
// runtime. We swallow errors — Drop cannot propagate.
|
||||
let session = Arc::clone(&self.session);
|
||||
let _ = self.runtime.block_on(async move { session.close().await });
|
||||
}
|
||||
}
|
||||
|
||||
struct SftpWriter {
|
||||
session: Arc<SftpSession>,
|
||||
handle: tokio::runtime::Handle,
|
||||
path: String,
|
||||
buffer: Vec<u8>,
|
||||
flushed: bool,
|
||||
}
|
||||
|
||||
impl Write for SftpWriter {
|
||||
fn write(&mut self, buf: &[u8]) -> io::Result<usize> {
|
||||
if self.flushed {
|
||||
return Err(io::Error::new(
|
||||
io::ErrorKind::WriteZero,
|
||||
"sftp writer already flushed",
|
||||
));
|
||||
}
|
||||
self.buffer.extend_from_slice(buf);
|
||||
Ok(buf.len())
|
||||
}
|
||||
|
||||
fn flush(&mut self) -> io::Result<()> {
|
||||
if self.flushed {
|
||||
return Ok(());
|
||||
}
|
||||
let session = Arc::clone(&self.session);
|
||||
let path = self.path.clone();
|
||||
let data = std::mem::take(&mut self.buffer);
|
||||
self.handle
|
||||
.block_on(async move { session.write(&path, &data).await })
|
||||
.map_err(|e| io::Error::other(format!("sftp write: {e}")))?;
|
||||
self.flushed = true;
|
||||
Ok(())
|
||||
}
|
||||
}
|
||||
|
||||
impl Drop for SftpWriter {
|
||||
fn drop(&mut self) {
|
||||
if !self.flushed {
|
||||
let _ = self.flush();
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
#[cfg(test)]
|
||||
mod tests {
|
||||
use super::*;
|
||||
|
||||
Reference in New Issue
Block a user