fix(upload,download): refresh access token before each parallel transfer task

The access token obtained at command startup could expire during a long
transfer session (e.g. uploading thousands of files or large files),
causing subsequent requests to fail with 401 Unauthorized.

Root cause: load_cache_with_token_refresh was called only once, and the
resulting MDRSConnection — including its now-stale token — was shared
across all parallel tasks via Arc.  There was no mechanism to update the
token in the shared instance after creation.

Fix:
- Add MDRSConnection::with_token(&self, token) that creates a new
  connection struct reusing the caller's HTTP client (cheap Arc clone,
  shares the connection pool) but carrying a fresh Bearer token.
- In upload.rs and download.rs, call load_cache_with_token_refresh
  inside each tokio::spawn task body, then create a task-local
  connection via conn.with_token(fresh_token) before transferring the
  file.  The shared reqwest::Client (connection pool) is preserved.

cp.rs is not changed: it uses only short server-side API calls with no
parallel tasks, so token expiry during a cp operation is negligible.

Co-authored-by: Copilot <223556219+Copilot@users.noreply.github.com>
This commit is contained in:
2026-04-20 13:29:14 +09:00
parent d58acd70e5
commit ecd244491b
3 changed files with 33 additions and 2 deletions
+9 -1
View File
@@ -110,9 +110,17 @@ pub async fn download(
}
let url = make_absolute_url(&conn, &f.download_url);
let conn = conn.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; }
};
let dest_str = dest_path.to_string_lossy().to_string();
match conn.download_file(&url, &dest_str).await {
match task_conn.download_file(&url, &dest_str).await {
Ok(_) => println!("{}", dest_path.display()),
Err(_) => {
eprintln!("Failed: {}", dest_path.display());
+9 -1
View File
@@ -102,8 +102,16 @@ pub async fn upload(
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 {
match conn.upload_file(&folder_id, &file_path_str).await {
// 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),
}
+15
View File
@@ -41,6 +41,21 @@ impl MDRSConnection {
}
}
/// Create a new connection that shares the HTTP client (and its connection
/// pool) with the receiver but uses a fresh access token. Useful for
/// spawning per-task connections without allocating a new connection pool
/// for every concurrent task.
///
/// `reqwest::Client` wraps an internal `Arc`; cloning it is cheap and
/// keeps the shared pool intact.
pub fn with_token(&self, access_token: String) -> Self {
MDRSConnection {
url: self.url.clone(),
client: self.client.clone(),
token: Some(access_token),
}
}
pub fn build_url(&self, path: &str) -> String {
format!("{}/{}", self.url.trim_end_matches('/'), path)
}