Files
mdrs-client-rust/src/api/files.rs
T
orrisroot d25ab69d13 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>
2026-04-20 17:25:14 +09:00

113 lines
3.8 KiB
Rust

use crate::connection::{ApiRequestLimiter, MDRSConnection};
pub use crate::models::file::File;
use anyhow::bail;
use unicode_normalization::UnicodeNormalization;
#[derive(serde::Deserialize)]
struct FileListResponse {
pub next: Option<String>,
pub results: Vec<File>,
}
impl MDRSConnection {
/// List all files in a folder, following pagination automatically.
pub async fn list_all_files(&self, folder_id: &str) -> Result<Vec<File>, anyhow::Error> {
let mut all_files = Vec::new();
let mut page: u32 = 1;
loop {
let params = [
("folder_id", folder_id.to_string()),
("page", page.to_string()),
];
let resp = self.get_with_query("v3/files/", &params).await?;
if !resp.status().is_success() {
anyhow::bail!("List files failed: {}", resp.status());
}
let list: FileListResponse = resp.json().await?;
let has_next = list.next.is_some();
all_files.extend(list.results);
if !has_next {
break;
}
page += 1;
}
Ok(all_files)
}
/// List all files in a folder while consuming the shared API concurrency budget.
pub async fn list_all_files_limited(
&self,
folder_id: &str,
limiter: &ApiRequestLimiter,
) -> Result<Vec<File>, anyhow::Error> {
let mut all_files = Vec::new();
let mut page: u32 = 1;
loop {
let params = [
("folder_id", folder_id.to_string()),
("page", page.to_string()),
];
let _permit = limiter.acquire().await?;
let resp = self.get_with_query("v3/files/", &params).await?;
if !resp.status().is_success() {
anyhow::bail!("List files failed: {}", resp.status());
}
let list: FileListResponse = resp.json().await?;
let has_next = list.next.is_some();
all_files.extend(list.results);
if !has_next {
break;
}
page += 1;
}
Ok(all_files)
}
/// Upload a local file into the given remote folder while consuming the
/// shared API concurrency budget.
pub async fn upload_file_limited(
&self,
folder_id: &str,
file_path: &str,
limiter: &ApiRequestLimiter,
) -> Result<(), anyhow::Error> {
use anyhow::{anyhow, bail};
use reqwest::multipart;
let file_name: String = std::path::Path::new(file_path)
.file_name()
.ok_or_else(|| anyhow!("Invalid file path: `{}`", file_path))?
.to_string_lossy()
.nfc()
.collect();
let file_bytes = tokio::fs::read(file_path).await?;
let part = multipart::Part::bytes(file_bytes).file_name(file_name.clone());
let form = multipart::Form::new()
.text("folder_id", folder_id.to_string())
.part("file", part);
let _permit = limiter.acquire().await?;
let resp = self.post_multipart("v3/files/", form).await?;
if !resp.status().is_success() {
bail!("Upload failed: {}", resp.status());
}
Ok(())
}
/// Download a file while consuming the shared API concurrency budget.
pub async fn download_file_limited(
&self,
url: &str,
dest: &str,
limiter: &ApiRequestLimiter,
) -> Result<(), anyhow::Error> {
let _permit = limiter.acquire().await?;
let resp = self.get_url(url).await?;
if !resp.status().is_success() {
bail!("Download failed: {}", resp.status());
}
let bytes = resp.bytes().await?;
drop(_permit);
tokio::fs::write(dest, &bytes).await?;
Ok(())
}
}