Every client built by the SDK now shares one builder. It sends TCP
keepalives and HTTP/2 PING frames every 15 seconds, also while the
connection is idle, and drops a connection whose PING goes unanswered for
10 seconds. A connection that a NAT or middlebox silently dropped is then
noticed before a request waits on it for the full request timeout.
Connecting is capped at 15 seconds, or the request timeout if that is
shorter, so an unreachable host fails fast and the retry loop moves on.
Port 6cbf2f44 (stop retrying permanent API errors on block transfers)
and 20822176 (small file upload interop); 6ce47922 is a no-op here.
- Add ProtonApiError::is_retriable / ProtonError::is_retriable mirroring
C# RetryPolicy.IsRetriable: 4xx is permanent except 404, 408 and 429,
with envelope codes able to override the HTTP status.
- Gate upload_one_block's retry loop on it so a permanent rejection of a
revision fails immediately instead of being replayed three times.
- Extract BlockTargets from RevisionReader so the whole-file download
path, the public link download and the reader all re-mint expired
block URLs through one generation-deduplicated refresh.
- Fall back from the small upload to the block path on AlreadyExists
only; the small upload is not idempotent, so everything else keeps
propagating.
- Bound a server-supplied Retry-After by RetryPolicy::max_retry_after
(default 120 s) so a long rate-limit answer cannot look like a hang.
- 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`.
Public links (public_link.rs, ported from the TS SDK — the C# SDK has no
equivalent): ProtonDrivePublicLinkClient lets a visitor with no Proton
account open someone else's share link. `info` reads pre-auth metadata,
`open` runs the SRP handshake against drive/urls/{token}/info + /auth and
unlocks the share key (bcrypt SharePasswordSalt -> key password -> the
SKESK'd SharePassphrase -> ShareKey). The read surface then reuses the
ordinary link/children/revision endpoints. Signature verification is
deliberately absent: an anonymous visitor cannot read core/v4/keys/all,
matching the TS SDK. Supporting primitives: http::get_unauthenticated and
crypto::decrypt_armored_with_password.
Revision history: enumerate_revisions / get_revision / restore_revision /
delete_revision, plus open_revision_at / download_revision{,_to} to read a
specific revision. Drafts are filtered on the wire value, since
RevisionState::from_raw maps 0 to Active. Three server behaviors found
live and worked around: ?NoBlockUrls=true still emits Blocks with null
BareURL, so get_revision deserializes into a DTO without that field; the
restore POST needs an empty object body, not null; and restore is async
(HTTP 202, non-1000 envelope) so the new active revision is not live on
return.
Path and hierarchy helpers: get_node_hierarchy, get_node_path,
get_node_by_path, create_folder_path. Names are encrypted and there is no
name-query endpoint, so lookup walks and decrypts one directory level per
segment.
Fix a silent-corruption path in block-size resolution. RevisionReader
accumulates block sizes to derive where each block starts in the
plaintext, so a vector that overstates any block but the last shifts every
block after it and the reader splices at the wrong offsets — full-length
reads of the wrong bytes, no error raised. resolve_block_sizes previously
guessed all-full blocks when both Common.BlockSizes and Common.Size were
unusable. It now errors for multi-block files and only assumes a full
block when there is exactly one (which cannot be misaligned). Regression
test in revision.rs::padded_block_sizes_shift_later_blocks.
Recover from stale upload drafts. An upload interrupted after the draft
node was created but before it was sealed leaves an unsealed draft, and
every later attempt at that name returns 422 AlreadyExists forever. Draft
creation now deletes the conflicting node and retries when the conflict is
an unsealed draft; by default only our own client's, with
upload_file_replacing_draft_from opting into any client's to recover
across a daemon restart (which rotates our client uid). A committed file
of that name is never touched.
Live tests for the public-link round trip, custom-password links, revision
history, and the path helpers.