Files
mdrs-client-rust/src/commands/upload.rs
T

166 lines
7.0 KiB
Rust

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,
};
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;
pub async fn upload(
local_path: &str,
remote_path: &str,
recursive: bool,
skip_if_exists: bool,
) -> Result<(), anyhow::Error> {
let (remote, labname, r_path) = parse_remote_path(remote_path)?;
let cache = load_cache_with_token_refresh(&remote).await?;
let conn = Arc::new(create_authenticated_conn(&remote, &cache)?);
let lab = find_lab_in_cache(&cache, &labname)?;
let dest_folder = find_folder(&conn, lab.id, &r_path, None).await?;
// Normalize local_path: resolve to an absolute canonical path so that
// trailing slashes and "./" prefixes are handled consistently (matching
// Python's os.path.abspath behaviour).
let local_abs = std::fs::canonicalize(local_path)
.map_err(|_| anyhow!("File or directory `{}` not found.", local_path))?;
let local = local_abs.as_path();
if local.is_file() {
let filename = local.file_name().unwrap().to_string_lossy().to_string();
let remote_files = conn.list_all_files(&dest_folder.id).await?;
if skip_if_exists {
if let Some(rf) = find_file_by_name(&remote_files, &filename) {
if rf.size == std::fs::metadata(local)?.len() {
println!("{}{}", dest_folder.path, filename);
return Ok(());
}
}
}
conn.upload_file(&dest_folder.id, &local.to_string_lossy())
.await?;
println!("{}{}", dest_folder.path, filename);
} else if local.is_dir() {
if !recursive {
bail!("Cannot upload `{}`: Is a directory.", local_path);
}
// Python always creates a sub-folder named after the local directory inside
// 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_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)];
while let Some((local_dir, remote_id)) = stack.pop() {
let folder_detail = conn.retrieve_folder(&remote_id).await?;
let remote_files = conn.list_all_files(&remote_id).await?;
let mut entries = fs::read_dir(&local_dir).await?;
let mut subdirs: Vec<PathBuf> = Vec::new();
let mut files: Vec<PathBuf> = Vec::new();
while let Some(entry) = entries.next_entry().await? {
let p = entry.path();
if p.is_dir() {
subdirs.push(p);
} else {
files.push(p);
}
}
// 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_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();
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();
if skip_if_exists {
if let Some(rf) = find_file_by_name(&remote_files, &filename) {
if let Ok(meta) = std::fs::metadata(&file_path) {
if rf.size == meta.len() {
let remote_path_prefix = folder_detail.path.clone();
println!("{}{}", remote_path_prefix, filename);
continue;
}
}
}
}
let conn = conn.clone();
let folder_id = remote_id.clone();
let remote_path_prefix = folder_detail.path.clone();
let fname = filename.clone();
let remote = remote.clone();
futs.push(tokio::spawn(async move {
// Refresh the access token if it has expired or is about to
// expire. conn.with_token() reuses the shared HTTP client
// (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;
}
};
match task_conn.upload_file(&folder_id, &file_path_str).await {
Ok(_) => println!("{}{}", remote_path_prefix, fname),
Err(e) => eprintln!("Error: {}", e),
}
}));
if futs.len() >= crate::settings::SETTINGS.concurrent {
let _ = futs.next().await;
}
}
while futs.next().await.is_some() {}
}
} else {
bail!("File or directory `{}` not found.", local_path);
}
Ok(())
}
/// Find an existing sub-folder by name or create it, returning its ID.
async fn find_or_create_folder(
conn: &crate::connection::MDRSConnection,
parent_id: &str,
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())
{
return Ok(sf.id.clone());
}
let resp = conn.create_folder(parent_id, &nfc(name)).await?;
if !resp.status().is_success() {
bail!("Failed to create remote 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 {}", name))
.map(|s| s.to_string())
}