跳到主要内容

文件系统

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())
}
}
}

下一步