diff --git a/Cargo.lock b/Cargo.lock
index 23f4bb0b..62acfde8 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/README.md b/README.md
index 33039295..96d41d8e 100644
--- a/README.md
+++ b/README.md
@@ -32,8 +32,8 @@ This repo and all plugins require a Rust version of at least **1.77.2**
| [sql](plugins/sql) | Interface with SQL databases. | ✅ | ✅ | ✅ | ✅ | ✅ |
| [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. | ✅ | ✅ | ✅ | ? | ? |
+| [updater](plugins/updater) | In-app updates for Tauri applications. | ✅ | ✅ | ✅ | ✅ | ✅ |
+| [upload](plugins/upload) | Tauri plugin for file downloads and uploads via 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/examples/api/package.json b/examples/api/package.json
index c81db111..7cd411cc 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 d9966254..9a2d349c 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 1fb9f244..8508bb6b 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 a19992b0..3c58f2c8 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 9396f6f9..8e114c4b 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 00000000..6c1c3852
--- /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/plugins/upload/src/lib.rs b/plugins/upload/src/lib.rs
index 23c33b11..cf5815b2 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 url = url.to_string();
+ let file_path = file_path.to_string();
- 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);
+ 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::other(e.to_string())))?
}
#[command]
@@ -119,46 +125,53 @@ 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();
- 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(),
- ))
- }
+ 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, file_len));
+
+ // 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(),
+ ))
+ }
+ })
+ .await
+ .map_err(|e| Error::Io(std::io::Error::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,
});
}),
@@ -183,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();
@@ -202,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();
@@ -213,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";
@@ -230,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,
+ }
+ }
}
diff --git a/pnpm-lock.yaml b/pnpm-lock.yaml
index 46a684e3..aa0ba090 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)
@@ -2339,9 +2342,9 @@ snapshots:
- encoding
- mocha
- '@covector/assemble@0.12.0(mocha@10.8.2)':
+ '@covector/assemble@0.12.0':
dependencies:
- '@covector/command': 0.8.0(mocha@10.8.2)
+ '@covector/command': 0.8.0
'@covector/files': 0.8.0
effection: 2.0.8(mocha@10.8.2)
js-yaml: 4.1.0
@@ -2352,10 +2355,9 @@ snapshots:
unified: 9.2.2
transitivePeerDependencies:
- encoding
- - mocha
- supports-color
- '@covector/changelog@0.12.0(mocha@10.8.2)':
+ '@covector/changelog@0.12.0':
dependencies:
'@covector/files': 0.8.0
effection: 2.0.8(mocha@10.8.2)
@@ -2365,16 +2367,14 @@ snapshots:
unified: 9.2.2
transitivePeerDependencies:
- encoding
- - mocha
- supports-color
- '@covector/command@0.8.0(mocha@10.8.2)':
+ '@covector/command@0.8.0':
dependencies:
'@effection/process': 2.1.4
effection: 2.0.8(mocha@10.8.2)
transitivePeerDependencies:
- encoding
- - mocha
'@covector/files@0.8.0':
dependencies:
@@ -2421,6 +2421,8 @@ snapshots:
dependencies:
effection: 2.0.8(mocha@10.8.2)
mocha: 10.8.2
+ transitivePeerDependencies:
+ - encoding
'@effection/process@2.1.4':
dependencies:
@@ -3258,9 +3260,9 @@ snapshots:
dependencies:
'@clack/prompts': 0.7.0
'@covector/apply': 0.10.0(mocha@10.8.2)
- '@covector/assemble': 0.12.0(mocha@10.8.2)
- '@covector/changelog': 0.12.0(mocha@10.8.2)
- '@covector/command': 0.8.0(mocha@10.8.2)
+ '@covector/assemble': 0.12.0
+ '@covector/changelog': 0.12.0
+ '@covector/command': 0.8.0
'@covector/files': 0.8.0
effection: 2.0.8(mocha@10.8.2)
globby: 11.1.0