ここで扱う「権限」は、OS がファイルごとに持つアクセス権(パーミッション)です。Tauri の capability に書く fs:allow-write-text-file などの「権限」は、フロントエンドがどのコマンドを呼べるかを決める別の仕組みで、capability で許可していても OS のパーミッションで拒否されれば書き込めません。秘密の情報を自分だけが読めるファイルにする、ダウンロードしたツールを実行できるようにする、設定ファイルを読み取り専用にする、といった変更は、fs プラグインでは作るときの mode でしか指定できず、既存のファイルは Rust の std::fs::set_permissions() で変えます。
変えられる内容は、Windows と macOS / Linux で大きく違います。
| 項目 | Windows | macOS / Linux |
|---|---|---|
| 変えられるもの | 読み取り専用属性のオン・オフだけ | 所有者・グループ・その他それぞれの読み・書き・実行 |
fs プラグインの mode | 無視される | ファイルを新しく作るときだけ使われる |
stat() で読めるもの | readonly | readonly と mode |
| 実行できるかどうか | 拡張子(.exe など)で決まる | 実行のビット(x)で決まる |
| Rust で変える方法 | Permissions::set_readonly() | PermissionsExt::set_mode() |
macOS / Linux のパーミッションは 0o640 のような 8 進数 3 桁で表し、左から所有者・グループ・その他、各桁は読み(4)・書き(2)・実行(1)の合計です。0o640 なら所有者は読み書き、グループは読みだけ、その他は何もできません(rw-r-----)。
前提条件
作るときの指定と、今の状態の読み取りには fs プラグインを使います。
npm run tauri add fs
writeTextFile() の権限 fs:allow-write-text-file と、stat() の権限 fs:allow-stat は fs:default に含まれないので追加します。アプリ用フォルダー($APPDATA など)の中なら、範囲(スコープ)は fs:default に含まれています。範囲の書き方は ファイルやディレクトリを削除する にまとめています。2 章の Rust のコマンドには capability もスコープも効かないので、変えてよいファイルかどうかはコマンドの中で確かめます。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-write-text-file",
"fs:allow-stat"
]
}
1. フロントエンドから実装する (TypeScript)
作るときにパーミッションを決める
writeTextFile()・writeFile()・open() の mode は、ファイルを新しく作るときのパーミッションです。既にあるファイルに書いてもパーミッションは変わりません。指定した値からは OS の umask(多くは 0o022)のビットが落とされるので、0o666 は 0o644 になることがあります。0o600 のようにグループとその他に何も与えない値は、よくある umask では変わりません。フォルダーは mkdir() の mode で同じように指定できます(新しいディレクトリ(フォルダ)を作成する)。
import { mkdir, writeTextFile, BaseDirectory } from '@tauri-apps/plugin-fs';
const baseDir = BaseDirectory.AppData;
// 自分だけが入れるフォルダーに、自分だけが読み書きできるファイルとして保存する
export async function saveToken(token: string): Promise<void> {
await mkdir('secrets', { baseDir, recursive: true, mode: 0o700 }); // rwx------
await writeTextFile('secrets/token.json', JSON.stringify({ token }), {
baseDir,
mode: 0o600, // rw-------。新しく作るときだけ使われ、Windows では無視される
});
}
今のパーミッションを読む
stat() の readonly はどの OS でも使え、macOS / Linux では mode も読めます(Windows では null)。mode の上位には種類を表すビットも入っているので、0o777 との AND を取ってから表示します。readonly は、Windows では読み取り専用属性、macOS / Linux では書き込みのビットが 1 つも無いことを表すだけで、false でも今のユーザーが書けるとは限りません。サイズや日時など、ほかの項目は ファイルのメタデータ(サイズ・作成日時・更新日時)を取得する で扱います。
import { stat, BaseDirectory } from '@tauri-apps/plugin-fs';
// 0o644 → "rw-r--r--"
export function toRwx(bits: number): string {
const flags = 'rwxrwxrwx';
let out = '';
for (let i = 0; i < 9; i++) out += bits & (0o400 >> i) ? flags[i] : '-';
return out;
}
export async function describePermissions(path: string, baseDir: BaseDirectory): Promise<string> {
const info = await stat(path, { baseDir }); // fs:allow-stat が必要
if (info.mode === null) return info.readonly ? '読み取り専用' : '読み取り専用ではない'; // Windows
const bits = info.mode & 0o777; // 種類を表す上位のビットを落とす
return `${bits.toString(8).padStart(3, '0')} (${toRwx(bits)})`;
}
2. バックエンドから実装する (Rust)
既存のファイルは std::fs::set_permissions() で変えます。読み取り専用を外すのに Permissions::set_readonly(false) を使うと、macOS / Linux では所有者だけでなくグループとその他にも書き込みを許してしまいます。そこで Unix では PermissionsExt でビットを直接操作し、set_readonly() は Windows でだけ使います。また macOS / Linux の set_permissions() はシンボリックリンクの先のファイルを変えるので、リンクは受け付けません。#[cfg(unix)] の部分は macOS / Linux でだけコンパイルされます。
use std::path::{Component, Path, PathBuf};
use tauri::Manager;
/// アプリデータの中の、名前 1 つだけのファイルにする。シンボリックリンクは拒否
fn app_data_file(app: &tauri::AppHandle, name: &str) -> Result<PathBuf, String> {
let mut parts = Path::new(name).components();
if !matches!((parts.next(), parts.next()), (Some(Component::Normal(_)), None)) {
return Err(format!("invalid file name: {name}"));
}
let path = app.path().app_data_dir().map_err(|e| e.to_string())?.join(name);
let meta = std::fs::symlink_metadata(&path).map_err(|e| format!("{}: {e}", path.display()))?;
if meta.file_type().is_symlink() {
return Err(format!("symlink is not allowed: {name}")); // リンク先を変えてしまうため
}
Ok(path)
}
/// macOS / Linux でパーミッションを 0o755 などにする。Windows では何もせず false
#[tauri::command]
fn chmod(app: tauri::AppHandle, name: String, mode: u32) -> Result<bool, String> {
let path = app_data_file(&app, &name)?;
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let perms = std::fs::Permissions::from_mode(mode & 0o777);
std::fs::set_permissions(&path, perms).map_err(|e| format!("{}: {e}", path.display()))?;
Ok(true)
}
#[cfg(not(unix))]
{
let _ = (path, mode); // Windows には rwx のビットが無い
Ok(false)
}
}
/// 読み取り専用のオン・オフ
#[tauri::command]
fn set_read_only(app: tauri::AppHandle, name: String, read_only: bool) -> Result<(), String> {
let path = app_data_file(&app, &name)?;
let mut perms = std::fs::metadata(&path).map_err(|e| e.to_string())?.permissions();
#[cfg(unix)]
{
use std::os::unix::fs::PermissionsExt;
let mode = perms.mode();
// オン: 全員の書き込みを外す / オフ: 所有者の書き込みだけを戻す
perms.set_mode(if read_only { mode & !0o222 } else { mode | 0o200 });
}
#[cfg(not(unix))]
{
perms.set_readonly(read_only); // Windows の読み取り専用属性
}
std::fs::set_permissions(&path, perms).map_err(|e| format!("{}: {e}", path.display()))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![chmod, set_read_only])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
JS からは、mode を 0o 付きの数値で渡します。Rust の read_only は readOnly と camelCase で渡します。
import { invoke } from '@tauri-apps/api/core';
// ダウンロードしたツールを実行できるようにする(rwxr-xr-x)
const applied = await invoke<boolean>('chmod', { name: 'mytool', mode: 0o755 });
console.log(applied ? '755 にしました' : 'Windows なので変更なし');
await invoke('set_read_only', { name: 'settings.json', readOnly: true });
動作確認
macOS / Linux で npm run tauri dev を起動し、saveToken('abc') の後に describePermissions('secrets/token.json', BaseDirectory.AppData) を呼ぶと、1 行目が返ります。chmod で 0o755 にしたファイルは 2 行目のようになり、ターミナルの ls -l でも同じ並びが見えます。
600 (rw-------)
755 (rwxr-xr-x)
Windows では chmod が false を返します。set_read_only で読み取り専用にすると、エクスプローラーのプロパティの「読み取り専用」にチェックが付き、writeTextFile() での上書きが失敗します。
よくあるエラーと対処法
- 「failed to open file at path: <パス> with error: …」: 読み取り専用のファイルに
writeTextFile()などで書き込もうとしました。続く OS のメッセージは、アクセスが拒否された趣旨のものです。set_read_onlyで外してから書きます。 - 所有者でないファイルを変えられない: アプリは起動したユーザーの権利で動くので、ほかのユーザーやシステムのファイルのパーミッションは変えられません。macOS / Linux では os error 1、Windows では os error 5 の、許可されていない趣旨のエラーになります。管理者として動いているかは アプリが管理者権限で動いているか確認する で確かめられます。
- 「invalid args
modefor commandchmod: …」:modeに'755'のような文字列を渡しました。数値の0o755を渡します。0oを付けずに755と書くと 10 進数の 755(8 進数の1363)になり、エラーにならないまま意図しないパーミッションが付きます。 mode: 0o600で書いたのに600にならない: ファイルが既にあったか、Windows で動いています。既存のファイルは書いた後にchmodで変えます。
OS ごとの違いと注意点
- Windows: 変えられるのは読み取り専用属性だけで、「ほかのユーザーには読ませない」といったアクセス許可は、この方法では変えられません。Windows 8 以降では、フォルダーに付けた読み取り専用属性は無視されます。
- macOS / Linux:
set_readonly(false)は全員に書き込みを許すので使わず、set_mode()で必要なビットだけを変えます。 - 共通: 読み取り専用は中身の書き換えを防ぐだけで、削除を防ぐ仕組みとしては頼れません。消されては困るファイルは、capability の
denyで守ります(ファイルやディレクトリを削除する)。 - 共通:
copyFile()はパーミッションも一緒にコピーするので、読み取り専用のファイルのコピーは読み取り専用になります(ファイルをコピー・複製する)。 - 秘密の情報: パーミッションは暗号化ではなく、Windows では
modeも効きません。パスワードやトークンは 暗号化ストレージ (Stronghold) の導入と利用 も検討します。
