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`.
2373 lines
82 KiB
Rust
2373 lines
82 KiB
Rust
//! Serde DTOs for the Drive read endpoints (shares, volumes, links, folders).
|
|
//!
|
|
//! Some fields are deserialized for wire-format fidelity / forthcoming
|
|
//! milestones (uploads, signature verification) but not yet read.
|
|
#![allow(dead_code)]
|
|
|
|
use serde::{Deserialize, Serialize};
|
|
|
|
use proton_sdk::ids::{
|
|
AddressId, AddressKeyId, DeviceUid, DriveEventId, LinkId, ShareId, ShareMembershipId, VolumeId,
|
|
};
|
|
|
|
/// `GET v2/shares/my-files`
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct MyFilesShareResponse {
|
|
#[serde(rename = "Volume")]
|
|
pub volume: ShareVolumeDto,
|
|
#[serde(rename = "Share")]
|
|
pub share: ShareDto,
|
|
#[serde(rename = "Link")]
|
|
pub link: LinkDetailsDto,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareVolumeDto {
|
|
#[serde(rename = "VolumeID")]
|
|
pub id: VolumeId,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareDto {
|
|
#[serde(rename = "ShareID")]
|
|
pub id: ShareId,
|
|
#[serde(rename = "Key")]
|
|
pub key: String,
|
|
#[serde(rename = "Passphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "PassphraseSignature")]
|
|
pub passphrase_signature: Option<String>,
|
|
#[serde(rename = "AddressID")]
|
|
pub address_id: AddressId,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/links` request body.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct LinkDetailsRequest<'a> {
|
|
#[serde(rename = "LinkIDs")]
|
|
pub link_ids: &'a [LinkId],
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/links` response.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct LinkDetailsResponse {
|
|
#[serde(rename = "Links")]
|
|
pub links: Vec<LinkDetailsDto>,
|
|
}
|
|
|
|
/// `GET volumes/{vid}/links/{lid}/context`.
|
|
///
|
|
/// Proton's C# SDK models this as `ContextShareResponse`; the API envelope also
|
|
/// carries the common `Code` field, which serde intentionally ignores here.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ContextShareResponse {
|
|
#[serde(rename = "ContextShareID")]
|
|
pub context_share_id: ShareId,
|
|
}
|
|
|
|
/// `GET volumes/{vid}/photos` response (C# `TimelinePhotoListResponse`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TimelinePhotoListResponse {
|
|
#[serde(rename = "Photos")]
|
|
pub photos: Vec<TimelinePhotoDto>,
|
|
}
|
|
|
|
/// `POST volumes/{vid}/photos/duplicates` request.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FindPhotoDuplicatesRequest {
|
|
#[serde(rename = "NameHashes")]
|
|
pub name_hashes: Vec<String>,
|
|
}
|
|
|
|
/// `POST volumes/{vid}/photos/duplicates` response.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FindPhotoDuplicatesResponse {
|
|
#[serde(rename = "DuplicateHashes", default)]
|
|
pub duplicate_hashes: Vec<FoundPhotoDuplicateDto>,
|
|
}
|
|
|
|
/// One server-side photo duplicate candidate.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FoundPhotoDuplicateDto {
|
|
#[serde(rename = "Hash", default)]
|
|
pub name_hash: String,
|
|
#[serde(rename = "ContentHash", default)]
|
|
pub content_hash: String,
|
|
#[serde(rename = "LinkState")]
|
|
pub link_state: Option<i32>,
|
|
#[serde(rename = "ClientUID")]
|
|
pub client_uid: Option<String>,
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: Option<LinkId>,
|
|
}
|
|
|
|
/// One timeline entry (C# `TimelinePhotoDto`). Only the id + capture time are
|
|
/// consumed; the remaining fields are kept for wire fidelity.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TimelinePhotoDto {
|
|
#[serde(rename = "LinkID")]
|
|
pub id: LinkId,
|
|
#[serde(rename = "CaptureTime")]
|
|
pub capture_time: i64,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: Option<String>,
|
|
#[serde(rename = "ContentHash")]
|
|
pub content_hash: Option<String>,
|
|
}
|
|
|
|
/// `GET volumes/{vid}/trash` response. Trashed links are grouped by the share
|
|
/// they belong to (C# `VolumeTrashResponse` / `ShareTrashDto`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VolumeTrashResponse {
|
|
#[serde(rename = "Trash")]
|
|
pub trash_by_share: Vec<ShareTrashDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareTrashDto {
|
|
#[serde(rename = "ShareID")]
|
|
pub share_id: ShareId,
|
|
#[serde(rename = "LinkIDs")]
|
|
pub link_ids: Vec<LinkId>,
|
|
#[serde(rename = "ParentIDs", default)]
|
|
pub parent_ids: Vec<LinkId>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct LinkDetailsDto {
|
|
#[serde(rename = "Link")]
|
|
pub link: LinkDto,
|
|
#[serde(rename = "Folder")]
|
|
pub folder: Option<FolderDto>,
|
|
#[serde(rename = "File")]
|
|
pub file: Option<FileDto>,
|
|
/// Photos-volume `/links` returns file properties under `Photo` (a superset
|
|
/// of `File`) rather than `File`. C# `linkDetailsDto.File ?? linkDetailsDto.Photo`.
|
|
#[serde(rename = "Photo")]
|
|
pub photo: Option<PhotoDto>,
|
|
/// Album properties, for a link of type [`LinkType::Album`]. An album
|
|
/// decrypts exactly like a folder — the block simply carries the hash key
|
|
/// under the same name plus the album-only metadata (C# `AlbumDto`, layered
|
|
/// onto a `FolderNode` by `DtoToMetadataConverter.ConvertAlbumMetadataAsync`).
|
|
#[serde(rename = "Album")]
|
|
pub album: Option<AlbumDto>,
|
|
/// Present when the node is shared (with members and/or via a public link).
|
|
#[serde(rename = "Sharing", default)]
|
|
pub sharing: Option<LinkSharingDto>,
|
|
/// Present when the node is shared *with us*: our membership in the sharer's
|
|
/// share. Carries the ids needed to leave the share.
|
|
#[serde(rename = "Membership", default)]
|
|
pub membership: Option<ShareMembershipSummaryDto>,
|
|
}
|
|
|
|
/// The sharing state of a node (C# `LinkSharingDto`). Its mere presence means
|
|
/// the node is shared; a `ShareURLID` means it is also shared by public link.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct LinkSharingDto {
|
|
#[serde(rename = "ShareID")]
|
|
pub share_id: ShareId,
|
|
#[serde(rename = "ShareURLID", default)]
|
|
pub share_url_id: Option<String>,
|
|
}
|
|
|
|
/// Our membership in a share someone else owns (C# `ShareMembershipSummaryDto`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareMembershipSummaryDto {
|
|
#[serde(rename = "ShareID")]
|
|
pub share_id: ShareId,
|
|
#[serde(rename = "MembershipID")]
|
|
pub membership_id: ShareMembershipId,
|
|
#[serde(rename = "Permissions", default)]
|
|
pub permissions: i32,
|
|
/// When the node was shared with us, epoch seconds.
|
|
#[serde(rename = "InviteTime", default)]
|
|
pub invite_time: Option<i64>,
|
|
#[serde(rename = "InviterEmail", default)]
|
|
pub inviter_email: Option<String>,
|
|
/// base64 PKESK: the share passphrase session key encrypted to us.
|
|
#[serde(rename = "MemberSharePassphraseKeyPacket", default)]
|
|
pub member_share_passphrase_key_packet: Option<String>,
|
|
/// The inviter's signature over that key packet, made under the
|
|
/// `drive.share-member.inviter` context.
|
|
#[serde(rename = "InviterSharePassphraseKeyPacketSignature", default)]
|
|
pub inviter_share_passphrase_key_packet_signature: Option<String>,
|
|
}
|
|
|
|
impl LinkDetailsDto {
|
|
/// File properties for a file/photo node, preferring `File` and falling back
|
|
/// to the photos-volume `Photo` block.
|
|
pub fn file_properties(&self) -> Option<&FileDto> {
|
|
self.file
|
|
.as_ref()
|
|
.or_else(|| self.photo.as_ref().map(|photo| &photo.file))
|
|
}
|
|
|
|
/// Folder properties for a folder/album node. An album's hash key and
|
|
/// extended attributes live in the `Album` block, which is otherwise a
|
|
/// superset of `Folder` — C# builds a synthetic `FolderDto` from it.
|
|
pub fn folder_properties(&self) -> Option<FolderProperties<'_>> {
|
|
if let Some(folder) = self.folder.as_ref() {
|
|
return Some(FolderProperties {
|
|
hash_key: &folder.hash_key,
|
|
extended_attributes: folder.extended_attributes.as_deref(),
|
|
});
|
|
}
|
|
self.album.as_ref().map(|album| FolderProperties {
|
|
hash_key: &album.hash_key,
|
|
extended_attributes: album.extended_attributes.as_deref(),
|
|
})
|
|
}
|
|
}
|
|
|
|
/// The folder-shaped part of a `Folder` or `Album` block (C#
|
|
/// `ConvertAlbumMetadataAsync`'s synthetic `FolderDto`).
|
|
pub struct FolderProperties<'a> {
|
|
pub hash_key: &'a str,
|
|
pub extended_attributes: Option<&'a str>,
|
|
}
|
|
|
|
/// Photo properties on a photos-volume link (C# `PhotoDto : FileDto`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PhotoDto {
|
|
/// The file properties a photo shares with any other file.
|
|
#[serde(flatten)]
|
|
pub file: FileDto,
|
|
/// Capture time in seconds since the Unix epoch.
|
|
#[serde(rename = "CaptureTime", default)]
|
|
pub capture_time: i64,
|
|
/// Lowercase-hex HMAC of the plaintext SHA-1 under the photos root's hash
|
|
/// key — the second half of duplicate detection.
|
|
#[serde(rename = "ContentHash", default)]
|
|
pub content_hash: Option<String>,
|
|
/// Set when this photo is a *related* photo (live-photo video, burst
|
|
/// sibling): the link id of the main photo it belongs to.
|
|
#[serde(rename = "MainPhotoLinkID", default)]
|
|
pub main_photo_link_id: Option<LinkId>,
|
|
/// The related photos of this (main) photo.
|
|
#[serde(rename = "RelatedPhotosLinkIDs", default)]
|
|
pub related_photo_link_ids: Vec<LinkId>,
|
|
/// Classification tags as their `PhotoTag` discriminants; unknown values are
|
|
/// dropped when the node is built.
|
|
#[serde(rename = "Tags", default)]
|
|
pub tags: Vec<i32>,
|
|
/// The albums this photo has been added to.
|
|
#[serde(rename = "Albums", default)]
|
|
pub album_inclusions: Vec<PhotoAlbumInclusionDto>,
|
|
}
|
|
|
|
/// One album a photo belongs to (C# `PhotoAlbumInclusionDto`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PhotoAlbumInclusionDto {
|
|
#[serde(rename = "AlbumLinkID")]
|
|
pub album_link_id: LinkId,
|
|
#[serde(rename = "AddedTime", default)]
|
|
pub creation_time: i64,
|
|
}
|
|
|
|
/// Album properties on a photos-volume link (C# `AlbumDto`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumDto {
|
|
/// The album's child-name hash key — same field a folder carries.
|
|
#[serde(rename = "NodeHashKey")]
|
|
pub hash_key: String,
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: Option<String>,
|
|
#[serde(rename = "PhotoCount", default)]
|
|
pub photo_count: i32,
|
|
#[serde(rename = "CoverLinkID", default)]
|
|
pub cover_link_id: Option<LinkId>,
|
|
/// Epoch seconds of the last change to the album's contents.
|
|
#[serde(rename = "LastActivityTime", default)]
|
|
pub last_activity_time: i64,
|
|
}
|
|
|
|
/// `GET photos/volumes/{vid}/albums` — the albums on a photos volume, paged on
|
|
/// `AnchorID` (C# `AlbumListResponse`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumListResponse {
|
|
#[serde(rename = "Albums", default)]
|
|
pub albums: Vec<AlbumListItemDto>,
|
|
#[serde(rename = "More", default)]
|
|
pub more: bool,
|
|
#[serde(rename = "AnchorID", default)]
|
|
pub anchor_id: Option<LinkId>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumListItemDto {
|
|
#[serde(rename = "LinkID")]
|
|
pub id: LinkId,
|
|
}
|
|
|
|
/// `GET photos/volumes/{vid}/albums/{lid}/children` — an album's photos, newest
|
|
/// capture first, paged on `AnchorID` (C# `AlbumItemListResponse`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumItemListResponse {
|
|
#[serde(rename = "Photos", default)]
|
|
pub photos: Vec<AlbumItemDto>,
|
|
#[serde(rename = "More", default)]
|
|
pub more: bool,
|
|
#[serde(rename = "AnchorID", default)]
|
|
pub anchor_id: Option<LinkId>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumItemDto {
|
|
#[serde(rename = "LinkID")]
|
|
pub id: LinkId,
|
|
#[serde(rename = "CaptureTime", default)]
|
|
pub capture_time: i64,
|
|
}
|
|
|
|
/// `GET photos/albums/shared-with-me` — albums other users share with us. Unlike
|
|
/// `v2/sharedwithme` each row carries its own volume (C# `SharedAlbumsResponse`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SharedAlbumsResponse {
|
|
#[serde(rename = "Albums", default)]
|
|
pub albums: Vec<SharedAlbumDto>,
|
|
#[serde(rename = "More", default)]
|
|
pub more: bool,
|
|
#[serde(rename = "AnchorID", default)]
|
|
pub anchor_id: Option<LinkId>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SharedAlbumDto {
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: VolumeId,
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
}
|
|
|
|
/// `POST photos/volumes/{vid}/albums` — create an album. Ported from the
|
|
/// TypeScript SDK (`PhotosAPIService.createAlbum`); C# has no public album
|
|
/// write API. The album is a folder node on the photos volume, so its `Link`
|
|
/// block carries exactly what a folder create does.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AlbumCreationRequest {
|
|
/// TS sends `Locked: false` — a locked album is one that cannot take new
|
|
/// photos, which a freshly created album never is.
|
|
#[serde(rename = "Locked")]
|
|
pub locked: bool,
|
|
#[serde(rename = "Link")]
|
|
pub link: AlbumCreationLink,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AlbumCreationLink {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "NodeKey")]
|
|
pub key: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: String,
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: String,
|
|
#[serde(rename = "NodeHashKey")]
|
|
pub node_hash_key: String,
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumCreationResponse {
|
|
#[serde(rename = "Album")]
|
|
pub album: AlbumCreationAlbum,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumCreationAlbum {
|
|
#[serde(rename = "Link")]
|
|
pub link: AlbumCreationLinkResponse,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AlbumCreationLinkResponse {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
}
|
|
|
|
/// `POST photos/volumes/{vid}/albums/{lid}/add-multiple` — add photos that
|
|
/// already live on the album's own volume. Ported from the TypeScript SDK
|
|
/// (`PhotosAPIService.addPhotosToAlbum`). Each entry re-encrypts one photo's
|
|
/// name and passphrase to the album key; a main photo's related photos are
|
|
/// flattened into the same list.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AddPhotosToAlbumRequest {
|
|
#[serde(rename = "AlbumData")]
|
|
pub album_data: Vec<AddPhotoToAlbumEntry>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct AddPhotoToAlbumEntry {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
/// Name hash under the *album's* hash key.
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
/// The photo's name re-encrypted to the album key.
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: String,
|
|
/// The photo's node passphrase re-encrypted to the album key.
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
/// HMAC of the plaintext SHA-1 under the album's hash key.
|
|
#[serde(rename = "ContentHash")]
|
|
pub content_hash: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AddPhotosToAlbumResponse {
|
|
#[serde(rename = "Responses", default)]
|
|
pub responses: Vec<PhotoLinkOutcome>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PhotoLinkOutcome {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "Response")]
|
|
pub response: PhotoLinkOutcomeBody,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PhotoLinkOutcomeBody {
|
|
#[serde(rename = "Code", default)]
|
|
pub code: i32,
|
|
#[serde(rename = "Error", default)]
|
|
pub error: Option<String>,
|
|
/// Carries `Missing` — related photos the server demands alongside this one.
|
|
#[serde(rename = "Details", default)]
|
|
pub details: Option<PhotoLinkOutcomeDetails>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct PhotoLinkOutcomeDetails {
|
|
#[serde(rename = "Missing", default)]
|
|
pub missing: Vec<LinkId>,
|
|
}
|
|
|
|
/// `PUT photos/volumes/{vid}/links/transfer-multiple` — move photos that
|
|
/// already live on our own photos volume into another node on it (C#
|
|
/// `PhotosApiClient.TransferPhotosAsync`, JS `transferPhotos`). Used to save
|
|
/// photos into the timeline root; a main photo's related photos travel in the
|
|
/// same `Links` list.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct TransferPhotosRequest {
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_link_id: LinkId,
|
|
#[serde(rename = "Links")]
|
|
pub links: Vec<TransferPhotoLinkItem>,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: String,
|
|
/// Sent explicitly as null: the API wants it only for an anonymous node,
|
|
/// which this path never moves (C# sends the same explicit null).
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct TransferPhotoLinkItem {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
/// Name hash under the *target's* hash key.
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
/// The photo's current name hash, under the hash key of the parent it is
|
|
/// moving out of — the server matches the link on it.
|
|
#[serde(rename = "OriginalHash")]
|
|
pub original_name_hash: String,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "ContentHash")]
|
|
pub content_hash: String,
|
|
/// As `SignatureEmail` above: anonymous nodes only.
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct TransferPhotosResponse {
|
|
#[serde(rename = "Responses", default)]
|
|
pub responses: Vec<PhotoLinkOutcome>,
|
|
}
|
|
|
|
/// `POST volumes/{vid}/links/{lid}/copy` — copy a photo that lives on *another*
|
|
/// volume (one shared with us) into our own (C# `PhotosApiClient.CopyPhotoAsync`,
|
|
/// JS `copyPhoto`). Unlike the transfer endpoint this takes one photo per
|
|
/// request, with its related photos nested in `Photos`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CopyPhotoRequest {
|
|
#[serde(rename = "TargetVolumeID")]
|
|
pub target_volume_id: VolumeId,
|
|
#[serde(rename = "TargetParentLinkID")]
|
|
pub target_parent_link_id: LinkId,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(
|
|
rename = "NodePassphraseSignature",
|
|
skip_serializing_if = "Option::is_none"
|
|
)]
|
|
pub passphrase_signature: Option<String>,
|
|
#[serde(rename = "SignatureEmail", skip_serializing_if = "Option::is_none")]
|
|
pub signature_email: Option<String>,
|
|
#[serde(rename = "Photos")]
|
|
pub photos: CopyPhotoContent,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CopyPhotoContent {
|
|
#[serde(rename = "ContentHash")]
|
|
pub content_hash: String,
|
|
#[serde(rename = "RelatedPhotos")]
|
|
pub related_photos: Vec<CopyPhotoRelatedItem>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct CopyPhotoRelatedItem {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "ContentHash")]
|
|
pub content_hash: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CopyPhotoResponse {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
}
|
|
|
|
/// `POST`/`DELETE photos/volumes/{vid}/links/{lid}/tags` (C# `PhotoTagsRequest`).
|
|
#[derive(Debug, Serialize)]
|
|
pub struct PhotoTagsRequest {
|
|
#[serde(rename = "Tags")]
|
|
pub tags: Vec<i32>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct LinkDto {
|
|
#[serde(rename = "LinkID")]
|
|
pub id: LinkId,
|
|
#[serde(rename = "Type")]
|
|
pub link_type: i32,
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_id: Option<LinkId>,
|
|
#[serde(rename = "State")]
|
|
pub state: i32,
|
|
#[serde(rename = "CreateTime")]
|
|
pub creation_time: i64,
|
|
#[serde(rename = "ModifyTime")]
|
|
pub modification_time: i64,
|
|
/// The API's name for the trash time (upstream `cdc0f260` dropped C#'s
|
|
/// `[JsonPropertyName("Trashed")]`, which never matched it).
|
|
#[serde(rename = "TrashTime", default)]
|
|
pub trash_time: Option<i64>,
|
|
/// The name this DTO used to read. Kept as its own field rather than a serde
|
|
/// `alias`, which would fail the whole link with "duplicate field" on a
|
|
/// response that carries both.
|
|
#[serde(rename = "Trashed", default)]
|
|
pub trashed_legacy: Option<i64>,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
/// Lowercase-hex HMAC-SHA256 name hash under the parent's hash key (C#
|
|
/// `LinkDto.NameHashDigest`, JSON `NameHash`). Cached as a node's
|
|
/// `OriginalHash` for later move/rename without re-decrypting the name.
|
|
#[serde(rename = "NameHash", default)]
|
|
pub name_hash: Option<String>,
|
|
#[serde(rename = "NodeKey")]
|
|
pub key: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: Option<String>,
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: Option<String>,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: Option<String>,
|
|
}
|
|
|
|
impl LinkDto {
|
|
pub fn parsed_type(&self) -> LinkType {
|
|
LinkType::from_raw(self.link_type)
|
|
}
|
|
|
|
pub fn is_trashed(&self) -> bool {
|
|
self.state == LinkState::Trashed as i32
|
|
|| self.trash_time.is_some()
|
|
|| self.trashed_legacy.is_some()
|
|
}
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FolderDto {
|
|
#[serde(rename = "NodeHashKey")]
|
|
pub hash_key: String,
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FileDto {
|
|
#[serde(rename = "MediaType")]
|
|
pub media_type: String,
|
|
#[serde(rename = "TotalEncryptedSize")]
|
|
pub total_size_on_storage: i64,
|
|
/// Base64 PKESK packet for the file's content key, addressed to the node key.
|
|
#[serde(rename = "ContentKeyPacket")]
|
|
pub content_key_packet: Option<String>,
|
|
/// Detached signature over the content key (best-effort verification).
|
|
#[serde(rename = "ContentKeyPacketSignature")]
|
|
pub content_key_signature: Option<String>,
|
|
#[serde(rename = "ActiveRevision")]
|
|
pub active_revision: Option<ActiveRevisionDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ActiveRevisionDto {
|
|
#[serde(rename = "RevisionID")]
|
|
pub id: String,
|
|
#[serde(rename = "CreateTime")]
|
|
pub creation_time: i64,
|
|
/// Wire revision state (C# `ApiRevisionState`): 0 draft, 1 active, 2 obsolete.
|
|
/// Absent on older responses; a link's active revision is Active by
|
|
/// definition, which is what C# `DtoToMetadataConverter` records.
|
|
#[serde(rename = "State", default)]
|
|
pub state: Option<i32>,
|
|
#[serde(rename = "EncryptedSize")]
|
|
pub encrypted_size: i64,
|
|
/// Email of the revision signer; empty/absent means the node key signed.
|
|
/// Resolves the `XAttr` authorship claim (C# `SignatureEmailAddress`).
|
|
#[serde(rename = "SignatureEmail", default)]
|
|
pub signature_email: Option<String>,
|
|
/// Armored PGP message (encrypted to the node key, signed) carrying the
|
|
/// revision's extended attributes. Decrypts to [`DecryptedExtendedAttributes`].
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: Option<String>,
|
|
}
|
|
|
|
/// The decrypted `XAttr` JSON payload, read side. Mirrors C# `ExtendedAttributes`
|
|
/// / `CommonExtendedAttributes`; every field is optional because the payload is
|
|
/// produced by heterogeneous clients (the upload-side [`ExtendedAttributes`]
|
|
/// struct only writes a subset).
|
|
#[derive(Debug, Default, Deserialize)]
|
|
pub struct DecryptedExtendedAttributes {
|
|
#[serde(rename = "Common", default)]
|
|
pub common: Option<DecryptedCommonExtendedAttributes>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
pub struct DecryptedCommonExtendedAttributes {
|
|
/// Authoritative plaintext file size, in bytes.
|
|
#[serde(rename = "Size", default)]
|
|
pub size: Option<i64>,
|
|
/// ISO-8601 modification timestamp, as written by the uploading client.
|
|
#[serde(rename = "ModificationTime", default)]
|
|
pub modification_time: Option<String>,
|
|
#[serde(rename = "BlockSizes", default)]
|
|
pub block_sizes: Option<Vec<i64>>,
|
|
#[serde(rename = "Digests", default)]
|
|
pub digests: Option<DecryptedFileContentDigests>,
|
|
}
|
|
|
|
#[derive(Debug, Default, Deserialize)]
|
|
pub struct DecryptedFileContentDigests {
|
|
/// Lowercase-hex SHA-1 of the full plaintext.
|
|
#[serde(rename = "SHA1", default)]
|
|
pub sha1: Option<String>,
|
|
}
|
|
|
|
/// `GET v2/volumes/{vid}/files/{lid}/revisions/{rid}` — one page of a revision's
|
|
/// block listing.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionResponse {
|
|
#[serde(rename = "Revision")]
|
|
pub revision: RevisionDto,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionDto {
|
|
#[serde(rename = "ID")]
|
|
pub id: String,
|
|
/// Detached signature over the content manifest (thumbnail + block digests).
|
|
#[serde(rename = "ManifestSignature")]
|
|
pub manifest_signature: Option<String>,
|
|
/// Email of the signer; empty/absent means the node key signed.
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: Option<String>,
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: Option<String>,
|
|
#[serde(rename = "Thumbnails", default)]
|
|
pub thumbnails: Vec<ThumbnailDto>,
|
|
#[serde(rename = "Blocks", default)]
|
|
pub blocks: Vec<BlockDto>,
|
|
}
|
|
|
|
/// One content block of a revision.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BlockDto {
|
|
#[serde(rename = "Index")]
|
|
pub index: i32,
|
|
/// Absolute URL on block storage.
|
|
#[serde(rename = "BareURL")]
|
|
pub bare_url: String,
|
|
/// Per-block storage authorization token (`pm-storage-token` header).
|
|
#[serde(rename = "Token")]
|
|
pub token: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ThumbnailDto {
|
|
/// Server-assigned thumbnail block id (C# `ThumbnailDto.Id`); resolved to a
|
|
/// download URL via the `volumes/{vid}/thumbnails` endpoint.
|
|
#[serde(rename = "ThumbnailID")]
|
|
pub id: Option<String>,
|
|
#[serde(rename = "Type")]
|
|
pub thumbnail_type: i32,
|
|
/// Base64 SHA-256 digest of the thumbnail's ciphertext (manifest input).
|
|
#[serde(rename = "Hash")]
|
|
pub hash_digest: Option<String>,
|
|
}
|
|
|
|
/// `POST volumes/{vid}/thumbnails` request: resolve thumbnail ids to download
|
|
/// URLs (C# `ThumbnailBlockListRequest`).
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ThumbnailBlockListRequest {
|
|
#[serde(rename = "ThumbnailIDs")]
|
|
pub thumbnail_ids: Vec<String>,
|
|
}
|
|
|
|
/// `POST volumes/{vid}/thumbnails` response (C# `ThumbnailBlockListResponse`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ThumbnailBlockListResponse {
|
|
#[serde(rename = "Thumbnails", default)]
|
|
pub blocks: Vec<ThumbnailBlock>,
|
|
#[serde(rename = "Errors", default)]
|
|
pub errors: Vec<ThumbnailBlockError>,
|
|
}
|
|
|
|
/// A resolved thumbnail block: where to fetch it and the storage token to use
|
|
/// (C# `ThumbnailBlock`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ThumbnailBlock {
|
|
#[serde(rename = "ThumbnailID")]
|
|
pub thumbnail_id: String,
|
|
#[serde(rename = "BareURL")]
|
|
pub bare_url: String,
|
|
#[serde(rename = "Token")]
|
|
pub token: String,
|
|
}
|
|
|
|
/// Per-thumbnail resolution error (C# `ThumbnailBlockError`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ThumbnailBlockError {
|
|
#[serde(rename = "ThumbnailID")]
|
|
pub thumbnail_id: String,
|
|
#[serde(rename = "Error")]
|
|
pub error: String,
|
|
#[serde(rename = "Code", default)]
|
|
pub code: i32,
|
|
}
|
|
|
|
/// `GET v2/volumes/{vid}/folders/{lid}/children`
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FolderChildrenResponse {
|
|
#[serde(rename = "LinkIDs")]
|
|
pub link_ids: Vec<LinkId>,
|
|
#[serde(rename = "AnchorID")]
|
|
pub anchor_id: Option<LinkId>,
|
|
#[serde(rename = "More")]
|
|
pub more_results_exist: bool,
|
|
}
|
|
|
|
/// Drive link (node) type.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum LinkType {
|
|
Folder,
|
|
File,
|
|
Album,
|
|
Unknown,
|
|
}
|
|
|
|
impl LinkType {
|
|
fn from_raw(raw: i32) -> Self {
|
|
match raw {
|
|
1 => Self::Folder,
|
|
2 => Self::File,
|
|
3 => Self::Album,
|
|
_ => Self::Unknown,
|
|
}
|
|
}
|
|
}
|
|
|
|
/// Drive link state.
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
pub enum LinkState {
|
|
Draft = 0,
|
|
Active = 1,
|
|
Trashed = 2,
|
|
Deleted = 3,
|
|
Restoring = 4,
|
|
}
|
|
|
|
// ---- Upload (write) DTOs --------------------------------------------------
|
|
|
|
/// `POST v2/volumes/{vid}/files` — create a new file draft.
|
|
///
|
|
/// Mirrors C# `FileCreationRequest` (+ its `NodeCreationRequest` base). All
|
|
/// encrypted/armored fields are produced client-side; `Hash` is the lowercase
|
|
/// hex name HMAC, `ContentKeyPacket` is base64.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FileCreationRequest {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_link_id: LinkId,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: String,
|
|
#[serde(rename = "NodeKey")]
|
|
pub key: String,
|
|
#[serde(rename = "MIMEType")]
|
|
pub media_type: String,
|
|
#[serde(rename = "ContentKeyPacket")]
|
|
pub content_key_packet: String,
|
|
#[serde(rename = "ContentKeyPacketSignature")]
|
|
pub content_key_signature: String,
|
|
#[serde(rename = "SignatureAddress")]
|
|
pub signature_address: String,
|
|
#[serde(rename = "ClientUID", skip_serializing_if = "Option::is_none")]
|
|
pub client_uid: Option<String>,
|
|
#[serde(rename = "IntendedUploadSize")]
|
|
pub intended_upload_size: i64,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/files` response.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FileCreationResponse {
|
|
#[serde(rename = "File")]
|
|
pub file: FileCreationIdentifiers,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FileCreationIdentifiers {
|
|
#[serde(rename = "ID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "RevisionID")]
|
|
pub revision_id: String,
|
|
}
|
|
|
|
/// JSON metadata part of `POST v2/volumes/{vid}/files/small`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct SmallFileUploadMetadataRequest {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NameHash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_link_id: LinkId,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: String,
|
|
#[serde(rename = "NodeKey")]
|
|
pub key: String,
|
|
#[serde(rename = "MIMEType")]
|
|
pub media_type: String,
|
|
#[serde(rename = "ContentKeyPacket")]
|
|
pub content_key_packet: String,
|
|
#[serde(rename = "ContentKeyPacketSignature")]
|
|
pub content_key_signature: String,
|
|
#[serde(rename = "ManifestSignature")]
|
|
pub manifest_signature: String,
|
|
#[serde(rename = "ChecksumVerified")]
|
|
pub checksum_verified: bool,
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: String,
|
|
#[serde(rename = "ContentBlockVerificationToken")]
|
|
pub content_block_verification_token: Option<String>,
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: String,
|
|
#[serde(rename = "Photo")]
|
|
pub photo: Option<PhotosAttributesDto>,
|
|
#[serde(rename = "ContentBlockEncSignature")]
|
|
pub content_block_encrypted_signature: Option<String>,
|
|
}
|
|
|
|
/// JSON metadata part of
|
|
/// `POST v2/volumes/{vid}/files/{lid}/revisions/small`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct SmallRevisionUploadMetadataRequest {
|
|
#[serde(rename = "CurrentRevisionID")]
|
|
pub current_revision_id: String,
|
|
#[serde(rename = "ManifestSignature")]
|
|
pub manifest_signature: String,
|
|
#[serde(rename = "ChecksumVerified")]
|
|
pub checksum_verified: bool,
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: String,
|
|
#[serde(rename = "ContentBlockEncSignature")]
|
|
pub content_block_encrypted_signature: Option<String>,
|
|
#[serde(rename = "ContentBlockVerificationToken")]
|
|
pub content_block_verification_token: Option<String>,
|
|
#[serde(rename = "XAttr")]
|
|
pub extended_attributes: String,
|
|
}
|
|
|
|
/// Response from either atomic small-upload endpoint.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SmallUploadResponse {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "RevisionID")]
|
|
pub revision_id: String,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/folders` — create a new folder.
|
|
///
|
|
/// Mirrors C# `FolderCreationRequest` (+ its `NodeCreationRequest` base). Like
|
|
/// [`FileCreationRequest`] but with a `NodeHashKey` (the folder's child-name
|
|
/// HMAC key, encrypted to its own node key) instead of any content-key fields.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FolderCreationRequest {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_link_id: LinkId,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: String,
|
|
#[serde(rename = "NodeKey")]
|
|
pub key: String,
|
|
#[serde(rename = "NodeHashKey")]
|
|
pub node_hash_key: String,
|
|
/// Folder create uses `SignatureEmail` (file create uses `SignatureAddress`).
|
|
#[serde(rename = "SignatureEmail")]
|
|
pub signature_email: String,
|
|
/// Encrypted+signed `ExtendedAttributes` JSON (modification time). C#
|
|
/// `FolderCreationRequest.ExtendedAttributes`. Omitted when no modification
|
|
/// time was supplied.
|
|
#[serde(rename = "XAttr", skip_serializing_if = "Option::is_none")]
|
|
pub extended_attributes: Option<String>,
|
|
}
|
|
|
|
/// `POST volumes` request body — create a new volume with its root share and
|
|
/// root folder. Mirrors C# `VolumeCreationRequest`. All PGP fields are armored
|
|
/// strings produced by [`proton_sdk::crypto::build_volume_creation_material`].
|
|
#[derive(Debug, Serialize)]
|
|
pub struct VolumeCreationRequest {
|
|
#[serde(rename = "AddressID")]
|
|
pub address_id: AddressId,
|
|
#[serde(rename = "AddressKeyID")]
|
|
pub address_key_id: AddressKeyId,
|
|
#[serde(rename = "ShareKey")]
|
|
pub share_key: String,
|
|
#[serde(rename = "SharePassphrase")]
|
|
pub share_passphrase: String,
|
|
#[serde(rename = "SharePassphraseSignature")]
|
|
pub share_passphrase_signature: String,
|
|
#[serde(rename = "FolderName")]
|
|
pub folder_name: String,
|
|
#[serde(rename = "FolderKey")]
|
|
pub folder_key: String,
|
|
#[serde(rename = "FolderPassphrase")]
|
|
pub folder_passphrase: String,
|
|
#[serde(rename = "FolderPassphraseSignature")]
|
|
pub folder_passphrase_signature: String,
|
|
#[serde(rename = "FolderHashKey")]
|
|
pub folder_hash_key: String,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/folders` response.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FolderCreationResponse {
|
|
#[serde(rename = "Folder")]
|
|
pub folder: FolderCreationIdentifiers,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct FolderCreationIdentifiers {
|
|
#[serde(rename = "ID")]
|
|
pub link_id: LinkId,
|
|
}
|
|
|
|
/// `PUT v2/volumes/{vid}/links/{lid}/rename` — rename a node.
|
|
///
|
|
/// Mirrors C# `RenameLinkRequest`. `Hash`/`OriginalHash` are lowercase-hex name
|
|
/// HMACs (new and current). `MIMEType` is always present: the media type for a
|
|
/// file, `null` for a folder.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct RenameLinkRequest {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: String,
|
|
#[serde(rename = "MIMEType")]
|
|
pub media_type: Option<String>,
|
|
#[serde(rename = "OriginalHash")]
|
|
pub original_hash: String,
|
|
}
|
|
|
|
/// `PUT v2/volumes/{vid}/links/{lid}/move` — move a node to a new parent.
|
|
///
|
|
/// Mirrors C# `MoveLinkRequest`. `NodePassphrase` is the node passphrase
|
|
/// rewrapped (session-key re-encrypted) to the destination parent key; the
|
|
/// secret is unchanged, so `NodePassphraseSignature` is carried over untouched.
|
|
/// `Name` is re-encrypted + signed to the destination parent. `Hash` is the new
|
|
/// name hash under the destination's hash key; `OriginalHash` the current hash
|
|
/// under the source parent's. Same-volume moves only (no `NewShareID`).
|
|
#[derive(Debug, Serialize)]
|
|
pub struct MoveLinkRequest {
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_link_id: LinkId,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(
|
|
rename = "NodePassphraseSignature",
|
|
skip_serializing_if = "Option::is_none"
|
|
)]
|
|
pub passphrase_signature: Option<String>,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: String,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "OriginalHash")]
|
|
pub original_hash: String,
|
|
/// Set only when moving an anonymously signed node, whose passphrase the
|
|
/// mover signs afresh (C# `MoveSingleLinkRequest.SignatureEmailAddress`).
|
|
#[serde(rename = "SignatureEmail", skip_serializing_if = "Option::is_none")]
|
|
pub signature_email: Option<String>,
|
|
}
|
|
|
|
/// `PUT volumes/{vid}/links/move-multiple` — batch move of several nodes under a
|
|
/// single destination parent. Mirrors C# `MoveMultipleLinksRequest`. Same-volume
|
|
/// only (the C# batch path throws for cross-volume too). `SignatureEmail` is the
|
|
/// anonymous-move passphrase signer and is omitted when not set.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct MoveMultipleLinksRequest {
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_link_id: LinkId,
|
|
#[serde(rename = "Links")]
|
|
pub links: Vec<MoveMultipleLinksItem>,
|
|
#[serde(rename = "NameSignatureEmail")]
|
|
pub name_signature_email: String,
|
|
#[serde(rename = "SignatureEmail", skip_serializing_if = "Option::is_none")]
|
|
pub signature_email: Option<String>,
|
|
}
|
|
|
|
/// One entry of a [`MoveMultipleLinksRequest`]. Mirrors C# `MoveMultipleLinksItem`:
|
|
/// per-node rewrapped passphrase + re-encrypted/signed name + new/original name
|
|
/// hashes under the destination/source hash keys.
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct MoveMultipleLinksItem {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "OriginalHash")]
|
|
pub original_hash: String,
|
|
#[serde(
|
|
rename = "NodePassphraseSignature",
|
|
skip_serializing_if = "Option::is_none"
|
|
)]
|
|
pub passphrase_signature: Option<String>,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/links/{folderId}/checkAvailableHashes` request: ask
|
|
/// which of a batch of candidate name hashes are free in a folder (C#
|
|
/// `NodeNameAvailabilityRequest`).
|
|
#[derive(Debug, Serialize)]
|
|
pub struct NodeNameAvailabilityRequest {
|
|
#[serde(rename = "Hashes")]
|
|
pub name_hashes: Vec<String>,
|
|
#[serde(rename = "ClientUID")]
|
|
pub client_uid: Vec<String>,
|
|
}
|
|
|
|
/// Response to `checkAvailableHashes` (C# `NodeNameAvailabilityResponse`): the
|
|
/// subset of the requested hashes that are available, plus the taken ones.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct NodeNameAvailabilityResponse {
|
|
#[serde(rename = "AvailableHashes", default)]
|
|
pub available_hashes: Vec<String>,
|
|
#[serde(rename = "PendingHashes", default)]
|
|
pub unavailable_hashes: Vec<NameHashUnavailabilityDto>,
|
|
}
|
|
|
|
/// One taken name hash and the node that holds it (C#
|
|
/// `NameHashDigestUnavailabilityDto`).
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct NameHashUnavailabilityDto {
|
|
#[serde(rename = "Hash")]
|
|
pub name_hash: String,
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "ClientUID")]
|
|
pub client_uid: Option<String>,
|
|
}
|
|
|
|
/// `{ LinkIDs: [...] }` — batch link-id body for trash / restore / delete.
|
|
/// Mirrors C# `MultipleLinksNullaryRequest`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct MultipleLinksRequest<'a> {
|
|
#[serde(rename = "LinkIDs")]
|
|
pub link_ids: &'a [LinkId],
|
|
}
|
|
|
|
/// Aggregate response for batch link operations: a per-link result list. The
|
|
/// top-level envelope is `1001 MultipleResponses`; the real per-link status is
|
|
/// in each [`LinkIdResponsePair`]. Mirrors C# `AggregateApiResponse<LinkIdResponsePair>`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct AggregateLinksResponse {
|
|
#[serde(rename = "Responses", default)]
|
|
pub responses: Vec<LinkIdResponsePair>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct LinkIdResponsePair {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "Response")]
|
|
pub response: proton_sdk::api::ApiResponse,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/files/{lid}/revisions` — create a new revision on an
|
|
/// existing file. Mirrors C# `RevisionCreationRequest`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct RevisionCreationRequest {
|
|
/// The currently active revision this draft supersedes.
|
|
#[serde(rename = "CurrentRevisionID")]
|
|
pub current_revision_id: String,
|
|
#[serde(rename = "ClientUID", skip_serializing_if = "Option::is_none")]
|
|
pub client_uid: Option<String>,
|
|
#[serde(rename = "IntendedUploadSize")]
|
|
pub intended_upload_size: i64,
|
|
}
|
|
|
|
/// `POST v2/volumes/{vid}/files/{lid}/revisions` response.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionCreationResponse {
|
|
#[serde(rename = "Revision")]
|
|
pub revision: RevisionCreationIdentity,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionCreationIdentity {
|
|
#[serde(rename = "ID")]
|
|
pub revision_id: String,
|
|
}
|
|
|
|
/// `Details` object attached to an `AlreadyExists` (2500) error from a file
|
|
/// creation (`POST .../files`) or a revision creation (`POST .../revisions`):
|
|
/// the server names the conflicting link, its committed revision (if any) and
|
|
/// the draft revision already open on it, plus which client left that draft.
|
|
/// Mirrors C# `RevisionConflict`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionConflict {
|
|
/// The link that already carries this name (set on file-creation conflicts).
|
|
#[serde(rename = "ConflictLinkID", default)]
|
|
pub link_id: Option<LinkId>,
|
|
/// The conflicting link's committed (active) revision, if it has one. A
|
|
/// value here means a real, sealed file — not a recoverable stale draft.
|
|
#[serde(rename = "ConflictRevisionID", default)]
|
|
pub revision_id: Option<String>,
|
|
#[serde(rename = "ConflictDraftRevisionID", default)]
|
|
pub draft_revision_id: Option<String>,
|
|
#[serde(rename = "ConflictDraftClientUID", default)]
|
|
pub draft_client_uid: Option<String>,
|
|
}
|
|
|
|
/// `GET v2/volumes/{vid}/links/{lid}/revisions/{rid}/verification`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BlockVerificationInputResponse {
|
|
/// Base64 verification code XORed with the block ciphertext prefix.
|
|
#[serde(rename = "VerificationCode")]
|
|
pub verification_code: String,
|
|
/// Base64 content key packet (re-encrypted to the node key) for the check.
|
|
#[serde(rename = "ContentKeyPacket")]
|
|
pub content_key_packet: String,
|
|
}
|
|
|
|
/// `POST blocks` — request upload targets for content/thumbnail blocks.
|
|
///
|
|
/// Mirrors C# `BlockUploadPreparationRequest`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct BlockUploadPreparationRequest {
|
|
#[serde(rename = "AddressID")]
|
|
pub address_id: AddressId,
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: VolumeId,
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
#[serde(rename = "RevisionID")]
|
|
pub revision_id: String,
|
|
#[serde(rename = "BlockList")]
|
|
pub blocks: Vec<BlockCreationRequest>,
|
|
#[serde(rename = "ThumbnailList")]
|
|
pub thumbnails: Vec<ThumbnailCreationRequest>,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct BlockCreationRequest {
|
|
#[serde(rename = "Index")]
|
|
pub index: i32,
|
|
#[serde(rename = "Size")]
|
|
pub size: i32,
|
|
/// Armored PGP message: the block's detached signature, encrypted to the
|
|
/// node key.
|
|
#[serde(rename = "EncSignature")]
|
|
pub encrypted_signature: String,
|
|
/// Base64 SHA-256 of the block ciphertext.
|
|
#[serde(rename = "Hash")]
|
|
pub hash: String,
|
|
#[serde(rename = "Verifier")]
|
|
pub verifier: BlockVerifier,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct BlockVerifier {
|
|
/// Base64 verification token (`code XOR ciphertext_prefix`).
|
|
#[serde(rename = "Token")]
|
|
pub token: String,
|
|
}
|
|
|
|
/// Thumbnail creation entry in a block-upload preparation request. Mirrors C#
|
|
/// `ThumbnailCreationRequest` (`Size`, `Type`, base64 ciphertext `Hash`).
|
|
#[derive(Debug, Clone, Serialize)]
|
|
pub struct ThumbnailCreationRequest {
|
|
#[serde(rename = "Size")]
|
|
pub size: i32,
|
|
#[serde(rename = "Type")]
|
|
pub thumbnail_type: i32,
|
|
#[serde(rename = "Hash")]
|
|
pub hash: String,
|
|
}
|
|
|
|
/// `POST blocks` response.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BlockUploadPreparationResponse {
|
|
#[serde(rename = "UploadLinks")]
|
|
pub upload_targets: Vec<BlockUploadTarget>,
|
|
#[serde(rename = "ThumbnailLinks", default)]
|
|
pub thumbnail_upload_targets: Vec<BlockUploadTarget>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BlockUploadTarget {
|
|
#[serde(rename = "BareURL")]
|
|
pub bare_url: String,
|
|
#[serde(rename = "Token")]
|
|
pub token: String,
|
|
/// Index of the content block this target is for, echoed from the request.
|
|
/// `Option` because it is absent on thumbnail targets and older responses;
|
|
/// the uploader then falls back to request order. TS reads the same field
|
|
/// (`UploadLinks[].Index` in `apiService.requestBlockUpload`).
|
|
#[serde(rename = "Index", default)]
|
|
pub index: Option<i32>,
|
|
/// Type of the thumbnail this target is for, echoed from the request
|
|
/// (TS `ThumbnailLinks[].ThumbnailType`). Absent on content-block targets.
|
|
#[serde(rename = "ThumbnailType", default)]
|
|
pub thumbnail_type: Option<i32>,
|
|
}
|
|
|
|
/// `PUT v2/volumes/{vid}/files/{lid}/revisions/{rid}` — seal the revision.
|
|
///
|
|
/// Mirrors C# `RevisionUpdateRequest`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct RevisionUpdateRequest {
|
|
#[serde(rename = "ManifestSignature")]
|
|
pub manifest_signature: String,
|
|
#[serde(rename = "SignatureAddress")]
|
|
pub signature_address: String,
|
|
#[serde(rename = "ChecksumVerified")]
|
|
pub checksum_verified: bool,
|
|
#[serde(rename = "XAttr", skip_serializing_if = "Option::is_none")]
|
|
pub extended_attributes: Option<String>,
|
|
/// Photo-specific seal metadata (capture time, content hash, tags). Present
|
|
/// only for photo uploads. Mirrors C# `RevisionUpdateRequest.PhotosAttributes`
|
|
/// (`[JsonPropertyName("Photo")]`).
|
|
#[serde(rename = "Photo", skip_serializing_if = "Option::is_none")]
|
|
pub photos_attributes: Option<PhotosAttributesDto>,
|
|
}
|
|
|
|
/// Photo-specific revision attributes, attached to the seal request for photo
|
|
/// uploads. Mirrors C# `PhotosAttributesDto`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct PhotosAttributesDto {
|
|
/// Capture time in seconds since the Unix epoch (C# `EpochSecondsJsonConverter`).
|
|
#[serde(rename = "CaptureTime")]
|
|
pub capture_time: i64,
|
|
/// Lowercase-hex HMAC-SHA256 of the lowercase-hex plaintext SHA-1, keyed by
|
|
/// the parent folder's hash key (C# `ContentHashDigest`,
|
|
/// `ForgivingBytesToHexJsonConverter`).
|
|
#[serde(rename = "ContentHash")]
|
|
pub content_hash: String,
|
|
/// Link id of the main photo, when this is a related photo (live/burst).
|
|
#[serde(rename = "MainPhotoLinkID", skip_serializing_if = "Option::is_none")]
|
|
pub main_photo_link_id: Option<LinkId>,
|
|
/// Photo classification tags (their `PhotoTag` discriminants); always
|
|
/// present, empty when none (C# `Tags ?? []`).
|
|
#[serde(rename = "Tags")]
|
|
pub tags: Vec<i32>,
|
|
}
|
|
|
|
/// The decrypted `XAttr` JSON payload for a revision (encrypted to the node key
|
|
/// before upload). Mirrors C# `ExtendedAttributes` / `CommonExtendedAttributes`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ExtendedAttributes {
|
|
#[serde(rename = "Common")]
|
|
pub common: CommonExtendedAttributes,
|
|
}
|
|
|
|
/// All fields are optional, mirroring C# `CommonExtendedAttributes` (every
|
|
/// property is nullable): a file-upload seal sets size/block-sizes/digests and
|
|
/// optionally a modification time, while a folder create sets only the
|
|
/// modification time. Unset fields are omitted from the JSON.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CommonExtendedAttributes {
|
|
#[serde(rename = "Size", skip_serializing_if = "Option::is_none")]
|
|
pub size: Option<i64>,
|
|
/// ISO-8601 UTC modification timestamp (C# `CommonExtendedAttributes
|
|
/// .ModificationTime`, written via `Iso8601DateTimeResultJsonConverter`).
|
|
#[serde(rename = "ModificationTime", skip_serializing_if = "Option::is_none")]
|
|
pub modification_time: Option<String>,
|
|
#[serde(rename = "BlockSizes", skip_serializing_if = "Option::is_none")]
|
|
pub block_sizes: Option<Vec<i32>>,
|
|
#[serde(rename = "Digests", skip_serializing_if = "Option::is_none")]
|
|
pub digests: Option<FileContentDigests>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct FileContentDigests {
|
|
/// Lowercase hex SHA-1 of the full plaintext.
|
|
#[serde(rename = "SHA1")]
|
|
pub sha1: String,
|
|
}
|
|
|
|
/// `GET volumes/{vid}/events/latest` — seeds the enumeration cursor.
|
|
/// C# `LatestVolumeEventResponse`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct LatestVolumeEventResponse {
|
|
#[serde(rename = "EventID")]
|
|
pub event_id: DriveEventId,
|
|
}
|
|
|
|
/// `GET v2/volumes/{vid}/events/{cursor}` — one page of volume events.
|
|
/// C# `VolumeEventListResponse`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VolumeEventListResponse {
|
|
/// Cursor to use for the next request (the last event id in this page).
|
|
#[serde(rename = "EventID")]
|
|
pub last_event_id: DriveEventId,
|
|
#[serde(rename = "Events", default)]
|
|
pub events: Vec<VolumeEventDto>,
|
|
/// More pages exist beyond this one.
|
|
#[serde(rename = "More")]
|
|
pub more_entries_exist: bool,
|
|
/// Continuity lost — caller must resync from server state.
|
|
#[serde(rename = "Refresh")]
|
|
pub refresh_required: bool,
|
|
}
|
|
|
|
/// A single volume event. C# `VolumeEventDto`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VolumeEventDto {
|
|
#[serde(rename = "EventID")]
|
|
pub id: DriveEventId,
|
|
/// `VolumeEventType`: 0 = Delete, 1 = Create, 2 = Update, 3 = UpdateMetadata.
|
|
#[serde(rename = "EventType")]
|
|
pub event_type: i32,
|
|
#[serde(rename = "Link")]
|
|
pub link: VolumeEventLinkDto,
|
|
}
|
|
|
|
/// The affected link of a volume event. C# `VolumeEventLinkDto`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct VolumeEventLinkDto {
|
|
#[serde(rename = "LinkID")]
|
|
pub id: LinkId,
|
|
#[serde(rename = "ParentLinkID")]
|
|
pub parent_id: Option<LinkId>,
|
|
#[serde(rename = "IsShared", default)]
|
|
pub is_shared: bool,
|
|
#[serde(rename = "IsTrashed", default)]
|
|
pub is_trashed: bool,
|
|
}
|
|
|
|
/// `GET shares/{sid}` — a share and the material needed to unlock its key.
|
|
/// C# `ShareResponse`; the share fields sit at the top level of the envelope, so
|
|
/// they are flattened into the same [`ShareDto`] the my-files lookup returns.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareResponse {
|
|
#[serde(flatten)]
|
|
pub share: ShareDto,
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: VolumeId,
|
|
#[serde(rename = "LinkID")]
|
|
pub root_link_id: LinkId,
|
|
}
|
|
|
|
/// `GET v2/sharedwithme` — one page of the items other users share with us.
|
|
/// C# `SharedWithMeResponse`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SharedWithMeResponse {
|
|
#[serde(rename = "Links", default)]
|
|
pub links: Vec<SharedWithMeLinkDto>,
|
|
/// Cursor for the next page.
|
|
#[serde(rename = "AnchorID", default)]
|
|
pub anchor_id: Option<String>,
|
|
#[serde(rename = "More", default)]
|
|
pub more: bool,
|
|
}
|
|
|
|
/// One shared-with-me item. C# `SharedWithMeLinkDto`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SharedWithMeLinkDto {
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: VolumeId,
|
|
#[serde(rename = "ShareID")]
|
|
pub share_id: ShareId,
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
/// What kind of item is shared. See [`ShareTargetType`].
|
|
#[serde(rename = "ShareTargetType", default)]
|
|
pub share_target_type: i32,
|
|
}
|
|
|
|
/// `GET drive/v2/volumes/{volumeID}/shares` — one page of the collaborative
|
|
/// shares I own that are still live (have members, invitations or a public URL).
|
|
/// TS SDK `SharedByMeResponseDto`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SharedByMeResponse {
|
|
#[serde(rename = "Links", default)]
|
|
pub links: Vec<SharedByMeLinkDto>,
|
|
/// Cursor for the next page.
|
|
#[serde(rename = "AnchorID", default)]
|
|
pub anchor_id: Option<String>,
|
|
#[serde(rename = "More", default)]
|
|
pub more: bool,
|
|
}
|
|
|
|
/// One shared-by-me item. TS SDK `LinkSharedByMeResponseDto`. The volume is the
|
|
/// one queried, so only the link (and its share) come back per entry.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct SharedByMeLinkDto {
|
|
#[serde(rename = "ShareID")]
|
|
pub share_id: ShareId,
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
/// The share the link is *reached through* — the context in which it lives,
|
|
/// which differs from `share_id` for an item inside a shared folder. C#
|
|
/// `SharedByMeLinkDto.ContextShareId`; optional here because the field is a
|
|
/// recent upstream addition and an older backend simply omits it.
|
|
#[serde(rename = "ContextShareID", default)]
|
|
pub context_share_id: Option<ShareId>,
|
|
}
|
|
|
|
/// The kind of item a share points at. C# `ShareTargetType`.
|
|
///
|
|
/// The Drive client exposes folders, files and vendor items; albums and photos
|
|
/// belong to the Photos client (C# `SharingOperations.DriveShareTargetTypes`).
|
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
|
#[repr(i32)]
|
|
pub enum ShareTargetType {
|
|
Root = 0,
|
|
Folder = 1,
|
|
File = 2,
|
|
Album = 3,
|
|
Photo = 4,
|
|
ProtonVendor = 5,
|
|
}
|
|
|
|
impl ShareTargetType {
|
|
pub fn from_raw(value: i32) -> Option<Self> {
|
|
match value {
|
|
0 => Some(Self::Root),
|
|
1 => Some(Self::Folder),
|
|
2 => Some(Self::File),
|
|
3 => Some(Self::Album),
|
|
4 => Some(Self::Photo),
|
|
5 => Some(Self::ProtonVendor),
|
|
_ => None,
|
|
}
|
|
}
|
|
|
|
/// Whether the Drive client (rather than the Photos client) owns this kind.
|
|
pub fn is_drive_item(self) -> bool {
|
|
matches!(self, Self::Folder | Self::File | Self::ProtonVendor)
|
|
}
|
|
|
|
/// Whether the Photos client owns this kind (C#
|
|
/// `ProtonPhotosClient.ShareTargetTypes`).
|
|
pub fn is_photos_item(self) -> bool {
|
|
matches!(self, Self::Album | Self::Photo)
|
|
}
|
|
}
|
|
|
|
/// `GET devices` — the account's registered devices. C# `DeviceListResponse`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceListResponse {
|
|
#[serde(rename = "Devices", default)]
|
|
pub devices: Vec<DeviceListItemDto>,
|
|
}
|
|
|
|
/// C# `DeviceListItemDto`: a device and the share holding its root folder.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceListItemDto {
|
|
#[serde(rename = "Device")]
|
|
pub device: DeviceDataDto,
|
|
#[serde(rename = "Share")]
|
|
pub share: DeviceShareDataDto,
|
|
}
|
|
|
|
/// C# `DeviceDataDto`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceDataDto {
|
|
#[serde(rename = "DeviceID")]
|
|
pub id: DeviceUid,
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: VolumeId,
|
|
/// `DeviceType`: 1 = Windows, 2 = macOS, 3 = Linux.
|
|
#[serde(rename = "Type")]
|
|
pub device_type: i32,
|
|
#[serde(rename = "CreateTime")]
|
|
pub creation_time: i64,
|
|
#[serde(rename = "LastSyncTime", default)]
|
|
pub last_sync_time: Option<i64>,
|
|
}
|
|
|
|
/// C# `DeviceShareDataDto`. `Name` is the *deprecated* device name: it used to
|
|
/// live on the share and must be cleared when renaming an old device.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceShareDataDto {
|
|
#[serde(rename = "ShareID")]
|
|
pub id: ShareId,
|
|
#[serde(rename = "LinkID")]
|
|
pub root_link_id: LinkId,
|
|
#[serde(rename = "Name", default)]
|
|
pub name: Option<String>,
|
|
}
|
|
|
|
/// `POST devices` — register a device with its own share and root folder.
|
|
/// C# `DeviceCreationRequest`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceCreationRequest {
|
|
#[serde(rename = "Device")]
|
|
pub device: DeviceCreationDeviceDto,
|
|
#[serde(rename = "Share")]
|
|
pub share: DeviceCreationShareDto,
|
|
#[serde(rename = "Link")]
|
|
pub link: DeviceCreationLinkDto,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceCreationDeviceDto {
|
|
#[serde(rename = "Type")]
|
|
pub device_type: i32,
|
|
/// Synchronisation state; 0 (off) when registering a new device.
|
|
#[serde(rename = "SyncState")]
|
|
pub sync_state: i32,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceCreationShareDto {
|
|
#[serde(rename = "AddressID")]
|
|
pub address_id: AddressId,
|
|
#[serde(rename = "AddressKeyID")]
|
|
pub address_key_id: AddressKeyId,
|
|
#[serde(rename = "Key")]
|
|
pub key: String,
|
|
#[serde(rename = "Passphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "PassphraseSignature")]
|
|
pub passphrase_signature: String,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceCreationLinkDto {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NodeKey")]
|
|
pub key: String,
|
|
#[serde(rename = "NodePassphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "NodePassphraseSignature")]
|
|
pub passphrase_signature: String,
|
|
#[serde(rename = "NodeHashKey")]
|
|
pub node_hash_key: String,
|
|
}
|
|
|
|
/// `POST devices` response. C# `DeviceCreationResponse`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceCreationResponse {
|
|
#[serde(rename = "Device")]
|
|
pub device: DeviceCreationResultDto,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct DeviceCreationResultDto {
|
|
#[serde(rename = "DeviceID")]
|
|
pub id: DeviceUid,
|
|
#[serde(rename = "ShareID")]
|
|
pub share_id: ShareId,
|
|
#[serde(rename = "LinkID")]
|
|
pub root_link_id: LinkId,
|
|
}
|
|
|
|
/// `PUT devices/{uid}` — only ever used to clear the deprecated share-held name.
|
|
/// C# `DeviceUpdateRequest`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceUpdateRequest {
|
|
#[serde(rename = "Share")]
|
|
pub share: DeviceUpdateShareDto,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct DeviceUpdateShareDto {
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
}
|
|
|
|
#[cfg(test)]
|
|
mod tests {
|
|
use super::*;
|
|
|
|
#[test]
|
|
fn context_share_response_parses_required_share_id() {
|
|
let response: ContextShareResponse =
|
|
serde_json::from_str(r#"{"Code":1000,"ContextShareID":"context-share-1"}"#)
|
|
.expect("context share response");
|
|
|
|
assert_eq!(response.context_share_id, ShareId::new("context-share-1"));
|
|
}
|
|
|
|
#[test]
|
|
fn album_link_details_expose_folder_properties() {
|
|
// An album's hash key arrives in the `Album` block, not `Folder`; the
|
|
// rest of the client reads it through `folder_properties` (C# builds a
|
|
// synthetic `FolderDto` from `AlbumDto`).
|
|
let raw = r#"{
|
|
"Link": {
|
|
"LinkID": "album-1", "Type": 3, "ParentLinkID": "root", "State": 1,
|
|
"CreateTime": 1, "ModifyTime": 2, "Name": "enc", "NodeKey": "k",
|
|
"NodePassphrase": "p"
|
|
},
|
|
"Album": {
|
|
"NodeHashKey": "hash-key", "XAttr": "xattr", "PhotoCount": 7,
|
|
"CoverLinkID": "photo-9", "LastActivityTime": 1700000000
|
|
}
|
|
}"#;
|
|
let details: LinkDetailsDto = serde_json::from_str(raw).unwrap();
|
|
let album = details.album.as_ref().expect("album block");
|
|
assert_eq!(album.photo_count, 7);
|
|
assert_eq!(album.cover_link_id.as_ref().unwrap().as_str(), "photo-9");
|
|
assert_eq!(album.last_activity_time, 1_700_000_000);
|
|
|
|
let folder = details
|
|
.folder_properties()
|
|
.expect("album reads as a folder");
|
|
assert_eq!(folder.hash_key, "hash-key");
|
|
assert_eq!(folder.extended_attributes, Some("xattr"));
|
|
assert!(matches!(details.link.parsed_type(), LinkType::Album));
|
|
}
|
|
|
|
#[test]
|
|
fn photo_link_details_carry_file_and_photo_properties() {
|
|
// The photos volume returns file properties under `Photo` — a superset
|
|
// of `File` — so both halves have to come out of the one block.
|
|
let raw = r#"{
|
|
"Link": {
|
|
"LinkID": "photo-1", "Type": 2, "ParentLinkID": "root", "State": 1,
|
|
"CreateTime": 1, "ModifyTime": 2, "Name": "enc", "NodeKey": "k",
|
|
"NodePassphrase": "p"
|
|
},
|
|
"Photo": {
|
|
"MediaType": "image/jpeg", "TotalEncryptedSize": 42,
|
|
"ContentKeyPacket": "packet",
|
|
"CaptureTime": 1700000000, "ContentHash": "abcd",
|
|
"MainPhotoLinkID": "photo-main",
|
|
"RelatedPhotosLinkIDs": ["photo-2"],
|
|
"Tags": [0, 2, 99],
|
|
"Albums": [{ "AlbumLinkID": "album-1", "AddedTime": 1700000001 }]
|
|
}
|
|
}"#;
|
|
let details: LinkDetailsDto = serde_json::from_str(raw).unwrap();
|
|
let file = details.file_properties().expect("file properties");
|
|
assert_eq!(file.media_type, "image/jpeg");
|
|
assert_eq!(file.total_size_on_storage, 42);
|
|
|
|
let photo = details.photo.as_ref().expect("photo block");
|
|
assert_eq!(photo.capture_time, 1_700_000_000);
|
|
assert_eq!(photo.content_hash.as_deref(), Some("abcd"));
|
|
assert_eq!(
|
|
photo.main_photo_link_id.as_ref().unwrap().as_str(),
|
|
"photo-main"
|
|
);
|
|
assert_eq!(photo.related_photo_link_ids.len(), 1);
|
|
// Unknown discriminants survive the wire; they are dropped where the
|
|
// node is built, not here.
|
|
assert_eq!(photo.tags, [0, 2, 99]);
|
|
assert_eq!(photo.album_inclusions[0].album_link_id.as_str(), "album-1");
|
|
}
|
|
|
|
#[test]
|
|
fn album_listings_page_on_anchor() {
|
|
let albums: AlbumListResponse = serde_json::from_str(
|
|
r#"{"Albums":[{"LinkID":"album-1"}],"More":true,"AnchorID":"album-1"}"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(albums.albums[0].id.as_str(), "album-1");
|
|
assert!(albums.more);
|
|
assert_eq!(albums.anchor_id.unwrap().as_str(), "album-1");
|
|
|
|
let items: AlbumItemListResponse = serde_json::from_str(
|
|
r#"{"Photos":[{"LinkID":"photo-1","CaptureTime":1700000000}],"More":false}"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(items.photos[0].capture_time, 1_700_000_000);
|
|
assert!(!items.more);
|
|
assert!(items.anchor_id.is_none());
|
|
|
|
// Shared albums live on the sharer's volume, so each row carries one.
|
|
let shared: SharedAlbumsResponse = serde_json::from_str(
|
|
r#"{"Albums":[{"VolumeID":"vol-9","LinkID":"album-3"}],"More":false}"#,
|
|
)
|
|
.unwrap();
|
|
assert_eq!(shared.albums[0].volume_id.as_str(), "vol-9");
|
|
assert_eq!(shared.albums[0].link_id.as_str(), "album-3");
|
|
}
|
|
|
|
#[test]
|
|
fn only_photos_share_targets_are_photos_items() {
|
|
// C# `ProtonPhotosClient.ShareTargetTypes` — the complement of the Drive
|
|
// client's set, minus the root share, which is neither's.
|
|
for (raw, expected) in [
|
|
(0, false),
|
|
(1, false),
|
|
(2, false),
|
|
(3, true),
|
|
(4, true),
|
|
(5, false),
|
|
] {
|
|
let kind = ShareTargetType::from_raw(raw).expect("known target type");
|
|
assert_eq!(kind.is_photos_item(), expected, "target type {raw}");
|
|
}
|
|
}
|
|
|
|
#[test]
|
|
fn only_drive_share_targets_are_drive_items() {
|
|
// C# `SharingOperations.DriveShareTargetTypes`; albums and photos belong
|
|
// to the Photos client, and a root share is not a shared item.
|
|
for (raw, expected) in [
|
|
(0, false),
|
|
(1, true),
|
|
(2, true),
|
|
(3, false),
|
|
(4, false),
|
|
(5, true),
|
|
] {
|
|
let kind = ShareTargetType::from_raw(raw).expect("known target type");
|
|
assert_eq!(kind.is_drive_item(), expected, "target type {raw}");
|
|
}
|
|
assert!(ShareTargetType::from_raw(6).is_none());
|
|
}
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Sharing: creating shares, inviting Proton users, members + invitations.
|
|
// Ported from the TypeScript SDK (`internal/sharing/apiService.ts`); the C#
|
|
// public SDK does not expose share creation.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `POST volumes/{volumeID}/shares` — create a standard share on a node.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateShareRequest {
|
|
#[serde(rename = "RootLinkID")]
|
|
pub root_link_id: LinkId,
|
|
#[serde(rename = "AddressID")]
|
|
pub address_id: AddressId,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "ShareKey")]
|
|
pub share_key: String,
|
|
#[serde(rename = "SharePassphrase")]
|
|
pub share_passphrase: String,
|
|
#[serde(rename = "SharePassphraseSignature")]
|
|
pub share_passphrase_signature: String,
|
|
#[serde(rename = "PassphraseKeyPacket")]
|
|
pub passphrase_key_packet: String,
|
|
#[serde(rename = "NameKeyPacket")]
|
|
pub name_key_packet: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreateShareResponse {
|
|
#[serde(rename = "Share")]
|
|
pub share: CreatedShareDto,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreatedShareDto {
|
|
#[serde(rename = "ID")]
|
|
pub id: ShareId,
|
|
#[serde(rename = "EditorsCanShare", default)]
|
|
pub editors_can_share: bool,
|
|
}
|
|
|
|
/// `POST v2/shares/{shareID}/invitations` — invite a Proton user.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct InviteProtonUserRequest {
|
|
#[serde(rename = "Invitation")]
|
|
pub invitation: InviteProtonUserInvitationDto,
|
|
#[serde(rename = "EmailDetails")]
|
|
pub email_details: InviteEmailDetailsDto,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct InviteProtonUserInvitationDto {
|
|
#[serde(rename = "InviterEmail")]
|
|
pub inviter_email: String,
|
|
#[serde(rename = "InviteeEmail")]
|
|
pub invitee_email: String,
|
|
#[serde(rename = "Permissions")]
|
|
pub permissions: i32,
|
|
#[serde(rename = "KeyPacket")]
|
|
pub key_packet: String,
|
|
#[serde(rename = "KeyPacketSignature")]
|
|
pub key_packet_signature: String,
|
|
#[serde(
|
|
rename = "ExternalInvitationID",
|
|
skip_serializing_if = "Option::is_none"
|
|
)]
|
|
pub external_invitation_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct InviteEmailDetailsDto {
|
|
#[serde(rename = "Message", skip_serializing_if = "Option::is_none")]
|
|
pub message: Option<String>,
|
|
#[serde(rename = "ItemName", skip_serializing_if = "Option::is_none")]
|
|
pub item_name: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InviteProtonUserResponse {
|
|
#[serde(rename = "Invitation")]
|
|
pub invitation: ShareInvitationDto,
|
|
}
|
|
|
|
/// `GET v2/shares/{shareID}/invitations`
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareInvitationsResponse {
|
|
#[serde(rename = "Invitations", default)]
|
|
pub invitations: Vec<ShareInvitationDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareInvitationDto {
|
|
#[serde(rename = "InvitationID")]
|
|
pub invitation_id: String,
|
|
#[serde(rename = "InviterEmail")]
|
|
pub inviter_email: String,
|
|
#[serde(rename = "InviteeEmail")]
|
|
pub invitee_email: String,
|
|
#[serde(rename = "Permissions", default)]
|
|
pub permissions: Option<i32>,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub create_time: i64,
|
|
}
|
|
|
|
/// `GET v2/shares/{shareID}/members`
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareMembersResponse {
|
|
#[serde(rename = "Members", default)]
|
|
pub members: Vec<ShareMemberDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareMemberDto {
|
|
#[serde(rename = "MemberID")]
|
|
pub member_id: ShareMembershipId,
|
|
#[serde(rename = "InviterEmail")]
|
|
pub inviter_email: String,
|
|
#[serde(rename = "Email")]
|
|
pub email: String,
|
|
#[serde(rename = "Permissions", default)]
|
|
pub permissions: Option<i32>,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub create_time: i64,
|
|
}
|
|
|
|
/// `PUT v2/shares/{shareID}/members/{memberID}` and the invitation equivalent.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct UpdatePermissionsRequest {
|
|
#[serde(rename = "Permissions")]
|
|
pub permissions: i32,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Incoming invitations: invitations addressed to the current user, and
|
|
// accept/reject. `GET v2/shares/invitations` lists them; the detail, accept and
|
|
// reject routes are keyed by invitation id alone.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `GET v2/shares/invitations` — the invitations where we are the invitee.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InvitationsListResponse {
|
|
#[serde(rename = "Invitations", default)]
|
|
pub invitations: Vec<InvitationListItemDto>,
|
|
#[serde(rename = "More", default)]
|
|
pub more: bool,
|
|
#[serde(rename = "AnchorID", default)]
|
|
pub anchor_id: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InvitationListItemDto {
|
|
#[serde(rename = "InvitationID")]
|
|
pub invitation_id: String,
|
|
#[serde(rename = "ShareID", default)]
|
|
pub share_id: String,
|
|
#[serde(rename = "ShareTargetType", default)]
|
|
pub share_target_type: i32,
|
|
}
|
|
|
|
/// `GET v2/shares/invitations/{invitationID}` — the encrypted invitation, the
|
|
/// share crypto needed to decrypt it, and the shared node's link.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InvitationDetailsResponse {
|
|
#[serde(rename = "Invitation")]
|
|
pub invitation: IncomingInvitationDto,
|
|
#[serde(rename = "Share")]
|
|
pub share: InvitationShareDto,
|
|
#[serde(rename = "Link")]
|
|
pub link: InvitationLinkDto,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct IncomingInvitationDto {
|
|
#[serde(rename = "InvitationID")]
|
|
pub invitation_id: String,
|
|
#[serde(rename = "InviterEmail")]
|
|
pub inviter_email: String,
|
|
#[serde(rename = "InviteeEmail")]
|
|
pub invitee_email: String,
|
|
#[serde(rename = "KeyPacket")]
|
|
pub key_packet: String,
|
|
#[serde(rename = "KeyPacketSignature", default)]
|
|
pub key_packet_signature: String,
|
|
#[serde(rename = "Permissions", default)]
|
|
pub permissions: Option<i32>,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub create_time: i64,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InvitationShareDto {
|
|
#[serde(rename = "ShareKey")]
|
|
pub share_key: String,
|
|
#[serde(rename = "Passphrase")]
|
|
pub passphrase: String,
|
|
#[serde(rename = "CreatorEmail", default)]
|
|
pub creator_email: String,
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: String,
|
|
#[serde(rename = "ShareTargetType", default)]
|
|
pub share_target_type: i32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InvitationLinkDto {
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: String,
|
|
#[serde(rename = "Type", default)]
|
|
pub link_type: i32,
|
|
#[serde(rename = "MIMEType", default)]
|
|
pub mime_type: Option<String>,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
}
|
|
|
|
/// `POST v2/shares/invitations/{invitationID}/accept`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct AcceptInvitationRequest {
|
|
#[serde(rename = "SessionKeySignature")]
|
|
pub session_key_signature: String,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// External (non-Proton) invitations. Same share-invitation model, but the
|
|
// invitee has no Proton key, so instead of a key packet the inviter carries a
|
|
// detached signature over the invitee email + share session key.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `POST v2/shares/{shareID}/external-invitations`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct InviteExternalUserRequest {
|
|
#[serde(rename = "ExternalInvitation")]
|
|
pub external_invitation: ExternalInvitationDto,
|
|
#[serde(rename = "EmailDetails")]
|
|
pub email_details: InviteEmailDetailsDto,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct ExternalInvitationDto {
|
|
#[serde(rename = "InviterAddressID")]
|
|
pub inviter_address_id: AddressId,
|
|
#[serde(rename = "InviteeEmail")]
|
|
pub invitee_email: String,
|
|
#[serde(rename = "Permissions")]
|
|
pub permissions: i32,
|
|
#[serde(rename = "ExternalInvitationSignature")]
|
|
pub external_invitation_signature: String,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct InviteExternalUserResponse {
|
|
#[serde(rename = "ExternalInvitation")]
|
|
pub external_invitation: ExternalInvitationResponseDto,
|
|
}
|
|
|
|
/// `GET v2/shares/{shareID}/external-invitations`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ExternalInvitationsResponse {
|
|
#[serde(rename = "ExternalInvitations", default)]
|
|
pub external_invitations: Vec<ExternalInvitationResponseDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ExternalInvitationResponseDto {
|
|
#[serde(rename = "ExternalInvitationID")]
|
|
pub external_invitation_id: String,
|
|
#[serde(rename = "InviterEmail")]
|
|
pub inviter_email: String,
|
|
#[serde(rename = "InviteeEmail")]
|
|
pub invitee_email: String,
|
|
#[serde(rename = "Permissions", default)]
|
|
pub permissions: Option<i32>,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub create_time: i64,
|
|
/// `1` = pending (invitee has no Proton account yet), else user-registered.
|
|
#[serde(rename = "State", default)]
|
|
pub state: i32,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Bookmarks: public links the user has saved to their account.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `GET v2/shared-bookmarks`.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BookmarksResponse {
|
|
#[serde(rename = "Bookmarks", default)]
|
|
pub bookmarks: Vec<BookmarkDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BookmarkDto {
|
|
#[serde(rename = "Token")]
|
|
pub token: BookmarkTokenDto,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub create_time: i64,
|
|
#[serde(rename = "EncryptedUrlPassword", default)]
|
|
pub encrypted_url_password: Option<String>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct BookmarkTokenDto {
|
|
#[serde(rename = "Token")]
|
|
pub token: String,
|
|
#[serde(rename = "ShareKey")]
|
|
pub share_key: String,
|
|
#[serde(rename = "SharePassphrase")]
|
|
pub share_passphrase: String,
|
|
#[serde(rename = "SharePasswordSalt")]
|
|
pub share_password_salt: String,
|
|
#[serde(rename = "LinkType", default)]
|
|
pub link_type: i32,
|
|
#[serde(rename = "MIMEType", default)]
|
|
pub mime_type: Option<String>,
|
|
#[serde(rename = "Name")]
|
|
pub name: String,
|
|
#[serde(rename = "NodeKey", default)]
|
|
pub node_key: String,
|
|
#[serde(rename = "NodePassphrase", default)]
|
|
pub node_passphrase: String,
|
|
#[serde(rename = "ContentKeyPacket", default)]
|
|
pub content_key_packet: Option<String>,
|
|
}
|
|
|
|
/// `POST v2/urls/{token}/bookmark`.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreateBookmarkRequest {
|
|
#[serde(rename = "BookmarkShareURL")]
|
|
pub bookmark_share_url: BookmarkShareUrlDto,
|
|
}
|
|
|
|
#[derive(Debug, Serialize)]
|
|
pub struct BookmarkShareUrlDto {
|
|
#[serde(rename = "EncryptedUrlPassword")]
|
|
pub encrypted_url_password: String,
|
|
#[serde(rename = "AddressID")]
|
|
pub address_id: AddressId,
|
|
#[serde(rename = "AddressKeyID")]
|
|
pub address_key_id: AddressKeyId,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public share links (`shares/{shareID}/urls`) + SRP modulus fetch.
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `POST auth/v4/modulus` (root route) — a fresh signed SRP modulus.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ModulusResponse {
|
|
#[serde(rename = "Modulus")]
|
|
pub modulus: String,
|
|
#[serde(rename = "ModulusID")]
|
|
pub modulus_id: String,
|
|
}
|
|
|
|
/// `POST shares/{shareID}/urls` — create a public link.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct CreatePublicLinkRequest {
|
|
#[serde(rename = "CreatorEmail")]
|
|
pub creator_email: String,
|
|
#[serde(rename = "Permissions")]
|
|
pub permissions: i32,
|
|
#[serde(rename = "Flags")]
|
|
pub flags: i32,
|
|
#[serde(rename = "ExpirationTime", skip_serializing_if = "Option::is_none")]
|
|
pub expiration_time: Option<i64>,
|
|
#[serde(rename = "SharePasswordSalt")]
|
|
pub share_password_salt: String,
|
|
#[serde(rename = "SharePassphraseKeyPacket")]
|
|
pub share_passphrase_key_packet: String,
|
|
#[serde(rename = "Password")]
|
|
pub password: String,
|
|
#[serde(rename = "UrlPasswordSalt")]
|
|
pub url_password_salt: String,
|
|
#[serde(rename = "SRPVerifier")]
|
|
pub srp_verifier: String,
|
|
#[serde(rename = "SRPModulusID")]
|
|
pub srp_modulus_id: String,
|
|
#[serde(rename = "MaxAccesses")]
|
|
pub max_accesses: i32,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct CreatePublicLinkResponse {
|
|
#[serde(rename = "ShareURL")]
|
|
pub share_url: ShareUrlDto,
|
|
}
|
|
|
|
/// `GET shares/{shareID}/urls`
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareUrlsResponse {
|
|
#[serde(rename = "ShareURLs", default)]
|
|
pub share_urls: Vec<ShareUrlDto>,
|
|
}
|
|
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct ShareUrlDto {
|
|
#[serde(rename = "ShareURLID")]
|
|
pub share_url_id: String,
|
|
#[serde(rename = "ShareID", default)]
|
|
pub share_id: Option<ShareId>,
|
|
#[serde(rename = "PublicUrl", default)]
|
|
pub public_url: String,
|
|
/// The link password (generated portion + any custom password), PGP-encrypted
|
|
/// to the share creator's address key. Decrypting it recovers the secret URL
|
|
/// fragment. Absent on the create response, present when listing.
|
|
#[serde(rename = "Password", default)]
|
|
pub password: Option<String>,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub create_time: i64,
|
|
#[serde(rename = "ExpirationTime", default)]
|
|
pub expiration_time: Option<i64>,
|
|
#[serde(rename = "Permissions", default)]
|
|
pub permissions: Option<i32>,
|
|
#[serde(rename = "Flags", default)]
|
|
pub flags: i32,
|
|
#[serde(rename = "NumAccesses", default)]
|
|
pub num_accesses: i64,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Public-link session (consuming someone else's shared link)
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `GET urls/{token}/info` — opens the SRP handshake for a public link.
|
|
///
|
|
/// Callable without any session (TS `SharingPublicSessionAPIService.initPublicLinkSession`).
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct PublicLinkInfoResponse {
|
|
/// SRP auth version.
|
|
#[serde(rename = "Version", default)]
|
|
pub version: i32,
|
|
/// The cleartext-signed SRP modulus.
|
|
#[serde(rename = "Modulus", default)]
|
|
pub modulus: String,
|
|
/// base64 server ephemeral `B`.
|
|
#[serde(rename = "ServerEphemeral", default)]
|
|
pub server_ephemeral: String,
|
|
/// base64 SRP salt for the URL password.
|
|
#[serde(rename = "UrlPasswordSalt", default)]
|
|
pub url_password_salt: String,
|
|
#[serde(rename = "SRPSession", default)]
|
|
pub srp_session: String,
|
|
/// Bit 0 set = the link also needs a custom password. `0` or `1` alone marks
|
|
/// a legacy link the SDK no longer supports.
|
|
#[serde(rename = "Flags", default)]
|
|
pub flags: i32,
|
|
/// Non-zero for vendor links (Proton Docs and friends) that belong to
|
|
/// another app rather than to Drive.
|
|
#[serde(rename = "VendorType", default)]
|
|
pub vendor_type: i32,
|
|
}
|
|
|
|
impl PublicLinkInfoResponse {
|
|
/// Whether the link needs a custom password on top of the URL fragment.
|
|
pub fn is_custom_password_protected(&self) -> bool {
|
|
self.flags & 1 == 1
|
|
}
|
|
|
|
/// Whether this is a legacy link, which neither this SDK nor the upstream
|
|
/// TypeScript SDK can open (TS: `Flags === 0 || Flags === 1`).
|
|
pub fn is_legacy(&self) -> bool {
|
|
self.flags == 0 || self.flags == 1
|
|
}
|
|
}
|
|
|
|
/// `POST urls/{token}/auth` — completes the SRP handshake.
|
|
#[derive(Debug, Serialize)]
|
|
pub struct PublicLinkAuthRequest {
|
|
#[serde(rename = "ClientProof")]
|
|
pub client_proof: String,
|
|
#[serde(rename = "ClientEphemeral")]
|
|
pub client_ephemeral: String,
|
|
#[serde(rename = "SRPSession")]
|
|
pub srp_session: String,
|
|
}
|
|
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct PublicLinkAuthResponse {
|
|
/// base64 `M2`, to be checked against the locally computed expectation.
|
|
#[serde(rename = "ServerProof", default)]
|
|
pub server_proof: String,
|
|
/// The session id for subsequent requests (`x-pm-uid`).
|
|
#[serde(rename = "UID", default)]
|
|
pub uid: String,
|
|
/// Bearer token. Absent when the caller already has a Proton session that
|
|
/// the server accepted instead of minting an anonymous one.
|
|
#[serde(rename = "AccessToken", default)]
|
|
pub access_token: Option<String>,
|
|
#[serde(rename = "Share")]
|
|
pub share: PublicLinkShareDto,
|
|
}
|
|
|
|
/// The link's share crypto, returned by the auth handshake.
|
|
#[derive(Debug, Clone, Deserialize)]
|
|
pub struct PublicLinkShareDto {
|
|
#[serde(rename = "VolumeID")]
|
|
pub volume_id: VolumeId,
|
|
/// The link id of the shared node — the root of what the visitor can see.
|
|
#[serde(rename = "LinkID")]
|
|
pub link_id: LinkId,
|
|
/// base64 bcrypt salt for deriving the key password from the link password.
|
|
#[serde(rename = "SharePasswordSalt", default)]
|
|
pub share_password_salt: String,
|
|
/// The armored share private key.
|
|
#[serde(rename = "ShareKey", default)]
|
|
pub share_key: String,
|
|
/// The share key's passphrase, symmetrically encrypted under the derived
|
|
/// key password.
|
|
#[serde(rename = "SharePassphrase", default)]
|
|
pub share_passphrase: String,
|
|
#[serde(rename = "PublicPermissions", default)]
|
|
pub public_permissions: Option<i32>,
|
|
}
|
|
|
|
// ---------------------------------------------------------------------------
|
|
// Revision history
|
|
// ---------------------------------------------------------------------------
|
|
|
|
/// `GET v2/volumes/{vid}/files/{lid}/revisions` — a file's revision history.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionListResponse {
|
|
#[serde(rename = "Revisions", default)]
|
|
pub revisions: Vec<RevisionListItemDto>,
|
|
}
|
|
|
|
/// `GET …/revisions/{rid}?NoBlockUrls=true` — one revision's metadata.
|
|
///
|
|
/// Deliberately reuses [`RevisionListItemDto`], which has no `Blocks` field: with
|
|
/// `NoBlockUrls` the server still emits a `Blocks` array but with null `BareURL`
|
|
/// and `Token`, which [`BlockDto`] rejects. Ignoring the array outright is both
|
|
/// correct for a metadata read and stops a null from breaking it.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionMetadataResponse {
|
|
#[serde(rename = "Revision")]
|
|
pub revision: RevisionListItemDto,
|
|
}
|
|
|
|
/// One entry of a revision listing.
|
|
///
|
|
/// Distinct from [`RevisionDto`], which is the *single-revision* response and
|
|
/// carries the block table. The listing omits blocks but adds `State` and
|
|
/// `Size`, since a history view needs to tell active from superseded.
|
|
#[derive(Debug, Deserialize)]
|
|
pub struct RevisionListItemDto {
|
|
#[serde(rename = "ID")]
|
|
pub id: String,
|
|
/// Wire `ApiRevisionState`: 0 draft, 1 active, 2 obsolete/superseded.
|
|
#[serde(rename = "State", default)]
|
|
pub state: Option<i32>,
|
|
#[serde(rename = "CreateTime", default)]
|
|
pub creation_time: i64,
|
|
/// Encrypted size on cloud storage.
|
|
#[serde(rename = "Size", default)]
|
|
pub size: i64,
|
|
#[serde(rename = "ManifestSignature", default)]
|
|
pub manifest_signature: Option<String>,
|
|
/// Email of the signer; empty/absent means the node key signed.
|
|
#[serde(rename = "SignatureEmail", default)]
|
|
pub signature_email: Option<String>,
|
|
#[serde(rename = "XAttr", default)]
|
|
pub extended_attributes: Option<String>,
|
|
#[serde(rename = "Thumbnails", default)]
|
|
pub thumbnails: Vec<ThumbnailDto>,
|
|
}
|