A review against the Python client, which shares this client's config and login cache, found the transfers holding whole files in memory and several commands answering differently from their Python counterparts. - Stream uploads and downloads instead of buffering the whole file, so memory no longer scales with file size times concurrency. Uploads still declare a Content-Length rather than going out chunked. - Write a download beside its destination and move it into place once complete, and refuse a destination that cannot be written, so a failed transfer leaves what was already there untouched. - Report the server's own error detail instead of the bare status. - List locked sub-folders in `ls --json --recursive` using the given password and skip only those that cannot be unlocked, instead of failing the whole listing. - Name each folder's laboratory in JSON output and sort its entries, matching the Python client's schema and order. - Ask the server who is logged in for `whoami`, rather than trusting the cached name. - Collapse repeated separators and refuse `..` in remote paths. - Fall back to the API when a laboratory is missing from the cache, so a newly added one no longer needs a fresh login. - Verify the login cache digest on read, as the Python client does. - Accept `-e` as the short form of `--exclude`. - Satisfy clippy and rustfmt across the crate.
153 lines
5.4 KiB
Rust
153 lines
5.4 KiB
Rust
use crate::connection::{ApiRequestLimiter, MDRSConnection};
|
|
use crate::error::response_error;
|
|
pub use crate::models::folder::{FolderDetail, FolderSimple};
|
|
use anyhow::{anyhow, bail};
|
|
|
|
impl MDRSConnection {
|
|
/// List folders matching the given path under a laboratory (GET v3/folders/?path=...&laboratory_id=...)
|
|
pub async fn list_folders_by_path(
|
|
&self,
|
|
lab_id: u32,
|
|
path: &str,
|
|
) -> Result<Vec<FolderSimple>, anyhow::Error> {
|
|
let params = [
|
|
("laboratory_id", lab_id.to_string()),
|
|
("path", path.to_string()),
|
|
];
|
|
let resp = self.get_with_query("v3/folders/", ¶ms).await?;
|
|
if !resp.status().is_success() {
|
|
return Err(response_error("List folders failed", resp).await);
|
|
}
|
|
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/", ¶ms).await?;
|
|
if !resp.status().is_success() {
|
|
return Err(response_error("List folders failed", resp).await);
|
|
}
|
|
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?;
|
|
if !resp.status().is_success() {
|
|
return Err(response_error("Retrieve folder failed", resp).await);
|
|
}
|
|
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() {
|
|
return Err(response_error("Retrieve folder failed", resp).await);
|
|
}
|
|
Ok(resp.json::<FolderDetail>().await?)
|
|
}
|
|
|
|
/// Create a new folder under `parent_id` (POST v3/folders/).
|
|
pub async fn create_folder(
|
|
&self,
|
|
parent_id: &str,
|
|
folder_name: &str,
|
|
) -> Result<reqwest::Response, anyhow::Error> {
|
|
let body = serde_json::json!({
|
|
"name": folder_name,
|
|
"parent_id": parent_id,
|
|
"description": "",
|
|
"template_id": -1,
|
|
});
|
|
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() {
|
|
return Err(response_error(
|
|
&format!("Failed to create remote folder `{}`", folder_name),
|
|
resp,
|
|
)
|
|
.await);
|
|
}
|
|
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> {
|
|
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() {
|
|
return Err(response_error("Folder auth failed", resp).await);
|
|
}
|
|
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() {
|
|
return Err(response_error("Folder auth failed", resp).await);
|
|
}
|
|
Ok(())
|
|
}
|
|
}
|