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,
|
||||
FileCreationRequest, FileCreationResponse, FindPhotoDuplicatesRequest,
|
||||
FindPhotoDuplicatesResponse, FolderChildrenResponse, FolderCreationRequest,
|
||||
FolderCreationResponse, InvitationDetailsResponse, InvitationsListResponse,
|
||||
FolderCreationResponse, FolderSizeResponse, InvitationDetailsResponse, InvitationsListResponse,
|
||||
InviteEmailDetailsDto, InviteExternalUserRequest, InviteExternalUserResponse,
|
||||
InviteProtonUserInvitationDto, InviteProtonUserRequest, InviteProtonUserResponse,
|
||||
LatestVolumeEventResponse, LinkDetailsDto, LinkDetailsRequest, LinkDetailsResponse, LinkDto,
|
||||
@@ -80,7 +80,7 @@ use crate::dtos::{
|
||||
};
|
||||
use crate::events::{DriveEvent, DriveEventScopeId};
|
||||
use crate::node::{
|
||||
AlbumProperties, AvailableNameTarget, FileThumbnail, Node, NodeKind, NodeMoveItem,
|
||||
AlbumProperties, AvailableNameTarget, FileThumbnail, FolderSize, Node, NodeKind, NodeMoveItem,
|
||||
PhotoLocation, PhotoProperties, RecentlyAccessedReportItem, RevisionState, Thumbnail,
|
||||
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.
|
||||
///
|
||||
/// Mirrors C# `VolumeOperations.EnumerateTrashAsync` (renamed to
|
||||
|
||||
@@ -1759,10 +1759,31 @@ pub struct DeviceUpdateShareDto {
|
||||
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)]
|
||||
mod tests {
|
||||
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]
|
||||
fn recently_accessed_request_uses_the_wire_names() {
|
||||
let request = ReportRecentlyAccessedRequest {
|
||||
|
||||
@@ -56,7 +56,7 @@ pub use event_manager::{
|
||||
};
|
||||
pub use events::{DriveEvent, DriveEventScopeId};
|
||||
pub use node::{
|
||||
AlbumProperties, AvailableNameTarget, FileThumbnail, Node, NodeKind, NodeMoveItem,
|
||||
AlbumProperties, AvailableNameTarget, FileThumbnail, FolderSize, Node, NodeKind, NodeMoveItem,
|
||||
PhotoLocation, PhotoProperties, RecentlyAccessedReportItem, RevisionState, Thumbnail,
|
||||
ThumbnailType,
|
||||
};
|
||||
|
||||
@@ -42,6 +42,20 @@ pub struct RecentlyAccessedReportItem {
|
||||
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).
|
||||
#[derive(Debug, Clone, Serialize, Deserialize)]
|
||||
pub struct Node {
|
||||
|
||||
@@ -511,3 +511,62 @@ async fn folder_path_helpers_round_trip() {
|
||||
.uid;
|
||||
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