文件系统
Tauri 提供了安全的文件系统访问 API,允许应用读取、写入和管理文件,同时通过权限系统保护用户数据。
启用文件系统权限
在 src-tauri/capabilities/default.json 中添加权限:
{
"permissions": [
"fs:default",
"fs:allow-read",
"fs:allow-write",
{
"identifier": "fs:scope",
"allow": [
{ "path": "$APP" },
{ "path": "$APP/**" },
{ "path": "$DOCUMENT" },
{ "path": "$DOCUMENT/**" }
]
}
]
}
基础文件操作
读取文件
前端代码:
import { readTextFile, readFile } from "@tauri-apps/api/fs";
// 读取文本文件
async function readText() {
try {
const content = await readTextFile("config.json", {
baseDir: BaseDirectory.AppData
});
console.log(content);
} catch (error) {
console.error("读取失败:", error);
}
}
// 读取二进制文件
async function readBinary() {
const bytes = await readFile("image.png", {
baseDir: BaseDirectory.AppData
});
// bytes 是 Uint8Array
const blob = new Blob([bytes], { type: "image/png" });
const url = URL.createObjectURL(blob);
return url;
}
Rust 代码:
use std::fs;
use tauri::path::BaseDirectory;
#[tauri::command]
fn read_file(app_handle: tauri::AppHandle, filename: String) -> Result<String, String> {
let app_dir = app_handle.path().app_data_dir()
.map_err(|e| e.to_string())?;
let file_path = app_dir.join(filename);
fs::read_to_string(&file_path)
.map_err(|e| format!("读取失败: {}", e))
}
写入文件
前端代码:
import { writeTextFile, writeFile, BaseDirectory } from "@tauri-apps/api/fs";
// 写入文本文件
async function writeText() {
await writeTextFile("notes.txt", "这是我的笔记内容", {
baseDir: BaseDirectory.Document
});
}
// 追加内容
async function appendText() {
await writeTextFile("log.txt", "\n新的日志条目", {
baseDir: BaseDirectory.AppData,
append: true
});
}
// 写入二进制文件
async function writeBinary(data: Uint8Array) {
await writeFile("data.bin", data, {
baseDir: BaseDirectory.AppData
});
}
Rust 代码:
use std::fs;
#[tauri::command]
fn write_file(
app_handle: tauri::AppHandle,
filename: String,
content: String
) -> Result<(), String> {
let app_dir = app_handle.path().app_data_dir()
.map_err(|e| e.to_string())?;
let file_path = app_dir.join(filename);
fs::write(&file_path, content)
.map_err(|e| format!("写入失败: {}", e))
}
删除文件
import { remove } from "@tauri-apps/api/fs";
async function deleteFile() {
await remove("old-file.txt", {
baseDir: BaseDirectory.AppData
});
}
use std::fs;
#[tauri::command]
fn delete_file(app_handle: tauri::AppHandle, filename: String) -> Result<(), String> {
let app_dir = app_handle.path().app_data_dir()
.map_err(|e| e.to_string())?;
let file_path = app_dir.join(filename);
fs::remove_file(&file_path)
.map_err(|e| format!("删除失败: {}", e))
}
目录操作
创建目录
import { createDir } from "@tauri-apps/api/fs";
async function createDirectory() {
await createDir("my-folder", {
baseDir: BaseDirectory.AppData,
recursive: true // 递归创建父目录
});
}
读取目录内容
import { readDir } from "@tauri-apps/api/fs";
async function listFiles() {
const entries = await readDir("", {
baseDir: BaseDirectory.AppData
});
for (const entry of entries) {
console.log(entry.name);
console.log(entry.isFile);
console.log(entry.isDirectory);
}
}
删除目录
import { remove } from "@tauri-apps/api/fs";
async function deleteDirectory() {
await remove("my-folder", {
baseDir: BaseDirectory.AppData,
recursive: true // 递归删除内容
});
}
路径操作
特殊目录
import { appDataDir, documentDir, downloadDir, homeDir } from "@tauri-apps/api/path";
async function getPaths() {
const appData = await appDataDir();
const documents = await documentDir();
const downloads = await downloadDir();
const home = await homeDir();
console.log("应用数据目录:", appData);
console.log("文档目录:", documents);
console.log("下载目录:", downloads);
console.log("用户主目录:", home);
}
路径拼接
import { join, appDataDir } from "@tauri-apps/api/path";
async function buildPath() {
const appDir = await appDataDir();
const configPath = await join(appDir, "config", "settings.json");
console.log(configPath);
}
文件元数据
import { metadata } from "@tauri-apps/api/fs";
async function getFileInfo() {
const info = await metadata("file.txt", {
baseDir: BaseDirectory.AppData
});
console.log("文件大小:", info.size);
console.log("创建时间:", info.createdAt);
console.log("修改时间:", info.modifiedAt);
console.log("访问时间:", info.accessedAt);
console.log("是否只读:", info.readonly);
}
文件复制和移动
import { copyFile, rename } from "@tauri-apps/api/fs";
// 复制文件
async function copy() {
await copyFile("source.txt", "backup.txt", {
fromPathBaseDir: BaseDirectory.AppData,
toPathBaseDir: BaseDirectory.AppData
});
}
// 移动/重命名文件
async function move() {
await rename("old-name.txt", "new-name.txt", {
oldPathBaseDir: BaseDirectory.AppData,
newPathBaseDir: BaseDirectory.AppData
});
}
实战示例
配置文件管理
use serde::{Deserialize, Serialize};
use std::fs;
use std::path::PathBuf;
#[derive(Serialize, Deserialize, Default)]
pub struct AppConfig {
pub theme: String,
pub language: String,
pub auto_save: bool,
}
impl AppConfig {
fn config_path(app_handle: &tauri::AppHandle) -> Result<PathBuf, String> {
let app_dir = app_handle.path().app_data_dir()
.map_err(|e| e.to_string())?;
Ok(app_dir.join("config.json"))
}
pub fn load(app_handle: &tauri::AppHandle) -> Result<Self, String> {
let path = Self::config_path(app_handle)?;
if !path.exists() {
return Ok(Self::default());
}
let content = fs::read_to_string(&path)
.map_err(|e| e.to_string())?;
serde_json::from_str(&content)
.map_err(|e| e.to_string())
}
pub fn save(&self, app_handle: &tauri::AppHandle) -> Result<(), String> {
let path = Self::config_path(app_handle)?;
// 确保目录存在
if let Some(parent) = path.parent() {
fs::create_dir_all(parent)
.map_err(|e| e.to_string())?;
}
let content = serde_json::to_string_pretty(self)
.map_err(|e| e.to_string())?;
fs::write(&path, content)
.map_err(|e| e.to_string())
}
}
#[tauri::command]
fn load_config(app_handle: tauri::AppHandle) -> Result<AppConfig, String> {
AppConfig::load(&app_handle)
}
#[tauri::command]
fn save_config(app_handle: tauri::AppHandle, config: AppConfig) -> Result<(), String> {
config.save(&app_handle)
}
文件备份系统
use std::fs;
use std::path::Path;
use chrono::Local;
#[tauri::command]
async fn backup_file(
app_handle: tauri::AppHandle,
source_path: String
) -> Result<String, String> {
let source = Path::new(&source_path);
// 验证源文件存在
if !source.exists() {
return Err("源文件不存在".to_string());
}
// 创建备份目录
let backup_dir = app_handle.path().app_data_dir()
.map_err(|e| e.to_string())?
.join("backups");
fs::create_dir_all(&backup_dir)
.map_err(|e| e.to_string())?;
// 生成备份文件名
let timestamp = Local::now().format("%Y%m%d_%H%M%S");
let filename = source.file_stem()
.and_then(|s| s.to_str())
.unwrap_or("backup");
let extension = source.extension()
.and_then(|s| s.to_str())
.unwrap_or("");
let backup_name = format!("{}_{}.backup.{}", filename, timestamp, extension);
let backup_path = backup_dir.join(&backup_name);
// 复制文件
fs::copy(&source, &backup_path)
.map_err(|e| format!("备份失败: {}", e))?;
Ok(backup_path.to_string_lossy().to_string())
}
安全最佳实践
1. 路径验证
始终在访问前验证路径:
#[tauri::command]
fn safe_read_file(
app_handle: tauri::AppHandle,
requested_path: String
) -> Result<String, String> {
let app_dir = app_handle.path().app_data_dir()
.map_err(|e| e.to_string())?;
let requested = PathBuf::from(&requested_path);
// 解析为绝对路径
let canonical = if requested.is_absolute() {
requested.canonicalize()
} else {
app_dir.join(&requested).canonicalize()
}.map_err(|_| "无效路径".to_string())?;
// 确保路径在允许范围内
if !canonical.starts_with(&app_dir) {
return Err("路径超出允许范围".to_string());
}
fs::read_to_string(&canonical)
.map_err(|e| e.to_string())
}
2. 权限最小化
{
"permissions": [
{
"identifier": "fs:allow-read",
"allow": [{ "path": "$APP/**" }]
},
{
"identifier": "fs:allow-write",
"allow": [{ "path": "$APP/**" }]
}
]
}
3. 错误处理
不要向用户暴露内部错误细节:
#[tauri::command]
fn user_friendly_read(path: String) -> Result<String, String> {
match fs::read_to_string(&path) {
Ok(content) => Ok(content),
Err(e) => {
log::error!("文件读取失败: path={}, error={}", path, e);
Err("无法读取文件,请检查文件是否存在".to_string())
}
}
}