implemented enumeration for drive

This commit is contained in:
nvrl committed 2026-08-10 14:10:43 +02:00
1 parent 4fe7f02ebb
commit e378db6ec8
10 files changed
+283 -97

No files matched your search

+2 -2
View File
@@ -251,7 +251,7 @@ Moving a file or folder within the same volume is an offline cryptographic opera
| | Telemetry | ✅ | Pluggable request tracking (`ITelemetry` analogue). |
| **Drive / Volume** | Volume Resolution | ✅ | Auto-resolves volume lists; auto-creates the `my-files` root volume on first login. |
| | Node Listing | ✅ | `get_node`, `enumerate_folder_children`, `enumerate_trash` with key resolution. |
| | File Operations | ✅ | Rename, trash, restore, delete, empty trash. |
| | File Operations | ✅ | Rename, trash, restore, delete, empty trash — batch calls report one outcome per node (`trash_nodes`), or stream them as each batch lands (`trash_nodes_streaming`). |
| | Move Operations | ✅ | Same-volume move. Batch moves are chunked automatically. |
| | Cross-volume Move | ❌ | Not supported (throwing `NotImplementedException` in the C# public API too). |
| **Uploads** | Block Uploading | ✅ | Encrypts and uploads chunks (4 MiB default) to block storage. |
@@ -275,7 +275,7 @@ Moving a file or folder within the same volume is an offline cryptographic opera
| **Photos** | Photos Timeline | ✅ | `ProtonPhotosClient` maps photostream, timeline enumeration, and photo downloads. |
| | Photo Uploads | ✅ | Uploading photos with `PhotoUploadMetadata` (capture time, tags, grouping). |
| | Albums (read) | ✅ | List albums (`enumerate_album_node_uids`) and their photos (`enumerate_album`); album nodes carry `Node::album`. |
| | Photo Tags | ✅ | Add/remove classification tags (`update_photos`); `Favorite` on photos in our own timeline. |
| | Photo Tags | ✅ | Add/remove classification tags (`update_photos`, or `update_photos_streaming` for per-photo outcomes as they finish); `Favorite` on photos in our own timeline. |
| | Shared Photos | ✅ | Photos/albums shared *with* us (`enumerate_shared_with_me_node_uids`, `enumerate_shared_with_me_album_uids`). |
| | Favorite Shared Photos | ❌ | Needs the photo re-encrypted for our timeline root; not ported. |
| | Album Writes | ❌ | Creating albums and adding/removing photos is not ported. |
+3 -2
View File
@@ -7,8 +7,8 @@ gitignored, so this file is the only durable record of what we last reviewed.
- **Upstream**: https://github.com/ProtonDriveApps/sdk
- **Reconciled subtree**: `client/cs/src` (was `client/cs/sdk/src` before the
mid-2026 projects reorg; the script watches both)
- **Pinned**: `941aafa55fb179c8ee347c9d0b4660a3fc893f09`
- **Date**: 2026-07-31
- **Pinned**: `df6b6bfde620c12868cd5a444d027f8d01974b75`
- **Date**: 2026-08-10
> **Note**: upstream force-pushes — the SHAs in rows before 2026-07-14 no longer
> exist in its history. Triage by commit subject/date when a pin goes stale.
@@ -33,3 +33,4 @@ gitignored, so this file is the only durable record of what we last reviewed.
| 2026-07-22 | `2219a42f` → `1b6ff835` | `ded1b716`, `071f976f` | ported: **atomic small uploads** for buffered new files and revisions (`DriveSmallFileUpload` is an explicit opt-in because this crate has no remote feature-flag provider), and **photo duplicate detection** using name/content HMAC matching. already covered/no-op: `ee0b3fba` (Rust rename accepts `Option<&str>`), `193c26f1` (Rust in-memory cache already has bounded LRU eviction), `29d359e0` (digest representation refactor retains hex JSON), `5c370289` (C# disposal lifetime only), `95752055` (Photos cache construction only). deliberately not ported: `2bf49aae` / `2590d227` cache storage-format changes; Rust exposes a distinct string repository contract and ships no persistent backend to migrate/reset. |
| 2026-07-31 | `e0ad37e3` → `941aafa5` | `42526703`, `f2fa871b`, `814ae781`, `c528a376` | ported: **node-name validation** (`validate_node_name`: empty or >255 chars rejected before create/rename; counted in `char`s — C# counts UTF-16 units, and neither is the server's own accounting); **album read ops** (`AlbumDto` + `enumerate_album_node_uids` / `enumerate_album`, `GET photos/volumes/{vid}/albums` and `…/albums/{lid}/children?Sort=Captured&Desc=1`, both paged on `AnchorID`; `LinkDetailsDto::folder_properties` reads an album's hash key out of the `Album` block, which also makes `parent_hash_key` work for album children); **photo + album node metadata** (`Node::photo` / `Node::album` — capture time, content hash, main/related photos, tags, album inclusions; photo count, cover photo, last activity — added as optional `Node` fields rather than `NodeKind` variants so persisted node blobs keep deserializing, mirroring how `membership` landed); **photo tag updates** (`update_photos`, `POST`/`DELETE photos/volumes/{vid}/links/{lid}/tags` via the new `ApiHttpClient::delete_with_body`, `POST …/favorite` for photos on our own photos volume, one outcome per input update like `move_nodes`); **shared-with-me photos/albums** (`ShareTargetType::is_photos_item` over `v2/sharedwithme` + the dedicated `GET photos/albums/shared-with-me`, whose rows carry their own `VolumeID`). deferred: `03b1cb7f` **favoriting shared photos** — needs `PhotosCrypto` + `PhotoTransferPayloadBuilder` (name/passphrase session keys rewrapped to the timeline root, related-photo fan-out, batches of 20); our `rewrap_message_to` / `recover_message_session_key` make it feasible, but until it lands `update_photos` fails that one update explicitly instead of silently no-oping. not ported: `4c1dd783` (`IsSharedPublicly` → `IsSharedByUrl` rename — `proton-drive-linux` consumes `is_shared_publicly`), `1477ea45` (C# user-facing error strings; our error text is our own), the second half of `42526703` (server-supplied names validated into a per-node `InvalidNameError` — we have no node error list to put it in). no-op: `82d605b7` (C# `FifoFlexibleSemaphore` cancel race — tokio's `Semaphore` is cancel-safe and drops queued waiters), `8bdd9420` (throw on resume-after-dispose; Rust ownership makes it unreachable), `ca11470f` + `c947fce7` (CExports/interop only). Album *writes* (create, add/remove photos) remain unported — upstream has no C# public API for them either. Live validation pending: every photos endpoint here needs a photos-volume account. |
| 2026-08-01 | — (no upstream delta) | — | **not an upstream port.** Extended the TypeScript-sourced public-link consume side with a streaming surface that has **no TS or C# counterpart** and is ours: `open_revision` / `open_revision_at` / `download_range` / `download_thumbnail` / `enumerate_thumbnails` / `refresh_session` / `with_max_inflight_blocks` on `ProtonDrivePublicLinkClient`. Enabling change: `RevisionReader` was welded to `ProtonDriveClient`, so it now takes a `RevisionTransport` (new `transport.rs` — block permits + a route-prefixed `ApiHttpClient` + a `BlockSession` for renewal); `fetch_revision_blocks` and the block-size ranking moved there out of `client.rs`. Also `ApiHttpClient::without_token_refresh` (proton-sdk), because the anonymous session's refresh token is empty and the built-in 401 refresh masked the real 401. Perf on the visitor path: LRU node-key cache + iterative ancestor walk behind `SingleFlight` (was re-deriving the whole chain, at an S2K each, per child), `enumerate_nodes` chunked at 150 and fanned out (it did not chunk at all), `download_file_to` paged (a single un-paged GET truncated anything past 50 blocks) and concurrent. Live-verified against the test account, including the one uncertain fact the design rested on: the `drive/unauth/` revision response **does** carry `XAttr`, so a visitor resolves true per-block sizes and can seek. |
| 2026-08-10 | `941aafa5` → `df6b6bfd` | `f493c5ee`, `62895702` | ported: **per-node trash/restore/delete outcomes, streamed**. Upstream turned `TrashNodesAsync` / `RestoreNodesAsync` / `DeleteNodesAsync` from `ValueTask<IReadOnlyDictionary<NodeUid, Result<Exception>>>` into `IAsyncEnumerable<NodeActionResult>`, and `UpdatePhotosAsync` into `IAsyncEnumerable<PhotoUpdateResult>`. Rust equivalent: `trash_nodes` / `restore_nodes` / `delete_nodes` now return `Vec<(NodeUid, Result<()>)>` (**breaking** — they returned `Result<()>` and failed the whole call through `check_aggregate`, which is now gone), plus `*_streaming` siblings returning `impl Stream<Item = Result<(NodeUid, Result<()>)>>` that yield each batch's outcomes as the batch lands; `update_photos_streaming` does the same for photo tag updates. Kept the `(NodeUid, Result<()>)` tuple used by `move_nodes` rather than adding `NodeActionResult` / `PhotoUpdateResult` record types — C# needs a named type for its iterators, Rust does not. Outcome mapping mirrors upstream exactly: only the links the aggregate envelope reports on produce an outcome, and the stream's outer `Err` (a request that never produced an envelope) ends it. diverged deliberately: upstream's rewrite also dropped its per-volume `Task.WhenAll`, so both sides are now sequential over volume groups — ours already was. not ported: `9dd63423` (explicit stream disposal through interop — CExports/proto only; Rust ownership drops readers/writers at scope end). |
+192 -70
View File
@@ -1,6 +1,6 @@
//! The high-level Drive client and its read operations.
use std::collections::{HashMap, HashSet};
use std::collections::{HashMap, HashSet, VecDeque};
use std::future::Future;
use std::io::{Cursor, Read};
use std::num::NonZeroUsize;
@@ -2882,55 +2882,131 @@ impl ProtonDriveClient {
/// Move `uids` to the trash. Mirrors C# `NodeOperations.TrashAsync`
/// (`POST v2/volumes/{vid}/trash_multiple`). Live validation pending.
pub async fn trash_nodes(&self, uids: &[NodeUid]) -> Result<()> {
///
/// One outcome per node the server reported on, like [`move_nodes`](Self::move_nodes):
/// a node the batch envelope rejects does not fail the others. The outer
/// `Err` is reserved for a request that never produced an envelope.
/// [`trash_nodes_streaming`](Self::trash_nodes_streaming) reports the same
/// outcomes one at a time as each batch lands.
pub async fn trash_nodes(&self, uids: &[NodeUid]) -> Result<Vec<(NodeUid, Result<()>)>> {
let mut timer = self.telemetry.start("trash_nodes");
timer.attr("node_count", uids.len());
for (volume_id, link_ids) in group_by_volume(uids) {
for chunk in link_ids.chunks(MAX_BATCH_COUNT) {
let path = format!("v2/volumes/{volume_id}/trash_multiple");
let body = MultipleLinksRequest { link_ids: chunk };
let response: AggregateLinksResponse = self.http.post(&path, &body).await?;
check_aggregate("trash", response)?;
}
}
let outcomes = self
.node_action_stream(NodeAction::Trash, uids)
.try_collect()
.await?;
timer.success();
Ok(())
Ok(outcomes)
}
/// [`trash_nodes`](Self::trash_nodes) as a stream: each node's outcome is
/// yielded as soon as the batch carrying it comes back, instead of after
/// every batch. Mirrors the C# `IAsyncEnumerable<NodeActionResult>`.
/// A failed request yields one `Err` and ends the stream.
pub fn trash_nodes_streaming<'a>(
&'a self,
uids: &[NodeUid],
) -> impl futures::Stream<Item = Result<(NodeUid, Result<()>)>> + 'a {
self.node_action_stream(NodeAction::Trash, uids)
}
/// Restore `uids` from the trash. Mirrors C#
/// `NodeOperations.RestoreFromTrashAsync`
/// (`PUT v2/volumes/{vid}/trash/restore_multiple`). Live validation pending.
pub async fn restore_nodes(&self, uids: &[NodeUid]) -> Result<()> {
/// Per-node outcomes, like [`trash_nodes`](Self::trash_nodes).
pub async fn restore_nodes(&self, uids: &[NodeUid]) -> Result<Vec<(NodeUid, Result<()>)>> {
let mut timer = self.telemetry.start("restore_nodes");
timer.attr("node_count", uids.len());
for (volume_id, link_ids) in group_by_volume(uids) {
for chunk in link_ids.chunks(MAX_BATCH_COUNT) {
let path = format!("v2/volumes/{volume_id}/trash/restore_multiple");
let body = MultipleLinksRequest { link_ids: chunk };
let response: AggregateLinksResponse = self.http.put(&path, &body).await?;
check_aggregate("restore", response)?;
}
}
let outcomes = self
.node_action_stream(NodeAction::Restore, uids)
.try_collect()
.await?;
timer.success();
Ok(())
Ok(outcomes)
}
/// [`restore_nodes`](Self::restore_nodes) as a stream of per-node outcomes.
pub fn restore_nodes_streaming<'a>(
&'a self,
uids: &[NodeUid],
) -> impl futures::Stream<Item = Result<(NodeUid, Result<()>)>> + 'a {
self.node_action_stream(NodeAction::Restore, uids)
}
/// Permanently delete `uids` (which must already be in the trash). Mirrors
/// C# `NodeOperations.DeleteFromTrashAsync`
/// (`POST v2/volumes/{vid}/trash/delete_multiple`). Live validation pending.
pub async fn delete_nodes(&self, uids: &[NodeUid]) -> Result<()> {
/// Per-node outcomes, like [`trash_nodes`](Self::trash_nodes).
pub async fn delete_nodes(&self, uids: &[NodeUid]) -> Result<Vec<(NodeUid, Result<()>)>> {
let mut timer = self.telemetry.start("delete_nodes");
timer.attr("node_count", uids.len());
for (volume_id, link_ids) in group_by_volume(uids) {
for chunk in link_ids.chunks(MAX_BATCH_COUNT) {
let path = format!("v2/volumes/{volume_id}/trash/delete_multiple");
let body = MultipleLinksRequest { link_ids: chunk };
let response: AggregateLinksResponse = self.http.post(&path, &body).await?;
check_aggregate("delete", response)?;
}
}
let outcomes = self
.node_action_stream(NodeAction::Delete, uids)
.try_collect()
.await?;
timer.success();
Ok(())
Ok(outcomes)
}
/// [`delete_nodes`](Self::delete_nodes) as a stream of per-node outcomes.
pub fn delete_nodes_streaming<'a>(
&'a self,
uids: &[NodeUid],
) -> impl futures::Stream<Item = Result<(NodeUid, Result<()>)>> + 'a {
self.node_action_stream(NodeAction::Delete, uids)
}
/// The shared body of the trash-family batch operations: group by volume,
/// chunk each group at [`MAX_BATCH_COUNT`], and yield the per-link outcomes
/// of every batch as it returns. Mirrors the C# async iterators, including
/// their mapping — only the links the aggregate envelope reports on produce
/// an outcome.
fn node_action_stream<'a>(
&'a self,
action: NodeAction,
uids: &[NodeUid],
) -> impl futures::Stream<Item = Result<(NodeUid, Result<()>)>> + 'a {
let batches: VecDeque<(VolumeId, Vec<LinkId>)> = group_by_volume(uids)
.into_iter()
.flat_map(|(volume_id, link_ids)| {
link_ids
.chunks(MAX_BATCH_COUNT)
.map(|chunk| (volume_id.clone(), chunk.to_vec()))
.collect::<Vec<_>>()
})
.collect();
stream::unfold(
(batches, VecDeque::new()),
move |(mut batches, mut pending): (VecDeque<_>, VecDeque<_>)| async move {
loop {
if let Some(outcome) = pending.pop_front() {
return Some((Ok(outcome), (batches, pending)));
}
let (volume_id, link_ids) = batches.pop_front()?;
let path = action.path(&volume_id);
let body = MultipleLinksRequest {
link_ids: &link_ids,
};
let response: Result<AggregateLinksResponse> = match action {
NodeAction::Restore => self.http.put(&path, &body).await,
NodeAction::Trash | NodeAction::Delete => {
self.http.post(&path, &body).await
}
};
match response {
Ok(response) => pending.extend(
aggregate_outcomes(response)
.into_iter()
.map(|(link_id, o)| (NodeUid::new(volume_id.clone(), link_id), o)),
),
// A request that never produced an envelope says nothing
// about its nodes: report it and stop.
Err(e) => return Some((Err(e), (VecDeque::new(), VecDeque::new()))),
}
}
},
)
}
/// Permanently empty the main volume's trash. Mirrors C#
@@ -5685,6 +5761,50 @@ impl ProtonDriveClient {
Ok(outcomes)
}
/// [`update_photos`](Self::update_photos) as a stream: one outcome per
/// photo, yielded as that photo's update finishes instead of after all of
/// them (C# `PhotoOperations.UpdatePhotosAsync`'s
/// `IAsyncEnumerable<PhotoUpdateResult>`). The single `Err` item is the
/// photos-root resolution that makes the whole call impossible; it ends the
/// stream.
pub(crate) fn update_photos_streaming<'a>(
&'a self,
updates: &[PhotoTagsUpdate],
) -> impl futures::Stream<Item = Result<(NodeUid, Result<()>)>> + 'a {
let updates: VecDeque<PhotoTagsUpdate> = updates.iter().cloned().collect();
// `None` until the first item resolves the photos volume once, exactly
// as the collecting variant does before its loop.
let own_photos_volume: Option<Option<VolumeId>> = None;
stream::unfold(
(updates, own_photos_volume),
move |(mut updates, own_photos_volume): (VecDeque<_>, Option<Option<VolumeId>>)| async move {
let update = updates.pop_front()?;
let own_photos_volume = match own_photos_volume {
Some(resolved) => resolved,
None => match self.ensure_photos().await {
Ok(true) => self
.cache
.lock()
.await
.photos_root
.clone()
.map(|root| root.volume_id),
Ok(false) => None,
Err(e) => return Some((Err(e), (VecDeque::new(), Some(None)))),
},
};
let outcome = self
.apply_tag_update(&update, own_photos_volume.as_ref())
.await;
Some((
Ok((update.node_uid.clone(), outcome)),
(updates, Some(own_photos_volume)),
))
},
)
}
/// One photo's tag update (C# `PhotoOperations.ApplyTagUpdateAsync`).
async fn apply_tag_update(
&self,
@@ -6520,6 +6640,25 @@ fn join_node_path<'a>(names: impl Iterator<Item = &'a str>) -> String {
path
}
/// Which trash-family batch endpoint a node action hits. C# has one async
/// iterator per action; the bodies differ only in route and verb.
#[derive(Debug, Clone, Copy)]
enum NodeAction {
Trash,
Restore,
Delete,
}
impl NodeAction {
fn path(self, volume_id: &VolumeId) -> String {
match self {
Self::Trash => format!("v2/volumes/{volume_id}/trash_multiple"),
Self::Restore => format!("v2/volumes/{volume_id}/trash/restore_multiple"),
Self::Delete => format!("v2/volumes/{volume_id}/trash/delete_multiple"),
}
}
}
/// Group node uids by volume, preserving order, so each volume's links are
/// batched into a single request family (C# groups by `VolumeId`).
fn group_by_volume(uids: &[NodeUid]) -> Vec<(VolumeId, Vec<LinkId>)> {
@@ -6613,26 +6752,6 @@ fn aggregate_outcomes(response: AggregateLinksResponse) -> Vec<(LinkId, Result<(
.collect()
}
fn check_aggregate(op: &str, response: AggregateLinksResponse) -> Result<()> {
let failures: Vec<String> = aggregate_outcomes(response)
.into_iter()
.filter_map(|(link_id, outcome)| match outcome {
Ok(()) => None,
Err(ProtonError::Api(e)) => Some(format!("{link_id} ({:?})", e.code)),
Err(e) => Some(format!("{link_id} ({e})")),
})
.collect();
if failures.is_empty() {
Ok(())
} else {
Err(ProtonError::invalid_operation(format!(
"{op} failed for {} link(s): {}",
failures.len(),
failures.join(", ")
)))
}
}
/// Build the photo seal attributes from a completed block write + caller
/// metadata. Mirrors C# `RevisionWriter.CreatePhotosRevisionUpdateRequest`:
/// `CaptureTime` defaults to now when unset, and `ContentHash` is the lowercase
@@ -6964,12 +7083,11 @@ fn drive_items(page: &SharedWithMeResponse) -> Vec<SharedWithMeItem> {
mod tests {
use super::{
CONTEXT_SHARE_CACHE_CAP, DriveCache, DriveEvent, FOLDER_KEY_CACHE_CAP,
MAX_NODE_NAME_LENGTH, aggregate_outcomes, alternate_names, assemble_manifest,
check_aggregate, context_share_path, drive_items, epoch_to_iso8601,
is_expired_upload_token, is_listable_revision_state, is_upload_timeout, join_node_path,
path_segments, run_context_share_mutation, share_membership_from_dto,
small_upload_applicable, take_block_target, take_thumbnail_target, to_drive_event,
validate_node_name,
MAX_NODE_NAME_LENGTH, NodeAction, aggregate_outcomes, alternate_names, assemble_manifest,
context_share_path, drive_items, epoch_to_iso8601, is_expired_upload_token,
is_listable_revision_state, is_upload_timeout, join_node_path, path_segments,
run_context_share_mutation, share_membership_from_dto, small_upload_applicable,
take_block_target, take_thumbnail_target, to_drive_event, validate_node_name,
};
use crate::dtos::{
AggregateLinksResponse, BlockUploadTarget, LinkIdResponsePair, ShareMembershipSummaryDto,
@@ -7009,6 +7127,23 @@ mod tests {
}
}
#[test]
fn node_action_routes_match_upstream() {
let volume = VolumeId::new("vol-1");
assert_eq!(
NodeAction::Trash.path(&volume),
"v2/volumes/vol-1/trash_multiple"
);
assert_eq!(
NodeAction::Restore.path(&volume),
"v2/volumes/vol-1/trash/restore_multiple"
);
assert_eq!(
NodeAction::Delete.path(&volume),
"v2/volumes/vol-1/trash/delete_multiple"
);
}
#[test]
fn aggregate_outcomes_keeps_per_link_codes() {
let outcomes = aggregate_outcomes(mixed_aggregate(ResponseCode::DoesNotExist));
@@ -7029,19 +7164,6 @@ mod tests {
}
}
#[test]
fn check_aggregate_reports_only_the_failed_links() {
check_aggregate("move", mixed_aggregate(ResponseCode::Success))
.expect("all-success envelope must pass");
let err = check_aggregate("move", mixed_aggregate(ResponseCode::DoesNotExist))
.expect_err("a failed link must fail the batch");
let message = err.to_string();
assert!(message.contains("move failed for 1 link(s)"), "{message}");
assert!(message.contains("bad-link"), "{message}");
assert!(!message.contains("ok-link"), "{message}");
}
#[test]
fn shared_with_me_items_keep_share_ids_order_and_drive_targets() {
let link = |link_id: &str, share_id: &str, target_type| SharedWithMeLinkDto {
+11
View File
@@ -304,6 +304,17 @@ impl ProtonPhotosClient {
) -> Result<Vec<(NodeUid, Result<()>)>> {
self.drive.update_photos(updates).await
}
/// [`update_photos`](Self::update_photos) as a stream: each photo's outcome
/// arrives as that photo's update finishes, mirroring the C#
/// `IAsyncEnumerable<PhotoUpdateResult>`. The `Err` item is a failure that
/// makes the whole call impossible and ends the stream.
pub fn update_photos_streaming<'a>(
&'a self,
updates: &[PhotoTagsUpdate],
) -> impl futures::Stream<Item = Result<(NodeUid, Result<()>)>> + 'a {
self.drive.update_photos_streaming(updates)
}
}
#[cfg(test)]
@@ -17,6 +17,7 @@ use hmac::{Hmac, KeyInit, Mac};
use proton_drive_rs::ProtonDriveClient;
use proton_sdk::config::ProtonClientConfiguration;
use proton_sdk::error::ProtonError;
use proton_sdk::ids::NodeUid;
use proton_sdk::session::{PasswordMode, ProtonApiSession, ResumeParameters};
use serde::{Deserialize, Serialize};
use sha1::Sha1;
@@ -362,3 +363,27 @@ pub fn unique_suffix() -> String {
.as_nanos();
format!("{nanos}")
}
/// Panic on any per-node failure in a batch outcome list (`trash_nodes`,
/// `restore_nodes`, `delete_nodes`, `move_nodes`): those calls report one
/// outcome per node instead of failing the whole call, so a test that expects
/// success has to look at every outcome.
#[allow(dead_code)]
pub fn expect_all_ok(what: &str, outcomes: &[(NodeUid, Result<(), ProtonError>)]) {
for (uid, outcome) in outcomes {
if let Err(e) = outcome {
panic!("{what} failed for {uid}: {e}");
}
}
}
/// Report per-node failures on a best-effort cleanup path without failing the
/// test that is tearing down.
#[allow(dead_code)]
pub fn log_outcomes(what: &str, outcomes: &[(NodeUid, Result<(), ProtonError>)]) {
for (uid, outcome) in outcomes {
if let Err(e) = outcome {
eprintln!("[cleanup] {what} failed for {uid}: {e}");
}
}
}
+13 -6
View File
@@ -15,12 +15,16 @@ use proton_sdk::ids::{LinkId, NodeUid};
/// 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 {
match client.trash_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("trash", &outcomes),
Err(e) => {
eprintln!("[cleanup] trash failed: {e}");
return;
}
if let Err(e) = client.delete_nodes(uids).await {
eprintln!("[cleanup] delete failed: {e}");
}
match client.delete_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("delete", &outcomes),
Err(e) => eprintln!("[cleanup] delete failed: {e}"),
}
}
@@ -77,10 +81,11 @@ async fn folder_create_rename_trash_restore() {
assert_eq!(node.name, renamed, "rename must take effect");
// trash
client
let outcomes = client
.trash_nodes(std::slice::from_ref(&uid))
.await
.expect("trash");
common::expect_all_ok("trash", &outcomes);
let node = get(client, &uid, "after trash").await;
assert!(node.trashed, "node must report trashed");
let trash = client
@@ -90,10 +95,11 @@ async fn folder_create_rename_trash_restore() {
assert!(trash.contains(&uid), "trash listing must include the node");
// restore
client
let outcomes = client
.restore_nodes(std::slice::from_ref(&uid))
.await
.expect("restore");
common::expect_all_ok("restore", &outcomes);
let node = get(client, &uid, "after restore").await;
assert!(!node.trashed, "node must no longer be trashed");
@@ -121,10 +127,11 @@ async fn folder_empty_trash() {
.await
.expect("create_folder");
client
let outcomes = client
.trash_nodes(std::slice::from_ref(&uid))
.await
.expect("trash");
common::expect_all_ok("trash", &outcomes);
assert!(
client
.enumerate_trash_node_uids()
+16 -8
View File
@@ -21,12 +21,16 @@ use proton_sdk::ids::{DriveEventId, NodeUid};
/// 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 {
match client.trash_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("trash", &outcomes),
Err(e) => {
eprintln!("[cleanup] trash failed: {e}");
return;
}
if let Err(e) = client.delete_nodes(uids).await {
eprintln!("[cleanup] delete failed: {e}");
}
match client.delete_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("delete", &outcomes),
Err(e) => eprintln!("[cleanup] delete failed: {e}"),
}
}
@@ -230,10 +234,11 @@ async fn trashing_surfaces_is_trashed() {
// Seed after creation so the trash is the change we look for.
let cursor = seed(client, &scope).await;
client
let outcomes = client
.trash_nodes(std::slice::from_ref(&folder))
.await
.expect("trash folder");
common::expect_all_ok("trash", &outcomes);
let found = poll_for(client, &scope, &cursor, |e| {
matches!(
@@ -248,8 +253,9 @@ async fn trashing_surfaces_is_trashed() {
);
// Best-effort permanent delete (already trashed).
if let Err(e) = client.delete_nodes(std::slice::from_ref(&folder)).await {
eprintln!("[cleanup] delete failed: {e}");
match client.delete_nodes(std::slice::from_ref(&folder)).await {
Ok(outcomes) => common::log_outcomes("delete", &outcomes),
Err(e) => eprintln!("[cleanup] delete failed: {e}"),
}
}
@@ -273,17 +279,19 @@ async fn permanent_delete_surfaces_node_deleted() {
.create_folder(&root.uid, &format!("evt-delete-{suffix}"), None)
.await
.expect("create folder");
client
let outcomes = client
.trash_nodes(std::slice::from_ref(&folder))
.await
.expect("trash folder");
common::expect_all_ok("trash", &outcomes);
// Seed after trashing so the permanent delete is the awaited change.
let cursor = seed(client, &scope).await;
client
let outcomes = client
.delete_nodes(std::slice::from_ref(&folder))
.await
.expect("delete folder");
common::expect_all_ok("delete", &outcomes);
let found = poll_for(
client,
+7 -3
View File
@@ -26,12 +26,16 @@ use proton_sdk::ids::NodeUid;
/// Trash then permanently delete the given node; best-effort, logs on failure.
async fn cleanup(client: &ProtonDriveClient, uid: &NodeUid) {
if let Err(e) = client.trash_nodes(std::slice::from_ref(uid)).await {
match client.trash_nodes(std::slice::from_ref(uid)).await {
Ok(outcomes) => common::log_outcomes("trash", &outcomes),
Err(e) => {
eprintln!("[cleanup] trash failed: {e}");
return;
}
if let Err(e) = client.delete_nodes(std::slice::from_ref(uid)).await {
eprintln!("[cleanup] delete failed: {e}");
}
match client.delete_nodes(std::slice::from_ref(uid)).await {
Ok(outcomes) => common::log_outcomes("delete", &outcomes),
Err(e) => eprintln!("[cleanup] delete failed: {e}"),
}
}
@@ -17,12 +17,16 @@ use proton_sdk::ids::NodeUid;
/// 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 {
match client.trash_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("trash", &outcomes),
Err(e) => {
eprintln!("[cleanup] trash failed: {e}");
return;
}
if let Err(e) = client.delete_nodes(uids).await {
eprintln!("[cleanup] delete failed: {e}");
}
match client.delete_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("delete", &outcomes),
Err(e) => eprintln!("[cleanup] delete failed: {e}"),
}
}
@@ -49,12 +49,16 @@ impl OpCounter {
/// 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 {
match client.trash_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("trash", &outcomes),
Err(e) => {
eprintln!("[cleanup] trash failed: {e}");
return;
}
if let Err(e) = client.delete_nodes(uids).await {
eprintln!("[cleanup] delete failed: {e}");
}
match client.delete_nodes(uids).await {
Ok(outcomes) => common::log_outcomes("delete", &outcomes),
Err(e) => eprintln!("[cleanup] delete failed: {e}"),
}
}