Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
12 changes: 6 additions & 6 deletions Cargo.lock

Some generated files are not rendered by default. Learn more about how customized files appear on GitHub.

2 changes: 1 addition & 1 deletion Cargo.toml
Original file line number Diff line number Diff line change
Expand Up @@ -10,7 +10,7 @@ members = [
resolver = "2"

[workspace.package]
version = "0.27.2"
version = "0.27.3"
edition = "2024"
license = "MIT"
repository = "https://github.com/cfms-dev/cfms_client_tauri"
Expand Down
49 changes: 47 additions & 2 deletions crates/core/src/types.rs
Original file line number Diff line number Diff line change
Expand Up @@ -285,9 +285,9 @@ pub struct ServerDocumentEntry {
pub id: String,
/// Display title of the document.
pub title: String,
/// File size in bytes.
/// File size in bytes, or `None` when the server cannot determine it.
#[serde(default)]
pub size: u64,
pub size: Option<u64>,
/// Last modification timestamp (Unix seconds).
#[serde(default)]
pub last_modified: Option<f64>,
Expand Down Expand Up @@ -475,3 +475,48 @@ pub struct RecentFileRecord {
#[serde(default, alias = "visited_at")]
pub visited_at: u64,
}

#[cfg(test)]
mod tests {
use super::*;

#[test]
fn list_directory_preserves_unknown_document_size() {
let raw = r#"{
"folders": [],
"documents": [
{
"id": "with-null-size",
"title": "Null size",
"size": null,
"last_modified": null
},
{
"id": "without-size",
"title": "Missing size",
"last_modified": 1710000000.0
},
{
"id": "with-size",
"title": "Known size",
"size": 4096,
"last_modified": 1710000001.0
},
{
"id": "with-zero-size",
"title": "Zero size",
"size": 0,
"last_modified": 1710000002.0
}
],
"parent_id": null
}"#;

let parsed: ListDirectoryResponse = serde_json::from_str(raw).unwrap();

assert_eq!(parsed.documents[0].size, None);
assert_eq!(parsed.documents[1].size, None);
assert_eq!(parsed.documents[2].size, Some(4096));
assert_eq!(parsed.documents[3].size, Some(0));
}
}
56 changes: 45 additions & 11 deletions crates/transfer/src/upload.rs
Original file line number Diff line number Diff line change
Expand Up @@ -102,17 +102,10 @@ pub async fn send(
let ready_str =
String::from_utf8(ready_raw).map_err(|e| cfms_core::Error::Protocol(e.to_string()))?;

if ready_str == "stop" {
return Err(cfms_core::Error::Protocol(
"upload rejected by server".into(),
));
}

let chunk_size: usize = ready_str
.strip_prefix("ready ")
.and_then(|s| s.split_whitespace().next())
.and_then(|n| n.parse().ok())
.unwrap_or(8192);
let Some(chunk_size) = parse_ready_signal(&ready_str, file_size)? else {
on_progress(0, file_size);
return Ok(());
};
Comment on lines +105 to +108

Copy link
Copy Markdown

Choose a reason for hiding this comment

The reason will be displayed to describe this comment to others. Learn more.

suggestion: Zero-byte uploads report progress as (0, 0) instead of a completed state, which may be surprising for callers.

When parse_ready_signal returns Ok(None) (zero-byte upload), send invokes on_progress(0, file_size) and returns Ok(()). With file_size == 0, the callback always sees (0, 0). If callers treat completion as (total, total), this may not be recognized as finished.

Consider either:

  • Invoking on_progress(file_size, file_size) specifically for zero-byte completion, or
  • Clearly documenting (0, 0) as the "completed empty upload" state and ensuring callers handle it accordingly.
Suggested change
let Some(chunk_size) = parse_ready_signal(&ready_str, file_size)? else {
on_progress(0, file_size);
return Ok(());
};
let Some(chunk_size) = parse_ready_signal(&ready_str, file_size)? else {
// Zero-byte upload: treat as completed and report (total, total)
on_progress(file_size, file_size);
return Ok(());
};


// --- Step 4: stream the file ---
let mut file = tokio::fs::File::open(source).await?;
Expand Down Expand Up @@ -142,3 +135,44 @@ pub async fn send(

Ok(())
}

fn parse_ready_signal(ready_str: &str, file_size: u64) -> Result<Option<usize>> {
if ready_str == "stop" {
return if file_size == 0 {
Ok(None)
} else {
Err(cfms_core::Error::Protocol(
"server sent stop for a non-empty upload".into(),
))
};
}

Ok(Some(
ready_str
.strip_prefix("ready ")
.and_then(|s| s.split_whitespace().next())
.and_then(|n| n.parse().ok())
.unwrap_or(8192),
))
}

#[cfg(test)]
mod tests {
use super::parse_ready_signal;

#[test]
fn stop_completes_zero_byte_upload() {
assert_eq!(parse_ready_signal("stop", 0).unwrap(), None);
}

#[test]
fn stop_rejects_non_empty_upload() {
let err = parse_ready_signal("stop", 1).unwrap_err().to_string();
assert!(err.contains("server sent stop for a non-empty upload"));
}

#[test]
fn ready_signal_uses_server_chunk_size() {
assert_eq!(parse_ready_signal("ready 16384", 42).unwrap(), Some(16384));
}
}
2 changes: 1 addition & 1 deletion package.json
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
{
"name": "cfms-client-tauri",
"version": "0.27.2",
"version": "0.27.3",
"description": "",
"type": "module",
"scripts": {
Expand Down
4 changes: 2 additions & 2 deletions src-tauri/gen/ios/CFMS Client/Info.plist
Original file line number Diff line number Diff line change
Expand Up @@ -17,9 +17,9 @@
<key>CFBundlePackageType</key>
<string>APPL</string>
<key>CFBundleShortVersionString</key>
<string>0.27.2</string>
<string>0.27.3</string>
<key>CFBundleVersion</key>
<string>27002</string>
<string>27003</string>
<key>NSFaceIDUsageDescription</key>
<string>Use Face ID to unlock CFMS Client when biometric app lock is enabled.</string>
<key>LSRequiresIPhoneOS</key>
Expand Down
Loading
Loading