文章里包含大量的个人探索学习,无法保证其是否为最佳实践,我会尽量客观地将相关资料及学习过程记录下来。虽然 Tauri 经发布 v1.0 版本,但是到目前为止,国内资料仍少的可怜,我个人想基于 Tauri 开发一款工具集(各种小功能)。在这个过程中,我遇到了一些,或者说是一系列的问题,我就想把它们以专栏系列的形式记录下来(好记性不如烂笔头),希望可以帮助到更多和我一样对新技术充满热情的人。 如果这些文章对你有所帮助,可以将文章转发给更多有需要的人。大家的支持也会给我更大的写作动力,感恩。点击“阅读原文”可以跳转到仓库有配套源码可供参考(star 收藏不迷路)。
文件读写可能是和系统交互的最直观操作了
Window 中如何访问 FS?
当 tauri.conf.json
中的 build.withGlobalTauri[1] 设置为 true
时(默认为 false
),可以通过 window.__TAURI__.fs
访问 fs 下的相关 API。
注意:必须将 tauri.allowlist.fs[2] 也添加到 tauri.conf.json,否则会报错:
Unhandled Promise Rejection: The `Fs` module is not enabled. You must enable one of its APIs in the allowlist.
复制
{
"build": {
"beforeBuildCommand": "yarn build",
"beforeDevCommand": "yarn dev",
"devPath": "http://localhost:3000",
"distDir": "../dist",
+ "withGlobalTauri": true // ✅ 新增
},
"tauri": {
"allowlist": {
"fs": {
+ "all": true, // ✅ 新增,启用所有 FS API
"readFile": true, // 也可以单独启用/禁用
"writeFile": true,
"readDir": true,
"copyFile": true,
"createDir": true,
"removeDir": true,
"removeFile": true,
"renameFile": true
}
}
}
}复制
build.withGlobalTauri
为 false
时:
build.withGlobalTauri
为 true
时:
安全
FS 模块防止路径遍历,不允许绝对路径或相对路径(即 /usr/path/to/file
或 ../path/to/file
路径是不被允许的)。使用 FS API 访问的路径必须与基本目录[3](如 App
、Home
、Cache
、Desktop
、Document
、Download
等,未完全列举)之一相关,因此如果需要访问任意文件系统路径,则必须在核心层上编写此类逻辑(自行扩展,可以查看此 issues 了解更多 "core layer" is unclear[4])。
FS API 有一个作用域配置,强制限制你使用 glob 模式的可访问路径。 作用域配置是一组描述允许的文件夹路径的全局模式。例如,以下配置仅允许访问 $APP 目录[5]下 databases 文件夹中的文件:
{
"tauri": {
"allowlist": {
"fs": {
"scope": ["$APP/databases/*"]
}
}
}
}复制
如果使用未在作用域配置上的 URL 执行任何 FS API 会因拒绝访问而导致 promise 拒绝。 请注意,作用域可配置的 API,查看 FS 安全[6]了解更多。
FS API
因 fs 相关 API 均为 异步 I/O(Asynchronous I/O)[7] 操作,故都以 Promise[8] 形式返回结果。需要使用基本目录时可以通过 @tauri-apps/api/path
下的 API 获取,也都以 Promise 形式返回结果。此处以文件写入举例,更多 API 使用,请自行阅读文档。
import { BaseDirectory, writeFile } from '@tauri-apps/api/fs';
// path: 文件路径,与基本目录拼接为完整路径
// contents: 需要写入的文件内容
// dir: 基本目录之一
await writeFile({ path: 'app.conf', contents: 'file contents' }, { dir: BaseDirectory.App });复制
实现任意路径
从 issues 作者评论[9]可知想要绕过 Tauri FS API 定义的基本目录,需要编写自己的 Rust 命令。此处为演示代码,仅提供参考:
Step 1
编辑
src-tauri/src/main.rs
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
fn main() {
let context = tauri::generate_context!();
tauri::Builder::default()
// ✅ 在这里传递自定义命令
.invoke_handler(tauri::generate_handler![my_read_file])
.run(context)
.expect("error while running OhMyBox application");
}
#[tauri::command]
async fn my_read_file(path: std::path::PathBuf) -> String {
// 读取文件内容,以文本字符串形式返回
std::fs::read_to_string(path).unwrap()
}复制
Step 2
在前端 js 中调用自定义方法
import { invoke } from '@tauri-apps/api';
async function read_hosts() {
// 注: `/etc/hosts` 为自定义路径,而非基本目录之一
const content = await invoke('my_read_file', { path: '/etc/hosts' });
console.log(content);
}
read_hosts();复制
扩展文件元数据
虽然 FS API 提供了文件读写能力,但是却无法获取到元数据(如创建,修改时间等)。当 API 不支持时,我们就需要编写 Tauri 插件来对其进行扩展。官方提供了一个名为 tauri-plugin-fs-extra[10] 的插件,但是截止到写文章时,并不能使用任何一种方式安装到本地,所以需要开发者手动将源代码复制到本地。
源码主要分为两个部分,一部分是 Rust 实现的 Tauri 插件,另一部分是插件调用:
插件部分
新建
src-tauri/src/fs_extra.rs
文件
// https://github.com/tauri-apps/tauri-plugin-fs-extra/blob/dev/src/lib.rs
use serde::{ser::Serializer, Serialize};
use tauri::{command, plugin::Plugin, Invoke, Runtime};
use std::{
path::PathBuf,
time::{SystemTime, UNIX_EPOCH},
};
#[cfg(unix)]
use std::os::unix::fs::{MetadataExt, PermissionsExt};
#[cfg(windows)]
use std::os::windows::fs::MetadataExt;
type Result<T> = std::result::Result<T, Error>;
#[derive(Debug, thiserror::Error)]
pub enum Error {
#[error(transparent)]
Io(#[from] std::io::Error),
}
impl Serialize for Error {
fn serialize<S>(&self, serializer: S) -> std::result::Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(self.to_string().as_ref())
}
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Permissions {
readonly: bool,
#[cfg(unix)]
mode: u32,
}
#[cfg(unix)]
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct UnixMetadata {
dev: u64,
ino: u64,
mode: u32,
nlink: u64,
uid: u32,
gid: u32,
rdev: u64,
blksize: u64,
blocks: u64,
}
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct Metadata {
accessed_at_ms: u64,
created_at_ms: u64,
modified_at_ms: u64,
is_dir: bool,
is_file: bool,
is_symlink: bool,
size: u64,
permissions: Permissions,
#[cfg(unix)]
#[serde(flatten)]
unix: UnixMetadata,
#[cfg(windows)]
file_attributes: u32,
}
fn system_time_to_ms(time: std::io::Result<SystemTime>) -> u64 {
time
.map(|t| {
let duration_since_epoch = t.duration_since(UNIX_EPOCH).unwrap();
duration_since_epoch.as_millis() as u64
})
.unwrap_or_default()
}
#[command]
async fn metadata(path: PathBuf) -> Result<Metadata> {
let metadata = std::fs::metadata(path)?;
let file_type = metadata.file_type();
let permissions = metadata.permissions();
Ok(Metadata {
accessed_at_ms: system_time_to_ms(metadata.accessed()),
created_at_ms: system_time_to_ms(metadata.created()),
modified_at_ms: system_time_to_ms(metadata.modified()),
is_dir: file_type.is_dir(),
is_file: file_type.is_file(),
is_symlink: file_type.is_symlink(),
size: metadata.len(),
permissions: Permissions {
readonly: permissions.readonly(),
#[cfg(unix)]
mode: permissions.mode(),
},
#[cfg(unix)]
unix: UnixMetadata {
dev: metadata.dev(),
ino: metadata.ino(),
mode: metadata.mode(),
nlink: metadata.nlink(),
uid: metadata.uid(),
gid: metadata.gid(),
rdev: metadata.rdev(),
blksize: metadata.blksize(),
blocks: metadata.blocks(),
},
#[cfg(windows)]
file_attributes: metadata.file_attributes(),
})
}
#[command]
async fn exists(path: PathBuf) -> bool {
path.exists()
}
/// Tauri plugin.
pub struct FsExtra<R: Runtime> {
invoke_handler: Box<dyn Fn(Invoke<R>) + Send + Sync>,
}
impl<R: Runtime> Default for FsExtra<R> {
fn default() -> Self {
Self {
invoke_handler: Box::new(tauri::generate_handler![exists, metadata]),
}
}
}
impl<R: Runtime> Plugin<R> for FsExtra<R> {
fn name(&self) -> &'static str {
"fs-extra"
}
fn extend_api(&mut self, message: Invoke<R>) {
(self.invoke_handler)(message)
}
}复制
将插件添加到
src-tauri/src/main.rs
中
#![cfg_attr(
all(not(debug_assertions), target_os = "windows"),
windows_subsystem = "windows"
)]
mod fs_extra;
fn main() {
let context = tauri::generate_context!();
tauri::Builder::default()
// ✅ 在这里使用插件
.plugin(fs_extra::FsExtra::default())
.run(context)
.expect("error while running OhMyBox application");
}复制
插件调用
新建
src/plugins/fsExtra.ts
文件
// https://github.com/tauri-apps/tauri-plugin-fs-extra/blob/dev/webview-src/index.ts
import { invoke } from '@tauri-apps/api/tauri';
export interface Permissions {
/**
* `true` if these permissions describe a readonly (unwritable) file.
*/
readonly: boolean;
/**
* The underlying raw `st_mode` bits that contain the standard Unix permissions for this file.
*/
mode: number | undefined;
}
/**
* Metadata information about a file.
* This structure is returned from the `metadata` function or method
* and represents known metadata about a file such as its permissions, size, modification times, etc.
*/
export interface Metadata {
/**
* The last access time of this metadata.
*/
accessedAt: Date;
/**
* The creation time listed in this metadata.
*/
createdAt: Date;
/**
* The last modification time listed in this metadata.
*/
modifiedAt: Date;
/**
* `true` if this metadata is for a directory.
*/
isDir: boolean;
/**
* `true` if this metadata is for a regular file.
*/
isFile: boolean;
/**
* `true` if this metadata is for a symbolic link.
*/
isSymlink: boolean;
/**
* The size of the file, in bytes, this metadata is for.
*/
size: number;
/**
* The permissions of the file this metadata is for.
*/
permissions: Permissions;
/**
* The ID of the device containing the file. Only available on Unix.
*/
dev: number | undefined;
/**
* The inode number. Only available on Unix.
*/
ino: number | undefined;
/**
* The rights applied to this file. Only available on Unix.
*/
mode: number | undefined;
/**
* The number of hard links pointing to this file. Only available on Unix.
*/
nlink: number | undefined;
/**
* The user ID of the owner of this file. Only available on Unix.
*/
uid: number | undefined;
/**
* The group ID of the owner of this file. Only available on Unix.
*/
gid: number | undefined;
/**
* The device ID of this file (if it is a special one). Only available on Unix.
*/
rdev: number | undefined;
/**
* The block size for filesystem I/O. Only available on Unix.
*/
blksize: number | undefined;
/**
* The number of blocks allocated to the file, in 512-byte units. Only available on Unix.
*/
blocks: number | undefined;
}
interface BackendMetadata {
accessedAtMs: number;
createdAtMs: number;
modifiedAtMs: number;
isDir: boolean;
isFile: boolean;
isSymlink: boolean;
size: number;
permissions: Permissions;
dev: number | undefined;
ino: number | undefined;
mode: number | undefined;
nlink: number | undefined;
uid: number | undefined;
gid: number | undefined;
rdev: number | undefined;
blksize: number | undefined;
blocks: number | undefined;
}
export async function metadata(path: string): Promise<Metadata> {
return await invoke<BackendMetadata>('plugin:fs-extra|metadata', { path }).then((metadata) => {
const { accessedAtMs, createdAtMs, modifiedAtMs, ...data } = metadata;
return {
accessedAt: new Date(accessedAtMs),
createdAt: new Date(createdAtMs),
modifiedAt: new Date(modifiedAtMs),
...data,
};
});
}
export async function exists(path: string): Promise<boolean> {
return await invoke('plugin:fs-extra|exists', { path });
}复制
在项目中使用
// `@/` 是 src 目录的别名
import { metadata } from '@/plugins/fsExtra';
// 需要获取元数据的路径,必须是被允许的基本目录下的路径
await metadata('/Users/lencx/.omb/canvas');复制
注:图中的 invoke('plugin:fs-extra|metadata', { path: '/Users/lencx/.omb/canvas'})
与 metadata('/Users/lencx/.omb/canvas')
是等价的。
References
build.withGlobalTauri: https://tauri.app/v1/api/config/#buildconfig.withglobaltauri
[2]tauri.allowlist.fs: https://tauri.app/v1/api/config/#allowlistconfig.fs
[3]基本目录: https://tauri.app/v1/api/js/enums/fs.basedirectory/
[4]"core layer" is unclear: https://github.com/tauri-apps/tauri-docs/issues/732
[5]$APP 目录: https://tauri.app/v1/api/js/modules/path/#appdir
[6]FS 安全: https://tauri.app/v1/api/js/modules/fs#security
[7]异步 I/O(Asynchronous I/O): https://en.wikipedia.org/wiki/Asynchronous_I/O
[8]Promise: https://developer.mozilla.org/en-US/docs/Web/JavaScript/Reference/Global_Objects/Promise
[9]issues 作者评论: https://github.com/tauri-apps/tauri-docs/issues/732#issuecomment-1174479078
[10]tauri-plugin-fs-extra: https://github.com/tauri-apps/tauri-plugin-fs-extra