mirror of
https://github.com/tauri-apps/plugins-workspace.git
synced 2026-01-31 00:45:24 +01:00
fix(upload): fix download() locks main thread on Android (#2838)
Co-authored-by: Fabian-Lars <github@fabianlars.de>
This commit is contained in:
committed by
GitHub
parent
2f9c71aae7
commit
b729203059
7
.changes/upload-locks-main-thread.md
Normal file
7
.changes/upload-locks-main-thread.md
Normal file
@@ -0,0 +1,7 @@
|
||||
---
|
||||
upload: patch
|
||||
upload-js: patch
|
||||
---
|
||||
|
||||
Fix `download` and `upload` locks main thread on Android.
|
||||
Use Tokio to spawn task when invoking commands.
|
||||
1
Cargo.lock
generated
1
Cargo.lock
generated
@@ -233,6 +233,7 @@ dependencies = [
|
||||
"tauri-plugin-shell",
|
||||
"tauri-plugin-store",
|
||||
"tauri-plugin-updater",
|
||||
"tauri-plugin-upload",
|
||||
"tauri-plugin-window-state",
|
||||
"time",
|
||||
"tiny_http",
|
||||
|
||||
@@ -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. | ✅ | ✅ | ✅ | ❌ | ❌ |
|
||||
|
||||
|
||||
@@ -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": {
|
||||
|
||||
@@ -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
|
||||
|
||||
@@ -95,6 +95,7 @@
|
||||
{
|
||||
"identifier": "opener:allow-open-path",
|
||||
"allow": [{ "path": "$APPDATA" }, { "path": "$APPDATA/**" }]
|
||||
}
|
||||
},
|
||||
"upload:default"
|
||||
]
|
||||
}
|
||||
|
||||
@@ -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)]
|
||||
{
|
||||
|
||||
@@ -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,
|
||||
|
||||
376
examples/api/src/views/Upload.svelte
Normal file
376
examples/api/src/views/Upload.svelte
Normal file
@@ -0,0 +1,376 @@
|
||||
<script>
|
||||
import { download, upload } 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'
|
||||
import { onMount } from 'svelte'
|
||||
|
||||
export let onMessage
|
||||
|
||||
let downloadUrl = 'https://httpbin.org/json'
|
||||
let downloadFolder = ''
|
||||
let downloadPath = ''
|
||||
let downloadProgress = null
|
||||
let downloadResult = null
|
||||
let isDownloading = false
|
||||
|
||||
let uploadUrl = 'https://httpbin.org/post'
|
||||
let uploadFilePath = ''
|
||||
let uploadProgress = null
|
||||
let uploadResult = null
|
||||
let isUploading = false
|
||||
|
||||
onMount(async () => {
|
||||
try {
|
||||
const defaultDir = await appDataDir()
|
||||
if (!downloadFolder) {
|
||||
downloadFolder = defaultDir
|
||||
updateDownloadPath()
|
||||
}
|
||||
} catch (error) {
|
||||
onMessage({ error: `Failed to get default directory: ${error.toString()}` })
|
||||
}
|
||||
})
|
||||
|
||||
async function selectDownloadFolder() {
|
||||
try {
|
||||
const selected = await open({
|
||||
directory: true,
|
||||
multiple: false,
|
||||
defaultPath: downloadFolder || undefined
|
||||
})
|
||||
if (selected) {
|
||||
downloadFolder = selected
|
||||
updateDownloadPath()
|
||||
}
|
||||
} catch (error) {
|
||||
onMessage({ error: error.toString() })
|
||||
}
|
||||
}
|
||||
|
||||
function getFilenameFromUrl(url) {
|
||||
try {
|
||||
const urlObj = new URL(url)
|
||||
let pathname = urlObj.pathname
|
||||
|
||||
// Remove leading slash
|
||||
if (pathname.startsWith('/')) {
|
||||
pathname = pathname.substring(1)
|
||||
}
|
||||
|
||||
// If pathname is empty or ends with slash, use a default name
|
||||
if (!pathname || pathname.endsWith('/')) {
|
||||
return 'downloaded-file.json'
|
||||
}
|
||||
|
||||
// Extract filename from pathname
|
||||
const segments = pathname.split('/')
|
||||
let filename = segments[segments.length - 1]
|
||||
|
||||
// If no extension, try to infer from URL or use default
|
||||
if (!filename.includes('.')) {
|
||||
// Check if URL suggests a file type
|
||||
if (url.includes('json') || urlObj.searchParams.has('format') && urlObj.searchParams.get('format') === 'json') {
|
||||
filename += '.json'
|
||||
} else if (url.includes('xml')) {
|
||||
filename += '.xml'
|
||||
} else if (url.includes('csv')) {
|
||||
filename += '.csv'
|
||||
} else {
|
||||
filename += '.txt'
|
||||
}
|
||||
}
|
||||
|
||||
return filename
|
||||
} catch (error) {
|
||||
return 'downloaded-file.json'
|
||||
}
|
||||
}
|
||||
|
||||
function updateDownloadPath() {
|
||||
if (downloadFolder && downloadUrl) {
|
||||
const filename = getFilenameFromUrl(downloadUrl)
|
||||
downloadPath = `${downloadFolder}/${filename}`
|
||||
} else {
|
||||
downloadPath = ''
|
||||
}
|
||||
}
|
||||
|
||||
// Update download path when URL changes
|
||||
$: if (downloadUrl) {
|
||||
updateDownloadPath()
|
||||
}
|
||||
|
||||
async function selectUploadFile() {
|
||||
try {
|
||||
const selected = await open({
|
||||
directory: false,
|
||||
multiple: false
|
||||
})
|
||||
if (selected) {
|
||||
uploadFilePath = selected
|
||||
}
|
||||
} catch (error) {
|
||||
onMessage({ error: error.toString() })
|
||||
}
|
||||
}
|
||||
|
||||
async function startDownload() {
|
||||
if (!downloadUrl || !downloadFolder) {
|
||||
onMessage({ error: 'Please provide both URL and download folder' })
|
||||
return
|
||||
}
|
||||
|
||||
// Ensure download path is updated
|
||||
updateDownloadPath()
|
||||
|
||||
if (!downloadPath) {
|
||||
onMessage({ error: 'Could not generate download path' })
|
||||
return
|
||||
}
|
||||
|
||||
isDownloading = true
|
||||
downloadProgress = null
|
||||
downloadResult = null
|
||||
|
||||
try {
|
||||
await download(
|
||||
downloadUrl,
|
||||
downloadPath,
|
||||
(progress) => {
|
||||
downloadProgress = {
|
||||
progress: progress.progress,
|
||||
progressTotal: progress.progressTotal,
|
||||
total: progress.total,
|
||||
transferSpeed: progress.transferSpeed,
|
||||
percentage: progress.total > 0 ? Math.round((progress.progressTotal / progress.total) * 100) : 0
|
||||
}
|
||||
},
|
||||
new Map([
|
||||
['User-Agent', 'Tauri Upload Plugin Demo']
|
||||
])
|
||||
)
|
||||
|
||||
downloadResult = {
|
||||
success: true,
|
||||
message: `File downloaded successfully to: ${downloadPath}`,
|
||||
finalProgress: downloadProgress
|
||||
}
|
||||
|
||||
onMessage({
|
||||
type: 'download',
|
||||
result: downloadResult
|
||||
})
|
||||
} catch (error) {
|
||||
downloadResult = {
|
||||
success: false,
|
||||
error: error.toString()
|
||||
}
|
||||
onMessage({ error: error.toString() })
|
||||
} finally {
|
||||
isDownloading = false
|
||||
}
|
||||
}
|
||||
|
||||
async function startUpload() {
|
||||
if (!uploadUrl || !uploadFilePath) {
|
||||
onMessage({ error: 'Please provide both URL and file path' })
|
||||
return
|
||||
}
|
||||
|
||||
isUploading = true
|
||||
uploadProgress = null
|
||||
uploadResult = null
|
||||
|
||||
try {
|
||||
const response = await upload(
|
||||
uploadUrl,
|
||||
uploadFilePath,
|
||||
(progress) => {
|
||||
uploadProgress = {
|
||||
progress: progress.progress,
|
||||
progressTotal: progress.progressTotal,
|
||||
total: progress.total,
|
||||
transferSpeed: progress.transferSpeed,
|
||||
percentage: progress.total > 0 ? Math.round((progress.progressTotal / progress.total) * 100) : 0
|
||||
}
|
||||
},
|
||||
new Map([
|
||||
['User-Agent', 'Tauri Upload Plugin Demo']
|
||||
])
|
||||
)
|
||||
|
||||
uploadResult = {
|
||||
success: true,
|
||||
response: response,
|
||||
finalProgress: uploadProgress
|
||||
}
|
||||
|
||||
onMessage({
|
||||
type: 'upload',
|
||||
result: uploadResult
|
||||
})
|
||||
} catch (error) {
|
||||
uploadResult = {
|
||||
success: false,
|
||||
error: error.toString()
|
||||
}
|
||||
onMessage({ error: error.toString() })
|
||||
} finally {
|
||||
isUploading = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<div class="space-y-6">
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-800">File Download</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label for="download-url" class="block text-sm font-medium text-gray-700 mb-1">Download URL:</label>
|
||||
<input
|
||||
id="download-url"
|
||||
bind:value={downloadUrl}
|
||||
type="url"
|
||||
placeholder="https://example.com/file.json"
|
||||
class="w-full px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
disabled={isDownloading}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="download-folder" class="block text-sm font-medium text-gray-700 mb-1">Download folder:</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="download-folder"
|
||||
bind:value={downloadFolder}
|
||||
type="text"
|
||||
placeholder="Select download folder..."
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-blue-500"
|
||||
disabled={isDownloading}
|
||||
/>
|
||||
<button
|
||||
on:click={selectDownloadFolder}
|
||||
class="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600 disabled:opacity-50"
|
||||
disabled={isDownloading}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</div>
|
||||
</div>
|
||||
|
||||
{#if downloadPath}
|
||||
<div class="bg-blue-50 border border-blue-200 p-3 rounded-md">
|
||||
<div class="text-sm text-blue-800">
|
||||
<strong>File will be saved as:</strong>
|
||||
<div class="font-mono text-xs mt-1 break-all">{downloadPath}</div>
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
<button
|
||||
on:click={startDownload}
|
||||
class="w-full px-4 py-2 bg-blue-500 text-white rounded-md hover:bg-blue-600 disabled:opacity-50 disabled:cursor-not-allowed"
|
||||
disabled={isDownloading || !downloadUrl || !downloadFolder}
|
||||
>
|
||||
{isDownloading ? 'Downloading...' : 'Download File'}
|
||||
</button>
|
||||
|
||||
{#if downloadProgress}
|
||||
<div class="bg-white p-3 rounded border">
|
||||
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
||||
<span>Progress: {downloadProgress.percentage}%</span>
|
||||
<span>Speed: {Math.round(downloadProgress.transferSpeed / 1024)} KB/s</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-blue-500 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {downloadProgress.percentage}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{Math.round(downloadProgress.progressTotal / 1024)} KB / {Math.round(downloadProgress.total / 1024)} KB
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if downloadResult}
|
||||
<div class="bg-white p-3 rounded border">
|
||||
<JsonView json={downloadResult} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
|
||||
<div class="bg-gray-50 p-4 rounded-lg">
|
||||
<h3 class="text-lg font-semibold mb-4 text-gray-800">File Upload</h3>
|
||||
|
||||
<div class="space-y-3">
|
||||
<div>
|
||||
<label for="upload-url" class="block text-sm font-medium text-gray-700 mb-1">Upload URL:</label>
|
||||
<input
|
||||
id="upload-url"
|
||||
bind:value={uploadUrl}
|
||||
type="url"
|
||||
placeholder="https://httpbin.org/post"
|
||||
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}
|
||||
/>
|
||||
</div>
|
||||
|
||||
<div>
|
||||
<label for="upload-file" class="block text-sm font-medium text-gray-700 mb-1">File to upload:</label>
|
||||
<div class="flex gap-2">
|
||||
<input
|
||||
id="upload-file"
|
||||
bind:value={uploadFilePath}
|
||||
type="text"
|
||||
placeholder="Select file to upload..."
|
||||
class="flex-1 px-3 py-2 border border-gray-300 rounded-md focus:outline-none focus:ring-2 focus:ring-green-500"
|
||||
disabled={isUploading}
|
||||
/>
|
||||
<button
|
||||
on:click={selectUploadFile}
|
||||
class="px-4 py-2 bg-gray-500 text-white rounded-md hover:bg-gray-600 disabled:opacity-50"
|
||||
disabled={isUploading}
|
||||
>
|
||||
Browse
|
||||
</button>
|
||||
</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'}
|
||||
</button>
|
||||
|
||||
{#if uploadProgress}
|
||||
<div class="bg-white p-3 rounded border">
|
||||
<div class="flex justify-between text-sm text-gray-600 mb-1">
|
||||
<span>Progress: {uploadProgress.percentage}%</span>
|
||||
<span>Speed: {Math.round(uploadProgress.transferSpeed / 1024)} KB/s</span>
|
||||
</div>
|
||||
<div class="w-full bg-gray-200 rounded-full h-2">
|
||||
<div
|
||||
class="bg-green-500 h-2 rounded-full transition-all duration-300"
|
||||
style="width: {uploadProgress.percentage}%"
|
||||
></div>
|
||||
</div>
|
||||
<div class="text-xs text-gray-500 mt-1">
|
||||
{Math.round(uploadProgress.progressTotal / 1024)} KB / {Math.round(uploadProgress.total / 1024)} KB
|
||||
</div>
|
||||
</div>
|
||||
{/if}
|
||||
|
||||
{#if uploadResult}
|
||||
<div class="bg-white p-3 rounded border">
|
||||
<JsonView json={uploadResult} />
|
||||
</div>
|
||||
{/if}
|
||||
</div>
|
||||
</div>
|
||||
</div>
|
||||
@@ -66,99 +66,106 @@ struct ProgressPayload {
|
||||
|
||||
#[command]
|
||||
async fn download(
|
||||
url: &str,
|
||||
file_path: &str,
|
||||
url: String,
|
||||
file_path: String,
|
||||
headers: HashMap<String, String>,
|
||||
body: Option<String>,
|
||||
on_progress: Channel<ProgressPayload>,
|
||||
) -> 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);
|
||||
}
|
||||
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 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 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 file = BufWriter::new(File::create(file_path).await?);
|
||||
let mut stream = response.bytes_stream();
|
||||
let mut file = BufWriter::new(File::create(&file_path).await?);
|
||||
let mut stream = response.bytes_stream();
|
||||
|
||||
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(())
|
||||
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]
|
||||
async fn upload(
|
||||
url: &str,
|
||||
file_path: &str,
|
||||
url: String,
|
||||
file_path: String,
|
||||
headers: HashMap<String, String>,
|
||||
on_progress: Channel<ProgressPayload>,
|
||||
) -> Result<String> {
|
||||
// Read the file
|
||||
let file = File::open(file_path).await?;
|
||||
let file_len = file.metadata().await.unwrap().len();
|
||||
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));
|
||||
// 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);
|
||||
}
|
||||
// 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::other(e.to_string())))?
|
||||
}
|
||||
|
||||
fn file_to_body(channel: Channel<ProgressPayload>, file: File) -> reqwest::Body {
|
||||
fn file_to_body(channel: Channel<ProgressPayload>, 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,9 +190,9 @@ 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;
|
||||
let result = download_file(mocked_server.url).await;
|
||||
mocked_server.mocked_endpoint.assert();
|
||||
assert!(result.is_err());
|
||||
}
|
||||
@@ -193,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(),
|
||||
@@ -202,8 +209,53 @@ mod tests {
|
||||
);
|
||||
}
|
||||
|
||||
async fn download_file(url: &str) -> Result<()> {
|
||||
let file_path = concat!(env!("CARGO_MANIFEST_DIR"), "/test/test.txt");
|
||||
#[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".to_string();
|
||||
let headers = HashMap::new();
|
||||
let sender: Channel<ProgressPayload> =
|
||||
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: String) -> Result<()> {
|
||||
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<()> {
|
||||
@@ -213,6 +265,17 @@ mod tests {
|
||||
download(url, file_path, headers, None, sender).await
|
||||
}
|
||||
|
||||
async fn upload_file(url: String) -> 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
|
||||
}
|
||||
|
||||
async fn spawn_server_mocked(return_status: usize) -> MockedServer {
|
||||
let mut _server = Server::new_async().await;
|
||||
let path = "/mock_test";
|
||||
@@ -230,4 +293,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,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
3
pnpm-lock.yaml
generated
3
pnpm-lock.yaml
generated
@@ -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)
|
||||
|
||||
Reference in New Issue
Block a user