feature (public link retrievel)

This commit is contained in:
nvrl committed 2026-07-15 20:37:00 +02:00
1 parent 64f977edb3
commit 2fa9b90265
2 files changed
+48 -19

No files matched your search

+43 -19
View File
@@ -54,10 +54,10 @@ use crate::dtos::{
NodeNameAvailabilityRequest, NodeNameAvailabilityResponse, PhotosAttributesDto, NodeNameAvailabilityRequest, NodeNameAvailabilityResponse, PhotosAttributesDto,
RenameLinkRequest, RevisionCreationRequest, RevisionCreationResponse, RevisionDto, RenameLinkRequest, RevisionCreationRequest, RevisionCreationResponse, RevisionDto,
RevisionResponse, RevisionUpdateRequest, ShareInvitationDto, ShareInvitationsResponse, RevisionResponse, RevisionUpdateRequest, ShareInvitationDto, ShareInvitationsResponse,
ShareMembersResponse, ShareResponse, ShareTargetType, ShareUrlsResponse, SharedWithMeResponse, ShareMembersResponse, ShareResponse, ShareTargetType, ShareUrlDto, ShareUrlsResponse,
ThumbnailBlockListRequest, ThumbnailBlockListResponse, ThumbnailCreationRequest, ThumbnailDto, SharedWithMeResponse, ThumbnailBlockListRequest, ThumbnailBlockListResponse,
TimelinePhotoListResponse, UpdatePermissionsRequest, VolumeCreationRequest, VolumeEventDto, ThumbnailCreationRequest, ThumbnailDto, TimelinePhotoListResponse, UpdatePermissionsRequest,
VolumeEventListResponse, VolumeTrashResponse, VolumeCreationRequest, VolumeEventDto, VolumeEventListResponse, VolumeTrashResponse,
}; };
use crate::events::{DriveEvent, DriveEventScopeId}; use crate::events::{DriveEvent, DriveEventScopeId};
use crate::node::{FileThumbnail, Node, NodeKind, RevisionState, Thumbnail, ThumbnailType}; use crate::node::{FileThumbnail, Node, NodeKind, RevisionState, Thumbnail, ThumbnailType};
@@ -1889,27 +1889,31 @@ impl ProtonDriveClient {
}) })
} }
/// The public link on a node, if one exists (metadata only — the secret URL /// The public link on a node, if one exists. The secret URL fragment is
/// fragment is not reconstructed). /// recovered by decrypting the stored link password with the owner's address
/// key and taking the generated (non-custom) portion — the same recovery the
/// web client does when re-displaying an existing link.
pub async fn get_public_link(&self, uid: &NodeUid) -> Result<Option<PublicLink>> { pub async fn get_public_link(&self, uid: &NodeUid) -> Result<Option<PublicLink>> {
let mut timer = self.telemetry.start("get_public_link"); let mut timer = self.telemetry.start("get_public_link");
let link = match self.node_share_id(uid).await? { let link = match self.node_share_id(uid).await? {
Some(share_id) => { Some(share_id) => {
let path = format!("shares/{share_id}/urls"); let path = format!("shares/{share_id}/urls");
let response: ShareUrlsResponse = self.http.get(&path).await?; let response: ShareUrlsResponse = self.http.get(&path).await?;
response match response.share_urls.into_iter().next() {
.share_urls Some(dto) => {
.into_iter() let url = self.recover_public_link_url(&dto).await;
.next() Some(PublicLink {
.map(|dto| PublicLink { share_id,
share_id, public_link_id: dto.share_url_id,
public_link_id: dto.share_url_id, url,
url: None, role: MemberRole::from_permissions(dto.permissions),
role: MemberRole::from_permissions(dto.permissions), creation_time: dto.create_time,
creation_time: dto.create_time, expiration_time: dto.expiration_time,
expiration_time: dto.expiration_time, has_custom_password: dto.flags == 3,
has_custom_password: dto.flags == 3, })
}) }
None => None,
}
} }
None => None, None => None,
}; };
@@ -1917,6 +1921,26 @@ impl ProtonDriveClient {
Ok(link) Ok(link)
} }
/// Rebuild the full public URL (`{public_url}#{generated}`) for an existing
/// link by decrypting its stored `Password` with the owner's address key. The
/// stored secret is the generated password with any custom password appended;
/// only the leading generated portion belongs in the URL fragment (the
/// recipient supplies the custom password separately). Returns `None` if the
/// link carries no password or it can't be decrypted, so the caller still gets
/// the link's metadata.
async fn recover_public_link_url(&self, dto: &ShareUrlDto) -> Option<String> {
if dto.public_url.is_empty() {
return None;
}
let enc = dto.password.as_deref()?;
let (_, email, _) = self.membership_address().await.ok()?;
let (address_keys, _) = self.own_address_keys(&email).await.ok()?;
let bytes = decrypt_armored_with_keys(enc, &address_keys).ok()?;
let full = String::from_utf8_lossy(&bytes);
let generated: String = full.chars().take(PUBLIC_LINK_PASSWORD_LEN).collect();
Some(format!("{}#{generated}", dto.public_url))
}
/// Remove a public link, revoking access via the URL /// Remove a public link, revoking access via the URL
/// (`DELETE shares/{sid}/urls/{urlID}`). /// (`DELETE shares/{sid}/urls/{urlID}`).
pub async fn remove_public_link(&self, link: &PublicLink) -> Result<()> { pub async fn remove_public_link(&self, link: &PublicLink) -> Result<()> {
+5
View File
@@ -1522,6 +1522,11 @@ pub struct ShareUrlDto {
pub share_id: Option<ShareId>, pub share_id: Option<ShareId>,
#[serde(rename = "PublicUrl", default)] #[serde(rename = "PublicUrl", default)]
pub public_url: String, 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)] #[serde(rename = "CreateTime", default)]
pub create_time: i64, pub create_time: i64,
#[serde(rename = "ExpirationTime", default)] #[serde(rename = "ExpirationTime", default)]