From ecd244491b71105b0dc96a45458988dd9eb91d24 Mon Sep 17 00:00:00 2001 From: Yoshihiro OKUMURA Date: Mon, 20 Apr 2026 13:29:14 +0900 Subject: [PATCH] fix(upload,download): refresh access token before each parallel transfer task MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 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> --- src/commands/download.rs | 10 +++++++++- src/commands/upload.rs | 10 +++++++++- src/connection.rs | 15 +++++++++++++++ 3 files changed, 33 insertions(+), 2 deletions(-) diff --git a/src/commands/download.rs b/src/commands/download.rs index 74817b8..c73c8f9 100644 --- a/src/commands/download.rs +++ b/src/commands/download.rs @@ -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()); diff --git a/src/commands/upload.rs b/src/commands/upload.rs index 6a25e6d..2d83be1 100644 --- a/src/commands/upload.rs +++ b/src/commands/upload.rs @@ -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), } diff --git a/src/connection.rs b/src/connection.rs index fa8dd88..b7a13aa 100644 --- a/src/connection.rs +++ b/src/connection.rs @@ -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) }