暂无图片
暂无图片
暂无图片
暂无图片
暂无图片

【九】Tauri 应用篇 - 系统文件

浮之静 2022-07-20
2073
文章里包含大量的个人探索学习,无法保证其是否为最佳实践,我会尽量客观地将相关资料及学习过程记录下来。虽然 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')
是等价的。


关注公众号,回复 “tauri”,进群和小伙伴一起交流

References

[1]

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


文章转载自浮之静,如果涉嫌侵权,请发送邮件至:contact@modb.pro进行举报,并提供相关证据,一经查实,墨天轮将立刻删除相关内容。

评论