mirror of
https://github.com/narrrl/proton-sdk-rs.git
synced 2026-11-03 16:41:26 +01:00
implemented missing drive endpoints
This commit is contained in:
1 parent
4c3058c618
commit
8eb7b8f56f
6 files changed
+243
-9
No files matched your search
@@ -15,8 +15,8 @@ use proton_sdk::crypto::PrivateKey;
|
|||||||
use proton_sdk::error::{ProtonError, Result};
|
use proton_sdk::error::{ProtonError, Result};
|
||||||
use proton_sdk::http::ApiHttpClient;
|
use proton_sdk::http::ApiHttpClient;
|
||||||
use proton_sdk::crypto::{
|
use proton_sdk::crypto::{
|
||||||
generate_node_hash_key, generate_node_key, verify_detached, ContentKey, VerificationKeyRing,
|
build_volume_creation_material, generate_node_hash_key, generate_node_key, verify_detached,
|
||||||
VerificationStatus,
|
ContentKey, VerificationKeyRing, VerificationStatus,
|
||||||
};
|
};
|
||||||
use proton_sdk::ids::{AddressId, DriveEventId, LinkId, NodeUid, ShareId, VolumeId};
|
use proton_sdk::ids::{AddressId, DriveEventId, LinkId, NodeUid, ShareId, VolumeId};
|
||||||
use proton_sdk::session::ProtonApiSession;
|
use proton_sdk::session::ProtonApiSession;
|
||||||
@@ -40,7 +40,7 @@ use crate::dtos::{
|
|||||||
RevisionCreationResponse, RevisionDto, RevisionResponse, RevisionUpdateRequest,
|
RevisionCreationResponse, RevisionDto, RevisionResponse, RevisionUpdateRequest,
|
||||||
LatestVolumeEventResponse, PhotosAttributesDto, ThumbnailBlockListRequest,
|
LatestVolumeEventResponse, PhotosAttributesDto, ThumbnailBlockListRequest,
|
||||||
ThumbnailBlockListResponse, ThumbnailCreationRequest, ThumbnailDto, TimelinePhotoListResponse,
|
ThumbnailBlockListResponse, ThumbnailCreationRequest, ThumbnailDto, TimelinePhotoListResponse,
|
||||||
VolumeEventDto, VolumeEventListResponse, VolumeTrashResponse,
|
VolumeCreationRequest, VolumeEventDto, VolumeEventListResponse, VolumeTrashResponse,
|
||||||
};
|
};
|
||||||
use crate::photos::{PhotoUploadMetadata, PhotosTimelineItem};
|
use crate::photos::{PhotoUploadMetadata, PhotosTimelineItem};
|
||||||
use crate::cache::DriveEntityCache;
|
use crate::cache::DriveEntityCache;
|
||||||
@@ -165,7 +165,7 @@ impl ProtonDriveClient {
|
|||||||
entity_repository: Arc<dyn CacheRepository>,
|
entity_repository: Arc<dyn CacheRepository>,
|
||||||
) -> Self {
|
) -> Self {
|
||||||
Self {
|
Self {
|
||||||
http: session.http().clone(),
|
http: session.http().with_base_route("drive/"),
|
||||||
account: AccountClient::new(session, mailbox_password),
|
account: AccountClient::new(session, mailbox_password),
|
||||||
cache: Arc::new(Mutex::new(DriveCache::default())),
|
cache: Arc::new(Mutex::new(DriveCache::default())),
|
||||||
entities: DriveEntityCache::new(entity_repository),
|
entities: DriveEntityCache::new(entity_repository),
|
||||||
@@ -1541,7 +1541,17 @@ impl ProtonDriveClient {
|
|||||||
return Ok(());
|
return Ok(());
|
||||||
}
|
}
|
||||||
|
|
||||||
let response: MyFilesShareResponse = self.http.get("v2/shares/my-files").await?;
|
// Mirrors C# `NodeOperations.GetOrCreateMyFilesFolderAsync`: a brand-new
|
||||||
|
// account has no My Files volume yet, so the share lookup fails. Create
|
||||||
|
// the volume, then re-read it through the normal path to populate caches.
|
||||||
|
let response: MyFilesShareResponse = match self.http.get("v2/shares/my-files").await {
|
||||||
|
Ok(response) => response,
|
||||||
|
Err(e) if is_my_files_missing(&e) => {
|
||||||
|
self.create_volume().await?;
|
||||||
|
self.http.get("v2/shares/my-files").await?
|
||||||
|
}
|
||||||
|
Err(e) => return Err(e),
|
||||||
|
};
|
||||||
let volume_id = response.volume.id.clone();
|
let volume_id = response.volume.id.clone();
|
||||||
let share_id = response.share.id.clone();
|
let share_id = response.share.id.clone();
|
||||||
|
|
||||||
@@ -1566,6 +1576,42 @@ impl ProtonDriveClient {
|
|||||||
Ok(())
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Create the account's main volume (root share + root folder).
|
||||||
|
///
|
||||||
|
/// Mirrors C# `VolumeOperations.CreateVolumeAsync`: build the root share and
|
||||||
|
/// folder crypto material against the default address's primary key and
|
||||||
|
/// `POST volumes`. The server-side state is then read back by the caller
|
||||||
|
/// ([`ensure_my_files`](Self::ensure_my_files)) via the normal share lookup,
|
||||||
|
/// so no local cache priming is needed here.
|
||||||
|
async fn create_volume(&self) -> Result<()> {
|
||||||
|
let mut timer = self.telemetry.start("create_volume");
|
||||||
|
|
||||||
|
let address = self.account.default_address().await?;
|
||||||
|
let address_keys = self.account.address_private_keys(&address.id).await?;
|
||||||
|
let address_key = address_keys
|
||||||
|
.get(address.primary_key_index)
|
||||||
|
.ok_or_else(|| ProtonError::invalid_operation("default address has no primary key"))?;
|
||||||
|
|
||||||
|
let material = build_volume_creation_material(address_key, "root")?;
|
||||||
|
|
||||||
|
let request = VolumeCreationRequest {
|
||||||
|
address_id: address.id.clone(),
|
||||||
|
address_key_id: address.primary_key_id.clone(),
|
||||||
|
share_key: material.share_key_armored,
|
||||||
|
share_passphrase: material.share_passphrase,
|
||||||
|
share_passphrase_signature: material.share_passphrase_signature,
|
||||||
|
folder_name: material.folder_name,
|
||||||
|
folder_key: material.folder_key_armored,
|
||||||
|
folder_passphrase: material.folder_passphrase,
|
||||||
|
folder_passphrase_signature: material.folder_passphrase_signature,
|
||||||
|
folder_hash_key: material.folder_hash_key,
|
||||||
|
};
|
||||||
|
|
||||||
|
let _: proton_sdk::api::ApiResponse = self.http.post("volumes", &request).await?;
|
||||||
|
timer.success();
|
||||||
|
Ok(())
|
||||||
|
}
|
||||||
|
|
||||||
async fn root_share_key(&self) -> Result<PrivateKey> {
|
async fn root_share_key(&self) -> Result<PrivateKey> {
|
||||||
self.ensure_my_files().await?;
|
self.ensure_my_files().await?;
|
||||||
Ok(self
|
Ok(self
|
||||||
@@ -2209,6 +2255,18 @@ fn read_full_block<R: Read>(reader: &mut R, buf: &mut [u8]) -> Result<usize> {
|
|||||||
|
|
||||||
/// Group node uids by volume, preserving order, so each volume's links are
|
/// Group node uids by volume, preserving order, so each volume's links are
|
||||||
/// batched into a single request family (C# groups by `VolumeId`).
|
/// batched into a single request family (C# groups by `VolumeId`).
|
||||||
|
/// Whether a `v2/shares/my-files` error means the account has no My Files volume
|
||||||
|
/// yet (and so one must be created). C# catches [`ResponseCode::DoesNotExist`];
|
||||||
|
/// in practice a fresh account's lookup also surfaces as a bare HTTP 404, so
|
||||||
|
/// both are treated as "missing".
|
||||||
|
fn is_my_files_missing(error: &ProtonError) -> bool {
|
||||||
|
matches!(
|
||||||
|
error,
|
||||||
|
ProtonError::Api(e)
|
||||||
|
if e.code == proton_sdk::api::ResponseCode::DoesNotExist || e.http_status == 404
|
||||||
|
)
|
||||||
|
}
|
||||||
|
|
||||||
fn group_by_volume(uids: &[NodeUid]) -> Vec<(VolumeId, Vec<LinkId>)> {
|
fn group_by_volume(uids: &[NodeUid]) -> Vec<(VolumeId, Vec<LinkId>)> {
|
||||||
let mut groups: Vec<(VolumeId, Vec<LinkId>)> = Vec::new();
|
let mut groups: Vec<(VolumeId, Vec<LinkId>)> = Vec::new();
|
||||||
for uid in uids {
|
for uid in uids {
|
||||||
|
|||||||
@@ -6,7 +6,7 @@
|
|||||||
|
|
||||||
use serde::{Deserialize, Serialize};
|
use serde::{Deserialize, Serialize};
|
||||||
|
|
||||||
use proton_sdk::ids::{AddressId, DriveEventId, LinkId, ShareId, VolumeId};
|
use proton_sdk::ids::{AddressId, AddressKeyId, DriveEventId, LinkId, ShareId, VolumeId};
|
||||||
|
|
||||||
/// `GET v2/shares/my-files`
|
/// `GET v2/shares/my-files`
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
@@ -429,6 +429,33 @@ pub struct FolderCreationRequest {
|
|||||||
pub signature_email: String,
|
pub signature_email: 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.
|
/// `POST v2/volumes/{vid}/folders` response.
|
||||||
#[derive(Debug, Deserialize)]
|
#[derive(Debug, Deserialize)]
|
||||||
pub struct FolderCreationResponse {
|
pub struct FolderCreationResponse {
|
||||||
|
|||||||
@@ -20,7 +20,7 @@ use tokio::sync::Mutex;
|
|||||||
use crate::crypto::{self, PrivateKey, PublicKey};
|
use crate::crypto::{self, PrivateKey, PublicKey};
|
||||||
use crate::error::{ProtonError, Result};
|
use crate::error::{ProtonError, Result};
|
||||||
use crate::http::ApiHttpClient;
|
use crate::http::ApiHttpClient;
|
||||||
use crate::ids::AddressId;
|
use crate::ids::{AddressId, AddressKeyId};
|
||||||
use crate::session::ProtonApiSession;
|
use crate::session::ProtonApiSession;
|
||||||
|
|
||||||
use dtos::{
|
use dtos::{
|
||||||
@@ -37,6 +37,8 @@ pub struct Address {
|
|||||||
pub status: i32,
|
pub status: i32,
|
||||||
/// Index, within the decrypted key list, of the primary key.
|
/// Index, within the decrypted key list, of the primary key.
|
||||||
pub primary_key_index: usize,
|
pub primary_key_index: usize,
|
||||||
|
/// Id of the address's primary key (the `AddressKeyID` for write requests).
|
||||||
|
pub primary_key_id: AddressKeyId,
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Resolves account keys needed to decrypt Drive metadata.
|
/// Resolves account keys needed to decrypt Drive metadata.
|
||||||
@@ -263,6 +265,7 @@ impl AccountClient {
|
|||||||
|
|
||||||
let mut keys = Vec::new();
|
let mut keys = Vec::new();
|
||||||
let mut primary_key_index = None;
|
let mut primary_key_index = None;
|
||||||
|
let mut primary_key_id = None;
|
||||||
|
|
||||||
for key_dto in &dto.keys {
|
for key_dto in &dto.keys {
|
||||||
if !key_dto.is_active() {
|
if !key_dto.is_active() {
|
||||||
@@ -295,6 +298,7 @@ impl AccountClient {
|
|||||||
Ok(key) => {
|
Ok(key) => {
|
||||||
if key_dto.is_primary() && primary_key_index.is_none() {
|
if key_dto.is_primary() && primary_key_index.is_none() {
|
||||||
primary_key_index = Some(keys.len());
|
primary_key_index = Some(keys.len());
|
||||||
|
primary_key_id = Some(key_dto.id.clone());
|
||||||
}
|
}
|
||||||
keys.push(key);
|
keys.push(key);
|
||||||
}
|
}
|
||||||
@@ -306,6 +310,8 @@ impl AccountClient {
|
|||||||
|
|
||||||
let primary_key_index = primary_key_index
|
let primary_key_index = primary_key_index
|
||||||
.ok_or_else(|| ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)))?;
|
.ok_or_else(|| ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)))?;
|
||||||
|
let primary_key_id = primary_key_id
|
||||||
|
.ok_or_else(|| ProtonError::invalid_operation(format!("address {} has no primary key", dto.id)))?;
|
||||||
|
|
||||||
let address = Address {
|
let address = Address {
|
||||||
id: dto.id.clone(),
|
id: dto.id.clone(),
|
||||||
@@ -313,6 +319,7 @@ impl AccountClient {
|
|||||||
order: dto.order,
|
order: dto.order,
|
||||||
status: dto.status,
|
status: dto.status,
|
||||||
primary_key_index,
|
primary_key_index,
|
||||||
|
primary_key_id,
|
||||||
};
|
};
|
||||||
|
|
||||||
Ok((address, keys))
|
Ok((address, keys))
|
||||||
|
|||||||
@@ -16,7 +16,10 @@ mod verify;
|
|||||||
|
|
||||||
pub use content::ContentKey;
|
pub use content::ContentKey;
|
||||||
pub use derive::derive_key_passphrase;
|
pub use derive::derive_key_passphrase;
|
||||||
pub use encrypt::{generate_node_hash_key, generate_node_key, GeneratedNodeKey};
|
pub use encrypt::{
|
||||||
|
build_volume_creation_material, generate_node_hash_key, generate_node_key, GeneratedNodeKey,
|
||||||
|
VolumeCreationMaterial,
|
||||||
|
};
|
||||||
pub use errors::CryptoError;
|
pub use errors::CryptoError;
|
||||||
pub use keys::{decrypt_armored_with_keys, PrivateKey};
|
pub use keys::{decrypt_armored_with_keys, PrivateKey};
|
||||||
pub use srp::{generate_proofs, SrpProofs, DEFAULT_BIT_LENGTH};
|
pub use srp::{generate_proofs, SrpProofs, DEFAULT_BIT_LENGTH};
|
||||||
|
|||||||
@@ -146,6 +146,76 @@ pub fn generate_node_hash_key(node_key: &PrivateKey) -> Result<String, CryptoErr
|
|||||||
encrypt_and_sign(node_key.key(), Some(node_key), &bytes, false, false)
|
encrypt_and_sign(node_key.key(), Some(node_key), &bytes, false, false)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// The encrypted/locked material for a new volume's root share + root folder.
|
||||||
|
///
|
||||||
|
/// Mirrors the output of C# `VolumeOperations.GetCreationRequest`: a fresh root
|
||||||
|
/// share key and root folder key (each Ed25519 + X25519, locked with a random
|
||||||
|
/// passphrase), plus the passphrases/name/hash-key encrypted into the request
|
||||||
|
/// shape the `volumes` endpoint expects.
|
||||||
|
pub struct VolumeCreationMaterial {
|
||||||
|
/// Locked root share key (armored secret key) — `ShareKey`.
|
||||||
|
pub share_key_armored: String,
|
||||||
|
/// Share passphrase encrypted (encrypt-only) to the address key — `SharePassphrase`.
|
||||||
|
pub share_passphrase: String,
|
||||||
|
/// Detached signature over the share passphrase by the address key.
|
||||||
|
pub share_passphrase_signature: String,
|
||||||
|
/// Root folder name encrypted + inline-signed to the share key — `FolderName`.
|
||||||
|
pub folder_name: String,
|
||||||
|
/// Locked root folder key (armored secret key) — `FolderKey`.
|
||||||
|
pub folder_key_armored: String,
|
||||||
|
/// Folder passphrase encrypted (encrypt-only) to the share key — `FolderPassphrase`.
|
||||||
|
pub folder_passphrase: String,
|
||||||
|
/// Detached signature over the folder passphrase by the address key.
|
||||||
|
pub folder_passphrase_signature: String,
|
||||||
|
/// Folder hash key encrypted + inline-signed to the folder key — `FolderHashKey`.
|
||||||
|
pub folder_hash_key: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
/// Build the crypto material for creating a volume's root share and folder.
|
||||||
|
///
|
||||||
|
/// Mirrors C# `VolumeOperations.GetCreationRequest`:
|
||||||
|
/// - root share key + folder key are generated and locked with random passphrases;
|
||||||
|
/// - the **share** passphrase is encrypted (encrypt-only) to `address_key` with a
|
||||||
|
/// detached signature by the address key (it is the address key that unwraps it);
|
||||||
|
/// - the **folder** passphrase is encrypted (encrypt-only) to the share key with a
|
||||||
|
/// detached signature by the address key;
|
||||||
|
/// - the folder **name** (`"root"`) is encrypted + inline-signed to the share key;
|
||||||
|
/// - the folder **hash key** (32 random bytes) is encrypted + inline-signed to the
|
||||||
|
/// folder key. All inline/detached signatures are made by `address_key`.
|
||||||
|
pub fn build_volume_creation_material(
|
||||||
|
address_key: &PrivateKey,
|
||||||
|
root_folder_name: &str,
|
||||||
|
) -> Result<VolumeCreationMaterial, CryptoError> {
|
||||||
|
let share = generate_node_key()?;
|
||||||
|
let folder = generate_node_key()?;
|
||||||
|
|
||||||
|
let share_passphrase = address_key.encrypt(&share.passphrase)?;
|
||||||
|
let share_passphrase_signature = address_key.sign_detached(&share.passphrase)?;
|
||||||
|
|
||||||
|
let folder_name = share
|
||||||
|
.key
|
||||||
|
.encrypt_and_sign(address_key, root_folder_name.as_bytes(), true, false)?;
|
||||||
|
let folder_passphrase = share.key.encrypt(&folder.passphrase)?;
|
||||||
|
let folder_passphrase_signature = address_key.sign_detached(&folder.passphrase)?;
|
||||||
|
|
||||||
|
let mut hash_key = [0u8; 32];
|
||||||
|
rand::thread_rng().fill_bytes(&mut hash_key);
|
||||||
|
let folder_hash_key = folder
|
||||||
|
.key
|
||||||
|
.encrypt_and_sign(address_key, &hash_key, false, false)?;
|
||||||
|
|
||||||
|
Ok(VolumeCreationMaterial {
|
||||||
|
share_key_armored: share.locked_armored,
|
||||||
|
share_passphrase,
|
||||||
|
share_passphrase_signature,
|
||||||
|
folder_name,
|
||||||
|
folder_key_armored: folder.locked_armored,
|
||||||
|
folder_passphrase,
|
||||||
|
folder_passphrase_signature,
|
||||||
|
folder_hash_key,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
/// Encrypt `data` to `recipient`, optionally inline-signing with `signer`, and
|
/// Encrypt `data` to `recipient`, optionally inline-signing with `signer`, and
|
||||||
/// return the armored PGP message. Mirrors C# `PgpEncrypter.EncryptAndSign`
|
/// return the armored PGP message. Mirrors C# `PgpEncrypter.EncryptAndSign`
|
||||||
/// (and the encrypt-only path when `signer` is `None`).
|
/// (and the encrypt-only path when `signer` is `None`).
|
||||||
@@ -260,6 +330,51 @@ mod tests {
|
|||||||
.expect("verify detached signature");
|
.expect("verify detached signature");
|
||||||
}
|
}
|
||||||
|
|
||||||
|
#[test]
|
||||||
|
fn volume_creation_material_round_trips() {
|
||||||
|
// Stand-in for the account's primary address key.
|
||||||
|
let address_key = generate_node_key().expect("generate address key");
|
||||||
|
|
||||||
|
let material =
|
||||||
|
build_volume_creation_material(&address_key.key, "root").expect("build material");
|
||||||
|
|
||||||
|
// The share passphrase is encrypted to the address key and unlocks the
|
||||||
|
// root share key; its detached signature verifies against the address key.
|
||||||
|
let share_pp = address_key
|
||||||
|
.key
|
||||||
|
.decrypt_armored_message(&material.share_passphrase)
|
||||||
|
.expect("decrypt share passphrase");
|
||||||
|
address_key
|
||||||
|
.key
|
||||||
|
.verify_detached_signature(&material.share_passphrase_signature, &share_pp)
|
||||||
|
.expect("verify share passphrase signature");
|
||||||
|
let share_key = PrivateKey::from_armored(&material.share_key_armored, &share_pp)
|
||||||
|
.expect("unlock share key");
|
||||||
|
|
||||||
|
// The folder passphrase is encrypted to the share key and unlocks the
|
||||||
|
// root folder key; its detached signature verifies against the address key.
|
||||||
|
let folder_pp = share_key
|
||||||
|
.decrypt_armored_message(&material.folder_passphrase)
|
||||||
|
.expect("decrypt folder passphrase");
|
||||||
|
address_key
|
||||||
|
.key
|
||||||
|
.verify_detached_signature(&material.folder_passphrase_signature, &folder_pp)
|
||||||
|
.expect("verify folder passphrase signature");
|
||||||
|
let folder_key = PrivateKey::from_armored(&material.folder_key_armored, &folder_pp)
|
||||||
|
.expect("unlock folder key");
|
||||||
|
|
||||||
|
// The folder name is encrypted to the share key; the hash key to the folder key.
|
||||||
|
let name = share_key
|
||||||
|
.decrypt_armored_message(&material.folder_name)
|
||||||
|
.expect("decrypt folder name");
|
||||||
|
assert_eq!(name, b"root");
|
||||||
|
|
||||||
|
let hash_key = folder_key
|
||||||
|
.decrypt_armored_message(&material.folder_hash_key)
|
||||||
|
.expect("decrypt folder hash key");
|
||||||
|
assert_eq!(hash_key.len(), 32);
|
||||||
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn node_hash_key_round_trips_under_node_key() {
|
fn node_hash_key_round_trips_under_node_key() {
|
||||||
// The folder hash key is 32 random bytes encrypted to the folder's own
|
// The folder hash key is 32 random bytes encrypted to the folder's own
|
||||||
|
|||||||
@@ -39,6 +39,12 @@ pub struct Tokens {
|
|||||||
#[derive(Clone)]
|
#[derive(Clone)]
|
||||||
pub struct ApiHttpClient {
|
pub struct ApiHttpClient {
|
||||||
inner: Arc<Inner>,
|
inner: Arc<Inner>,
|
||||||
|
/// Extra path segment prepended to every request path (after `base_url`,
|
||||||
|
/// before the per-call `path`). Mirrors C# `session.GetHttpClient(baseRoute)`
|
||||||
|
/// — the Drive client targets `…/drive/` while account/auth calls stay at the
|
||||||
|
/// root. Empty by default. Lives on the outer struct (not `Inner`) so clones
|
||||||
|
/// can carry different prefixes while sharing one token/telemetry store.
|
||||||
|
route_prefix: Arc<str>,
|
||||||
}
|
}
|
||||||
|
|
||||||
struct Inner {
|
struct Inner {
|
||||||
@@ -77,9 +83,22 @@ impl ApiHttpClient {
|
|||||||
tokens: Mutex::new(tokens),
|
tokens: Mutex::new(tokens),
|
||||||
telemetry: std::sync::Mutex::new(NoopTelemetry::shared()),
|
telemetry: std::sync::Mutex::new(NoopTelemetry::shared()),
|
||||||
}),
|
}),
|
||||||
|
route_prefix: Arc::from(""),
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Derive a clone that prepends `route` to every request path, sharing this
|
||||||
|
/// client's token store, telemetry sink and connection pool. Mirrors C#
|
||||||
|
/// `session.GetHttpClient(baseRoute)`: the Drive client passes `"drive/"` so
|
||||||
|
/// its routes resolve under `…/drive/` while auth/account calls (and token
|
||||||
|
/// refresh) stay at the root. `route` should end in `/`.
|
||||||
|
pub fn with_base_route(&self, route: impl Into<Arc<str>>) -> Self {
|
||||||
|
Self {
|
||||||
|
inner: Arc::clone(&self.inner),
|
||||||
|
route_prefix: route.into(),
|
||||||
|
}
|
||||||
|
}
|
||||||
|
|
||||||
/// Snapshot the current tokens (e.g. to persist for a later `resume`).
|
/// Snapshot the current tokens (e.g. to persist for a later `resume`).
|
||||||
pub async fn current_tokens(&self) -> Tokens {
|
pub async fn current_tokens(&self) -> Tokens {
|
||||||
self.inner.tokens.lock().await.clone()
|
self.inner.tokens.lock().await.clone()
|
||||||
@@ -274,7 +293,12 @@ impl ApiHttpClient {
|
|||||||
body: Option<&B>,
|
body: Option<&B>,
|
||||||
access_token: &str,
|
access_token: &str,
|
||||||
) -> Result<reqwest::Response> {
|
) -> Result<reqwest::Response> {
|
||||||
let url = format!("{}{}", self.inner.base_url, path.trim_start_matches('/'));
|
let url = format!(
|
||||||
|
"{}{}{}",
|
||||||
|
self.inner.base_url,
|
||||||
|
self.route_prefix,
|
||||||
|
path.trim_start_matches('/')
|
||||||
|
);
|
||||||
send_retrying(&self.inner.config.retry_policy, || {
|
send_retrying(&self.inner.config.retry_policy, || {
|
||||||
let mut request = self
|
let mut request = self
|
||||||
.inner
|
.inner
|
||||||
|
|||||||
Reference in new issue
Block a user