mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
Port af391cbb (require node type for getting an available name); c6b45188 (file-backed transfer buffers) is C# memory plumbing and a no-op here. - get_available_name takes an AvailableNameTarget. A file keeps its extension last (name (1).ext); a folder appends to the whole name (name (1)), so a folder named v1.2 no longer resolves to v1 (1).2. - Split alternate_names into alternate_file_names and alternate_folder_names, mirroring AlternateFileNameGenerator. Breaking: get_available_name gains a parameter.
24 KiB
24 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:
8ffae007 - Date: 2026-10-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. |
| 2026-09-19 | ce000edd → 6cbf2f44 |
6cbf2f44, 20822176 |
retriability on block transfers (6cbf2f44): new ProtonError::is_retriable() / ProtonApiError::is_retriable() (C# RetryPolicy.IsRetriable — a 4xx is permanent except 404, 408 and 429; the envelope codes TooManyRequests and DoesNotExist override the status, as a rate limit can arrive under a 2xx-shaped envelope). upload_one_block now ends the transfer on a permanent rejection instead of spending its remaining attempts on it, keeping the existing expired-upload-token re-mint and timeout downshift. The whole-file download path gained the expired-URL recovery RevisionReader already had: the block table plus its deduplicated one-shot re-list moved into revision::BlockTargets, which RevisionReader::block_plaintext, ProtonDriveClient::write_content_blocks and the public-link download_file_to all fetch through — so a download long enough to outlive its block URLs re-lists the revision once per expiry rather than failing. diverged deliberately: thumbnail blob fetches got no change — upstream passes them refreshTransferTargetAsync: null and relies on the same predicate to stop retrying, and our thumbnail fetch is a single get_storage_blob whose only retry is http::send_retrying, which already retries 408/429/502/503/504 and nothing else. small-upload interop (20822176): upload_file / upload_new_revision now fall back to the regular block path when the one-request endpoint reports AlreadyExists (2500) — the Proton API reports upstream's NodeWithSameNameExists and RevisionDraftConflict under that one code, and the block path knows how to delete our own stale draft and retry while the small endpoint is one shot. Everything else still propagates: a small upload creates, uploads and seals in a single non-idempotent request, so replaying it on 429/424/5xx/a dropped connection risks a duplicate file (upstream excludes 429 and 424 explicitly). Not ported: upstream's SmallUploadNotApplicable condition has no envelope code we can recognise, and HttpRequestType.SmallUpload maps to our telemetry op name rather than a new request type. no-op: 6ce47922 (thumbnail retry leaking upload-queue slots) — we have no UploadQueue and no per-file block accounting, only the client-wide in-flight block semaphore, and our thumbnail list is a slice rather than a lazily re-enumerated IEnumerable, so the double-reservation it fixes cannot occur. Ours, not upstream: RetryPolicy::max_retry_after (default 120 s) now bounds a server-supplied Retry-After, which was honoured uncapped inside a sleep a daemon thread blocks on — an hour-long one is indistinguishable from a hang. Live validation pending: a multi-block upload, a <128 KiB upload, and a download left running past its block-URL lifetime. |
| 2026-09-27 | 6cbf2f44 → f28a93ce |
— | no behavioral changes. not ported: 0c054f0c (DriveClientEnumerateNodes / DrivePhotosClientEnumerateNodes interop messages exposing C# EnumerateNodesAsync to the Kotlin bindings — CExports/proto only; ProtonDriveClient::enumerate_nodes and ProtonPhotosClient::enumerate_nodes already exist). |
| 2026-10-06 | f28a93ce → df3a717b |
0aabea59 |
ported: recently accessed report (ProtonDriveClient::report_recently_accessed → POST recently-accessed-items, ProtonPhotosClient::report_recently_accessed → POST photos/recently-accessed-items; batches of 50, missing AccessTime defaults to now, epoch seconds; RecentlyAccessedReportItem takes Option<i64> rather than DateTimeOffset). Also ported the DTO half of f257c20e: LinkDto / ActiveRevisionDto read ThirdParty and Sdk (default false). not ported: rest of f257c20e (DecryptionErrorEvent / VerificationErrorEvent with Recency and CreatedBy — telemetry plumbing for C#'s interop metric callback, we emit no such events; same reasoning as 29e58378). Live validation pending: the recently-accessed endpoints. |
| 2026-10-15 | df3a717b → 8ffae007 |
af391cbb |
ported: folder-aware available names (af391cbb, breaking upstream and here): get_available_name takes an AvailableNameTarget (File / Folder). A file keeps its extension last (name (1).ext, as before); a folder appends to the whole name (name (1)), so a folder named v1.2 no longer resolves to v1 (1).2. alternate_names split into alternate_file_names / alternate_folder_names (C# AlternateFileNameGenerator.GetFileNames / GetFolderNames). no-op: c6b45188 (C# transfer buffers optionally backed by an unlinked mkstemp file instead of RecyclableMemoryStream, behind a compile-time flag) — memory plumbing only; our blocks are Vec<u8> bounded by the client-wide in-flight block semaphore, and the verifier prefix it now reads from the stream is the same min(prefix, len) bytes our verification_token already zero-pads. Downstream: no get_available_name caller in proton-drive-linux or proton-stream. |