跳到主要内容

系统文件

lencx
{折腾 ⇌ 迷茫 ⇌ 思考]ing,在路上...
​关注他
15 人赞同了该文章
仓库:lencx/OhMyBox
阅读原文(首发于 GitHub)
关注《浮之静》公众号,私信作者,进 Tauri 技术交流群
文件读写可能是和系统交互的最直观操作了
Window 中如何访问 FS?
当 tauri.conf.json 中的 build.withGlobalTauri 设置为 true 时(默认为 false),可以通过 window.__TAURI__.fs 访问 fs 下的相关 API。

注意:必须将 tauri.allowlist.fs 也添加到 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 访问的路径必须与基本目录(如 App、Home、Cache、Desktop、Document、Download等,未完全列举)之一相关,因此如果需要访问任意文件系统路径,则必须在核心层上编写此类逻辑(自行扩展,可以查看此 issues 了解更多 "core layer" is unclear)。

FS API 有一个作用域配置,强制限制你使用 glob 模式的可访问路径。 作用域配置是一组描述允许的文件夹路径的全局模式。例如,以下配置仅允许访问 $APP 目录下 databases 文件夹中的文件:

{
"tauri": {
"allowlist": {
"fs": {
"scope": ["$APP/databases/*"]
}
}
}
}
如果使用未在作用域配置上的 URL 执行任何 FS API 会因拒绝访问而导致 promise 拒绝。 请注意,作用域可配置的 API,查看 FS 安全了解更多。

FS API
因 fs 相关 API 均为 异步 I/O(Asynchronous I/O) 操作,故都以 Promise 形式返回结果。需要使用基本目录时可以通过 @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 作者评论可知想要绕过 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 的插件,但是截止到写文章时,并不能使用任何一种方式安装到本地,所以需要开发者手动将源代码复制到本地。

源码主要分为两个部分,一部分是 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') 是等价的。