use thiserror::Error;
use crate::file_transfer::{Compression, FileError};
const FALLBACK_CHUNK_SIZE: usize = 256;
pub(crate) fn resolve_chunk_size(requested: usize, device_max: u16) -> usize {
let device_max = if device_max == 0 {
FALLBACK_CHUNK_SIZE
} else {
device_max as usize
};
if requested == 0 {
device_max
} else {
requested.min(device_max)
}
}
#[derive(Debug, Clone, Copy, Default)]
pub struct Progress {
pub bytes_sent: u64,
pub chunks_sent: u64,
pub source_bytes: u64,
}
impl Progress {
pub fn fraction(&self) -> Option<f32> {
if self.source_bytes == 0 {
return None;
}
let f = self.bytes_sent as f32 / self.source_bytes as f32;
Some(f.min(1.0))
}
pub fn percent(&self) -> Option<f32> {
self.fraction().map(|f| f * 100.0)
}
}
pub type ProgressCallback = Box<dyn FnMut(Progress) + Send>;
pub struct UploadOptions {
pub dest_filename: String,
pub compression: Compression,
pub dummy: bool,
pub chunk_size: usize,
pub progress: Option<ProgressCallback>,
}
impl std::fmt::Debug for UploadOptions {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.debug_struct("UploadOptions")
.field("dest_filename", &self.dest_filename)
.field("compression", &self.compression)
.field("dummy", &self.dummy)
.field("chunk_size", &self.chunk_size)
.field("progress", &self.progress.as_ref().map(|_| "<callback>"))
.finish()
}
}
impl Default for UploadOptions {
fn default() -> Self {
Self {
dest_filename: String::new(),
compression: Compression::None,
dummy: false,
chunk_size: 0,
progress: None,
}
}
}
#[derive(Debug, Clone, Default)]
pub struct UploadStats {
pub source_bytes: u64,
pub bytes_sent: u64,
pub chunks_sent: u64,
pub compression: Compression,
}
#[derive(Debug, Error)]
pub enum UploadError {
#[error("transport I/O error: {0}")]
Io(#[from] std::io::Error),
#[error("file transfer failed: {0}")]
Transfer(#[from] FileError),
#[error("upload stalled: {0}")]
Stalled(&'static str),
#[error("SYNC handshake did not complete")]
HandshakeFailed,
#[cfg(not(feature = "heatshrink"))]
#[error("heatshrink compression requested but the `heatshrink` feature is disabled")]
CompressionFeatureDisabled,
#[cfg(feature = "heatshrink")]
#[error("heatshrink error: {0}")]
Heatshrink(#[from] crate::compression::HeatshrinkError),
}
#[cfg(test)]
mod tests {
use super::*;
#[test]
fn requested_zero_uses_device_max() {
assert_eq!(resolve_chunk_size(0, 512), 512);
}
#[test]
fn requested_under_max_is_honored() {
assert_eq!(resolve_chunk_size(128, 512), 128);
}
#[test]
fn requested_over_max_is_capped() {
assert_eq!(resolve_chunk_size(4096, 512), 512);
}
#[test]
fn requested_zero_with_zero_device_max_falls_back() {
assert_eq!(resolve_chunk_size(0, 0), FALLBACK_CHUNK_SIZE);
}
#[test]
fn requested_nonzero_with_zero_device_max_capped_to_fallback() {
assert_eq!(resolve_chunk_size(1024, 0), FALLBACK_CHUNK_SIZE);
}
#[test]
fn requested_equal_to_max_is_honored() {
assert_eq!(resolve_chunk_size(512, 512), 512);
}
#[test]
fn progress_percent_zero_source_is_none() {
let p = Progress {
bytes_sent: 0,
chunks_sent: 0,
source_bytes: 0,
};
assert!(p.percent().is_none());
assert!(p.fraction().is_none());
}
#[test]
fn progress_percent_halfway() {
let p = Progress {
bytes_sent: 500,
chunks_sent: 5,
source_bytes: 1000,
};
assert_eq!(p.fraction(), Some(0.5));
assert_eq!(p.percent(), Some(50.0));
}
#[test]
fn progress_percent_complete() {
let p = Progress {
bytes_sent: 1000,
chunks_sent: 10,
source_bytes: 1000,
};
assert_eq!(p.percent(), Some(100.0));
}
#[test]
fn progress_percent_clamps_when_expanded_by_compression() {
let p = Progress {
bytes_sent: 1500,
chunks_sent: 15,
source_bytes: 1000,
};
assert_eq!(p.percent(), Some(100.0));
}
}