feat(sharing,public-link): update public links, name bookmarks, walk shared trees

- update_public_link sets the role, custom password and expiry of a node's
  public link and keeps its URL (TS updateSharedLink). Create and update
  share the sealing of the share key and SRP verifier.
- list_bookmarks decrypts the bookmarked item's name with the share key the
  stored password unlocks, and returns the custom password in the new
  Bookmark::custom_password. The URL fragment is now only the generated
  password.
- ProtonDrivePublicLinkClient::enumerate_subtree walks a shared folder
  breadth first, several folders of a level at a time.
- Add CHANGELOG.md, update the README feature table for this and the
  previous unreleased commits, and record the TS ports in UPSTREAM_SYNC.
This commit is contained in:
nvrl committed 2026-10-25 21:27:05 +01:00
1 parent 3da18b9309
commit 781301a77b
8 files changed
+632 -99

No files matched your search

+1
View File
@@ -44,3 +44,4 @@ gitignored, so this file is the only durable record of what we last reviewed.
| 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`. |
| 2026-10-25 | — (no C# delta) | — | **not an upstream port of C#** — two ports from the **TypeScript** SDK, plus one change of ours. **Folder size** (TS `getFolderSize`, `243f11f4`): `ProtonDriveClient::get_folder_size` → `GET volumes/{vid}/folders/{lid}/calculate-descendents-size`, returning `FolderSize { size, descendants }`. Live-verified, which settled what the TS docs leave open: folders count as descendants, and the size is encrypted bytes on storage (two single-block files of 301 000 plaintext bytes report 301 112). **Copy** (TS `NodesManagement.copyNode`): `ProtonDriveClient::copy_node(uid, new_parent, name)` → `POST volumes/{vid}/links/{lid}/copy`, a server-side copy of a file with its content or a folder with its subtree. The crypto is the move's (`build_move_parts`: passphrase rewrapped to the new parent, name encrypted and signed by the My Files address, signature fields only for an anonymously signed node); `CopyPhotoResponse` became the shared `CopyLinkResponse`. Diverged deliberately: the returned uid takes the **target** parent's volume, where TS uses the source's, which only holds within one volume; no `copyNodes` batch, which in TS is a sequential loop over `copyNode`. Live-verified: file content, rename-on-copy, folder subtree, and `AlreadyExists` for a taken name. **Ours**: reads are retried after a dropped connection — `send_retrying` reads the body inside its loop and takes a `Replay` mode; a `GET`, a block download and the new `ApiHttpClient::post_query` (link details, thumbnail URLs, photo duplicates, `checkAvailableHashes`) are sent again when the connection drops mid-request or mid-body, while writes keep the old rule. |
| 2026-10-25 | `8ffae007` → `b68d64bd` | `a36cc423` | ported: **album capabilities** (`a36cc423`), the C# album write API, against the TS-sourced album writes we already had. `add_photos_to_album` now goes through the timeline transfer driver, renamed `transfer_photos` over a `PhotoTransferTarget` with a `PhotoTransferKind` (C# `TransferPhotosToTargetAsync` / `PhotoTransferTarget`): same-volume photos through `add-multiple` (the per-link answers share `TransferPhotosResponse`, so `AddPhotosToAlbumResponse` is gone), photos on **another volume** copied into the album through `POST volumes/{vid}/links/{lid}/copy` instead of failing, and a photo whose group the server reports incomplete re-sent once with the related photos it named; a related photo that cannot be re-encrypted is skipped rather than failing its main photo. The call still answers in input order. `add-multiple` entries never carry a passphrase signature (C# `ToAlbumPhotoLinkItem`). New `ProtonPhotosClient::set_album_cover` (the cover half of `UpdateAlbumAsync`; same volume only, refused before sending), and `AlbumUpdateRequest` now omits a `None` cover or link like C#. New `delete_album_saving_photos` (`DeleteAlbumAsync` with `saveToTimeline`): on `ResponseCode::AlbumContainsPhotosNotInTimeline` (200302, new) it saves the photos `Details.ChildLinkIDs` names, then deletes again; the first photo that cannot be saved fails the call. `remove_photos_from_album` fails a photo on another volume without sending it. Kept additive: `delete_album(uid, delete_photos)` and `rename_album` keep their signatures (`proton-drive-linux` calls both) rather than taking C#'s `(force, saveToTimeline)` and `(name?, cover?)`. Live-verified on the test account: add, adding twice (`Photo already exists in the album.`), cover, remove, delete keeping photos. Not live-verified: the cross-volume copy and the 200302 save, which need a second account sharing an album. **not ported: experimental search** (`e861ef99`): the engine is a Rust crate under `incubating/search` whose source is not in the public tree — only its C# bindings' wiring (`Search.targets`, `ProtonDriveSearchClient`) and a `BinaryKeyValueStore` for its index are. The rest of that commit is cosmetic (`await foreach` over a local, `SqliteConnectionStringBuilder`). |
| 2026-10-25 | — (no C# delta) | — | **Not an upstream port from C#**: three sharing gaps closed from the **TypeScript** SDK. **Public link update** (TS `SharingManagement.updateSharedLink` / `apiService.updatePublicLink`): `ProtonDriveClient::update_public_link(uid, role, custom_password, expiration_time)` → `PUT shares/{sid}/urls/{urlID}`. The generated password is taken from the recovered URL fragment and must be 12 characters (TS refuses legacy links the same way); the share key and SRP verifier are sealed again under it plus the new custom password. Create and update now share `seal_public_link_settings` and a flattened `PublicLinkSettingsDto`; update sends `ExpirationTime: null` to clear an expiry, as TS does. **Bookmark names** (TS `SharingCryptoService.decryptBookmark`): the whole stored password, bcrypt-derived with `SharePasswordSalt`, unlocks the share key, which decrypts `Token.Name`; signatures are not checked, as in TS. `Bookmark` gains `custom_password` (TS `splitGeneratedAndCustomPassword`), and the URL fragment is now only the generated 12 characters. **Ours**: `ProtonDrivePublicLinkClient::enumerate_subtree` walks a shared folder breadth first, 6 folders per level at once (lifted from proton-stream's crawl; neither SDK has it). Live-verified on the test account: update to Editor + custom password + expiry, then clear both; bookmark on our own link lists its name and custom password; a nested subtree walks in level order. |