chore(rust): update lockfile and format sources

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-20 15:59:28 +09:00
parent 723017a11c
commit d05bd8a08d
25 changed files with 390 additions and 177 deletions
+4 -16
View File
@@ -10,10 +10,7 @@ struct FileListResponse {
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> {
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 {
@@ -36,13 +33,9 @@ impl MDRSConnection {
}
/// Upload a local file into the given remote folder.
pub async fn upload_file(
&self,
folder_id: &str,
file_path: &str,
) -> Result<(), anyhow::Error> {
pub async fn upload_file(&self, folder_id: &str, file_path: &str) -> Result<(), anyhow::Error> {
use anyhow::{anyhow, bail};
use reqwest::multipart;
use anyhow::{anyhow, bail};
let file_name: String = std::path::Path::new(file_path)
.file_name()
.ok_or_else(|| anyhow!("Invalid file path: `{}`", file_path))?
@@ -62,11 +55,7 @@ use anyhow::{anyhow, bail};
}
/// Download a file from `url` and write it to `dest`.
pub async fn download_file(
&self,
url: &str,
dest: &str,
) -> Result<(), anyhow::Error> {
pub async fn download_file(&self, url: &str, dest: &str) -> Result<(), anyhow::Error> {
let resp = self
.client
.get(url)
@@ -78,4 +67,3 @@ use anyhow::{anyhow, bail};
Ok(())
}
}
+2 -7
View File
@@ -1,6 +1,6 @@
use crate::connection::MDRSConnection;
use anyhow::{bail};
pub use crate::models::folder::{FolderDetail, FolderSimple};
use anyhow::bail;
impl MDRSConnection {
/// List folders matching the given path under a laboratory (GET v3/folders/?path=...&laboratory_id=...)
@@ -50,11 +50,7 @@ impl MDRSConnection {
/// 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> {
pub async fn folder_auth(&self, folder_id: &str, password: &str) -> Result<(), anyhow::Error> {
let resp = self
.client
.post(self.build_url(&format!("v3/folders/{}/auth/", folder_id)))
@@ -71,4 +67,3 @@ impl MDRSConnection {
Ok(())
}
}
+8 -7
View File
@@ -13,13 +13,14 @@ impl MDRSConnection {
let resp = self.get("v3/laboratories/").await?;
// The API may return a paginated object or a direct array
let text = resp.text().await?;
let items: Vec<Laboratory> = if let Ok(list) = serde_json::from_str::<Vec<Laboratory>>(&text) {
list
} else if let Ok(paged) = serde_json::from_str::<LabListResponse>(&text) {
paged.results.unwrap_or_default()
} else {
vec![]
};
let items: Vec<Laboratory> =
if let Ok(list) = serde_json::from_str::<Vec<Laboratory>>(&text) {
list
} else if let Ok(paged) = serde_json::from_str::<LabListResponse>(&text) {
paged.results.unwrap_or_default()
} else {
vec![]
};
Ok(Laboratories { items })
}
}
+2 -5
View File
@@ -1,7 +1,7 @@
use crate::connection::MDRSConnection;
use crate::models::user::User as ModelUser;
use anyhow::bail;
use serde::Deserialize;
use anyhow::{bail};
/// Full API response shape from GET v3/users/current/
#[derive(Debug, Deserialize)]
@@ -38,10 +38,7 @@ impl MDRSConnection {
/// Refresh the access token using the refresh token.
/// POST v3/users/token/refresh/ {refresh: ...} -> {access: new_access}
pub async fn token_refresh(
&self,
refresh_token: &str,
) -> Result<String, anyhow::Error> {
pub async fn token_refresh(&self, refresh_token: &str) -> Result<String, anyhow::Error> {
let body = serde_json::json!({ "refresh": refresh_token });
let resp = self
.client
+21 -11
View File
@@ -4,8 +4,8 @@ pub mod types;
pub use digest::compute_digest;
pub use types::{Cache, CacheLaboratory, CacheLabsWrapper, CacheUser};
use anyhow::{anyhow, bail};
use crate::connection::MDRSConnection;
use anyhow::{anyhow, bail};
use std::collections::HashMap;
use std::fs;
use std::sync::{Arc, LazyLock, Mutex};
@@ -43,10 +43,21 @@ fn cache_file_path(remote: &str) -> std::path::PathBuf {
pub fn load_cache(remote: &str) -> Result<Cache, anyhow::Error> {
let cache_path = cache_file_path(remote);
if !cache_path.exists() {
bail!("Not logged in to `{}`. Run `mdrs login {}` first.", remote, remote);
bail!(
"Not logged in to `{}`. Run `mdrs login {}` first.",
remote,
remote
);
}
let data = fs::read_to_string(&cache_path)?;
serde_json::from_str::<Cache>(&data).map_err(|e| anyhow!("Cache for `{}` is invalid or outdated ({}). Run `mdrs login {}` to refresh it.", remote, e, remote))
serde_json::from_str::<Cache>(&data).map_err(|e| {
anyhow!(
"Cache for `{}` is invalid or outdated ({}). Run `mdrs login {}` to refresh it.",
remote,
e,
remote
)
})
}
// ---------------------------------------------------------------------------
@@ -61,9 +72,7 @@ pub fn load_cache(remote: &str) -> Result<Cache, anyhow::Error> {
/// - `flock(LOCK_EX)` on a dedicated `cache/{remote}.lock` file serializes
/// the entire read-check-refresh-write cycle across separate processes on
/// the same host.
pub async fn load_cache_with_token_refresh(
remote: &str,
) -> Result<Cache, anyhow::Error> {
pub async fn load_cache_with_token_refresh(remote: &str) -> Result<Cache, anyhow::Error> {
let lock = get_remote_lock(remote);
let _guard = lock.lock().await;
@@ -81,7 +90,11 @@ pub async fn load_cache_with_token_refresh(
let mut cache = load_cache(remote)?;
if crate::token::is_expired(&cache.token.refresh) {
bail!("Session for `{}` has expired. Please run `mdrs login {}` again.", remote, remote);
bail!(
"Session for `{}` has expired. Please run `mdrs login {}` again.",
remote,
remote
);
}
if crate::token::is_refresh_required(&cache.token.access, &cache.token.refresh) {
@@ -99,10 +112,7 @@ pub async fn load_cache_with_token_refresh(
/// Call the token-refresh endpoint and write the new access token back to the
/// cache file. The caller must already hold the per-remote async mutex.
async fn refresh_and_persist(
remote: &str,
cache: &Cache,
) -> Result<String, anyhow::Error> {
async fn refresh_and_persist(remote: &str, cache: &Cache) -> Result<String, anyhow::Error> {
let url = crate::commands::config::get_remote_url(remote)?
.ok_or_else(|| anyhow!("Remote `{}` is not configured.", remote))?;
let conn = MDRSConnection::new(&url);
+6 -5
View File
@@ -1,8 +1,6 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_folder, find_lab_in_cache, parse_remote_path,
};
use anyhow::{bail};
use crate::commands::shared::{find_folder, find_lab_in_cache, parse_remote_path};
use anyhow::bail;
pub async fn chacl(
remote_path: &str,
@@ -29,7 +27,10 @@ pub async fn chacl(
let folder = find_folder(&conn, lab.id, &folder_path, None).await?;
let mut data = serde_json::Map::new();
data.insert("access_level".to_string(), serde_json::json!(access_level_id));
data.insert(
"access_level".to_string(),
serde_json::json!(access_level_id),
);
if recursive {
data.insert("lower".to_string(), serde_json::json!(1));
}
+1 -1
View File
@@ -1,7 +1,7 @@
use anyhow::bail;
use configparser::ini::Ini;
use std::fs;
use std::path::PathBuf;
use anyhow::{bail};
fn config_path() -> PathBuf {
crate::settings::SETTINGS.config_dirname.join("config.ini")
+19 -11
View File
@@ -1,15 +1,11 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
find_subfolder_by_name, nfc, parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, find_subfolder_by_name, nfc,
parse_remote_path,
};
use anyhow::{bail};
use anyhow::bail;
pub async fn cp(
src_path: &str,
dest_path: &str,
recursive: bool,
) -> Result<(), anyhow::Error> {
pub async fn cp(src_path: &str, dest_path: &str, recursive: bool) -> Result<(), anyhow::Error> {
let (s_remote, s_lab, s_path) = parse_remote_path(src_path)?;
let dest_ends_with_slash = dest_path.ends_with('/');
let (d_remote, d_lab, d_path) = parse_remote_path(dest_path)?;
@@ -51,7 +47,11 @@ pub async fn cp(
bail!("File `{}` already exists.", d_basename);
}
if find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename).is_some() {
bail!("Cannot overwrite non-folder `{}` with folder `{}`.", d_basename, d_path);
bail!(
"Cannot overwrite non-folder `{}` with folder `{}`.",
d_basename,
d_path
);
}
// No-op if source and destination are identical
if s_parent_folder.id == d_parent_folder.id && d_basename == s_basename {
@@ -81,13 +81,21 @@ pub async fn cp(
}
let src_folder_id = src_folder.id.clone();
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
bail!("Cannot overwrite non-folder `{}` with folder `{}`.", d_basename, s_path);
bail!(
"Cannot overwrite non-folder `{}` with folder `{}`.",
d_basename,
s_path
);
}
if let Some(d_folder) = find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename) {
if d_folder.id == src_folder_id {
bail!("`{}` and `{}` are the same folder.", s_path, s_path);
}
bail!("Cannot move `{}` to `{}`: Folder not empty.", s_path, d_path);
bail!(
"Cannot move `{}` to `{}`: Folder not empty.",
s_path,
d_path
);
}
// No-op if source and destination are identical
if s_parent_folder.id == d_parent_folder.id && s_basename == d_basename {
+16 -15
View File
@@ -1,13 +1,12 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
find_subfolder_by_name, parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, find_subfolder_by_name, parse_remote_path,
};
use crate::connection::MDRSConnection;
use anyhow::{anyhow, bail};
use futures::stream::{FuturesUnordered, StreamExt};
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{anyhow, bail};
pub async fn download(
remote_path: &str,
@@ -93,8 +92,7 @@ pub async fn download(
let dir_files = conn.list_all_files(&folder_id).await?;
// Download files in this folder (up to 10 concurrent).
let mut futs: FuturesUnordered<tokio::task::JoinHandle<()>> =
FuturesUnordered::new();
let mut futs: FuturesUnordered<tokio::task::JoinHandle<()>> = FuturesUnordered::new();
for f in &dir_files {
if is_excluded(&excludes, &lab.name, &folder.path, Some(&f.name)) {
continue;
@@ -119,7 +117,10 @@ pub async fn download(
// (connection pool) while supplying a fresh Bearer token.
let task_conn = match load_cache_with_token_refresh(&remote).await {
Ok(c) => conn.with_token(c.token.access),
Err(e) => { eprintln!("Error: {}", e); return; }
Err(e) => {
eprintln!("Error: {}", e);
return;
}
};
let dest_str = dest_path.to_string_lossy().to_string();
match task_conn.download_file(&url, &dest_str).await {
@@ -163,18 +164,18 @@ pub async fn download(
/// Return true if the given lab/folder/file path matches any exclude pattern.
/// Constructs: `/{lab_name}{folder_path}{file_name}` lowercased, trailing slash stripped.
/// `folder_path` is expected to already start (and end) with "/".
fn is_excluded(excludes: &[String], lab_name: &str, folder_path: &str, file_name: Option<&str>) -> bool {
fn is_excluded(
excludes: &[String],
lab_name: &str,
folder_path: &str,
file_name: Option<&str>,
) -> bool {
if excludes.is_empty() {
return false;
}
let path = format!(
"/{}{}{}",
lab_name,
folder_path,
file_name.unwrap_or("")
)
.trim_end_matches('/')
.to_lowercase();
let path = format!("/{}{}{}", lab_name, folder_path, file_name.unwrap_or(""))
.trim_end_matches('/')
.to_lowercase();
excludes.iter().any(|e| e == &path)
}
+2 -3
View File
@@ -1,9 +1,8 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use anyhow::{anyhow};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, parse_remote_path,
};
use anyhow::anyhow;
pub async fn file_metadata(remote_path: &str, password: Option<&str>) -> Result<(), anyhow::Error> {
let (remote, labname, r_path) = parse_remote_path(remote_path)?;
+12 -5
View File
@@ -18,8 +18,12 @@ pub async fn labs(remote: &str) -> Result<(), anyhow::Error> {
println!(
"{:<w_name$} {:<w_pi$} {:<w_full$}",
header.0, header.1, header.2,
w_name = w_name, w_pi = w_pi, w_full = w_full,
header.0,
header.1,
header.2,
w_name = w_name,
w_pi = w_pi,
w_full = w_full,
);
let sep_len = w_name + 2 + w_pi + 2 + w_full;
println!("{}", "-".repeat(sep_len));
@@ -27,11 +31,14 @@ pub async fn labs(remote: &str) -> Result<(), anyhow::Error> {
for lab in &labs.items {
println!(
"{:<w_name$} {:<w_pi$} {:<w_full$}",
lab.name, lab.pi_name, lab.full_name,
w_name = w_name, w_pi = w_pi, w_full = w_full,
lab.name,
lab.pi_name,
lab.full_name,
w_name = w_name,
w_pi = w_pi,
w_full = w_full,
);
}
Ok(())
}
+2 -8
View File
@@ -1,8 +1,8 @@
use anyhow::{anyhow, bail};
use crate::cache::{CacheLabsWrapper, CacheUser, compute_digest};
use crate::connection::MDRSConnection;
use crate::models::laboratory::Laboratories;
use crate::models::user::User;
use anyhow::{anyhow, bail};
use reqwest::Client;
use serde::Deserialize;
use serde_json::{Value, json};
@@ -41,12 +41,7 @@ struct TokenResp {
refresh: String,
}
pub async fn login(
username: &str,
password: &str,
remote: &str,
) -> Result<(), anyhow::Error> {
pub async fn login(username: &str, password: &str, remote: &str) -> Result<(), anyhow::Error> {
// resolve remote label to URL from config
let url_opt = crate::commands::config::get_remote_url(remote)?;
let base_url = url_opt.ok_or_else(|| anyhow!("Remote host `{}` is not configured", remote))?;
@@ -141,4 +136,3 @@ pub async fn login(
println!("Login Successful");
Ok(())
}
+5 -11
View File
@@ -1,11 +1,9 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{find_folder, find_lab_in_cache, fmt_datetime, parse_remote_path};
use crate::connection::MDRSConnection;
use crate::models::file::File;
use crate::models::folder::{FolderDetail, FolderSimple};
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_folder, find_lab_in_cache, fmt_datetime, parse_remote_path,
};
use crate::connection::MDRSConnection;
use serde_json::{json, Value};
use serde_json::{Value, json};
use std::future::Future;
use std::pin::Pin;
@@ -214,11 +212,7 @@ fn file_to_json(f: &File, base_url: &str) -> Value {
let download_url = if f.download_url.starts_with("http") {
f.download_url.clone()
} else {
format!(
"{}{}",
base_url.trim_end_matches('/'),
f.download_url
)
format!("{}{}", base_url.trim_end_matches('/'), f.download_url)
};
json!({
"id": f.id,
+1 -3
View File
@@ -1,7 +1,5 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_folder, find_lab_in_cache, parse_remote_path,
};
use crate::commands::shared::{find_folder, find_lab_in_cache, parse_remote_path};
pub async fn metadata(remote_path: &str, password: Option<&str>) -> Result<(), anyhow::Error> {
let (remote, labname, folder_path) = parse_remote_path(remote_path)?;
+2 -2
View File
@@ -1,7 +1,7 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
find_subfolder_by_name, nfc, parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, find_subfolder_by_name, nfc,
parse_remote_path,
};
use anyhow::{anyhow, bail};
+1 -1
View File
@@ -14,8 +14,8 @@ pub mod metadata;
pub mod mkdir;
pub mod mv;
pub mod rm;
pub mod selfupdate;
pub mod shared;
pub mod upload;
pub mod version;
pub mod whoami;
pub mod selfupdate;
+18 -6
View File
@@ -1,9 +1,9 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
find_subfolder_by_name, nfc, parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, find_subfolder_by_name, nfc,
parse_remote_path,
};
use anyhow::{bail};
use anyhow::bail;
pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), anyhow::Error> {
let (s_remote, s_lab, s_path) = parse_remote_path(src_path)?;
@@ -47,7 +47,11 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), anyhow::Error> {
bail!("File `{}` already exists.", d_basename);
}
if find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename).is_some() {
bail!("Cannot overwrite non-folder `{}` with folder `{}`.", d_basename, d_path);
bail!(
"Cannot overwrite non-folder `{}` with folder `{}`.",
d_basename,
d_path
);
}
// No-op if source and destination are identical
if s_parent_folder.id == d_parent_folder.id && d_basename == s_basename {
@@ -74,13 +78,21 @@ pub async fn mv(src_path: &str, dest_path: &str) -> Result<(), anyhow::Error> {
};
let src_folder_id = src_folder.id.clone();
if find_file_by_name(&d_parent_files, &d_basename).is_some() {
bail!("Cannot overwrite non-folder `{}` with folder `{}`.", d_basename, s_path);
bail!(
"Cannot overwrite non-folder `{}` with folder `{}`.",
d_basename,
s_path
);
}
if let Some(d_folder) = find_subfolder_by_name(&d_parent_folder.sub_folders, &d_basename) {
if d_folder.id == src_folder_id {
bail!("`{}` and `{}` are the same folder.", s_path, s_path);
}
bail!("Cannot move `{}` to `{}`: Folder not empty.", s_path, d_path);
bail!(
"Cannot move `{}` to `{}`: Folder not empty.",
s_path,
d_path
);
}
// No-op if source and destination are identical
if s_parent_folder.id == d_parent_folder.id && s_basename == d_basename {
+1 -2
View File
@@ -1,7 +1,6 @@
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
find_subfolder_by_name, parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, find_subfolder_by_name, parse_remote_path,
};
use anyhow::{anyhow, bail};
+10 -10
View File
@@ -50,7 +50,11 @@ fn is_newer(current: &str, latest: &str) -> bool {
/// Extract the binary named `bin_name` from a `.tar.gz` archive at `archive_path`
/// and write it to `dest_path`.
fn extract_from_tar_gz(archive_path: &Path, bin_name: &str, dest_path: &Path) -> anyhow::Result<()> {
fn extract_from_tar_gz(
archive_path: &Path,
bin_name: &str,
dest_path: &Path,
) -> anyhow::Result<()> {
use flate2::read::GzDecoder;
use tar::Archive;
@@ -98,12 +102,12 @@ fn extract_from_zip(archive_path: &Path, bin_name: &str, dest_path: &Path) -> an
pub async fn selfupdate(yes: bool) -> anyhow::Result<()> {
let current_version = env!("CARGO_PKG_VERSION");
println!("Checking for updates (current version: {current_version}, target: {BUILD_TARGET})...");
let api_url = format!(
"{GITEA_HOST}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/releases?limit=1"
println!(
"Checking for updates (current version: {current_version}, target: {BUILD_TARGET})..."
);
let api_url = format!("{GITEA_HOST}/api/v1/repos/{REPO_OWNER}/{REPO_NAME}/releases?limit=1");
let client = reqwest::Client::new();
let mut req = client
.get(&api_url)
@@ -178,10 +182,7 @@ pub async fn selfupdate(yes: bool) -> anyhow::Result<()> {
let download_resp = download_req.send().await?;
if !download_resp.status().is_success() {
bail!(
"Failed to download asset: HTTP {}",
download_resp.status()
);
bail!("Failed to download asset: HTTP {}", download_resp.status());
}
let bytes = download_resp.bytes().await?;
@@ -215,4 +216,3 @@ pub async fn selfupdate(yes: bool) -> anyhow::Result<()> {
println!("Successfully updated to version {latest_version}.");
Ok(())
}
+11 -6
View File
@@ -2,17 +2,15 @@ use crate::cache::{Cache, CacheLaboratory};
use crate::connection::MDRSConnection;
use crate::models::file::File;
use crate::models::folder::{FolderDetail, FolderSimple};
use unicode_normalization::UnicodeNormalization;
use anyhow::{anyhow, bail};
use unicode_normalization::UnicodeNormalization;
// ---------------------------------------------------------------------------
// Path helpers
// ---------------------------------------------------------------------------
/// Parse "remote:/labname/path/" into (remote, labname, folder_path).
pub fn parse_remote_path(
remote_path: &str,
) -> Result<(String, String, String), anyhow::Error> {
pub fn parse_remote_path(remote_path: &str) -> Result<(String, String, String), anyhow::Error> {
let parts: Vec<&str> = remote_path.splitn(2, ':').collect();
if parts.len() != 2 {
bail!("remote_path must be in the form 'remote:/labname/path/'");
@@ -75,13 +73,20 @@ pub async fn find_folder(
bail!("Folder `{}` not found.", path);
}
if folders.len() != 1 {
bail!("Ambiguous path `{}`: {} folders matched.", path, folders.len());
bail!(
"Ambiguous path `{}`: {} folders matched.",
path,
folders.len()
);
}
let folder_simple = &folders[0];
if folder_simple.lock {
match password {
None => {
bail!("Folder `{}` is locked. Use -p/--password to provide a password.", path);
bail!(
"Folder `{}` is locked. Use -p/--password to provide a password.",
path
);
}
Some(pw) => conn.folder_auth(&folder_simple.id, pw).await?,
}
+25 -13
View File
@@ -1,14 +1,13 @@
use crate::models::folder::FolderSimple;
use crate::cache::{create_authenticated_conn, load_cache_with_token_refresh};
use crate::commands::shared::{
find_file_by_name, find_folder, find_lab_in_cache,
nfc, parse_remote_path,
find_file_by_name, find_folder, find_lab_in_cache, nfc, parse_remote_path,
};
use crate::models::folder::FolderSimple;
use anyhow::{anyhow, bail};
use futures::stream::{FuturesUnordered, StreamExt};
use std::path::PathBuf;
use std::sync::Arc;
use tokio::fs;
use anyhow::{anyhow, bail};
pub async fn upload(
local_path: &str,
@@ -40,7 +39,8 @@ pub async fn upload(
}
}
}
conn.upload_file(&dest_folder.id, &local.to_string_lossy()).await?;
conn.upload_file(&dest_folder.id, &local.to_string_lossy())
.await?;
println!("{}{}", dest_folder.path, filename);
} else if local.is_dir() {
if !recursive {
@@ -50,13 +50,18 @@ pub async fn upload(
// remote_path. E.g. `upload ./mydir remote:/lab/path/` creates
// `/lab/path/mydir/` on the remote and uploads into that folder.
let local_basename = local.file_name().unwrap().to_string_lossy().to_string();
let top_remote_id = find_or_create_folder(&conn, &dest_folder.id, &dest_folder.sub_folders, &local_basename).await?;
let top_remote_id = find_or_create_folder(
&conn,
&dest_folder.id,
&dest_folder.sub_folders,
&local_basename,
)
.await?;
let top_folder = conn.retrieve_folder(&top_remote_id).await?;
println!("{}", top_folder.path.trim_end_matches('/'));
// Iterative depth-first walk: each entry is (local_dir, remote_folder_id)
let mut stack: Vec<(PathBuf, String)> =
vec![(local.to_path_buf(), top_remote_id)];
let mut stack: Vec<(PathBuf, String)> = vec![(local.to_path_buf(), top_remote_id)];
while let Some((local_dir, remote_id)) = stack.pop() {
let folder_detail = conn.retrieve_folder(&remote_id).await?;
@@ -77,15 +82,16 @@ pub async fn upload(
// Ensure each local sub-directory exists on the remote side
for subdir in subdirs {
let dirname = subdir.file_name().unwrap().to_string_lossy().to_string();
let sub_remote_id = find_or_create_folder(&conn, &remote_id, &folder_detail.sub_folders, &dirname).await?;
let sub_remote_id =
find_or_create_folder(&conn, &remote_id, &folder_detail.sub_folders, &dirname)
.await?;
let sub_folder = conn.retrieve_folder(&sub_remote_id).await?;
println!("{}", sub_folder.path.trim_end_matches('/'));
stack.push((subdir, sub_remote_id));
}
// Upload files in this directory (up to 10 concurrent)
let mut futs: FuturesUnordered<tokio::task::JoinHandle<()>> =
FuturesUnordered::new();
let mut futs: FuturesUnordered<tokio::task::JoinHandle<()>> = FuturesUnordered::new();
for file_path in files {
let filename = file_path.file_name().unwrap().to_string_lossy().to_string();
let file_path_str = file_path.to_string_lossy().to_string();
@@ -111,7 +117,10 @@ pub async fn upload(
// (connection pool) while supplying a fresh Bearer token.
let task_conn = match load_cache_with_token_refresh(&remote).await {
Ok(c) => conn.with_token(c.token.access),
Err(e) => { eprintln!("Error: {}", e); return; }
Err(e) => {
eprintln!("Error: {}", e);
return;
}
};
match task_conn.upload_file(&folder_id, &file_path_str).await {
Ok(_) => println!("{}{}", remote_path_prefix, fname),
@@ -138,7 +147,10 @@ async fn find_or_create_folder(
existing: &[FolderSimple],
name: &str,
) -> Result<String, anyhow::Error> {
if let Some(sf) = existing.iter().find(|f| nfc(&f.name).to_lowercase() == nfc(name).to_lowercase()) {
if let Some(sf) = existing
.iter()
.find(|f| nfc(&f.name).to_lowercase() == nfc(name).to_lowercase())
{
return Ok(sf.id.clone());
}
let resp = conn.create_folder(parent_id, &nfc(name)).await?;
-1
View File
@@ -100,4 +100,3 @@ impl MDRSConnection {
.await
}
}
+11 -13
View File
@@ -13,10 +13,7 @@ use cli::{Cli, Commands};
use error::handle_error;
fn run(cli: Cli) {
let build_rt = || {
tokio::runtime::Runtime::new()
.unwrap_or_else(|e| handle_error(e.into()))
};
let build_rt = || tokio::runtime::Runtime::new().unwrap_or_else(|e| handle_error(e.into()));
match cli.command {
Commands::Config(subcmd) => {
@@ -150,9 +147,10 @@ fn run(cli: Cli) {
remote_path,
password,
} => {
if let Err(e) = build_rt()
.block_on(commands::metadata::metadata(&remote_path, password.as_deref()))
{
if let Err(e) = build_rt().block_on(commands::metadata::metadata(
&remote_path,
password.as_deref(),
)) {
handle_error(e);
}
}
@@ -169,10 +167,11 @@ fn run(cli: Cli) {
handle_error(e);
}
}
Commands::Mv { src_path, dest_path } => {
if let Err(e) =
build_rt().block_on(commands::mv::mv(&src_path, &dest_path))
{
Commands::Mv {
src_path,
dest_path,
} => {
if let Err(e) = build_rt().block_on(commands::mv::mv(&src_path, &dest_path)) {
handle_error(e);
}
}
@@ -181,8 +180,7 @@ fn run(cli: Cli) {
dest_path,
recursive,
} => {
if let Err(e) =
build_rt().block_on(commands::cp::cp(&src_path, &dest_path, recursive))
if let Err(e) = build_rt().block_on(commands::cp::cp(&src_path, &dest_path, recursive))
{
handle_error(e);
}
+1 -1
View File
@@ -1,8 +1,8 @@
// JWT utilities for token expiry checking (no signature verification required)
use anyhow::{anyhow, bail};
use base64::{Engine, engine::general_purpose::URL_SAFE_NO_PAD};
use std::time::{SystemTime, UNIX_EPOCH};
use anyhow::{anyhow, bail};
fn now_secs() -> i64 {
SystemTime::now()