mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
feat(drive): report a folder's size from the server
ProtonDriveClient::get_folder_size returns the total size and number of
a folder's descendants without walking the tree, ported from the TS
SDK's getFolderSize (GET volumes/{vid}/folders/{lid}/calculate-
descendents-size). Trashed descendants and folders count, and the size
is encrypted bytes on storage; the live test pins both, which the TS
docs leave open.
This commit is contained in:
1 parent
ee16f4326e
commit
8611989364
5 files changed
+119
-3
No files matched your search
@@ -59,7 +59,7 @@ use crate::dtos::{
|
|||||||
ExternalInvitationResponseDto, ExternalInvitationsResponse, FileContentDigests,
|
ExternalInvitationResponseDto, ExternalInvitationsResponse, FileContentDigests,
|
||||||
FileCreationRequest, FileCreationResponse, FindPhotoDuplicatesRequest,
|
FileCreationRequest, FileCreationResponse, FindPhotoDuplicatesRequest,
|
||||||
FindPhotoDuplicatesResponse, FolderChildrenResponse, FolderCreationRequest,
|
FindPhotoDuplicatesResponse, FolderChildrenResponse, FolderCreationRequest,
|
||||||
FolderCreationResponse, InvitationDetailsResponse, InvitationsListResponse,
|
FolderCreationResponse, FolderSizeResponse, InvitationDetailsResponse, InvitationsListResponse,
|
||||||
InviteEmailDetailsDto, InviteExternalUserRequest, InviteExternalUserResponse,
|
InviteEmailDetailsDto, InviteExternalUserRequest, InviteExternalUserResponse,
|
||||||
InviteProtonUserInvitationDto, InviteProtonUserRequest, InviteProtonUserResponse,
|
InviteProtonUserInvitationDto, InviteProtonUserRequest, InviteProtonUserResponse,
|
||||||
LatestVolumeEventResponse, LinkDetailsDto, LinkDetailsRequest, LinkDetailsResponse, LinkDto,
|
LatestVolumeEventResponse, LinkDetailsDto, LinkDetailsRequest, LinkDetailsResponse, LinkDto,
|
||||||
@@ -80,7 +80,7 @@ use crate::dtos::{
|
|||||||
};
|
};
|
||||||
use crate::events::{DriveEvent, DriveEventScopeId};
|
use crate::events::{DriveEvent, DriveEventScopeId};
|
||||||
use crate::node::{
|
use crate::node::{
|
||||||
AlbumProperties, AvailableNameTarget, FileThumbnail, Node, NodeKind, NodeMoveItem,
|
AlbumProperties, AvailableNameTarget, FileThumbnail, FolderSize, Node, NodeKind, NodeMoveItem,
|
||||||
PhotoLocation, PhotoProperties, RecentlyAccessedReportItem, RevisionState, Thumbnail,
|
PhotoLocation, PhotoProperties, RecentlyAccessedReportItem, RevisionState, Thumbnail,
|
||||||
ThumbnailType,
|
ThumbnailType,
|
||||||
};
|
};
|
||||||
@@ -1137,6 +1137,28 @@ impl ProtonDriveClient {
|
|||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The total size and number of a folder's descendants, computed by the
|
||||||
|
/// server, so the caller does not walk the tree.
|
||||||
|
///
|
||||||
|
/// Mirrors TS `ProtonDriveClient.getFolderSize`
|
||||||
|
/// (`GET volumes/{vid}/folders/{lid}/calculate-descendents-size`). Trashed
|
||||||
|
/// descendants count too, so this is the space the folder holds rather
|
||||||
|
/// than what a listing shows. Folders count as descendants, and the size is
|
||||||
|
/// encrypted bytes on storage, a little above the plaintext.
|
||||||
|
pub async fn get_folder_size(&self, uid: &NodeUid) -> Result<FolderSize> {
|
||||||
|
let mut timer = self.telemetry.start("get_folder_size");
|
||||||
|
let path = format!(
|
||||||
|
"volumes/{}/folders/{}/calculate-descendents-size",
|
||||||
|
uid.volume_id, uid.link_id
|
||||||
|
);
|
||||||
|
let response: FolderSizeResponse = self.http.get(&path).await?;
|
||||||
|
timer.success();
|
||||||
|
Ok(FolderSize {
|
||||||
|
size: response.descendents_size,
|
||||||
|
descendants: response.descendents_count,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Enumerate the [`NodeUid`]s of the main volume's trashed nodes.
|
/// Enumerate the [`NodeUid`]s of the main volume's trashed nodes.
|
||||||
///
|
///
|
||||||
/// Mirrors C# `VolumeOperations.EnumerateTrashAsync` (renamed to
|
/// Mirrors C# `VolumeOperations.EnumerateTrashAsync` (renamed to
|
||||||
|
|||||||
@@ -1759,10 +1759,31 @@ pub struct DeviceUpdateShareDto {
|
|||||||
pub name: String,
|
pub name: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `GET volumes/{vid}/folders/{lid}/calculate-descendents-size` response (TS
|
||||||
|
/// `FolderSizeResponseDto`). The server spells it "descendents".
|
||||||
|
#[derive(Debug, Deserialize)]
|
||||||
|
pub struct FolderSizeResponse {
|
||||||
|
#[serde(rename = "DescendentsSize")]
|
||||||
|
pub descendents_size: u64,
|
||||||
|
#[serde(rename = "DescendentsCount")]
|
||||||
|
pub descendents_count: u64,
|
||||||
|
}
|
||||||
|
|
||||||
#[cfg(test)]
|
#[cfg(test)]
|
||||||
mod tests {
|
mod tests {
|
||||||
use super::*;
|
use super::*;
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn folder_size_response_reads_the_wire_names() {
|
||||||
|
let raw = r#"{
|
||||||
|
"Code": 1000, "VolumeID": "vol", "LinkID": "folder",
|
||||||
|
"DescendentsSize": 5368709120, "DescendentsCount": 42
|
||||||
|
}"#;
|
||||||
|
let response: FolderSizeResponse = serde_json::from_str(raw).unwrap();
|
||||||
|
assert_eq!(response.descendents_size, 5_368_709_120);
|
||||||
|
assert_eq!(response.descendents_count, 42);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn recently_accessed_request_uses_the_wire_names() {
|
fn recently_accessed_request_uses_the_wire_names() {
|
||||||
let request = ReportRecentlyAccessedRequest {
|
let request = ReportRecentlyAccessedRequest {
|
||||||
|
|||||||
@@ -56,7 +56,7 @@ pub use event_manager::{
|
|||||||
};
|
};
|
||||||
pub use events::{DriveEvent, DriveEventScopeId};
|
pub use events::{DriveEvent, DriveEventScopeId};
|
||||||
pub use node::{
|
pub use node::{
|
||||||
AlbumProperties, AvailableNameTarget, FileThumbnail, Node, NodeKind, NodeMoveItem,
|
AlbumProperties, AvailableNameTarget, FileThumbnail, FolderSize, Node, NodeKind, NodeMoveItem,
|
||||||
PhotoLocation, PhotoProperties, RecentlyAccessedReportItem, RevisionState, Thumbnail,
|
PhotoLocation, PhotoProperties, RecentlyAccessedReportItem, RevisionState, Thumbnail,
|
||||||
ThumbnailType,
|
ThumbnailType,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -42,6 +42,20 @@ pub struct RecentlyAccessedReportItem {
|
|||||||
pub access_time: Option<i64>,
|
pub access_time: Option<i64>,
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// What [`get_folder_size`](crate::ProtonDriveClient::get_folder_size) reports
|
||||||
|
/// about a folder.
|
||||||
|
///
|
||||||
|
/// Mirrors TS `FolderSizeInfo`. Both numbers count every descendant the user
|
||||||
|
/// can see, in the trash as well as active.
|
||||||
|
#[derive(Debug, Clone, Copy, PartialEq, Eq)]
|
||||||
|
pub struct FolderSize {
|
||||||
|
/// Sum of the descendants' sizes in encrypted bytes on storage, which is a
|
||||||
|
/// little more than the plaintext.
|
||||||
|
pub size: u64,
|
||||||
|
/// Number of descendants, files and folders.
|
||||||
|
pub descendants: u64,
|
||||||
|
}
|
||||||
|
|
||||||
/// 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 {
|
||||||
|
|||||||
@@ -511,3 +511,62 @@ async fn folder_path_helpers_round_trip() {
|
|||||||
.uid;
|
.uid;
|
||||||
cleanup(client, &[top_uid]).await;
|
cleanup(client, &[top_uid]).await;
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// `get_folder_size` counts every descendant, nested folders included, and
|
||||||
|
/// sums their encrypted sizes.
|
||||||
|
///
|
||||||
|
/// The TS docs leave both points open; the first live run answered them with
|
||||||
|
/// `size: 301112, descendants: 3` — two single-block files, 56 bytes of
|
||||||
|
/// encryption overhead each.
|
||||||
|
#[tokio::test]
|
||||||
|
#[ignore = "live: needs test-account credentials"]
|
||||||
|
async fn folder_size_counts_nested_descendants() {
|
||||||
|
let Some(live) = common::live_client().await else {
|
||||||
|
return;
|
||||||
|
};
|
||||||
|
let client = &live.client;
|
||||||
|
let root = client
|
||||||
|
.get_my_files_folder()
|
||||||
|
.await
|
||||||
|
.expect("get my-files root");
|
||||||
|
|
||||||
|
let top_name = format!("size-ops-{}", common::unique_suffix());
|
||||||
|
let top = client
|
||||||
|
.create_folder(&root.uid, &top_name, None)
|
||||||
|
.await
|
||||||
|
.expect("create top folder");
|
||||||
|
let nested = client
|
||||||
|
.create_folder(&top, "nested", None)
|
||||||
|
.await
|
||||||
|
.expect("create nested folder");
|
||||||
|
let small = vec![b'a'; 1000];
|
||||||
|
let large = vec![b'b'; 300_000];
|
||||||
|
client
|
||||||
|
.upload_file(&top, "small.bin", "application/octet-stream", &small)
|
||||||
|
.await
|
||||||
|
.expect("upload small file");
|
||||||
|
client
|
||||||
|
.upload_file(&nested, "large.bin", "application/octet-stream", &large)
|
||||||
|
.await
|
||||||
|
.expect("upload nested file");
|
||||||
|
|
||||||
|
let size = client.get_folder_size(&top).await.expect("get_folder_size");
|
||||||
|
eprintln!("[folder size] {size:?} for 1 folder + 2 files of 301000 plaintext bytes");
|
||||||
|
assert_eq!(
|
||||||
|
size.descendants, 3,
|
||||||
|
"two files and the nested folder: {size:?}"
|
||||||
|
);
|
||||||
|
let plaintext = (small.len() + large.len()) as u64;
|
||||||
|
assert!(
|
||||||
|
(plaintext..plaintext + 1024).contains(&size.size),
|
||||||
|
"encrypted size, a little above the plaintext: {size:?}"
|
||||||
|
);
|
||||||
|
|
||||||
|
let empty = client
|
||||||
|
.get_folder_size(&nested)
|
||||||
|
.await
|
||||||
|
.expect("get_folder_size(nested)");
|
||||||
|
assert_eq!(empty.descendants, 1, "nested holds one file: {empty:?}");
|
||||||
|
|
||||||
|
cleanup(client, &[top]).await;
|
||||||
|
}
|
||||||
Reference in new issue
Block a user