Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 6 additions & 0 deletions .changes/upload-http-method.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,6 @@
---
"upload": minor
"upload-js": minor
---

Upload plugin now supports specifying an HTTP method i.e. POST, PUT etc.
47 changes: 44 additions & 3 deletions examples/api/src/views/Upload.svelte
Original file line number Diff line number Diff line change
@@ -1,5 +1,5 @@
<script>
import { download, upload } from '@tauri-apps/plugin-upload'
import { download, upload, HttpMethod } from '@tauri-apps/plugin-upload'
import { open } from '@tauri-apps/plugin-dialog'
import { JsonView } from '@zerodevx/svelte-json-view'
import { appDataDir } from '@tauri-apps/api/path'
Expand All @@ -16,6 +16,22 @@

let uploadUrl = 'https://httpbin.org/post'
let uploadFilePath = ''
let uploadMethod = HttpMethod.Post

// Update URL when method changes
$: {
switch (uploadMethod) {
case HttpMethod.Post:
uploadUrl = 'https://httpbin.org/post'
break
case HttpMethod.Put:
uploadUrl = 'https://httpbin.org/put'
break
case HttpMethod.Patch:
uploadUrl = 'https://httpbin.org/patch'
break
}
}
let uploadProgress = null
let uploadResult = null
let isUploading = false
Expand Down Expand Up @@ -197,7 +213,8 @@
},
new Map([
['User-Agent', 'Tauri Upload Plugin Demo']
])
]),
uploadMethod
)

