feat(drive,photos): report recently accessed nodes

Port upstream 0aabea59: report_recently_accessed on ProtonDriveClient and
ProtonPhotosClient, batches of 50. Also read the ThirdParty/Sdk flags from
f257c20e on link and revision DTOs. Pin bumped to df3a717b.
This commit is contained in:
nvrl committed 2026-10-06 23:49:58 +02:00
1 parent 7950eff337
commit 32014b1f07
7 files changed
+141 -7

No files matched your search

+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 - **Upstream**: https://github.com/ProtonDriveApps/sdk
- **Reconciled subtree**: `client/cs/src` (was `client/cs/sdk/src` before the - **Reconciled subtree**: `client/cs/src` (was `client/cs/sdk/src` before the
mid-2026 projects reorg; the script watches both) mid-2026 projects reorg; the script watches both)
- **Pinned**: `f28a93ce` - **Pinned**: `df3a717b`
- **Date**: 2026-09-27 - **Date**: 2026-10-06
> **Note**: upstream force-pushes — the SHAs in rows before 2026-07-14 no longer > **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. > exist in its history. Triage by commit subject/date when a pin goes stale.
@@ -40,3 +40,4 @@ gitignored, so this file is the only durable record of what we last reviewed.
| 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-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-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-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. |
+1 -1
View File
@@ -14,7 +14,7 @@ readme = "../../README.md"
documentation = "https://docs.rs/proton-drive-rs" documentation = "https://docs.rs/proton-drive-rs"
[dependencies] [dependencies]
proton-sdk = { path = "../proton-sdk", version = "0.7.3" } proton-sdk = { path = "../proton-sdk", version = "0.7.4" }
tokio.workspace = true tokio.workspace = true
reqwest.workspace = true reqwest.workspace = true
serde.workspace = true serde.workspace = true
+54 -2
View File
@@ -66,7 +66,8 @@ use crate::dtos::{
LinkType, ModulusResponse, MoveLinkRequest, MoveMultipleLinksItem, MoveMultipleLinksRequest, LinkType, ModulusResponse, MoveLinkRequest, MoveMultipleLinksItem, MoveMultipleLinksRequest,
MultipleLinksRequest, MyFilesShareResponse, NodeNameAvailabilityRequest, MultipleLinksRequest, MyFilesShareResponse, NodeNameAvailabilityRequest,
NodeNameAvailabilityResponse, PhotoTagsRequest, PhotosAttributesDto, NodeNameAvailabilityResponse, PhotoTagsRequest, PhotosAttributesDto,
RemovePhotosFromAlbumRequest, RenameLinkRequest, RevisionConflict, RevisionCreationRequest, RemovePhotosFromAlbumRequest, RenameLinkRequest, ReportRecentlyAccessedItem,
ReportRecentlyAccessedRequest, RevisionConflict, RevisionCreationRequest,
RevisionCreationResponse, RevisionDto, RevisionListItemDto, RevisionListResponse, RevisionCreationResponse, RevisionDto, RevisionListItemDto, RevisionListResponse,
RevisionMetadataResponse, RevisionUpdateRequest, ShareInvitationDto, ShareInvitationsResponse, RevisionMetadataResponse, RevisionUpdateRequest, ShareInvitationDto, ShareInvitationsResponse,
ShareMembersResponse, ShareMembershipSummaryDto, ShareResponse, ShareTargetType, ShareUrlDto, ShareMembersResponse, ShareMembershipSummaryDto, ShareResponse, ShareTargetType, ShareUrlDto,
@@ -80,7 +81,7 @@ use crate::dtos::{
use crate::events::{DriveEvent, DriveEventScopeId}; use crate::events::{DriveEvent, DriveEventScopeId};
use crate::node::{ use crate::node::{
AlbumProperties, FileThumbnail, Node, NodeKind, NodeMoveItem, PhotoLocation, PhotoProperties, AlbumProperties, FileThumbnail, Node, NodeKind, NodeMoveItem, PhotoLocation, PhotoProperties,
RevisionState, Thumbnail, ThumbnailType, RecentlyAccessedReportItem, RevisionState, Thumbnail, ThumbnailType,
}; };
use crate::photos::{ use crate::photos::{
AlbumItem, PhotoTag, PhotoTagsUpdate, PhotoUploadMetadata, PhotosTimelineItem, AlbumItem, PhotoTag, PhotoTagsUpdate, PhotoUploadMetadata, PhotosTimelineItem,
@@ -792,6 +793,55 @@ impl ProtonDriveClient {
Ok(uids) Ok(uids)
} }
/// Tell the server which nodes the user recently opened.
///
/// Mirrors upstream `ReportRecentlyAccessedAsync` (`POST recently-accessed-items`),
/// in batches of 50. An item without an access time is stamped with the
/// moment of the call. A failed batch ends the call; earlier batches stay
/// reported.
pub async fn report_recently_accessed(
&self,
items: &[RecentlyAccessedReportItem],
) -> Result<()> {
self.report_recently_accessed_to("recently-accessed-items", items)
.await
}
/// As [`report_recently_accessed`](Self::report_recently_accessed), for
/// photos (`POST photos/recently-accessed-items`).
pub(crate) async fn report_photos_recently_accessed(
&self,
items: &[RecentlyAccessedReportItem],
) -> Result<()> {
self.report_recently_accessed_to("photos/recently-accessed-items", items)
.await
}
async fn report_recently_accessed_to(
&self,
path: &str,
items: &[RecentlyAccessedReportItem],
) -> Result<()> {
/// Items per request (upstream `RecentlyAccessedOperations.BatchSize`).
const REPORT_BATCH: usize = 50;
let now = now_epoch_seconds();
for batch in items.chunks(REPORT_BATCH) {
let request = ReportRecentlyAccessedRequest {
items: batch
.iter()
.map(|item| ReportRecentlyAccessedItem {
volume_id: item.uid.volume_id.clone(),
link_id: item.uid.link_id.clone(),
access_time: item.access_time.unwrap_or(now),
})
.collect(),
};
let _: proton_sdk::api::ApiResponse = self.http.post(path, &request).await?;
}
Ok(())
}
/// Fetch decrypted metadata for many nodes in one pass. /// Fetch decrypted metadata for many nodes in one pass.
/// ///
/// Mirrors C# `NodeOperations.EnumerateNodesAsync(uids)`: the uids are /// Mirrors C# `NodeOperations.EnumerateNodesAsync(uids)`: the uids are
@@ -9432,6 +9482,8 @@ mod tests {
passphrase_signature: None, passphrase_signature: None,
signature_email: None, signature_email: None,
name_signature_email: None, name_signature_email: None,
third_party: false,
sdk: false,
}; };
LinkDetailsDto { LinkDetailsDto {
link, link,
+61
View File
@@ -415,6 +415,26 @@ pub struct AlbumUpdateLink {
pub name_signature_email: String, pub name_signature_email: String,
} }
/// `POST recently-accessed-items` (and its `photos/` twin) — tell the server
/// which nodes the user opened. Upstream `ReportRecentlyAccessedItemsRequest`.
#[derive(Debug, Serialize)]
pub struct ReportRecentlyAccessedRequest {
#[serde(rename = "RecentlyAccessedItems")]
pub items: Vec<ReportRecentlyAccessedItem>,
}
/// One entry of [`ReportRecentlyAccessedRequest`].
#[derive(Debug, Serialize)]
pub struct ReportRecentlyAccessedItem {
#[serde(rename = "VolumeID")]
pub volume_id: VolumeId,
#[serde(rename = "LinkID")]
pub link_id: LinkId,
/// Epoch seconds.
#[serde(rename = "AccessTime")]
pub access_time: i64,
}
/// `POST photos/volumes/{vid}/albums/{lid}/remove-multiple` — take photos out /// `POST photos/volumes/{vid}/albums/{lid}/remove-multiple` — take photos out
/// of an album; they stay in the timeline. Ported from the TypeScript SDK /// of an album; they stay in the timeline. Ported from the TypeScript SDK
/// (`PhotosAPIService.removePhotosFromAlbum`). /// (`PhotosAPIService.removePhotosFromAlbum`).
@@ -637,6 +657,13 @@ pub struct LinkDto {
pub signature_email: Option<String>, pub signature_email: Option<String>,
#[serde(rename = "NameSignatureEmail")] #[serde(rename = "NameSignatureEmail")]
pub name_signature_email: Option<String>, pub name_signature_email: Option<String>,
/// Created by a client other than Proton's own (upstream `f257c20e`, for
/// telemetry's `CreatedBy`). Absent on older backends, hence `false`.
#[serde(rename = "ThirdParty", default)]
pub third_party: bool,
/// Created through the SDK; only meaningful with [`third_party`](Self::third_party).
#[serde(rename = "Sdk", default)]
pub sdk: bool,
} }
impl LinkDto { impl LinkDto {
@@ -696,6 +723,12 @@ pub struct ActiveRevisionDto {
/// revision's extended attributes. Decrypts to [`DecryptedExtendedAttributes`]. /// revision's extended attributes. Decrypts to [`DecryptedExtendedAttributes`].
#[serde(rename = "XAttr")] #[serde(rename = "XAttr")]
pub extended_attributes: Option<String>, pub extended_attributes: Option<String>,
/// As [`LinkDto::third_party`], for the revision (upstream `f257c20e`).
#[serde(rename = "ThirdParty", default)]
pub third_party: bool,
/// As [`LinkDto::sdk`], for the revision.
#[serde(rename = "Sdk", default)]
pub sdk: bool,
} }
/// The decrypted `XAttr` JSON payload, read side. Mirrors C# `ExtendedAttributes` /// The decrypted `XAttr` JSON payload, read side. Mirrors C# `ExtendedAttributes`
@@ -1711,6 +1744,34 @@ pub struct DeviceUpdateShareDto {
mod tests { mod tests {
use super::*; use super::*;
#[test]
fn recently_accessed_request_uses_the_wire_names() {
let request = ReportRecentlyAccessedRequest {
items: vec![ReportRecentlyAccessedItem {
volume_id: VolumeId::new("v"),
link_id: LinkId::new("l"),
access_time: 1_700_000_000,
}],
};
assert_eq!(
serde_json::to_value(&request).expect("report"),
serde_json::json!({
"RecentlyAccessedItems": [
{"VolumeID": "v", "LinkID": "l", "AccessTime": 1_700_000_000}
]
})
);
}
#[test]
fn link_creator_flags_default_to_first_party() {
let revision: ActiveRevisionDto = serde_json::from_value(serde_json::json!({
"RevisionID": "r", "CreateTime": 1, "EncryptedSize": 2, "XAttr": null
}))
.expect("revision");
assert!(!revision.third_party && !revision.sdk);
}
#[test] #[test]
fn album_rename_sends_the_name_and_no_cover() { fn album_rename_sends_the_name_and_no_cover() {
let request = AlbumUpdateRequest { let request = AlbumUpdateRequest {
+1 -1
View File
@@ -57,7 +57,7 @@ pub use event_manager::{
pub use events::{DriveEvent, DriveEventScopeId}; pub use events::{DriveEvent, DriveEventScopeId};
pub use node::{ pub use node::{
AlbumProperties, FileThumbnail, Node, NodeKind, NodeMoveItem, PhotoLocation, PhotoProperties, AlbumProperties, FileThumbnail, Node, NodeKind, NodeMoveItem, PhotoLocation, PhotoProperties,
RevisionState, Thumbnail, ThumbnailType, RecentlyAccessedReportItem, RevisionState, Thumbnail, ThumbnailType,
}; };
pub use photos::{ pub use photos::{
AlbumItem, PhotoTag, PhotoTagsUpdate, PhotoUploadMetadata, PhotosTimelineItem, AlbumItem, PhotoTag, PhotoTagsUpdate, PhotoUploadMetadata, PhotosTimelineItem,
+10
View File
@@ -20,6 +20,16 @@ pub struct NodeMoveItem {
pub target_name: Option<String>, pub target_name: Option<String>,
} }
/// One node for [`report_recently_accessed`](crate::ProtonDriveClient::report_recently_accessed).
///
/// Mirrors upstream `RecentlyAccessedReportItem`.
#[derive(Debug, Clone, PartialEq, Eq)]
pub struct RecentlyAccessedReportItem {
pub uid: NodeUid,
/// When the node was accessed, epoch seconds; `None` means now.
pub access_time: Option<i64>,
}
/// A decrypted Drive node (folder or file). /// A decrypted Drive node (folder or file).
#[derive(Debug, Clone, Serialize, Deserialize)] #[derive(Debug, Clone, Serialize, Deserialize)]
pub struct Node { pub struct Node {
+11 -1
View File
@@ -20,7 +20,7 @@ use proton_sdk::session::ProtonApiSession;
use crate::client::ProtonDriveClient; use crate::client::ProtonDriveClient;
use crate::events::{DriveEvent, DriveEventScopeId}; use crate::events::{DriveEvent, DriveEventScopeId};
use crate::node::{FileThumbnail, Node, Thumbnail, ThumbnailType}; use crate::node::{FileThumbnail, Node, RecentlyAccessedReportItem, Thumbnail, ThumbnailType};
/// One photos-timeline entry: a photo node and its capture time (epoch /// One photos-timeline entry: a photo node and its capture time (epoch
/// seconds). C# `PhotosTimelineItem(NodeUid Uid, DateTime CaptureTime)`. /// seconds). C# `PhotosTimelineItem(NodeUid Uid, DateTime CaptureTime)`.
@@ -144,6 +144,16 @@ impl ProtonPhotosClient {
self.drive.enumerate_photos_nodes(uids).await self.drive.enumerate_photos_nodes(uids).await
} }
/// Tell the server which photos the user recently opened. See
/// [`ProtonDriveClient::report_recently_accessed`]; this posts to the
/// photos route.
pub async fn report_recently_accessed(
&self,
items: &[RecentlyAccessedReportItem],
) -> Result<()> {
self.drive.report_photos_recently_accessed(items).await
}
/// Enumerate the photos timeline newest-first. /// Enumerate the photos timeline newest-first.
/// C# `ProtonPhotosClient.EnumerateTimelineAsync`. /// C# `ProtonPhotosClient.EnumerateTimelineAsync`.
pub async fn enumerate_timeline(&self) -> Result<Vec<PhotosTimelineItem>> { pub async fn enumerate_timeline(&self) -> Result<Vec<PhotosTimelineItem>> {