mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-04-21 11:26:15 +02:00
feat(upload): Add HTTP method selection for upload (#2991)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
This commit is contained in:
committed by
GitHub
parent
6b854421a1
commit
ad910b1135
@@ -0,0 +1,6 @@
|
||||
---
|
||||
"upload": minor
|
||||
"upload-js": minor
|
||||
---
|
||||
|
||||
Upload plugin now supports specifying an HTTP method i.e. POST, PUT etc.
|
||||
@@ -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'
|
||||
@@ -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
|
||||
@@ -197,7 +213,8 @@
|
||||
},
|
||||
new Map([
|
||||
['User-Agent', 'Tauri Upload Plugin Demo']
|
||||
])
|
||||
]),
|
||||
uploadMethod
|
||||
)
|
||||
|
||||
uploadResult = {
|
||||
@@ -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}
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -1 +1 @@
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_UPLOAD__=function(t){"use strict";function e(t,e,n,s){if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?s:"a"===n?s.call(t):s?s.value:e.get(t)}function n(t,e,n,s,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var s,i,o,a;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class _{constructor(t){s.set(this,void 0),i.set(this,0),o.set(this,[]),a.set(this,void 0),n(this,s,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((t=>{const r=t.index;if("end"in t)return void(r==e(this,i,"f")?this.cleanupCallback():n(this,a,r));const _=t.message;if(r==e(this,i,"f")){for(e(this,s,"f").call(this,_),n(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,o,"f");){const t=e(this,o,"f")[e(this,i,"f")];e(this,s,"f").call(this,t),delete e(this,o,"f")[e(this,i,"f")],n(this,i,e(this,i,"f")+1)}e(this,i,"f")===e(this,a,"f")&&this.cleanupCallback()}else e(this,o,"f")[r]=_}))}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){n(this,s,t)}get onmessage(){return e(this,s,"f")}[(s=new WeakMap,i=new WeakMap,o=new WeakMap,a=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}async function c(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}return t.download=async function(t,e,n,s,i){const o=new Uint32Array(1);window.crypto.getRandomValues(o);const a=o[0],r=new _;n&&(r.onmessage=n),await c("plugin:upload|download",{id:a,url:t,filePath:e,headers:s??{},onProgress:r,body:i})},t.upload=async function(t,e,n,s){const i=new Uint32Array(1);window.crypto.getRandomValues(i);const o=i[0],a=new _;return n&&(a.onmessage=n),await c("plugin:upload|upload",{id:o,url:t,filePath:e,headers:s??{},onProgress:a})},t}({});Object.defineProperty(window.__TAURI__,"upload",{value:__TAURI_PLUGIN_UPLOAD__})}
|
||||
if("__TAURI__"in window){var __TAURI_PLUGIN_UPLOAD__=function(t){"use strict";function e(t,e,n,s){if("function"==typeof e?t!==e||!s:!e.has(t))throw new TypeError("Cannot read private member from an object whose class did not declare it");return"m"===n?s:"a"===n?s.call(t):s?s.value:e.get(t)}function n(t,e,n,s,i){if("function"==typeof e||!e.has(t))throw new TypeError("Cannot write private member to an object whose class did not declare it");return e.set(t,n),n}var s,i,o,a;"function"==typeof SuppressedError&&SuppressedError;const r="__TAURI_TO_IPC_KEY__";class h{constructor(t){s.set(this,void 0),i.set(this,0),o.set(this,[]),a.set(this,void 0),n(this,s,t||(()=>{})),this.id=function(t,e=!1){return window.__TAURI_INTERNALS__.transformCallback(t,e)}((t=>{const r=t.index;if("end"in t)return void(r==e(this,i,"f")?this.cleanupCallback():n(this,a,r));const h=t.message;if(r==e(this,i,"f")){for(e(this,s,"f").call(this,h),n(this,i,e(this,i,"f")+1);e(this,i,"f")in e(this,o,"f");){const t=e(this,o,"f")[e(this,i,"f")];e(this,s,"f").call(this,t),delete e(this,o,"f")[e(this,i,"f")],n(this,i,e(this,i,"f")+1)}e(this,i,"f")===e(this,a,"f")&&this.cleanupCallback()}else e(this,o,"f")[r]=h}))}cleanupCallback(){window.__TAURI_INTERNALS__.unregisterCallback(this.id)}set onmessage(t){n(this,s,t)}get onmessage(){return e(this,s,"f")}[(s=new WeakMap,i=new WeakMap,o=new WeakMap,a=new WeakMap,r)](){return`__CHANNEL__:${this.id}`}toJSON(){return this[r]()}}async function d(t,e={},n){return window.__TAURI_INTERNALS__.invoke(t,e,n)}var c;return t.HttpMethod=void 0,(c=t.HttpMethod||(t.HttpMethod={})).Post="POST",c.Put="PUT",c.Patch="PATCH",t.download=async function(t,e,n,s,i){const o=new Uint32Array(1);window.crypto.getRandomValues(o);const a=o[0],r=new h;n&&(r.onmessage=n),await d("plugin:upload|download",{id:a,url:t,filePath:e,headers:s??{},onProgress:r,body:i})},t.upload=async function(e,n,s,i,o){const a=new Uint32Array(1);window.crypto.getRandomValues(a);const r=a[0],c=new h;return s&&(c.onmessage=s),await d("plugin:upload|upload",{id:r,url:e,filePath:n,headers:i??{},method:o??t.HttpMethod.Post,onProgress:c})},t}({});Object.defineProperty(window.__TAURI__,"upload",{value:__TAURI_PLUGIN_UPLOAD__})}
|
||||
|
||||
@@ -13,11 +13,19 @@ 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>
|
||||
// TODO: V3 - Combine headers and methods into one `options` object
|
||||
headers?: Map<string, string>,
|
||||
method?: HttpMethod
|
||||
): Promise<string> {
|
||||
const ids = new Uint32Array(1)
|
||||
window.crypto.getRandomValues(ids)
|
||||
@@ -33,6 +41,7 @@ async function upload(
|
||||
url,
|
||||
filePath,
|
||||
headers: headers ?? {},
|
||||
method: method ?? HttpMethod.Post,
|
||||
onProgress
|
||||
})
|
||||
}
|
||||
@@ -67,4 +76,4 @@ async function download(
|
||||
})
|
||||
}
|
||||
|
||||
export { download, upload }
|
||||
export { download, upload, HttpMethod }
|
||||
|
||||
+59
-16
@@ -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,
|
||||
@@ -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)]
|
||||
@@ -120,6 +128,7 @@ 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 {
|
||||
@@ -127,12 +136,18 @@ async fn upload(
|
||||
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.
|
||||
@@ -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() {
|
||||
@@ -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> =
|
||||
@@ -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(_) => {}
|
||||
@@ -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(),
|
||||
@@ -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();
|
||||
@@ -265,7 +308,7 @@ 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> =
|
||||
@@ -273,7 +316,7 @@ mod tests {
|
||||
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 {
|
||||
@@ -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")
|
||||
|
||||
Reference in New Issue
Block a user