用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱
用 Tauri 2.0 + React + Rust 打造跨平台文件工具箱
前言
最近在整理电脑文件时,发现了大量重复的照片和视频,占用了几十 GB 的空间。市面上的去重工具要么收费,要么功能臃肿,于是萌生了自己造一个的想法。
正好 Tauri 2.0 正式发布,相比 Electron,它有着更小的包体积和更好的性能。于是决定用 Tauri 2.0 + React + Rust 来实现这个工具。
最终成品:File Toolkit —— 一个跨平台的文件工具箱,支持文件统计、文件去重、视频截取。
GitHub: github.com/220529/file…
功能展示
📊 文件统计
递归扫描文件夹,按类型统计文件数量和大小:
- 支持拖拽选择文件夹
- 按文件类型分组
- 显示占比和总大小
🔍 文件去重
这是核心功能,支持:
- 两阶段扫描:先按文件大小筛选,再计算哈希
- xxHash3 快速哈希:比 MD5 快 5-10 倍
- 并行计算:充分利用多核 CPU
- 大文件采样:只读头部 + 中间 + 尾部,避免全量读取
- 缩略图预览:图片直接显示,视频用 FFmpeg 截帧
- 智能选择:自动选中较新的文件,保留最早的
✂️ 视频截取
- 快速模式:无损截取(-c copy),秒级完成
- 精确模式:重新编码,时间精确到毫秒
- 时间轴预览:8 帧缩略图,快速定位
- 实时进度:精确模式显示编码进度
技术选型
为什么选 Tauri 而不是 Electron?
| 对比项 | Electron | Tauri |
|---|---|---|
| 包体积 | 150MB+ | 10MB+ |
| 内存占用 | 高(Chromium) | 低(系统 WebView) |
| 后端语言 | Node.js | Rust |
| 性能 | 一般 | 优秀 |
对于文件处理这种 CPU 密集型任务,Rust 的性能优势非常明显。
技术栈
┌─────────────────────────────────────────────────────┐
│ Frontend │
│ React 19 + TypeScript + Tailwind CSS │
├─────────────────────────────────────────────────────┤
│ Tauri IPC │
├─────────────────────────────────────────────────────┤
│ Backend │
│ Rust + Tauri 2.0 │
│ ┌─────────────┬─────────────┬─────────────────┐ │
│ │ file_stats │ dedup │ video │ │
│ │ walkdir │ xxHash3 │ FFmpeg │ │
│ │ │ rayon │ │ │
│ │ │ memmap2 │ │ │
│ └─────────────┴─────────────┴─────────────────┘ │
└─────────────────────────────────────────────────────┘
核心实现
1. 文件去重算法
去重的核心是计算文件哈希,但如果对每个文件都完整计算哈希,效率会很低。我采用了两阶段策略:
第一阶段:按文件大小筛选
let mut size_map: HashMap<u64, Vec<String>> = HashMap::new();
for entry in WalkDir::new(&path)
.into_iter()
.filter_map(|e| e.ok())
.filter(|e| e.file_type().is_file())
{
if let Ok(meta) = entry.metadata() {
let size = meta.len();
if size > 0 {
size_map.entry(size).or_default().push(entry.path().to_string_lossy().to_string());
}
}
}
// 只对大小相同的文件计算哈希
let files_to_hash: Vec<_> = size_map
.iter()
.filter(|(_, files)| files.len() >= 2)
.flat_map(|(size, files)| files.iter().map(move |f| (*size, f.clone())))
.collect();
这一步可以过滤掉大部分文件,因为大小不同的文件肯定不重复。
第二阶段:并行计算哈希
use rayon::prelude::*;
let results: Vec<(String, FileInfo)> = files_to_hash
.par_iter() // 并行迭代
.filter_map(|(size, file_path)| {
let hash = calculate_fast_hash(Path::new(file_path), *size).ok()?;
// ...
Some((hash, file_info))
})
.collect();
使用 rayon 实现并行计算,充分利用多核 CPU。
2. 快速哈希算法
传统的 MD5 哈希速度较慢,我选择了 xxHash3,它是目前最快的非加密哈希算法之一:
fn calculate_fast_hash(path: &Path, size: u64) -> Result<String, String> {
use memmap2::Mmap;
use xxhash_rust::xxh3::Xxh3;
const SMALL_FILE: u64 = 1024 * 1024; // 1MB
const THRESHOLD: u64 = 10 * 1024 * 1024; // 10MB
const SAMPLE_SIZE: usize = 1024 * 1024; // 采样 1MB
let file = File::open(path).map_err(|e| e.to_string())?;
if size <= SMALL_FILE {
// 小文件:内存映射,零拷贝
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
let hash = xxhash_rust::xxh3::xxh3_64(&mmap);
return Ok(format!("{:016x}", hash));
}
let mut hasher = Xxh3::new();
if size <= THRESHOLD {
// 中等文件:完整读取
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
hasher.update(&mmap);
} else {
// 大文件:只读头部 + 中间 + 尾部
let mmap = unsafe { Mmap::map(&file) }.map_err(|e| e.to_string())?;
let len = mmap.len();
hasher.update(&mmap[..SAMPLE_SIZE]); // 头部
hasher.update(&mmap[len/2 - SAMPLE_SIZE/2..][..SAMPLE_SIZE]); // 中间
hasher.update(&mmap[len - SAMPLE_SIZE..]); // 尾部
hasher.update(&size.to_le_bytes()); // 文件大小
}
Ok(format!("{:016x}", hasher.digest()))
}
优化点:
- xxHash3:比 MD5 快 5-10 倍
- memmap2:内存映射,零拷贝读取
- 大文件采样:只读头中尾各 1MB,避免全量读取
3. 前后端通信
Tauri 使用 IPC 进行前后端通信。后端定义命令:
#[tauri::command]
pub async fn find_duplicates(app: AppHandle, path: String) -> Result<DedupResult, String> {
// ...
}
前端调用:
import { invoke } from "@tauri-apps/api/core";
const result = await invoke<DedupResult>("find_duplicates", { path });
4. 进度反馈
长时间任务需要显示进度,Tauri 支持事件机制:
后端发送进度:
use tauri::{AppHandle, Emitter};
let _ = app.emit("dedup-progress", DedupProgress {
stage: "计算文件指纹".into(),
current,
total: total_to_hash,
percent,
});
前端监听:
import { listen } from "@tauri-apps/api/event";
useEffect(() => {
const unlisten = listen<DedupProgress>("dedup-progress", (event) => {
setProgress(event.payload);
});
return () => { unlisten.then((fn) => fn()); };
}, []);
5. 内嵌 FFmpeg
视频功能依赖 FFmpeg,为了让用户开箱即用,我把 FFmpeg 打包进了应用:
配置 tauri.conf.json:
{
"bundle": {
"externalBin": [
"binaries/ffmpeg",
"binaries/ffprobe"
]
}
}
Rust 中获取路径:
fn get_ffmpeg_path(app: &AppHandle) -> PathBuf {
app.path()
.resource_dir()
.ok()
.map(|p| p.join("binaries").join("ffmpeg"))
.filter(|p| p.exists())
.unwrap_or_else(|| PathBuf::from("ffmpeg")) // 回退到系统 PATH
}
踩坑记录
1. Tauri 拖拽事件是全局的
最初使用 CSS hidden 隐藏非活动 Tab 来保持状态,但发现拖拽文件时所有 Tab 都会响应。
解决方案:给每个组件传递 active 属性,只有激活的组件才监听拖拽事件。
useEffect(() => {
if (!active) return; // 非激活状态不监听
const unlisten = listen("tauri://drag-drop", (event) => {
if (!active) return;
// 处理拖拽
});
// ...
}, [active]);
2. 并行计算进度跳动
使用 rayon 并行计算时,多线程同时更新计数器,导致进度显示不连续。
解决方案:使用原子变量 + compare_exchange 确保进度单调递增:
let progress_counter = Arc::new(AtomicUsize::new(0));
let last_reported = Arc::new(AtomicUsize::new(0));
// 在并行迭代中
let current = progress_counter.fetch_add(1, Ordering::Relaxed) + 1;
let last = last_reported.load(Ordering::Relaxed);
if current > last && (current - last >= 20 || current == total) {
if last_reported.compare_exchange(last, current, Ordering::SeqCst, Ordering::Relaxed).is_ok() {
// 发送进度
}
}
3. Release 比 Debug 快很多
开发时觉得去重速度一般,打包后发现快了好几倍。
| 模式 | 说明 |
|---|---|
| Debug | 无优化,保留调试信息 |
| Release | LTO、内联、循环展开等优化 |
对于 CPU 密集型任务,Release 版本可能快 3-5 倍。
打包发布
本地打包
# 1. 下载 FFmpeg 静态版本(macOS 示例)
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
unzip /tmp/ffmpeg.zip -d src-tauri/binaries/
mv src-tauri/binaries/ffmpeg src-tauri/binaries/ffmpeg-x86_64-apple-darwin
# 2. 执行打包
pnpm tauri build
产物:src-tauri/target/release/bundle/macos/File Toolkit.app(约 62MB,含 FFmpeg)
GitHub Actions 多平台自动打包
本地打包只能生成当前平台的安装包。要支持 Windows、Linux,需要在对应平台编译。
解决方案:用 GitHub Actions,它提供 macOS、Windows、Linux 虚拟机,可以并行打包。
git push tag v0.2.0
↓
GitHub Actions 触发
↓
┌─────────────────────────────────────────────────────┐
│ 同时启动 4 台虚拟机(并行执行) │
├─────────────┬─────────────┬────────────┬────────────┤
│ macOS VM │ macOS VM │ Windows VM │ Linux VM │
│ (Intel) │ (ARM) │ │ │
├─────────────┼─────────────┼────────────┼────────────┤
│ 装环境 │ 装环境 │ 装环境 │ 装环境 │
│ 下载 FFmpeg │ 下载 FFmpeg │ 下载FFmpeg │ 下载FFmpeg │
│ tauri build │ tauri build │ tauri build│ tauri build│
├─────────────┼─────────────┼────────────┼────────────┤
│ .dmg │ .dmg │ .msi │ .deb │
│ (x86_64) │ (aarch64) │ .exe │ .AppImage │
└─────────────┴─────────────┴────────────┴────────────┘
↓
全部上传到 GitHub Release
核心配置 .github/workflows/release.yml:
name: Release
on:
push:
tags:
- 'v*'
jobs:
build:
strategy:
matrix:
include:
- platform: macos-latest
target: x86_64-apple-darwin
- platform: macos-latest
target: aarch64-apple-darwin
- platform: windows-latest
target: x86_64-pc-windows-msvc
- platform: ubuntu-22.04
target: x86_64-unknown-linux-gnu
runs-on: ${{ matrix.platform }}
steps:
- uses: actions/checkout@v4
- uses: actions/setup-node@v4
- uses: pnpm/action-setup@v4
- uses: dtolnay/rust-toolchain@stable
# 各平台下载对应的 FFmpeg
- name: Download FFmpeg (macOS)
if: matrix.platform == 'macos-latest'
run: |
curl -L "https://evermeet.cx/ffmpeg/getrelease/ffmpeg/zip" -o /tmp/ffmpeg.zip
# ...
- name: Download FFmpeg (Windows)
if: matrix.platform == 'windows-latest'
shell: pwsh
run: |
Invoke-WebRequest -Uri "https://www.gyan.dev/ffmpeg/builds/..." -OutFile ...
# ...
# 打包并上传到 Release
- uses: tauri-apps/tauri-action@v0
with:
tagName: ${{ github.ref_name }}
releaseDraft: true
args: --target ${{ matrix.target }}
使用方式:
git tag v0.2.0
git push origin v0.2.0
# 自动触发,完成后去 Releases 页面发布
为什么能跨平台?
- 前端:React 编译成 HTML/CSS/JS,哪都能跑
- 后端:Rust 在目标平台的虚拟机上编译,生成原生二进制
- FFmpeg:每个平台下载对应的静态版本
本质是在真实目标平台上编译,GitHub 免费提供这些虚拟机。
总结
这个项目让我对 Tauri 2.0 有了更深的理解:
- Rust 性能确实强:文件哈希、并行计算等场景优势明显
- Tauri 开发体验不错:前后端分离,IPC 通信简单
- 包体积小:不含 FFmpeg 只有 10MB 左右
- 跨平台:一套代码,多端运行
如果你也想尝试 Tauri,这个项目可以作为参考。
GitHub: github.com/220529/file…
欢迎 Star ⭐️
相关文章: