人人都会AI编程

文件下载、上传、断点续传

更新时间:2026-07-11

在 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 报告进度。这套方案稳定、资源占用低,并且具备完全的断点续传能力,非常适合网盘客户端、下载器等桌面应用。