feat(zmodem): enhance file upload conflict handling and user prompts (#270)
- Introduced a new conflict mode for Zmodem uploads, allowing users to choose between "overwrite" and "skip" options when handling duplicate files. - Updated the `TransferDuplicateDialog` to display different titles and descriptions based on the verification status of the upload request. - Enhanced the backend logic to support conflict mode during file uploads, ensuring proper handling of user choices. - Added new functions to resolve unverified uploads and manage conflict modes effectively.
This commit is contained in:
1 parent
bcc9235163
commit
03b76c44dc
17 files changed
+310
-46
No files matched your search
+2
@@ -25,6 +25,8 @@ pub enum Error {
|
||||
Read(String),
|
||||
#[error("out of memory")]
|
||||
OutOfMemory,
|
||||
#[error("remote aborted transfer")]
|
||||
RemoteAborted,
|
||||
#[error("unexpected CRC-16")]
|
||||
UnexpectedCrc16,
|
||||
#[error("unexpected CRC-32")]
|
||||
|
||||
+27
-2
@@ -672,8 +672,9 @@ impl Sender {
|
||||
fn queue_zfile(&mut self) -> Result<(), Error> {
|
||||
let file_size = self.file_size;
|
||||
let file_name = &self.file_name;
|
||||
let file_options = self.file_options;
|
||||
let mut writer = BufferWriter::new(&mut self.outgoing);
|
||||
if write_zfile(&mut writer, &mut self.buf, file_name, file_size)?.is_none() {
|
||||
if write_zfile(&mut writer, &mut self.buf, file_name, file_size, file_options)?.is_none() {
|
||||
return Err(Error::OutOfMemory);
|
||||
}
|
||||
Ok(())
|
||||
@@ -740,6 +741,8 @@ impl Sender {
|
||||
match header.frame() {
|
||||
Frame::ZRINIT => self.on_zrinit(header),
|
||||
Frame::ZRPOS | Frame::ZACK => self.on_zrpos(header.count()),
|
||||
Frame::ZSKIP => self.on_zskip(),
|
||||
Frame::ZFERR | Frame::ZABORT | Frame::ZCAN => Err(Error::RemoteAborted),
|
||||
Frame::ZFIN => self.on_zfin(),
|
||||
_ => {
|
||||
if self.state == SendState::WaitReceiverInit {
|
||||
@@ -832,6 +835,27 @@ impl Sender {
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_zskip(&mut self) -> Result<(), Error> {
|
||||
if matches!(
|
||||
self.state,
|
||||
SendState::WaitFilePos
|
||||
| SendState::NeedFileData
|
||||
| SendState::WaitFileAck
|
||||
| SendState::WaitFileDone
|
||||
) {
|
||||
self.pending_request = None;
|
||||
self.pending_event = Some(SenderEvent::FileComplete);
|
||||
self.has_file = false;
|
||||
if self.finish_requested {
|
||||
self.queue_zfin()?;
|
||||
self.state = SendState::WaitFinish;
|
||||
} else {
|
||||
self.state = SendState::ReadyForFile;
|
||||
}
|
||||
}
|
||||
Ok(())
|
||||
}
|
||||
|
||||
fn on_zfin(&mut self) -> Result<(), Error> {
|
||||
if self.state == SendState::WaitFinish {
|
||||
self.queue_oo()?;
|
||||
@@ -1419,6 +1443,7 @@ fn write_zfile<P>(
|
||||
buf: &mut Buffer<SUBPACKET_MAX_SIZE>,
|
||||
name: &[u8],
|
||||
size: u32,
|
||||
file_options: [u8; 4],
|
||||
) -> Result<Option<()>, Error>
|
||||
where
|
||||
P: Write + ?Sized,
|
||||
@@ -1430,7 +1455,7 @@ where
|
||||
|
||||
write!(buf, "{size}\0").map_err(|_| Error::OutOfMemory)?;
|
||||
|
||||
if Header::new(Encoding::ZBIN32, Frame::ZFILE, &[0; 4])
|
||||
if Header::new(Encoding::ZBIN32, Frame::ZFILE, &file_options)
|
||||
.write(port)?
|
||||
.is_none()
|
||||
{
|
||||
|
||||
+77
-2
@@ -4,8 +4,8 @@
|
||||
|
||||
use rstest::rstest;
|
||||
use zmodem2::{
|
||||
Encoding, Frame, Header, Receiver, ReceiverEvent, Sender, SubpacketType, XON, ZDLE, ZPAD,
|
||||
Zrinit,
|
||||
Encoding, Frame, Header, Receiver, ReceiverEvent, Sender, SenderEvent, SubpacketType, XON,
|
||||
ZDLE, ZPAD, ZfileManagementOption, Zrinit,
|
||||
};
|
||||
|
||||
#[rstest]
|
||||
@@ -70,6 +70,35 @@ fn write_zrpos(offset: u32) -> Vec<u8> {
|
||||
wire
|
||||
}
|
||||
|
||||
fn write_zskip() -> Vec<u8> {
|
||||
let mut wire = Vec::new();
|
||||
Header::new(Encoding::ZHEX, Frame::ZSKIP, &[0; 4])
|
||||
.write(&mut wire)
|
||||
.expect("write zskip")
|
||||
.expect("complete zskip");
|
||||
wire
|
||||
}
|
||||
|
||||
fn write_zabort() -> Vec<u8> {
|
||||
let mut wire = Vec::new();
|
||||
Header::new(Encoding::ZHEX, Frame::ZABORT, &[0; 4])
|
||||
.write(&mut wire)
|
||||
.expect("write zabort")
|
||||
.expect("complete zabort");
|
||||
wire
|
||||
}
|
||||
|
||||
fn zbin32_header_from_wire(wire: &[u8]) -> Header {
|
||||
assert!(
|
||||
wire.starts_with(&[ZPAD, ZDLE, Encoding::ZBIN32 as u8]),
|
||||
"expected ZBIN32 header, got {wire:?}"
|
||||
);
|
||||
let mut port = &wire[2..];
|
||||
Header::read(&mut port)
|
||||
.expect("read header")
|
||||
.expect("header")
|
||||
}
|
||||
|
||||
fn has_subpacket_kind(wire: &[u8], kind: SubpacketType) -> bool {
|
||||
wire.windows(2).any(|window| window == [ZDLE, kind as u8])
|
||||
}
|
||||
@@ -106,6 +135,52 @@ fn test_sender_requests_8k_file_chunks_after_large_zrinit() {
|
||||
assert_eq!(request.len, 8 * 1024);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sender_writes_zfile_management_options() {
|
||||
let mut sender = Sender::new().unwrap();
|
||||
sender.set_file_options(ZfileManagementOption::ZMCLOB);
|
||||
sender.start_file(b"overwrite.bin", 1).unwrap();
|
||||
sender.advance_outgoing(sender.drain_outgoing().len());
|
||||
|
||||
sender.feed_incoming(&write_zrinit(&[0; 4])).unwrap();
|
||||
|
||||
let header = zbin32_header_from_wire(sender.drain_outgoing());
|
||||
assert_eq!(header.frame(), Frame::ZFILE);
|
||||
assert_eq!(
|
||||
header.count().to_le_bytes(),
|
||||
[1, ZfileManagementOption::ZMCLOB.bits(), 0, 0]
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sender_treats_zskip_as_file_complete() {
|
||||
let mut sender = Sender::new().unwrap();
|
||||
sender.set_file_options(ZfileManagementOption::ZMSKNOLOC);
|
||||
sender.start_file(b"existing.bin", 1).unwrap();
|
||||
sender.advance_outgoing(sender.drain_outgoing().len());
|
||||
sender.feed_incoming(&write_zrinit(&[0; 4])).unwrap();
|
||||
sender.advance_outgoing(sender.drain_outgoing().len());
|
||||
|
||||
sender.feed_incoming(&write_zskip()).unwrap();
|
||||
|
||||
assert_eq!(sender.poll_event(), Some(SenderEvent::FileComplete));
|
||||
assert!(sender.poll_file().is_none());
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sender_reports_remote_abort() {
|
||||
let mut sender = Sender::new().unwrap();
|
||||
sender.start_file(b"blocked.bin", 1).unwrap();
|
||||
sender.advance_outgoing(sender.drain_outgoing().len());
|
||||
sender.feed_incoming(&write_zrinit(&[0; 4])).unwrap();
|
||||
sender.advance_outgoing(sender.drain_outgoing().len());
|
||||
|
||||
assert_eq!(
|
||||
sender.feed_incoming(&write_zabort()),
|
||||
Err(zmodem2::Error::RemoteAborted)
|
||||
);
|
||||
}
|
||||
|
||||
#[test]
|
||||
fn test_sender_uses_streaming_subpackets_until_ack_boundary() {
|
||||
let mut sender = Sender::new().unwrap();
|
||||
|
||||
Reference in new issue
Block a user