ファイルやディレクトリを削除する

fs プラグインの remove()(権限 fs:allow-remove)でファイルとフォルダーを削除する。recursive の使い分け、$APPDATA などのスコープの絞り方、Rust で安全に消す方法も示す。

ファイルシステム 対象: Tauri 2.x 更新日: 読了目安: 約9分 fs-007
目次
  1. 前提条件
  2. 範囲(スコープ)の決まり方
  3. 1. フロントエンドから実装する (TypeScript)
  4. ファイル・空のフォルダー・中身ごとのフォルダー
  5. 無ければ何もしない
  6. ユーザーが選んだファイルを確認してから消す
  7. 2. バックエンドから実装する (Rust)
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

一時ファイルの掃除やキャッシュの一掃、ユーザーが選んだファイルの削除には、File System プラグインの remove() を使います。ファイルもフォルダーもこの 1 つで消せ、中身のあるフォルダーは recursive: true を付けたときだけ消えます。削除はゴミ箱を通らず元に戻せないので、どこを消してよいかをスコープ(許可するパスの範囲)で絞ることが何より大切です。Rust の std::fs で消す場合はスコープが効かないため、コマンドの側で対象を確かめます。

前提条件

fs プラグインを追加します。ユーザーに選ばせて消す例ではダイアログプラグインも使います。

npm run tauri add fs
npm run tauri add dialog

削除の権限 fs:allow-remove は fs:default に含まれないので追加します。fs プラグインの操作は、コマンドの許可とパスの範囲の両方がそろって初めて通ります。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "dialog:default",
    {
      "identifier": "fs:allow-remove",
      "allow": [{ "path": "$DOWNLOAD/MyApp/**" }],
      "deny": [
        { "path": "$APPDATA" },
        { "path": "$APPDATA/settings.json" },
        { "path": "$APPDATA/db" },
        { "path": "$APPDATA/db/**" }
      ]
    }
  ]
}

