perf(transfer): parallelize folder traversal API calls

Use a shared API request limiter across recursive upload and
download traversal so folder detail fetches, file listings,
folder auth, and transfers can run concurrently under one
budget.

Refactor the traversal loops into task-driven pipelines while
preserving skip-if-exists, excludes, cleanup, and current
output behavior.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-05-07 15:49:02 +09:00
co-authored by Copilot
parent 4e73766732
commit 3f2ca938bd
7 changed files with 771 additions and 153 deletions
+85 -2
View File
@@ -1,6 +1,6 @@
use crate::connection::MDRSConnection;
use crate::connection::{ApiRequestLimiter, MDRSConnection};
pub use crate::models::folder::{FolderDetail, FolderSimple};
use anyhow::bail;
use anyhow::{anyhow, bail};
impl MDRSConnection {
/// List folders matching the given path under a laboratory (GET v3/folders/?path=...&laboratory_id=...)
@@ -20,6 +20,25 @@ impl MDRSConnection {
Ok(resp.json::<Vec<FolderSimple>>().await?)
}
/// List folders by path while consuming the shared API concurrency budget.
pub async fn list_folders_by_path_limited(
&self,
lab_id: u32,
path: &str,
limiter: &ApiRequestLimiter,
) -> Result<Vec<FolderSimple>, anyhow::Error> {
let params = [
("laboratory_id", lab_id.to_string()),
("path", path.to_string()),
];
let _permit = limiter.acquire().await?;
let resp = self.get_with_query("v3/folders/", &params).await?;
if !resp.status().is_success() {
bail!("List folders failed: {}", resp.status());
}
Ok(resp.json::<Vec<FolderSimple>>().await?)
}
/// Retrieve full folder details including sub_folders (GET v3/folders/{id}/)
pub async fn retrieve_folder(&self, id: &str) -> Result<FolderDetail, anyhow::Error> {
let resp = self.get(&format!("v3/folders/{}/", id)).await?;
@@ -29,6 +48,20 @@ impl MDRSConnection {
Ok(resp.json::<FolderDetail>().await?)
}
/// Retrieve folder details while consuming the shared API concurrency budget.
pub async fn retrieve_folder_limited(
&self,
id: &str,
limiter: &ApiRequestLimiter,
) -> Result<FolderDetail, anyhow::Error> {
let _permit = limiter.acquire().await?;
let resp = self.get(&format!("v3/folders/{}/", id)).await?;
if !resp.status().is_success() {
bail!("Retrieve folder failed: {}", resp.status());
}
Ok(resp.json::<FolderDetail>().await?)
}
/// Create a new folder under `parent_id` (POST v3/folders/).
pub async fn create_folder(
&self,
@@ -44,6 +77,32 @@ impl MDRSConnection {
self.post_json("v3/folders/", &body).await
}
/// Create a new folder under `parent_id` and return its ID while consuming
/// the shared API concurrency budget.
pub async fn create_folder_id_limited(
&self,
parent_id: &str,
folder_name: &str,
limiter: &ApiRequestLimiter,
) -> Result<String, anyhow::Error> {
let body = serde_json::json!({
"name": folder_name,
"parent_id": parent_id,
"description": "",
"template_id": -1,
});
let _permit = limiter.acquire().await?;
let resp = self.post_json("v3/folders/", &body).await?;
if !resp.status().is_success() {
bail!("Failed to create remote folder: {}", folder_name);
}
let json: serde_json::Value = resp.json().await?;
json["id"]
.as_str()
.ok_or_else(|| anyhow!("No id in create_folder response for {}", folder_name))
.map(|s| s.to_string())
}
/// Authenticate against a password-locked folder (POST v3/folders/{id}/auth/).
/// Returns `Err` if the password is incorrect or the request fails.
pub async fn folder_auth(&self, folder_id: &str, password: &str) -> Result<(), anyhow::Error> {
@@ -61,4 +120,28 @@ impl MDRSConnection {
}
Ok(())
}
/// Authenticate against a locked folder while consuming the shared API
/// concurrency budget.
pub async fn folder_auth_limited(
&self,
folder_id: &str,
password: &str,
limiter: &ApiRequestLimiter,
) -> Result<(), anyhow::Error> {
let _permit = limiter.acquire().await?;
let resp = self
.post_json(
&format!("v3/folders/{}/auth/", folder_id),
&serde_json::json!({"password": password}),
)
.await?;
if resp.status() == reqwest::StatusCode::UNAUTHORIZED {
bail!("Password is incorrect.");
}
if !resp.status().is_success() {
bail!("Folder auth failed: {}", resp.status());
}
Ok(())
}
}