uploadResult = {
Expand Down Expand Up @@ -340,12 +357,36 @@
</div>
</div>

<div>
<label for="upload-method" class="block text-sm font-medium text-gray-700 mb-1">HTTP Method:</label>
<select
id="upload-method"
bind:value={uploadMethod}
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
disabled={isUploading}
>
<option value={HttpMethod.Post}>POST</option>
<option value={HttpMethod.Put}>PUT</option>
<option value={HttpMethod.Patch}>PATCH</option>
</select>
<p class="text-xs text-gray-500 mt-1">Choose the HTTP method for the upload request</p>
</div>

<div class="bg-blue-50 border border-blue-200 p-3 rounded-md">
<div class="text-sm text-blue-800">
<strong>Upload Configuration:</strong>
<div class="font-mono text-xs mt-1">
Method: {uploadMethod} | URL: {uploadUrl || 'Not set'}
</div>
</div>
</div>

<button
on:click={startUpload}
class="w-full px-4 py-2 bg-green-500 text-white rounded-md hover:bg-green-600 disabled:opacity-50 disabled:cursor-not-allowed"
disabled={isUploading || !uploadUrl || !uploadFilePath}
>
{isUploading ? 'Uploading...' : 'Upload File'}
{isUploading ? `Uploading (${uploadMethod})...` : `Upload File (${uploadMethod})`}
</button>

{#if uploadProgress}
Expand Down
12 changes: 11 additions & 1 deletion plugins/upload/README.md
Original file line number Diff line number Diff line change
Expand Up @@ -60,14 +60,24 @@ fn main() {
Afterwards all the plugin's APIs are available through the JavaScript guest bindings:

```javascript
import { upload } from '@tauri-apps/plugin-upload'
import { upload, HttpMethod } from '@tauri-apps/plugin-upload'

// Upload with default POST method
upload(
'https://example.com/file-upload',
'./path/to/my/file.txt',
(progress, total) => console.log(`Uploaded ${progress} of ${total} bytes`), // a callback that will be called with the upload progress
{ 'Content-Type': 'text/plain' } // optional headers to send with the request
)

// Upload with specific HTTP method
upload(
'https://example.com/file-upload',
'./path/to/my/file.txt',
(progress, total) => console.log(`Uploaded ${progress} of ${total} bytes`),
{ 'Content-Type': 'text/plain' },
HttpMethod.Put // Use HttpMethod enum - supports POST, PUT, PATCH
)
```

```javascript
Expand Down
12 changes: 10 additions & 2 deletions plugins/upload/guest-js/index.ts
Original file line number Diff line number Diff line change
Expand Up @@ -13,11 +13,18 @@ interface ProgressPayload {

type ProgressHandler = (progress: ProgressPayload) => void

enum HttpMethod {
Post = 'POST',
Put = 'PUT',
Patch = 'PATCH'
}

async function upload(
url: string,
filePath: string,
progressHandler?: ProgressHandler,
headers?: Map<string, string>
headers?: Map<string, string>,
method?: HttpMethod
): Promise<string> {
const ids = new Uint32Array(1)
window.crypto.getRandomValues(ids)
Expand All @@ -33,6 +40,7 @@ async function upload(
url,
filePath,
headers: headers ?? {},
method: method ?? HttpMethod.Post,
onProgress
})
}
Expand Down Expand Up @@ -67,4 +75,4 @@ async function download(
})
}

export { download, upload }
export { download, upload, HttpMethod }
75 changes: 59 additions & 16 deletions plugins/upload/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -15,7 +15,7 @@ mod transfer_stats;
use transfer_stats::TransferStats;

use futures_util::TryStreamExt;
use serde::{ser::Serializer, Serialize};
use serde::{ser::Serializer, Deserialize, Serialize};
use tauri::{
command,
ipc::Channel,
Expand All @@ -32,6 +32,14 @@ use read_progress_stream::ReadProgressStream;

use std::collections::HashMap;

#[derive(Debug, Clone, Serialize, Deserialize)]
#[serde(rename_all = "UPPERCASE")]
pub enum HttpMethod {
Post,
Put,
Patch,
}

type Result<T> = std::result::Result<T, Error>;

#[derive(Debug, thiserror::Error)]
Expand Down Expand Up @@ -120,19 +128,26 @@ async fn upload(
url: String,
file_path: String,
headers: HashMap<String, String>,
method: Option<HttpMethod>,
on_progress: Channel<ProgressPayload>,
) -> Result<String> {
tokio::spawn(async move {
// Read the file
let file = File::open(&file_path).await?;
let file_len = file.metadata().await.unwrap().len();

// Get HTTP method (defaults to POST)
let http_method = method.unwrap_or(HttpMethod::Post);

// 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));
let mut request = match http_method {
HttpMethod::Put => client.put(&url),
HttpMethod::Patch => client.patch(&url),
HttpMethod::Post => 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.
Expand Down Expand Up @@ -211,8 +226,8 @@ 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 mocked_server = spawn_upload_server_mocked(500, "POST").await;
let result = upload_file(mocked_server.url, None).await;
mocked_server.mocked_endpoint.assert();
assert!(result.is_err());
match result.unwrap_err() {
Expand All @@ -223,7 +238,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 mocked_server = spawn_upload_server_mocked(200, "POST").await;
let file_path = "/nonexistent/file.txt".to_string();
let headers = HashMap::new();
let sender: Channel<ProgressPayload> =
Expand All @@ -232,7 +247,7 @@ mod tests {
Ok(())
});

let result = upload(mocked_server.url, file_path, headers, sender).await;
let result = upload(mocked_server.url, file_path, headers, None, sender).await;
assert!(result.is_err());
match result.unwrap_err() {
Error::Io(_) => {}
Expand All @@ -241,9 +256,9 @@ 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;
async fn should_upload_file_with_post_method() {
let mocked_server = spawn_upload_server_mocked(200, "POST").await;
let result = upload_file(mocked_server.url, Some(HttpMethod::Post)).await;
mocked_server.mocked_endpoint.assert();
assert!(
result.is_ok(),
Expand All @@ -254,6 +269,34 @@ mod tests {
assert_eq!(response_body, "upload successful");
}

#[tokio::test]
async fn should_upload_file_with_put_method() {
let mocked_server = spawn_upload_server_mocked(200, "PUT").await;
let result = upload_file(mocked_server.url, Some(HttpMethod::Put)).await;
mocked_server.mocked_endpoint.assert();
assert!(
result.is_ok(),
"failed to upload file with PUT: {}",
result.unwrap_err()
);
let response_body = result.unwrap();
assert_eq!(response_body, "upload successful");
}

#[tokio::test]
async fn should_upload_file_with_patch_method() {
let mocked_server = spawn_upload_server_mocked(200, "PATCH").await;
let result = upload_file(mocked_server.url, Some(HttpMethod::Patch)).await;
mocked_server.mocked_endpoint.assert();
assert!(
result.is_ok(),
"failed to upload file with PATCH: {}",
result.unwrap_err()
);
let response_body = result.unwrap();
assert_eq!(response_body, "upload successful");
}

async fn download_file(url: String) -> Result<()> {
let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt").to_string();
let headers = HashMap::new();
Expand All @@ -265,15 +308,15 @@ mod tests {
download(url, file_path, headers, None, sender).await
}

async fn upload_file(url: String) -> Result<String> {
async fn upload_file(url: String, method: Option<HttpMethod>) -> Result<String> {
let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt").to_string();
let headers = HashMap::new();
let sender: Channel<ProgressPayload> =
Channel::new(|msg: InvokeResponseBody| -> tauri::Result<()> {
let _ = msg;
Ok(())
});
upload(url, file_path, headers, sender).await
upload(url, file_path, headers, method, sender).await
}

async fn spawn_server_mocked(return_status: usize) -> MockedServer {
Expand All @@ -294,11 +337,11 @@ mod tests {
}
}

async fn spawn_upload_server_mocked(return_status: usize) -> MockedServer {
async fn spawn_upload_server_mocked(return_status: usize, method: &str) -> MockedServer {
let mut _server = Server::new_async().await;
let path = "/upload_test";
let mock = _server
.mock("POST", path)
.mock(method, path)
.with_status(return_status)
.with_body("upload successful")
.match_header("content-length", "20")
Expand Down
Loading