From bb7185c21cb63e4bc028f7002c3a26d673fc773d Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Fri, 11 Jul 2025 17:01:31 +0100 Subject: [PATCH 1/8] feat: Add Upload demo view --- Cargo.lock | 1 + examples/api/package.json | 1 + examples/api/src-tauri/Cargo.toml | 1 + examples/api/src-tauri/capabilities/base.json | 3 +- examples/api/src-tauri/src/lib.rs | 1 + examples/api/src/App.svelte | 6 + examples/api/src/views/Upload.svelte | 376 ++++++++++++++++++ pnpm-lock.yaml | 3 + 8 files changed, 391 insertions(+), 1 deletion(-) create mode 100644 examples/api/src/views/Upload.svelte diff --git a/Cargo.lock b/Cargo.lock index 23f4bb0b2a..62acfde8f9 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -233,6 +233,7 @@ dependencies = [ "tauri-plugin-shell", "tauri-plugin-store", "tauri-plugin-updater", + "tauri-plugin-upload", "tauri-plugin-window-state", "time", "tiny_http", diff --git a/examples/api/package.json b/examples/api/package.json index c81db1115f..7cd411cc16 100644 --- a/examples/api/package.json +++ b/examples/api/package.json @@ -29,6 +29,7 @@ "@tauri-apps/plugin-shell": "^2.3.0", "@tauri-apps/plugin-store": "^2.3.0", "@tauri-apps/plugin-updater": "^2.9.0", + "@tauri-apps/plugin-upload": "^2.3.0", "@zerodevx/svelte-json-view": "1.0.11" }, "devDependencies": { diff --git a/examples/api/src-tauri/Cargo.toml b/examples/api/src-tauri/Cargo.toml index d996625421..9a2d349c6e 100644 --- a/examples/api/src-tauri/Cargo.toml +++ b/examples/api/src-tauri/Cargo.toml @@ -38,6 +38,7 @@ tauri-plugin-process = { path = "../../../plugins/process", version = "2.3.0" } tauri-plugin-opener = { path = "../../../plugins/opener", version = "2.4.0" } tauri-plugin-shell = { path = "../../../plugins/shell", version = "2.3.0" } tauri-plugin-store = { path = "../../../plugins/store", version = "2.3.0" } +tauri-plugin-upload = { path = "../../../plugins/upload", version = "2.3.0" } [dependencies.tauri] workspace = true diff --git a/examples/api/src-tauri/capabilities/base.json b/examples/api/src-tauri/capabilities/base.json index 1fb9f244ce..8508bb6bcf 100644 --- a/examples/api/src-tauri/capabilities/base.json +++ b/examples/api/src-tauri/capabilities/base.json @@ -95,6 +95,7 @@ { "identifier": "opener:allow-open-path", "allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }] - } + }, + "upload:default" ] } diff --git a/examples/api/src-tauri/src/lib.rs b/examples/api/src-tauri/src/lib.rs index a19992b0fd..3c58f2c81d 100644 --- a/examples/api/src-tauri/src/lib.rs +++ b/examples/api/src-tauri/src/lib.rs @@ -39,6 +39,7 @@ pub fn run() { .plugin(tauri_plugin_opener::init()) .plugin(tauri_plugin_shell::init()) .plugin(tauri_plugin_store::Builder::default().build()) + .plugin(tauri_plugin_upload::init()) .setup(move |app| { #[cfg(desktop)] { diff --git a/examples/api/src/App.svelte b/examples/api/src/App.svelte index 9396f6f93c..8e114c4b9d 100644 --- a/examples/api/src/App.svelte +++ b/examples/api/src/App.svelte @@ -16,6 +16,7 @@ import Opener from './views/Opener.svelte' import Store from './views/Store.svelte' import Updater from './views/Updater.svelte' + import Upload from './views/Upload.svelte' import Clipboard from './views/Clipboard.svelte' import WebRTC from './views/WebRTC.svelte' import Scanner from './views/Scanner.svelte' @@ -107,6 +108,11 @@ component: Updater, icon: 'i-codicon-cloud-download' }, + { + label: 'Upload', + component: Upload, + icon: 'i-codicon-cloud-upload' + }, { label: 'Clipboard', component: Clipboard, diff --git a/examples/api/src/views/Upload.svelte b/examples/api/src/views/Upload.svelte new file mode 100644 index 0000000000..6c1c38520b --- /dev/null +++ b/examples/api/src/views/Upload.svelte @@ -0,0 +1,376 @@ + + +
+
+

File Download

+ +
+
+ + +
+ +
+ +
+ + +
+
+ + {#if downloadPath} +
+
+ File will be saved as: +
{downloadPath}
+
+
+ {/if} + + + + {#if downloadProgress} +
+
+ Progress: {downloadProgress.percentage}% + Speed: {Math.round(downloadProgress.transferSpeed / 1024)} KB/s +
+
+
+
+
+ {Math.round(downloadProgress.progressTotal / 1024)} KB / {Math.round(downloadProgress.total / 1024)} KB +
+
+ {/if} + + {#if downloadResult} +
+ +
+ {/if} +
+
+ +
+

File Upload

+ +
+
+ + +
+ +
+ +
+ + +
+
+ + + + {#if uploadProgress} +
+
+ Progress: {uploadProgress.percentage}% + Speed: {Math.round(uploadProgress.transferSpeed / 1024)} KB/s +
+
+
+
+
+ {Math.round(uploadProgress.progressTotal / 1024)} KB / {Math.round(uploadProgress.total / 1024)} KB +
+
+ {/if} + + {#if uploadResult} +
+ +
+ {/if} +
+
+
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml index b338deb387..9b832652f6 100644 --- a/pnpm-lock.yaml +++ b/pnpm-lock.yaml @@ -110,6 +110,9 @@ importers: '@tauri-apps/plugin-updater': specifier: ^2.9.0 version: link:../../plugins/updater + '@tauri-apps/plugin-upload': + specifier: ^2.3.0 + version: link:../../plugins/upload '@zerodevx/svelte-json-view': specifier: 1.0.11 version: 1.0.11(svelte@5.28.2) From a17376c3fc73449ad02ee61db50a7fb9e4bfc60a Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Fri, 11 Jul 2025 17:19:35 +0100 Subject: [PATCH 2/8] feat: Spawn threads using Tokio --- plugins/upload/src/lib.rs | 133 +++++++++++++++++++++----------------- 1 file changed, 73 insertions(+), 60 deletions(-) diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index 23c33b115d..68be069d77 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -72,44 +72,50 @@ async fn download( body: Option, on_progress: Channel, ) -> Result<()> { - let client = reqwest::Client::new(); - let mut request = if let Some(body) = body { - client.post(url).body(body) - } else { - client.get(url) - }; - // Loop trought the headers keys and values - // and add them to the request object. - for (key, value) in headers { - request = request.header(&key, value); - } - - let response = request.send().await?; - if !response.status().is_success() { - return Err(Error::HttpErrorCode( - response.status().as_u16(), - response.text().await.unwrap_or_default(), - )); - } - let total = response.content_length().unwrap_or(0); + let url = url.to_string(); + let file_path = file_path.to_string(); + + tokio::spawn(async move { + let client = reqwest::Client::new(); + let mut request = if let Some(body) = body { + client.post(&url).body(body) + } else { + client.get(&url) + }; + // Loop trought the headers keys and values + // and add them to the request object. + for (key, value) in headers { + request = request.header(&key, value); + } - let mut file = BufWriter::new(File::create(file_path).await?); - let mut stream = response.bytes_stream(); + let response = request.send().await?; + if !response.status().is_success() { + return Err(Error::HttpErrorCode( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )); + } + let total = response.content_length().unwrap_or(0); - let mut stats = TransferStats::default(); - while let Some(chunk) = stream.try_next().await? { - file.write_all(&chunk).await?; - stats.record_chunk_transfer(chunk.len()); - let _ = on_progress.send(ProgressPayload { - progress: chunk.len() as u64, - progress_total: stats.total_transferred, - total, - transfer_speed: stats.transfer_speed, - }); - } - file.flush().await?; + let mut file = BufWriter::new(File::create(&file_path).await?); + let mut stream = response.bytes_stream(); - Ok(()) + let mut stats = TransferStats::default(); + while let Some(chunk) = stream.try_next().await? { + file.write_all(&chunk).await?; + stats.record_chunk_transfer(chunk.len()); + let _ = on_progress.send(ProgressPayload { + progress: chunk.len() as u64, + progress_total: stats.total_transferred, + total, + transfer_speed: stats.transfer_speed, + }); + } + file.flush().await?; + Ok(()) + }) + .await + .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))? } #[command] @@ -119,32 +125,39 @@ async fn upload( headers: HashMap, on_progress: Channel, ) -> Result { - // Read the file - let file = File::open(file_path).await?; - let file_len = file.metadata().await.unwrap().len(); - - // Create the request and attach the file to the body - let client = reqwest::Client::new(); - let mut request = client - .post(url) - .header(reqwest::header::CONTENT_LENGTH, file_len) - .body(file_to_body(on_progress, file)); - - // Loop through the headers keys and values - // and add them to the request object. - for (key, value) in headers { - request = request.header(&key, value); - } + let url = url.to_string(); + let file_path = file_path.to_string(); + + tokio::spawn(async move { + // Read the file + let file = File::open(&file_path).await?; + let file_len = file.metadata().await.unwrap().len(); + + // Create the request and attach the file to the body + let client = reqwest::Client::new(); + let mut request = client + .post(&url) + .header(reqwest::header::CONTENT_LENGTH, file_len) + .body(file_to_body(on_progress, file)); + + // Loop through the headers keys and values + // and add them to the request object. + for (key, value) in headers { + request = request.header(&key, value); + } - let response = request.send().await?; - if response.status().is_success() { - response.text().await.map_err(Into::into) - } else { - Err(Error::HttpErrorCode( - response.status().as_u16(), - response.text().await.unwrap_or_default(), - )) - } + let response = request.send().await?; + if response.status().is_success() { + response.text().await.map_err(Into::into) + } else { + Err(Error::HttpErrorCode( + response.status().as_u16(), + response.text().await.unwrap_or_default(), + )) + } + }) + .await + .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))? } fn file_to_body(channel: Channel, file: File) -> reqwest::Body { From cdba094dd66efbb35f854a9c48b1e2b169a0f36d Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Fri, 11 Jul 2025 17:21:54 +0100 Subject: [PATCH 3/8] fix: Fix reported upload total bytes --- plugins/upload/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index 68be069d77..36f3e7157d 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -138,7 +138,7 @@ async fn upload( let mut request = client .post(&url) .header(reqwest::header::CONTENT_LENGTH, file_len) - .body(file_to_body(on_progress, file)); + .body(file_to_body(on_progress, file, file_len)); // Loop through the headers keys and values // and add them to the request object. @@ -160,18 +160,18 @@ async fn upload( .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))? } -fn file_to_body(channel: Channel, file: File) -> reqwest::Body { +fn file_to_body(channel: Channel, file: File, file_len: u64) -> reqwest::Body { let stream = FramedRead::new(file, BytesCodec::new()).map_ok(|r| r.freeze()); let mut stats = TransferStats::default(); reqwest::Body::wrap_stream(ReadProgressStream::new( stream, - Box::new(move |progress, total| { + Box::new(move |progress, _total| { stats.record_chunk_transfer(progress as usize); let _ = channel.send(ProgressPayload { progress, progress_total: stats.total_transferred, - total, + total: file_len, transfer_speed: stats.transfer_speed, }); }), From a730ea9075d5eb05c4927ee2a20ef95747ba00f3 Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Fri, 11 Jul 2025 17:47:46 +0100 Subject: [PATCH 4/8] fix: Fix upload command not returning --- plugins/upload/src/lib.rs | 8 ++++---- 1 file changed, 4 insertions(+), 4 deletions(-) diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index 36f3e7157d..fd9eb026a7 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -74,7 +74,7 @@ async fn download( ) -> Result<()> { let url = url.to_string(); let file_path = file_path.to_string(); - + tokio::spawn(async move { let client = reqwest::Client::new(); let mut request = if let Some(body) = body { @@ -115,7 +115,7 @@ async fn download( Ok(()) }) .await - .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))? + .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))? } #[command] @@ -127,7 +127,7 @@ async fn upload( ) -> Result { let url = url.to_string(); let file_path = file_path.to_string(); - + tokio::spawn(async move { // Read the file let file = File::open(&file_path).await?; @@ -157,7 +157,7 @@ async fn upload( } }) .await - .map_err(|e| Error::Io(std::io::Error::new(std::io::ErrorKind::Other, e.to_string())))? + .map_err(|e| Error::Io(std::io::Error::other(e.to_string())))? } fn file_to_body(channel: Channel, file: File, file_len: u64) -> reqwest::Body { From ccbb15ebd553c21033e9962196641471a791fb60 Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Wed, 16 Jul 2025 10:53:29 +0100 Subject: [PATCH 5/8] chore: Add upload tests --- plugins/upload/src/lib.rs | 77 ++++++++++++++++++++++++++++++++++++++- 1 file changed, 76 insertions(+), 1 deletion(-) diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index fd9eb026a7..cf5815b2fe 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -196,7 +196,7 @@ mod tests { } #[tokio::test] - async fn should_error_if_status_not_success() { + async fn should_error_on_download_if_status_not_success() { let mocked_server = spawn_server_mocked(400).await; let result = download_file(&mocked_server.url).await; mocked_server.mocked_endpoint.assert(); @@ -215,6 +215,51 @@ mod tests { ); } + #[tokio::test] + async fn should_error_on_upload_if_status_not_success() { + let mocked_server = spawn_upload_server_mocked(500).await; + let result = upload_file(&mocked_server.url).await; + mocked_server.mocked_endpoint.assert(); + assert!(result.is_err()); + match result.unwrap_err() { + Error::HttpErrorCode(status, _) => assert_eq!(status, 500), + _ => panic!("Expected HttpErrorCode error"), + } + } + + #[tokio::test] + async fn should_error_on_upload_if_file_not_found() { + let mocked_server = spawn_upload_server_mocked(200).await; + let file_path = "/nonexistent/file.txt"; + let headers = HashMap::new(); + let sender: Channel = + Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { + let _ = msg; + Ok(()) + }); + + let result = upload(&mocked_server.url, file_path, headers, sender).await; + assert!(result.is_err()); + match result.unwrap_err() { + Error::Io(_) => {} + _ => panic!("Expected IO error for missing file"), + } + } + + #[tokio::test] + async fn should_upload_file_successfully() { + let mocked_server = spawn_upload_server_mocked(200).await; + let result = upload_file(&mocked_server.url).await; + mocked_server.mocked_endpoint.assert(); + assert!( + result.is_ok(), + "failed to upload file: {}", + result.unwrap_err() + ); + let response_body = result.unwrap(); + assert_eq!(response_body, "upload successful"); + } + async fn download_file(url: &str) -> Result<()> { let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt"); let headers = HashMap::new(); @@ -226,6 +271,17 @@ mod tests { download(url, file_path, headers, None, sender).await } + async fn upload_file(url: &str) -> Result { + let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt"); + let headers = HashMap::new(); + let sender: Channel = + Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { + let _ = msg; + Ok(()) + }); + upload(url, file_path, headers, sender).await + } + async fn spawn_server_mocked(return_status: usize) -> MockedServer { let mut _server = Server::new_async().await; let path = "/mock_test"; @@ -243,4 +299,23 @@ mod tests { mocked_endpoint: mock, } } + + async fn spawn_upload_server_mocked(return_status: usize) -> MockedServer { + let mut _server = Server::new_async().await; + let path = "/upload_test"; + let mock = _server + .mock("POST", path) + .with_status(return_status) + .with_body("upload successful") + .match_header("content-length", "20") + .create_async() + .await; + + let url = _server.url() + path; + MockedServer { + _server, + url, + mocked_endpoint: mock, + } + } } From 53f7c6689d2e9992150154ef1ed8ca1b1710ebde Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Wed, 16 Jul 2025 12:57:28 +0100 Subject: [PATCH 6/8] chore: Changes per review --- README.md | 2 +- plugins/upload/src/lib.rs | 34 ++++++++++++++-------------------- 2 files changed, 15 insertions(+), 21 deletions(-) diff --git a/README.md b/README.md index 33039295a3..cdf239bcf5 100644 --- a/README.md +++ b/README.md @@ -33,7 +33,7 @@ This repo and all plugins require a Rust version of at least **1.77.2** | [store](plugins/store) | Persistent key value storage. | ✅ | ✅ | ✅ | ✅ | ✅ | | [stronghold](plugins/stronghold) | Encrypted, secure database. | ✅ | ✅ | ✅ | ? | ? | | [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ❌ | ❌ | -| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ? | ? | +| [upload](plugins/upload) | Tauri plugin for file uploads through HTTP. | ✅ | ✅ | ✅ | ✅ | ✅ | | [websocket](plugins/websocket) | Open a WebSocket connection using a Rust client in JS. | ✅ | ✅ | ✅ | ? | ? | | [window-state](plugins/window-state) | Persist window sizes and positions. | ✅ | ✅ | ✅ | ❌ | ❌ | diff --git a/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs index cf5815b2fe..f38ad92b20 100644 --- a/plugins/upload/src/lib.rs +++ b/plugins/upload/src/lib.rs @@ -66,15 +66,12 @@ struct ProgressPayload { #[command] async fn download( - url: &str, - file_path: &str, + url: String, + file_path: String, headers: HashMap, body: Option, on_progress: Channel, ) -> Result<()> { - let url = url.to_string(); - let file_path = file_path.to_string(); - tokio::spawn(async move { let client = reqwest::Client::new(); let mut request = if let Some(body) = body { @@ -120,14 +117,11 @@ async fn download( #[command] async fn upload( - url: &str, - file_path: &str, + url: String, + file_path: String, headers: HashMap, on_progress: Channel, ) -> Result { - let url = url.to_string(); - let file_path = file_path.to_string(); - tokio::spawn(async move { // Read the file let file = File::open(&file_path).await?; @@ -198,7 +192,7 @@ mod tests { #[tokio::test] async fn should_error_on_download_if_status_not_success() { let mocked_server = spawn_server_mocked(400).await; - let result = download_file(&mocked_server.url).await; + let result = download_file(mocked_server.url).await; mocked_server.mocked_endpoint.assert(); assert!(result.is_err()); } @@ -206,7 +200,7 @@ mod tests { #[tokio::test] async fn should_download_file_successfully() { let mocked_server = spawn_server_mocked(200).await; - let result = download_file(&mocked_server.url).await; + let result = download_file(mocked_server.url).await; mocked_server.mocked_endpoint.assert(); assert!( result.is_ok(), @@ -218,7 +212,7 @@ mod tests { #[tokio::test] async fn should_error_on_upload_if_status_not_success() { let mocked_server = spawn_upload_server_mocked(500).await; - let result = upload_file(&mocked_server.url).await; + let result = upload_file(mocked_server.url).await; mocked_server.mocked_endpoint.assert(); assert!(result.is_err()); match result.unwrap_err() { @@ -230,7 +224,7 @@ mod tests { #[tokio::test] async fn should_error_on_upload_if_file_not_found() { let mocked_server = spawn_upload_server_mocked(200).await; - let file_path = "/nonexistent/file.txt"; + let file_path = "/nonexistent/file.txt".to_string(); let headers = HashMap::new(); let sender: Channel = Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { @@ -238,7 +232,7 @@ mod tests { Ok(()) }); - let result = upload(&mocked_server.url, file_path, headers, sender).await; + let result = upload(mocked_server.url, file_path, headers, sender).await; assert!(result.is_err()); match result.unwrap_err() { Error::Io(_) => {} @@ -249,7 +243,7 @@ mod tests { #[tokio::test] async fn should_upload_file_successfully() { let mocked_server = spawn_upload_server_mocked(200).await; - let result = upload_file(&mocked_server.url).await; + let result = upload_file(mocked_server.url).await; mocked_server.mocked_endpoint.assert(); assert!( result.is_ok(), @@ -260,8 +254,8 @@ mod tests { assert_eq!(response_body, "upload successful"); } - async fn download_file(url: &str) -> Result<()> { - let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt"); + async fn download_file(url: String) -> Result<()> { + let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt").to_string(); let headers = HashMap::new(); let sender: Channel = Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { @@ -271,8 +265,8 @@ mod tests { download(url, file_path, headers, None, sender).await } - async fn upload_file(url: &str) -> Result { - let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt"); + async fn upload_file(url: String) -> Result { + let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt").to_string(); let headers = HashMap::new(); let sender: Channel = Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> { From 2cd12e4b37eb50d09411b9cd39c81d310b345803 Mon Sep 17 00:00:00 2001 From: Matthew Richardson Date: Fri, 18 Jul 2025 12:35:39 +0100 Subject: [PATCH 7/8] chore: Add .changes --- .changes/upload-locks-main-thread.md | 7 +++++++ 1 file changed, 7 insertions(+) create mode 100644 .changes/upload-locks-main-thread.md diff --git a/.changes/upload-locks-main-thread.md b/.changes/upload-locks-main-thread.md new file mode 100644 index 0000000000..8108cfb785 --- /dev/null +++ b/.changes/upload-locks-main-thread.md @@ -0,0 +1,7 @@ +--- +upload: minor +upload-js: minor +--- + +Fix `download` and `upload` locks main thread on Android. +Use Tokio to spawn task when invoking commands. \ No newline at end of file From 9549d50b01e39943fb25d17b633792ac62321f5f Mon Sep 17 00:00:00 2001 From: Fabian-Lars Date: Fri, 18 Jul 2025 20:11:05 +0200 Subject: [PATCH 8/8] Update .changes/upload-locks-main-thread.md --- .changes/upload-locks-main-thread.md | 4 ++-- 1 file changed, 2 insertions(+), 2 deletions(-) diff --git a/.changes/upload-locks-main-thread.md b/.changes/upload-locks-main-thread.md index 8108cfb785..d4f2971419 100644 --- a/.changes/upload-locks-main-thread.md +++ b/.changes/upload-locks-main-thread.md @@ -1,6 +1,6 @@ --- -upload: minor -upload-js: minor +upload: patch +upload-js: patch --- Fix `download` and `upload` locks main thread on Android.