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 = Vec::new(); let mut files: Vec = 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> = 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 { 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()) }