put: check the SFTP close status after uploads

ssh2::File's Drop discards the close-handshake error ('too late to
recover'), so a quota or server-side abort surfacing in the final ACKs
was recorded as a successful upload of a truncated file. Close upload
handles explicitly and propagate the error; also applies to the ssh
context driver's write_file and upload_recursive, which had the same
silent-drop issue.
This commit is contained in:
2026-09-17 16:36:36 +02:00
parent efb18bfa37
commit 8ad50aaf83
2 changed files with 29 additions and 0 deletions
+19
View File
@@ -288,6 +288,13 @@ impl ContextDriver for SshDriver {
} }
let mut remote_file = sftp.create(path).map_err(io::Error::other)?; let mut remote_file = sftp.create(path).map_err(io::Error::other)?;
remote_file.write_all(content.as_bytes())?; remote_file.write_all(content.as_bytes())?;
// Close explicitly: the `Drop` impl of `ssh2::File` discards a
// close-time error ("too late to recover"), silently truncating the
// remote file. Writes are unbuffered (`Write::flush` is a no-op), so
// no flush is needed before closing.
remote_file.close().map_err(|e| {
io::Error::other(format!("Failed to close remote file {:?}: {}", path, e))
})?;
Ok(()) Ok(())
} }
@@ -327,6 +334,18 @@ impl SshDriver {
io::Error::other(format!("Failed to create remote file {:?}: {}", dest, e)) io::Error::other(format!("Failed to create remote file {:?}: {}", dest, e))
})?; })?;
io::copy(&mut file, &mut remote_file)?; io::copy(&mut file, &mut remote_file)?;
// Close explicitly: quota-exceeded and similar failures only
// surface in the final ACKs and the close handshake, and the
// `Drop` impl of `ssh2::File` discards that error ("too late to
// recover"), leaving a truncated remote file behind. Writes are
// unbuffered (`ssh2::File`'s `Write::flush` is a no-op), so no
// flush is needed before closing.
remote_file.close().map_err(|e| {
io::Error::other(format!(
"Failed to close remote file {:?} after upload: {}",
dest, e
))
})?;
} }
Ok(()) Ok(())
} }
+10
View File
@@ -433,6 +433,16 @@ pub fn upload_file(
bar.inc(n as u64); bar.inc(n as u64);
} }
// Close explicitly: quota-exceeded and similar failures only surface in
// the final ACKs and the close handshake, and the `Drop` impl of
// `ssh2::File` discards that error ("too late to recover"), recording a
// truncated remote file as a successful upload. `ssh2::File::write` is
// unbuffered (`Write::flush` is a documented no-op) and `close`
// finalizes the pending writes server-side, so no flush is needed.
remote_file
.close()
.map_err(|e| format!("failed to close remote file '{remote}' after upload: {e}"))?;
Ok(()) Ok(())
} }