d05bd8a08d
Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
166 lines
7.0 KiB
Rust
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())
|
|
}
|