在 Tauri 应用中,文件的下载、上传和断点续传通常是后台任务,不适合完全放在 WebView 的前端去做——因为浏览器的下载机制受到同源策略、后台运行限制,而且无法直接操作本地文件系统。最佳实践是在 Rust 后端处理网络 IO 与文件读写,通过 IPC 向前端实时推送进度。这样既可靠又高效,还能轻松实现断点续传。
下面以 Tauri v2 为例,展示如何用 Rust 的 reqwest 和标准库文件 API 实现这三大功能。
1. 文件下载(带进度)
Rust 端开启一个独立 tauri::ipc::Channel 将下载进度实时传回前端。
Rust 命令
use tauri::ipc::Channel;
use reqwest::Client;
use std::fs::File;
use std::io::Write;
#[tauri::command]
async fn download_file(url: String, save_path: String, on_progress: Channel<u64>) -> Result<(), String> {
let client = Client::new();
let response = client.get(&url).send().await.map_err(|e| e.to_string())?;
let total_size = response.content_length().unwrap_or(0);
let mut downloaded: u64 = 0;
let mut file = File::create(&save_path).map_err(|e| e.to_string())?;
let mut stream = response.bytes_stream();
use futures::StreamExt;
while let Some(chunk) = stream.next().await {
let chunk = chunk.map_err(|e| e.to_string())?;
file.write_all(&chunk).map_err(|e| e.to_string())?;
downloaded += chunk.len() as u64;
// 通过 Channel 发送已下载字节数
let _ = on_progress.send(downloaded);
}
Ok(())
}
前端调用(TypeScript)
import { invoke } from '@tauri-apps/api/core';
import { Channel } from '@tauri-apps/api/core';
async function startDownload(url: string, savePath: string) {
const onProgress = new Channel<number>();
onProgress.onmessage = (downloadedBytes) => {
console.log('已下载:', downloadedBytes, 'bytes');
};
await invoke('download_file', {
url,
savePath,
onProgress,
});
}
2. 文件上传(支持断点续传)
断点续传的核心思想是将文件分割为多个块,每次上传一块并记录状态;如果中断,下次从最后一个未完成的块继续。
这里以“服务端支持 Range 续传”为例,通常在上传前需要向服务端查询已接收的文件大小。
Rust 命令(分块上传)
use std::fs::File;
use std::io::{Seek, SeekFrom, Read};
use reqwest::Client;
#[tauri::command]
async fn upload_file(file_path: String, upload_url: String, chunk_size: u64, on_progress: Channel<(u64, u64)>) -> Result<(), String> {
let mut file = File::open(&file_path).map_err(|e| e.to_string())?;
let total_size = file.metadata().map_err(|e| e.to_string())?.len();
let client = Client::new();
// 查询服务端已接收的字节数(假设接口返回 Range 头或 JSON)
let uploaded = get_uploaded_bytes(&client, &upload_url).await?;
if uploaded >= total_size {
return Ok(()); // 已经上传完成
}
file.seek(SeekFrom::Start(uploaded)).map_err(|e| e.to_string())?;
let mut sent = uploaded;
let mut buffer = vec![0u8; chunk_size as usize];
while sent < total_size {
let n = file.read(&mut buffer).map_err(|e| e.to_string())?;
if n == 0 { break; }
let actual_chunk = &buffer[..n];
let range_header = format!("bytes {}-{}/{}", sent, sent + n as u64 - 1, total_size);
let res = client.put(&upload_url)
.header("Content-Range", &range_header)
.body(actual_chunk.to_vec())
.send()
.await
.map_err(|e| e.to_string())?;
if res.status().is_success() {
sent += n as u64;
let _ = on_progress.send((sent, total_size));
} else {
// 可根据状态码决定重试还是终止
return Err(format!("Upload failed with status: {}", res.status()));
}
}
Ok(())
}
前端调用
const progressChannel = new Channel<[number, number]>();
progressChannel.onmessage = ([sent, total]) => {
const percent = ((sent / total) * 100).toFixed(2);
console.log(`上传进度: ${percent}%`);
};
invoke('upload_file', {
filePath: '/path/to/largefile.zip',
uploadUrl: 'https://example.com/upload',
chunkSize: 1024 * 1024, // 1MB 每块
onProgress: progressChannel,
});
3. 断点续传的关键要素
- 状态持久化:可以在本地 SQLite 或 JSON 文件中记录已上传的字节数、最后修改时间等,确保应用关闭后重启也能续传。
- 服务端配合:服务端必须支持
Content-Range或类似机制,能告知客户端当前已上传的偏移量。 - 重试与错误处理:对于网络中断,可以加入有限次数的重试逻辑,避免一次波动就导致整个任务失败。
4. 开箱即用的插件选择
如果不想从零实现,可以使用社区插件:
tauri-plugin-upload:封装了分片上传和下载功能,支持进度、暂停/恢复。tauri-plugin-fs搭配前端fetch:也可以实现简单下载,但缺乏原生进度管理和断点续传。
总结:在 Tauri 中,文件传输的最佳路径是 Rust 负责 IO + 自定义 Channel 报告进度。这套方案稳定、资源占用低,并且具备完全的断点续传能力,非常适合网盘客户端、下载器等桌面应用。