mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
- Moves go through `move-multiple` in batches of 150, with an optional target name per node (`move_nodes_streaming` / `NodeMoveItem`), a single `InvalidRequirements` retry against the server's current name hash, and passphrase signing for anonymously created nodes. A batch rejected as a whole keeps its API error code on every node. - Share memberships carry invite time and inviter email, and the inviter's `drive.share-member.inviter` signature is verified as non-fatal metadata (`verify_detached_with_context`). - `Node` gains `direct_role` and `share_id`. - Trashed detection reads `TrashTime` as well as the legacy `Trashed`. Breaking: new public struct fields on `Node` and `ShareMembership`.
19 KiB
19 KiB
Upstream sync
Tracks reconciliation of this pure-Rust port against the canonical C# SDK
(ProtonDriveApps/sdk, subtree client/cs/sdk/src). The sdk/ checkout is
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(wasclient/cs/sdk/srcbefore the mid-2026 projects reorg; the script watches both) - Pinned:
ce000edd053768bf8fd2da04f3ef4fa68ba9e71e - Date: 2026-09-15
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.
Workflow
./scripts/upstream-sync.sh— fetches upstream, lists cs-relevant commits since the pinned SHA, drops noise (chore/docs/test/ci/build), prints diffs.- Triage each surviving commit: behavioral change to port, or structural/cosmetic.
- Port the behavioral diffs into
crates/. - Bump Pinned (and Date) above to the new upstream HEAD; commit.
Log
| date | from → to | ported | notes |
|---|---|---|---|
| 2026-06-25 | initial pin fb4173ab |
— | baseline; delta reviewed, 0 behavioral changes outstanding |
| 2026-06-26 | fb4173ab → 36430318 |
36430318 |
ported: enumeration returns NodeUids (enumerate_folder_children_node_uids / enumerate_trash_node_uids), caller materializes via enumerate_nodes. noise dropped: BOM/deps/kt-enum-order/cs-account-refactor |
| 2026-07-10 | 36430318 → f2496161 |
9a1c39b3 |
ported: validation check in Thumbnail::new returning Result instead of panicking. noise dropped: projects reorg / C# Account client move to incubating. deferred: device support (32a8eed0). |
| 2026-07-14 | f2496161 → 2219a42f |
d15fa9b9, bead8f65, f6b12911, c0940f52, b88b8041, 2eb69c46 |
upstream force-pushed + moved the cs subtree, so the old pin was dangling and the script silently reported "up to date" (both fixed). ported: devices (devices.rs + enumerate_devices / create_device / rename_device / delete_device; volume-free DeviceUid), node sharing flags (Node::is_shared / is_shared_publicly from the link Sharing block), revision state (NodeKind::File::active_revision_state), shared-with-me (enumerate_shared_with_me_node_uids, GET v2/sharedwithme), leave shared node (leave_shared_node, DELETE v2/shares/{sid}/members/{mid}). no-op: 15fa845e (non-JSON error bodies — http.rs already falls back to a status-only error when the envelope won't parse). diverged deliberately: 9a7f1d02 + b4dadf02 removed the C# entity cache and its tags; ours stays. cosmetic, not ported: 30ab32c1 / 388d3900 (C# field renames), 1a781ade (buffer lengths). |
| 2026-07-24 | 1b6ff835 → e0ad37e3 |
d2ffde20, 724b6771 |
both features were already ported from the TypeScript SDK, so this closed the remaining parity deltas. ported: shared-by-me on the photos volume (SharingOperations.EnumerateSharedNodeUidsAsync now takes a volume resolver — page_shared_by_me(volume_id) + ProtonPhotosClient::enumerate_shared_node_uids, empty when the account has no photos volume) and SharedByMeLinkDto.ContextShareID (kept Option — recent upstream addition, older backends omit it); per-node move outcomes (move_nodes now returns Vec<(NodeUid, Result<()>)> in input order, mirroring the breaking MoveNodesAsync result map; per-link envelope codes via the new aggregate_outcomes, which check_aggregate now sits on). cosmetic, not ported: AnchorID retyped string → LinkId (ours is String). diverged deliberately: we keep the batch endpoint PUT volumes/{vid}/links/move-multiple — upstream still loops per node behind a // FIXME: … use the batch move endpoint; and the drive-side listing keeps the _by_me suffix (upstream renamed both to EnumerateSharedNodeUids) because proton-drive-linux already consumes that name. |
| 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 chars — 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). |
| 2026-08-10 | — (no upstream delta) | — | not an upstream port. ProtonDriveClient::with_entity_repository — the chainable form of with_entity_cache, added because that one is a constructor and so could not be combined with with_key_salts; a client that resumes a persisted session needs the salts and wants a persistent entity cache, and upstream C# has no equivalent tension (its cache is set on the client options object). Additive only: with_entity_cache and the in-memory default are unchanged. Consumed by proton-drive-linux, which now backs the entity cache with an encrypted SQLite store. |
| 2026-08-18 | — (no upstream delta) | — | not an upstream port of C# — ported from the TypeScript SDK, which is the only upstream with an album write API. Ported: album creation (ProtonPhotosClient::create_album — POST photos/volumes/{vid}/albums, from AlbumsManager.createAlbum + AlbumsCryptoService.createAlbum; an album is a folder node parented to the photos root, so the crypto is the folder create: generated node key, passphrase encrypted to the photos root key and signed by the photos share's membership address, plus the album's own child-name hash key, and TS's Locked: false) and adding photos to an album (add_photos_to_album — POST photos/volumes/{vid}/albums/{lid}/add-multiple, from AddToAlbumProcess.processSameVolumeQueue + PhotoTransferPayloadBuilder; per photo the name is re-encrypted to the album key, name/content hashes recomputed under the album hash key, and the node passphrase rewrapped via rewrap_message_to rather than re-encrypted from plaintext — the ciphertext and therefore the original NodePassphraseSignature survive, which is why the entry carries no signature of its own; related photos are flattened in beside their main photo, batches of 10 like TS's ADD_PHOTOS_BATCH_SIZE, one outcome per input photo in input order like move_nodes). Not ported: TS copyPhoto (adding a photo that lives on another volume, i.e. from an album shared with us — needs the cross-volume copy path; those inputs fail their own outcome), updateAlbum (rename / cover photo) and deleteAlbum. Also ours, with no upstream counterpart: find_duplicates_many and name_collisions — the duplicates endpoint already takes a NameHashes array but both SDKs only ever send one, and a Google Takeout import asks the question tens of thousands of times; name_collisions answers on the name hash alone so the caller only has to read and SHA-1 the files whose names actually collide. Live validation pending: everything here needs a photos-volume account. |
| 2026-08-27 | df6b6bfd → c8d03244 |
e0e198e2, c8d03244 |
ported: save photos to the timeline (ProtonPhotosClient::save_photos_to_timeline — C# PhotoOperations.SavePhotosToTimelineAsync, itself the JS saveToTimeline). Photos already on our photos volume are moved into the timeline root through PUT photos/volumes/{vid}/links/transfer-multiple, batched so no request carries more than ten links (main photo + related photos counted together, never split across requests); photos on another volume — from a shared-with-me album — are copied through POST volumes/{vid}/links/{lid}/copy, one request each, with their related photos nested in Photos. This closes the cross-volume copy hole logged in the 2026-08-18 row (TS copyPhoto), so photos from albums shared with us can now be kept. The re-encryption is what add_photos_to_album already did — name re-encrypted to the target key and signed by the photos membership address, name/content hashes recomputed under the target hash key, node passphrase rewrapped via rewrap_message_to so the original NodePassphraseSignature survives — so it was lifted into a shared photo_target_payload (C# PhotosCrypto.EncryptPhotoForTarget + PhotoTransferPayloadBuilder) that both paths build on. Retry semantics mirror upstream exactly: a photo the server reports an incomplete group for is re-queued once with the related photos it named, and only its second answer becomes an outcome; a photo already parented to the timeline root fails its own outcome. Also ported: ProtonPhotosClient::enumerate_events (c8d03244's only non-interop hunk — a passthrough to the Drive client's, which we already had). diverged deliberately: upstream returns IAsyncEnumerable<NodeActionResult>; ours collects Vec<(NodeUid, Result<()>)> like move_nodes and the other batch ops — and, because of the retry queue, outcome order follows completion, not input, which the API docs state. not ported: 29e58378 per-file upload performance metrics (UploadPerformanceEvent, size/route/block-count classification, active-vs-paused attempt timing on UploadController) — it is telemetry plumbing for C#'s interop metric callback; our proton-sdk::telemetry has its own TelemetryEvent/OpTimer shape and we have no pause/resume upload controller, and nothing about the upload itself changes. structural, not ported: 0537d906 (C# merges its batch loaders and DtoToMetadataConverter into one NodeProvider; the only behavior-adjacent bit is dropping the cache's claim/acquisition dance, which our single-flight already covers). Live validation pending: the whole path needs a photos-volume account, and the cross-volume half needs a second account sharing an album with it. |
| 2026-09-15 | c8d03244 → ce000edd |
cdc0f260, 4b1129aa, 63a7fc1e, 5cbbb0ad, e33712b9 |
ported: trash time field name (cdc0f260 — C# dropped a [JsonPropertyName("Trashed")] that never matched the API's TrashTime; ours had copied the same wrong name, so LinkDto now reads TrashTime and keeps Trashed as a separate legacy field rather than a serde alias, which would fail a link carrying both). direct role + membership detail (4b1129aa): Node::direct_role — Admin on our own main or photos volume (is_own_volume, C# VolumeOperations.IsOwnVolumeAsync), otherwise the membership's permissions or Inherited; ShareMembership gains invite_time, inviter_email and inviter_verification, the last from checking the inviter's drive.share-member.inviter signature over our share-passphrase key packet against the claimed inviter's keys (C# NodeCrypto.VerifyMembershipInviter; new proton_sdk::crypto::verify_detached_with_context, which requires the context@proton.ch notation to match and accepts armored or base64-binary signatures). Non-fatal like every other verification. Kept optional (Option<MemberRole> / Option<VerificationStatus>, skip_serializing_if on the membership additions) so persisted Node blobs keep deserializing and read back as "unknown" rather than as a wrong role; C# models SharedBy as Result<Author, SignatureVerificationError> — ours is the claimed email plus the status. share id (63a7fc1e): Node::share_id, the Sharing block's ShareID (C# DeprecatedShareId, internal there; public here as the JS SDK has it). batched move with rename, streamed (5cbbb0ad + e33712b9 — upstream finally replaced its per-node loop with the move-multiple endpoint we already used): new move_nodes_streaming(Vec<NodeMoveItem>, NodeUid) yielding (NodeUid, Result<()>) per node as each batch lands, with NodeMoveItem::target_name for rename-on-move; batches of 150 that never mix anonymously signed nodes with signed ones; an anonymously signed node now gets its passphrase signed by the mover plus SignatureEmail — also fixed in move_node, which never did this (C# MoveSingleAsync always had); a link rejected InvalidRequirements (2000) is retried once with the server's NameHash provided parent and decrypted name are unchanged, else fails out of sync (C# PrepareItemWithRemoteNameDigestAsync). move_nodes keeps its signature and input-order contract and now collects the stream. diverged deliberately: NodeMoveItem carries no CurrentParentUid/CurrentName — we read both off the link rather than trust the caller, so upstream's retry mostly exists for a case we only hit through a stale cached OriginalHash; per-code exception types (NodeWithSameNameExistsException, FolderCapacityExceededException, FolderNestingTooDeepException, CannotMoveFavoritePhotoFromShareException, NodeOutOfSyncException, ParentFolderNotFoundException) are not ported — the per-link ProtonError::Api keeps the ResponseCode to match on; RenameNodeAsync removal not mirrored (rename_node stays, proton-drive-linux uses it). no-op: e3d602b9 (C# HashingReadStream/HashingWriteStream hashed the whole buffer instead of offset..count; our upload hashes buf.truncate(n)-ed blocks and has no stream wrapper). noise: df87c3aa (AEAD feature-flag removal, chore). Downstream: adding Node::{direct_role, share_id} and ShareMembership::{invite_time, inviter_email, inviter_verification} breaks struct literals in proton-drive-linux tests (pdfs-fuse/src/state.rs) at the next SDK bump. Live validation pending for all of it — membership verification and anonymous-node moves need a second account. |