ファイルのパーミッション(権限)を変更する

OS がファイルに持つアクセス権を変える。fs プラグインに chmod は無く、作成時の mode と Rust の set_permissions() を使う。Windows の読み取り専用と Unix の rwx の違いも示す。

ファイルシステム 対象: Tauri 2.x 更新日: 読了目安: 約10分 fs-016
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 作るときにパーミッションを決める
  4. 今のパーミッションを読む
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

ここで扱う「権限」は、OS がファイルごとに持つアクセス権(パーミッション)です。Tauri の capability に書く fs:allow-write-text-file などの「権限」は、フロントエンドがどのコマンドを呼べるかを決める別の仕組みで、capability で許可していても OS のパーミッションで拒否されれば書き込めません。秘密の情報を自分だけが読めるファイルにする、ダウンロードしたツールを実行できるようにする、設定ファイルを読み取り専用にする、といった変更は、fs プラグインでは作るときの mode でしか指定できず、既存のファイルは Rust の std::fs::set_permissions() で変えます。

変えられる内容は、Windows と macOS / Linux で大きく違います。

項目WindowsmacOS / Linux
変えられるもの読み取り専用属性のオン・オフだけ所有者・グループ・その他それぞれの読み・書き・実行
fs プラグインの mode無視されるファイルを新しく作るときだけ使われる
stat() で読めるものreadonlyreadonly と 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 mode for command chmod: …」: 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) の導入と利用 も検討します。

関連レシピ

参考リンク(公式ドキュメント)

Web Ninja

この記事を書いた人

Web Ninja ウェブエンジニア (Web Engineer)

会社員ネットワークエンジニアから独立してかれこれ 25 年以上 Web エンジニアとして活動中。普段は JavaScript と Node.js を自在に操り、時には C++ や Perl といった古流の技も嗜みます。近年は Tauri × Rust という新たな武器を手に、デスクトップアプリ開発の最前線を駆け抜けています。「作りたい」を「作れる」に変えるための、実践的な「技」をお届けします。

お問い合わせ: tauri.ninja@gmail.com

内容の誤り・動かないコードを報告する