範囲(スコープ)の決まり方

  • パスは $APPDATA・$APPCACHE・$DOWNLOAD などの変数で始めます。BaseDirectory.AppData が $APPDATA に当たり、実際の場所は OS で変わります(アプリ専用のデータ保存フォルダパスを取得する)。
  • * は直下の 1 階層、** は下の階層すべてに一致します。フォルダー自体と中身は別の指定で、公式の fs:scope-appdata-recursive も $APPDATA と $APPDATA/** を並べています。
  • fs:allow-remove の中の allow / deny は削除だけに効きます。fs:scope-download-recursive のような範囲だけの権限は、fs のすべての操作に効きます。
  • deny は allow より優先されます。
  • fs:default は、アプリ用の 5 つのフォルダー($APPCONFIG・$APPDATA・$APPLOCALDATA・$APPCACHE・$APPLOG)とその中身を、すべての操作に効く範囲として含みます。そのため fs:default があれば、fs:allow-remove を足すだけでアプリ用フォルダーの中もフォルダー自体も消せます。

上の例の allow はアプリ用フォルダー以外の場所を足し、deny は消されては困るものを守ります。範囲の判定は渡したパス 1 つにだけ行われ、recursive: true で親フォルダーを消すと deny に書いた中身も一緒に消えるため、$APPDATA 自体も deny に入れています。

ダイアログの open() / save() で選ばれたパスと、ウィンドウにドロップされたファイルは、実行中だけ自動で範囲に加わります。再起動後も覚えておくなら Persisted Scope で権限状態を維持する を使います。

1. フロントエンドから実装する (TypeScript)

ファイル・空のフォルダー・中身ごとのフォルダー

import { remove, BaseDirectory } from '@tauri-apps/plugin-fs';

const baseDir = BaseDirectory.AppCache;

// ファイル。recursive は付けても付けなくても同じ
await remove('cache/thumb-001.png', { baseDir });

// 空のフォルダー。中身があると失敗する(消しすぎを防げる)
await remove('cache/empty', { baseDir });

// フォルダーを中身ごと消す(rm -rf 相当)。元に戻せない
await remove('cache', { baseDir, recursive: true });

パスに空文字列や . を渡すと、baseDir のフォルダーそのものが対象になります。変数が空のまま recursive: true で呼ぶとアプリのデータがまるごと消えるので、呼ぶ前に確かめます。シンボリックリンクはリンクだけが消え、リンク先は残ります。

無ければ何もしない

存在しないパスの remove() は「failed to get metadata of path: …」で始まるエラーになります。「あれば消す」は exists() で確かめてから呼びます。exists() の権限はアプリ用フォルダーなら fs:default に含まれますが、それ以外の場所では fs:allow-exists にも範囲が要ります。

import { exists, remove, BaseDirectory } from '@tauri-apps/plugin-fs';

// 消したら true、もともと無ければ false
export async function removeIfExists(path: string, baseDir: BaseDirectory, recursive = false): Promise<boolean> {
  const p = path.trim();
  if (p === '' || p === '.' || p === './') throw new Error('refusing to remove the base directory');
  if (!(await exists(p, { baseDir }))) return false;
  await remove(p, { baseDir, recursive });
  return true;
}

ユーザーが選んだファイルを確認してから消す

import { ask, open } from '@tauri-apps/plugin-dialog';
import { remove } from '@tauri-apps/plugin-fs';

export async function pickAndDelete(): Promise<void> {
  const file = await open({ multiple: false, directory: false });
  if (!file) return; // キャンセル
  const ok = await ask(`${file}\nを削除します。ゴミ箱には入らず、元に戻せません。`, {
    title: '削除の確認',
    kind: 'warning',
  });
  if (ok) await remove(file); // 選ばれたファイルは範囲に加わっているので allow の追加は不要
}

2. バックエンドから実装する (Rust)

Rust の std::fs には capability もスコープも効きません。受け取ったパスをそのまま remove_dir_all() に渡すコマンドは、JS から何でも消せる穴になります。ファイル名だけを受け取り、消してよいフォルダーの下でパスを組み立てます。ファイルは remove_file()、空のフォルダーは remove_dir()、中身ごとは remove_dir_all() で、対象が無いことは ErrorKind::NotFound で見分けられます。

use std::io::ErrorKind;
use std::path::{Component, Path, PathBuf};
use tauri::Manager;

/// アプリデータの exports フォルダーの中のパスにする。ファイル名 1 つ以外は拒否
fn export_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 dir = app.path().app_data_dir().map_err(|e| e.to_string())?;
    Ok(dir.join("exports").join(name))
}

/// 1 ファイルを消す。消したら true、もともと無ければ false
#[tauri::command]
fn delete_export(app: tauri::AppHandle, name: String) -> Result<bool, String> {
    let path = export_file(&app, &name)?;
    match std::fs::remove_file(&path) {
        Ok(()) => Ok(true),
        Err(e) if e.kind() == ErrorKind::NotFound => Ok(false),
        Err(e) => Err(format!("{}: {e}", path.display())),
    }
}

/// キャッシュ専用のサブフォルダーを中身ごと消し、空で作り直す
#[tauri::command]
fn reset_cache(app: tauri::AppHandle) -> Result<(), String> {
    let dir = app.path().app_cache_dir().map_err(|e| e.to_string())?.join("cache");
    match std::fs::remove_dir_all(&dir) {
        Ok(()) => {}
        Err(e) if e.kind() == ErrorKind::NotFound => {}
        Err(e) => return Err(format!("{}: {e}", dir.display())),
    }
    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![delete_export, reset_cache])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

const removed = await invoke<boolean>('delete_export', { name: 'report-2026-09.csv' });
console.log(removed ? '削除しました' : 'もともとありませんでした');
await invoke('delete_export', { name: '../settings.json' }).catch((e) => console.error(e));
await invoke('reset_cache');

動作確認

npm run tauri dev で起動し、removeIfExists('cache', BaseDirectory.AppCache, true) を 2 回呼ぶと true、false の順に返ります。deny に入れた settings.json の remove() は失敗し、ファイルは残ります。

true
false
forbidden path: C:\Users\me\AppData\Roaming\com.example.app\settings.json

Rust の delete_export に ../settings.json を渡すと「invalid file name: ../settings.json」で拒否されます。

よくあるエラーと対処法

  • 「fs.remove not allowed. Permissions associated with this command: fs:allow-app-write, …」: fs:allow-remove の追加漏れです(この詳しい文面は開発ビルドのもので、リリースビルドでは「Command plugin:fs|remove not allowed by ACL」)。追加して tauri dev を再起動します。
  • 「forbidden path: <パス>, maybe it is not allowed on the scope for allow-remove permission in your capability file」: パスが範囲の外です。変数、* と ** の違い、baseDir の指定漏れを見直します。deny に一致した場合と、リリースビルドでは「forbidden path: <パス>」だけになります。
  • 「failed to remove path: …」: 中身のあるフォルダーを recursive なしで消そうとしたか、ほかのアプリが使っている、書き込み権が無いなどで OS が拒否しました。続く OS のメッセージで見分けます。
  • 「failed to get metadata of path: …」: 消す対象がありません。exists() で確かめるか、このエラーを無視します。

OS ごとの違いと注意点

アプリ用の変数には、OS によって同じフォルダーを指すものがあります。フォルダーの中身を丸ごと消すと別の用途のファイルまで消えるので、キャッシュは cache のような専用のサブフォルダーに置き、消すのもそこだけにします。

  • Windows: $APPCACHE と $APPLOCALDATA は同じフォルダーで、$APPLOG はその中の logs、WebView のデータ(EBWebView)もここにあります。
  • macOS: $APPCONFIG・$APPDATA・$APPLOCALDATA はどれも ~/Library/Application Support/<identifier> です。
  • Linux: $APPDATA と $APPLOCALDATA は同じフォルダーで、$APPLOG と WebView のデータもその中です。
  • macOS / Linux: 範囲の * と ** は . で始まる名前(隠しファイル)に一致しません。.cache などを消すなら範囲に . から書くか、tauri.conf.json の plugins.fs.requireLiteralLeadingDot を false にします。
  • 共通: fs プラグインにはゴミ箱へ移す機能はありません。移動や改名で「消したように見せる」なら ファイル名を変更する・別の場所へ移動する を使います。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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