mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
729 lines
24 KiB
Rust
729 lines
24 KiB
Rust
//! Live integration: upload → download → byte-equality round-trips.
|
|
//!
|
|
//! Skipped by default. Run against the test account with:
|
|
//! PROTON_TOTP_SECRET=... cargo test -p proton-drive-rs --test live_upload_download -- --ignored --nocapture
|
|
//!
|
|
//! Each test cleans up after itself (trash + delete-from-trash) so the account
|
|
//! stays reusable across runs.
|
|
|
|
mod common;
|
|
|
|
use std::collections::HashMap;
|
|
use std::sync::{Arc, Mutex};
|
|
|
|
use proton_sdk::ids::NodeUid;
|
|
use proton_sdk::telemetry::{Telemetry, TelemetryEvent};
|
|
|
|
/// Counts telemetry events per operation, so a test can assert *which* work an
|
|
/// SDK call actually did (e.g. that a read issued no API requests).
|
|
#[derive(Default)]
|
|
struct OpCounter {
|
|
counts: Mutex<HashMap<&'static str, usize>>,
|
|
}
|
|
|
|
impl Telemetry for OpCounter {
|
|
fn record(&self, event: &TelemetryEvent) {
|
|
*self
|
|
.counts
|
|
.lock()
|
|
.expect("counter poisoned")
|
|
.entry(event.operation)
|
|
.or_default() += 1;
|
|
}
|
|
}
|
|
|
|
impl OpCounter {
|
|
fn count(&self, operation: &str) -> usize {
|
|
self.counts
|
|
.lock()
|
|
.expect("counter poisoned")
|
|
.get(operation)
|
|
.copied()
|
|
.unwrap_or(0)
|
|
}
|
|
|
|
fn reset(&self) {
|
|
self.counts.lock().expect("counter poisoned").clear();
|
|
}
|
|
}
|
|
|
|
/// Trash then permanently delete the given nodes; best-effort, logs on failure.
|
|
async fn cleanup(client: &proton_drive_rs::ProtonDriveClient, uids: &[NodeUid]) {
|
|
if let Err(e) = client.trash_nodes(uids).await {
|
|
eprintln!("[cleanup] trash failed: {e}");
|
|
return;
|
|
}
|
|
if let Err(e) = client.delete_nodes(uids).await {
|
|
eprintln!("[cleanup] delete failed: {e}");
|
|
}
|
|
}
|
|
|
|
/// Small single-block legacy SEIPDv1 upload, downloaded back, bytes compared.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn upload_download_small_roundtrip() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
let name = format!("rt-small-{}.txt", common::unique_suffix());
|
|
let payload = b"hello proton drive integration test \x00\x01\x02 bytes".to_vec();
|
|
|
|
let uid = client
|
|
.upload_file(&root.uid, &name, "text/plain", &payload)
|
|
.await
|
|
.expect("upload_file");
|
|
|
|
let got = client.download_file(&uid).await.expect("download_file");
|
|
assert_eq!(got, payload, "downloaded bytes must match uploaded");
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// Multi-block streaming upload (> 4 MiB) exercising `upload_file_from` and the
|
|
/// paginated revision-block download path.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn upload_download_multiblock_roundtrip() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// 10 MiB of non-trivial, non-repeating-friendly content (3 full 4 MiB blocks).
|
|
let size = 10 * 1024 * 1024;
|
|
let mut payload = vec![0u8; size];
|
|
for (i, b) in payload.iter_mut().enumerate() {
|
|
*b = (i as u32).wrapping_mul(2_654_435_761) as u8;
|
|
}
|
|
|
|
let name = format!("rt-multi-{}.bin", common::unique_suffix());
|
|
let reader = std::io::Cursor::new(payload.clone());
|
|
|
|
let uid = client
|
|
.upload_file_from(
|
|
&root.uid,
|
|
&name,
|
|
"application/octet-stream",
|
|
reader,
|
|
size as i64,
|
|
Vec::new(), // thumbnails
|
|
None, // last_modification_time
|
|
false, // aead
|
|
)
|
|
.await
|
|
.expect("upload_file_from");
|
|
|
|
let got = client.download_file(&uid).await.expect("download_file");
|
|
assert_eq!(got.len(), payload.len(), "size mismatch");
|
|
assert_eq!(got, payload, "multi-block bytes must match");
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// New revision over an existing file: upload v1, replace with v2, download must
|
|
/// yield v2.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn new_revision_roundtrip() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
let name = format!("rt-rev-{}.txt", common::unique_suffix());
|
|
let v1 = b"first revision".to_vec();
|
|
let v2 = b"second revision, longer than the first".to_vec();
|
|
|
|
let uid = client
|
|
.upload_file(&root.uid, &name, "text/plain", &v1)
|
|
.await
|
|
.expect("upload v1");
|
|
|
|
client
|
|
.upload_new_revision(&uid, &v2)
|
|
.await
|
|
.expect("upload v2");
|
|
|
|
let got = client.download_file(&uid).await.expect("download");
|
|
assert_eq!(got, v2, "active revision must be v2");
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// Single-block partial reads: every `download_range` slice must equal the
|
|
/// corresponding plaintext slice, with clamping/empty edge cases.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn download_range_single_block() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// Sub-block payload (well under 4 MiB) → exercises the single-block path.
|
|
let size = 64 * 1024usize;
|
|
let mut payload = vec![0u8; size];
|
|
for (i, b) in payload.iter_mut().enumerate() {
|
|
*b = (i as u32).wrapping_mul(2_654_435_761) as u8;
|
|
}
|
|
|
|
let name = format!("rt-range-1blk-{}.bin", common::unique_suffix());
|
|
let uid = client
|
|
.upload_file(&root.uid, &name, "application/octet-stream", &payload)
|
|
.await
|
|
.expect("upload_file");
|
|
|
|
let total = size as u64;
|
|
// (offset, length, expected slice end clamped to size)
|
|
let cases: &[(u64, u64)] = &[
|
|
(0, 100), // head
|
|
(1000, 5000), // interior
|
|
(size as u64 - 10, 10), // exact tail
|
|
(size as u64 - 10, 1000), // past EOF → clamps to tail
|
|
(0, total), // whole file via range
|
|
(total, 100), // offset == size → empty
|
|
(total + 50, 100), // offset past EOF → empty
|
|
(1000, 0), // zero length → empty
|
|
];
|
|
|
|
for &(off, len) in cases {
|
|
let got = client
|
|
.download_range(&uid, off, len)
|
|
.await
|
|
.unwrap_or_else(|e| panic!("download_range({off},{len}): {e}"));
|
|
let from = (off as usize).min(size);
|
|
let to = ((off + len) as usize).min(size);
|
|
let want = &payload[from..to];
|
|
assert_eq!(
|
|
got,
|
|
want,
|
|
"range(off={off}, len={len}) mismatch: got {} bytes, want {}",
|
|
got.len(),
|
|
want.len()
|
|
);
|
|
}
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// Multi-block partial reads (> 4 MiB): ranges that land mid-block, straddle
|
|
/// block boundaries, and cover the short final block must all match.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn download_range_multi_block() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// 10 MiB → blocks of 4 MiB, 4 MiB, 2 MiB (1-indexed, contiguous).
|
|
let block = 4 * 1024 * 1024u64;
|
|
let size = (10 * 1024 * 1024) as usize;
|
|
let mut payload = vec![0u8; size];
|
|
for (i, b) in payload.iter_mut().enumerate() {
|
|
*b = (i as u32).wrapping_mul(2_654_435_761) as u8;
|
|
}
|
|
|
|
let name = format!("rt-range-multi-{}.bin", common::unique_suffix());
|
|
let reader = std::io::Cursor::new(payload.clone());
|
|
let uid = client
|
|
.upload_file_from(
|
|
&root.uid,
|
|
&name,
|
|
"application/octet-stream",
|
|
reader,
|
|
size as i64,
|
|
Vec::new(),
|
|
None,
|
|
false,
|
|
)
|
|
.await
|
|
.expect("upload_file_from");
|
|
|
|
let total = size as u64;
|
|
let cases: &[(u64, u64)] = &[
|
|
(0, 256), // start of block 1
|
|
(block - 128, 256), // straddles block 1 → block 2
|
|
(block, 4096), // exact start of block 2
|
|
(2 * block - 1, 2), // straddles block 2 → block 3 (final short block)
|
|
(2 * block + 1000, 50_000), // interior of final short block
|
|
(total - 100, 500), // tail, length past EOF → clamps
|
|
(block - 10, block + 20), // spans a full block plus both neighbors
|
|
(0, total), // whole file
|
|
];
|
|
|
|
for &(off, len) in cases {
|
|
let got = client
|
|
.download_range(&uid, off, len)
|
|
.await
|
|
.unwrap_or_else(|e| panic!("download_range({off},{len}): {e}"));
|
|
let from = (off as usize).min(size);
|
|
let to = ((off + len) as usize).min(size);
|
|
let want = &payload[from..to];
|
|
assert_eq!(
|
|
got.len(),
|
|
want.len(),
|
|
"range(off={off}, len={len}) length mismatch"
|
|
);
|
|
assert_eq!(got, want, "range(off={off}, len={len}) byte mismatch");
|
|
}
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// A held `RevisionReader` serves the same ranges as `download_range`, and —
|
|
/// the point of the handle — resolves the revision's metadata only once: the
|
|
/// reads themselves must issue no API requests at all, only block fetches.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn open_revision_reader_reuses_resolved_metadata() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
|
|
let counter = Arc::new(OpCounter::default());
|
|
// Clones share the session, caches and connection pool; only the telemetry
|
|
// sink differs, so the counts below are this test's own work.
|
|
let client = live.client.clone().with_telemetry(counter.clone());
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// 10 MiB → blocks of 4 MiB, 4 MiB, 2 MiB.
|
|
let block = 4 * 1024 * 1024u64;
|
|
let size = (10 * 1024 * 1024) as usize;
|
|
let mut payload = vec![0u8; size];
|
|
for (i, b) in payload.iter_mut().enumerate() {
|
|
*b = (i as u32).wrapping_mul(2_654_435_761) as u8;
|
|
}
|
|
|
|
let name = format!("rt-reader-{}.bin", common::unique_suffix());
|
|
let uid = client
|
|
.upload_file_from(
|
|
&root.uid,
|
|
&name,
|
|
"application/octet-stream",
|
|
std::io::Cursor::new(payload.clone()),
|
|
size as i64,
|
|
Vec::new(),
|
|
None,
|
|
false,
|
|
)
|
|
.await
|
|
.expect("upload_file_from");
|
|
|
|
let reader = client.open_revision(&uid).await.expect("open_revision");
|
|
|
|
assert_eq!(reader.size(), size as u64, "reader size");
|
|
assert_eq!(
|
|
reader.block_sizes(),
|
|
&[block, block, 2 * 1024 * 1024],
|
|
"reader block sizes"
|
|
);
|
|
|
|
// Everything the reader needs is now resolved. From here on, reads must
|
|
// only fetch block bodies.
|
|
counter.reset();
|
|
|
|
let total = size as u64;
|
|
let cases: &[(u64, u64)] = &[
|
|
(0, 256), // start of block 1
|
|
(block - 128, 256), // straddles block 1 → block 2
|
|
(block, 4096), // exact start of block 2
|
|
(2 * block - 1, 2), // straddles block 2 → block 3 (final short block)
|
|
(2 * block + 1000, 50_000), // interior of final short block
|
|
(total - 100, 500), // tail, length past EOF → clamps
|
|
(block - 10, block + 20), // spans a full block plus both neighbors
|
|
(0, total), // whole file
|
|
(total, 4096), // at EOF → empty
|
|
];
|
|
|
|
for &(off, len) in cases {
|
|
let got = reader
|
|
.read_at(off, len)
|
|
.await
|
|
.unwrap_or_else(|e| panic!("read_at({off},{len}): {e}"));
|
|
let from = (off as usize).min(size);
|
|
let to = ((off + len) as usize).min(size);
|
|
let want = &payload[from..to];
|
|
assert_eq!(
|
|
got.len(),
|
|
want.len(),
|
|
"read_at({off},{len}) length mismatch"
|
|
);
|
|
assert_eq!(got, want, "read_at({off},{len}) byte mismatch");
|
|
}
|
|
|
|
assert_eq!(
|
|
counter.count("http_request"),
|
|
0,
|
|
"reads must not re-resolve link details or the revision listing"
|
|
);
|
|
assert!(
|
|
counter.count("storage_download") > 0,
|
|
"reads must fetch block bodies"
|
|
);
|
|
|
|
cleanup(&client, &[uid]).await;
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Revision history
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// Upload a file, replace it twice, then walk the history: list → read an old
|
|
/// revision → restore it → delete a superseded one.
|
|
///
|
|
/// This is the only test that exercises revisions as a *history* rather than as
|
|
/// upload plumbing, so it covers `enumerate_revisions`, `get_revision`,
|
|
/// `download_revision`, `restore_revision` and `delete_revision` together.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn revision_history_lists_reads_restores_and_deletes() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
let name = format!("revisions-{}.txt", common::unique_suffix());
|
|
let v1 = b"first revision contents".to_vec();
|
|
let v2 = b"second revision contents, longer than the first".to_vec();
|
|
let v3 = b"third".to_vec();
|
|
|
|
let file = client
|
|
.upload_file(&root.uid, &name, "text/plain", &v1)
|
|
.await
|
|
.expect("upload_file");
|
|
client
|
|
.upload_new_revision(&file, &v2)
|
|
.await
|
|
.expect("upload second revision");
|
|
client
|
|
.upload_new_revision(&file, &v3)
|
|
.await
|
|
.expect("upload third revision");
|
|
|
|
// Three sealed revisions, exactly one of them active.
|
|
let revisions = client
|
|
.enumerate_revisions(&file)
|
|
.await
|
|
.expect("enumerate_revisions");
|
|
assert_eq!(revisions.len(), 3, "three uploads means three revisions");
|
|
let active: Vec<_> = revisions.iter().filter(|r| r.is_active()).collect();
|
|
assert_eq!(active.len(), 1, "exactly one revision may be active");
|
|
|
|
// The active one must describe the newest upload. `claimed_size` is the
|
|
// uploader's plaintext size, so it is comparable to what we wrote —
|
|
// `size_on_storage` is the ciphertext and is not.
|
|
let active = active[0];
|
|
assert_eq!(
|
|
active.claimed_size,
|
|
Some(v3.len() as i64),
|
|
"the active revision must claim the newest payload's size"
|
|
);
|
|
assert!(
|
|
active.size_on_storage > 0,
|
|
"a sealed revision occupies storage"
|
|
);
|
|
|
|
// Fetching one by id must agree with the listing.
|
|
let fetched = client
|
|
.get_revision(&file, &active.revision_id)
|
|
.await
|
|
.expect("get_revision")
|
|
.expect("the active revision must exist");
|
|
assert_eq!(fetched.revision_id, active.revision_id);
|
|
assert!(fetched.is_active());
|
|
assert_eq!(fetched.claimed_size, active.claimed_size);
|
|
|
|
// A malformed id is a caller bug, not a missing entity: the server rejects
|
|
// it with 400 `InvalidEncryptedIdFormat` and that must surface as an error,
|
|
// not be flattened into `None`. (The genuine not-found path is checked
|
|
// against a deleted-but-well-formed id at the end of this test.)
|
|
assert!(
|
|
client.get_revision(&file, "does-not-exist").await.is_err(),
|
|
"a malformed revision id must error rather than read as absent"
|
|
);
|
|
|
|
// Superseded revisions, oldest first, are the two earlier uploads.
|
|
let mut superseded: Vec<_> = revisions.iter().filter(|r| !r.is_active()).collect();
|
|
superseded.sort_by_key(|r| r.creation_time);
|
|
assert_eq!(superseded.len(), 2);
|
|
|
|
let oldest = superseded[0];
|
|
assert_eq!(
|
|
oldest.claimed_size,
|
|
Some(v1.len() as i64),
|
|
"the oldest revision must still claim the first payload's size"
|
|
);
|
|
|
|
// Reading a superseded revision must give back its own content, not the
|
|
// active one's — the whole point of keeping history.
|
|
let old_bytes = client
|
|
.download_revision(&file, &oldest.revision_id)
|
|
.await
|
|
.expect("download_revision");
|
|
assert_eq!(
|
|
old_bytes, v1,
|
|
"a superseded revision must read back verbatim"
|
|
);
|
|
assert_eq!(
|
|
client.download_file(&file).await.expect("download_file"),
|
|
v3,
|
|
"the file itself must still serve the active revision"
|
|
);
|
|
|
|
// Restoring does not move the pointer in place: it mints a *new* active
|
|
// revision carrying the old content. The server applies it asynchronously
|
|
// (HTTP 202), so poll rather than assume it is live on return.
|
|
client
|
|
.restore_revision(&file, &oldest.revision_id)
|
|
.await
|
|
.expect("restore_revision");
|
|
|
|
let mut restored = false;
|
|
for _ in 0..30 {
|
|
if client
|
|
.download_file(&file)
|
|
.await
|
|
.expect("download after restore")
|
|
== v1
|
|
{
|
|
restored = true;
|
|
break;
|
|
}
|
|
tokio::time::sleep(std::time::Duration::from_secs(1)).await;
|
|
}
|
|
assert!(
|
|
restored,
|
|
"after restore the file must serve the restored content within 30s"
|
|
);
|
|
|
|
let after_restore = client
|
|
.enumerate_revisions(&file)
|
|
.await
|
|
.expect("enumerate after restore");
|
|
assert!(
|
|
after_restore.len() >= 3,
|
|
"restoring must not drop history, got {}",
|
|
after_restore.len()
|
|
);
|
|
assert_eq!(
|
|
after_restore.iter().filter(|r| r.is_active()).count(),
|
|
1,
|
|
"still exactly one active revision after restore"
|
|
);
|
|
|
|
// Deleting a superseded revision removes it and leaves the rest alone.
|
|
let victim = after_restore
|
|
.iter()
|
|
.find(|r| !r.is_active())
|
|
.expect("a superseded revision to delete");
|
|
client
|
|
.delete_revision(&file, &victim.revision_id)
|
|
.await
|
|
.expect("delete_revision");
|
|
|
|
let after_delete = client
|
|
.enumerate_revisions(&file)
|
|
.await
|
|
.expect("enumerate after delete");
|
|
assert_eq!(
|
|
after_delete.len(),
|
|
after_restore.len() - 1,
|
|
"exactly one revision must disappear"
|
|
);
|
|
assert!(
|
|
!after_delete
|
|
.iter()
|
|
.any(|r| r.revision_id == victim.revision_id),
|
|
"the deleted revision must be gone from the history"
|
|
);
|
|
assert_eq!(
|
|
client
|
|
.download_file(&file)
|
|
.await
|
|
.expect("download after delete"),
|
|
v1,
|
|
"deleting a superseded revision must not disturb the active one"
|
|
);
|
|
|
|
// Now the real not-found path: a well-formed id whose revision is gone.
|
|
assert!(
|
|
client
|
|
.get_revision(&file, &victim.revision_id)
|
|
.await
|
|
.expect("a deleted revision must not error")
|
|
.is_none(),
|
|
"a deleted revision must resolve to None"
|
|
);
|
|
|
|
cleanup(client, &[file]).await;
|
|
}
|
|
|
|
/// Multi-block upload carrying thumbnails: their tokens now ride the first
|
|
/// block-token request instead of being prepared and stored one request each,
|
|
/// so the manifest layout (thumbnail digests first, in type order) has to
|
|
/// survive a pipeline that stores blocks out of order.
|
|
#[tokio::test]
|
|
#[ignore = "live: needs test-account credentials"]
|
|
async fn upload_download_multiblock_with_thumbnails_roundtrip() {
|
|
use proton_drive_rs::{Thumbnail, ThumbnailType};
|
|
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// 10 MiB → 3 content blocks, plus both thumbnail types.
|
|
let size = 10 * 1024 * 1024;
|
|
let mut payload = vec![0u8; size];
|
|
for (i, b) in payload.iter_mut().enumerate() {
|
|
*b = (i as u32).wrapping_mul(2_654_435_761) as u8;
|
|
}
|
|
let small = b"small-thumbnail-bytes".to_vec();
|
|
let preview = b"preview-thumbnail-bytes-which-are-longer".to_vec();
|
|
|
|
let name = format!("rt-thumbs-{}.bin", common::unique_suffix());
|
|
let uid = client
|
|
.upload_file_from(
|
|
&root.uid,
|
|
&name,
|
|
"application/octet-stream",
|
|
std::io::Cursor::new(payload.clone()),
|
|
size as i64,
|
|
vec![
|
|
// Deliberately out of type order: the uploader sorts.
|
|
Thumbnail::new(ThumbnailType::Preview, preview.clone()).expect("preview thumbnail"),
|
|
Thumbnail::new(ThumbnailType::Thumbnail, small.clone()).expect("small thumbnail"),
|
|
],
|
|
None, // last_modification_time
|
|
false, // aead
|
|
)
|
|
.await
|
|
.expect("upload_file_from with thumbnails");
|
|
|
|
assert_eq!(
|
|
client.download_file(&uid).await.expect("download_file"),
|
|
payload,
|
|
"content must round-trip alongside thumbnails"
|
|
);
|
|
assert_eq!(
|
|
client
|
|
.download_thumbnail(&uid, ThumbnailType::Thumbnail)
|
|
.await
|
|
.expect("download small thumbnail"),
|
|
Some(small),
|
|
);
|
|
assert_eq!(
|
|
client
|
|
.download_thumbnail(&uid, ThumbnailType::Preview)
|
|
.await
|
|
.expect("download preview thumbnail"),
|
|
Some(preview),
|
|
);
|
|
|
|
cleanup(client, &[uid]).await;
|
|
}
|
|
|
|
/// Upload throughput, pipelined vs. serial — the measurement behind PERF_PLAN's
|
|
/// upload item, and the same methodology its download counterpart used.
|
|
///
|
|
/// `with_max_inflight_blocks(1)` reproduces the pre-pipeline behavior exactly:
|
|
/// one block is read, encrypted and stored before the next one starts. The
|
|
/// default client keeps several blocks in flight. Both figures are printed
|
|
/// rather than asserted on — this is a benchmark, and a shared link's capacity
|
|
/// is not something a test can pin down.
|
|
#[tokio::test]
|
|
#[ignore = "live: benchmark, needs test-account credentials"]
|
|
async fn upload_throughput_pipelined_vs_serial() {
|
|
let Some(live) = common::live_client().await else {
|
|
return;
|
|
};
|
|
let client = &live.client;
|
|
|
|
let root = client
|
|
.get_my_files_folder()
|
|
.await
|
|
.expect("get my-files root");
|
|
|
|
// 40 MiB → 10 blocks, matching the download measurement's payload.
|
|
let size = 40 * 1024 * 1024;
|
|
let mut payload = vec![0u8; size];
|
|
for (i, b) in payload.iter_mut().enumerate() {
|
|
*b = (i as u32).wrapping_mul(2_654_435_761) as u8;
|
|
}
|
|
|
|
let serial = live.client.clone().with_max_inflight_blocks(1);
|
|
let mut uploaded = Vec::new();
|
|
|
|
for round in 1..=3 {
|
|
for (label, client) in [("serial", &serial), ("pipelined", client)] {
|
|
let name = format!("bench-{label}-{round}-{}.bin", common::unique_suffix());
|
|
let started = std::time::Instant::now();
|
|
let uid = client
|
|
.upload_file_from(
|
|
&root.uid,
|
|
&name,
|
|
"application/octet-stream",
|
|
std::io::Cursor::new(payload.clone()),
|
|
size as i64,
|
|
Vec::new(),
|
|
None,
|
|
false,
|
|
)
|
|
.await
|
|
.expect("upload_file_from");
|
|
let elapsed = started.elapsed();
|
|
|
|
eprintln!(
|
|
"[upload {label} round {round}] {:.1}s, {:.2} MiB/s",
|
|
elapsed.as_secs_f64(),
|
|
(size as f64 / (1024.0 * 1024.0)) / elapsed.as_secs_f64()
|
|
);
|
|
uploaded.push(uid);
|
|
}
|
|
}
|
|
|
|
cleanup(client, &uploaded).await;
|
|
}
|