chore(rust): update lockfile and format sources
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
+4
-16
@@ -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
@@ -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(())
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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
|
||||
|
||||
Vendored
+21
-11
@@ -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);
|
||||
|
||||
@@ -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,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
@@ -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
@@ -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)
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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(())
|
||||
}
|
||||
|
||||
|
||||
@@ -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
@@ -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,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)?;
|
||||
|
||||
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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
@@ -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?;
|
||||
|
||||
@@ -100,4 +100,3 @@ impl MDRSConnection {
|
||||
.await
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
+11
-13
@@ -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
@@ -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()
|
||||
|
||||
Reference in New Issue
Block a user