書き出し先やログ、プロジェクトごとのフォルダーを作るには、File System プラグインの mkdir() を使います。recursive: true を付けると途中のフォルダーもまとめて作り、既にあってもエラーになりません(mkdir -p 相当)。見落としやすいのは、$APPDATA などのアプリ用フォルダー自体が、初回起動の時点ではまだ無い場合があることです。Rust からは std::fs::create_dir_all() を使い、起動時に作っておけばフロントエンドは作成を気にせずに済みます。
前提条件
fs プラグインを追加します。書き出し先を選ばせる例ではダイアログプラグインも使います。
npm run tauri add fs
npm run tauri add dialog
mkdir() の権限 fs:allow-mkdir は fs:default に含まれ、アプリ用の 5 つのフォルダー($APPCONFIG・$APPDATA・$APPLOCALDATA・$APPCACHE・$APPLOG)の中なら追加は要りません。それ以外の場所に作るときは、範囲(スコープ)を付けて足します。範囲の書き方と fs:default との関係は ファイルやディレクトリを削除する にまとめています。
{
"$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-mkdir",
"allow": [{ "path": "$DOCUMENT/MyApp" }, { "path": "$DOCUMENT/MyApp/**" }]
}
]
}
範囲の判定は、mkdir() に渡した最後のパスにだけ行われます。recursive: true で MyApp/exports/2026 を作ると、判定されるのは $DOCUMENT/MyApp/exports/2026 だけで、途中の MyApp と exports も一緒に作られます。MyApp だけを作ることもあるなら、上の例のようにフォルダー自体($DOCUMENT/MyApp)も書いておきます。
1. フロントエンドから実装する (TypeScript)
recursive を付けて作る
アプリ用フォルダーは、アプリが最初に何かを作るまで存在しないことがあります。recursive なしで mkdir('logs', { baseDir: BaseDirectory.AppData }) を呼ぶと、親の $APPDATA がまだ無ければ失敗します。WebView のデータなどで先にできているフォルダーもあるので、開発した OS では気付かず、別の OS や別のフォルダーで初めて失敗することがあります。アプリ用フォルダーの中には常に recursive: true で作ります。
import { mkdir, BaseDirectory } from '@tauri-apps/plugin-fs';
const baseDir = BaseDirectory.AppData;
// $APPDATA 自体が無くても、親ごとまとめて作る
await mkdir('exports/2026-09', { baseDir, recursive: true });
// 2 回目もエラーにならない(フォルダーとして既にあれば何もしない)
await mkdir('exports/2026-09', { baseDir, recursive: true });
// macOS / Linux では自分だけが読み書きできるフォルダーにする(Windows では mode は無視される)
await mkdir('private', { baseDir, recursive: true, mode: 0o700 });
recursive: true は「フォルダーとして既にあれば何もしない」ので、exists() で確かめてから作る必要はありません。確かめてから作るまでの間に別の処理が同じフォルダーを作っても失敗しません。ただし、同じ名前のファイルがあるときは失敗します。
同じ名前を避けて新しく作る
「新しいプロジェクト」のように既存のフォルダーを使い回したくないときは、逆に recursive を付けません。既にあると失敗するので、そのときだけ番号をずらします。エラー文だけでは失敗の理由を判定しにくいため、exists() で「既にあるから失敗した」のかを確かめ、それ以外のエラーはそのまま呼び出し元に返します。
import { exists, mkdir, BaseDirectory } from '@tauri-apps/plugin-fs';
// projects/<name> を作る。同名があれば「name (2)」「name (3)」とずらし、作った名前を返す
export async function createUniqueFolder(name: string): Promise<string> {
const baseDir = BaseDirectory.AppData;
await mkdir('projects', { baseDir, recursive: true });
for (let n = 1; n <= 100; n++) {
const candidate = n === 1 ? name : `${name} (${n})`;
try {
await mkdir(`projects/${candidate}`, { baseDir }); // recursive なし: 既にあると失敗する
return candidate;
} catch (e) {
// 「既にある」以外の失敗(書き込み権が無いなど)はそのまま投げる
if (!(await exists(`projects/${candidate}`, { baseDir }))) throw e;
}
}
throw new Error(`too many folders named ${name}`);
}
ユーザーが選んだ場所に作る
ダイアログで選ばれたフォルダーは、その起動中だけ自動で範囲に加わるので、capability に場所を書かずに直下へ作れます。範囲に入るのは選んだフォルダーとその直下だけなので、MyApp Export/images のように 2 階層以上を作るなら open() に recursive: true を付けます(フォルダ(ディレクトリ)を選択させる)。
import { open } from '@tauri-apps/plugin-dialog';
import { mkdir } from '@tauri-apps/plugin-fs';
import { sep } from '@tauri-apps/api/path';
// 書き出し先を選ばせ、その直下に「MyApp Export」を作ってパスを返す
export async function prepareExportFolder(): Promise<string | null> {
const dir = await open({ directory: true, title: '書き出し先を選択' });
if (!dir) return null; // キャンセル
const target = `${dir}${sep()}MyApp Export`;
await mkdir(target, { recursive: true }); // 直下なので範囲の中。前回作った分があってもよい
return target;
}
2. バックエンドから実装する (Rust)
std::fs::create_dir_all() が recursive: true、create_dir() が recursive なしに当たります。setup で必要なフォルダーを作っておけば、フロントエンドのコードはすべて「フォルダーはある」前提で書けます。create_dir() が既存のフォルダーで失敗したことは ErrorKind::AlreadyExists で見分けられるので、JS 版より素直に同名を避けられます。Rust の std::fs には capability もスコープも効かないため、JS から受け取った名前は .. や区切りを含まないか確かめてから使います。
use std::io::ErrorKind;
use std::path::{Component, Path};
use tauri::Manager;
/// 起動時にアプリ用のフォルダーを作っておく(既にあれば何もしない)
fn prepare_dirs(app: &tauri::App) -> Result<(), Box<dyn std::error::Error>> {
let data = app.path().app_data_dir()?;
for sub in ["exports", "projects"] {
std::fs::create_dir_all(data.join(sub))?;
}
std::fs::create_dir_all(app.path().app_log_dir()?)?;
Ok(())
}
/// `..` や区切りを含まない、名前 1 つだけか
fn is_plain_name(name: &str) -> bool {
let mut parts = Path::new(name).components();
matches!((parts.next(), parts.next()), (Some(Component::Normal(_)), None))
}
/// projects の中に新しいフォルダーを作る。同名があれば番号をずらし、作ったパスを返す
#[tauri::command]
fn create_project(app: tauri::AppHandle, name: String) -> Result<String, String> {
if !is_plain_name(&name) {
return Err(format!("invalid folder name: {name}"));
}
let base = app.path().app_data_dir().map_err(|e| e.to_string())?.join("projects");
std::fs::create_dir_all(&base).map_err(|e| e.to_string())?;
for n in 1..=100 {
let candidate = if n == 1 { name.clone() } else { format!("{name} ({n})") };
let path = base.join(&candidate);
match std::fs::create_dir(&path) {
Ok(()) => return Ok(path.display().to_string()),
Err(e) if e.kind() == ErrorKind::AlreadyExists => continue, // 同名がある
Err(e) => return Err(format!("{}: {e}", path.display())),
}
}
Err(format!("too many folders named {name}"))
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.plugin(tauri_plugin_dialog::init())
.setup(|app| {
prepare_dirs(app)?;
Ok(())
})
.invoke_handler(tauri::generate_handler![create_project])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
console.log(await invoke<string>('create_project', { name: '日報' }));
console.log(await invoke<string>('create_project', { name: '日報' })); // 2 回目は「日報 (2)」
await invoke('create_project', { name: '../日報' }).catch((e) => console.error(e));
動作確認
npm run tauri dev で起動し、上の 3 行を実行すると、Windows では次のように出ます(ユーザー名と identifier は環境によって変わります)。
C:\Users\me\AppData\Roaming\com.example.app\projects\日報
C:\Users\me\AppData\Roaming\com.example.app\projects\日報 (2)
invalid folder name: ../日報
JS の createUniqueFolder('日報') を続けて呼ぶと、同じ projects の中に「日報 (3)」「日報 (4)」と増えていきます。
よくあるエラーと対処法
- 「failed to create directory at path: <パス> with error: …」: OS がフォルダーの作成を拒否しました。続くメッセージで、親フォルダーが無い(
recursiveの付け忘れ、初回起動のアプリ用フォルダー)、既にある(recursiveなし)、同じ名前のファイルがある、書き込み権が無い、のどれかを見分けます。 - 「forbidden path: <パス>, maybe it is not allowed on the scope for
allow-mkdirpermission in your capability file」: 作ろうとした最後のパスが範囲の外です。baseDirを付け忘れて相対パスのまま渡していないかも確かめます。リリースビルドでは「forbidden path: <パス>」だけになります。 - 「fs.mkdir not allowed. Permissions associated with this command: fs:allow-app-write, …」: capability に
fs:defaultもfs:allow-mkdirもありません(リリースビルドでは「Command plugin:fs|mkdir not allowed by ACL」)。 - ユーザーが入力した名前で失敗する:
..を含むパスは「cannot traverse directory, rewrite the path without the use of../」を含むエラーで拒否されます。Windows ではフォルダー名に\ / : * ? " < > |を使えないので、入力から取り除いてから渡します。
OS ごとの違いと注意点
- Windows:
modeは無視されます。 - macOS / Linux:
modeの既定は0o777で、実際にはそこからプロセスの umask(よくある値は022)が引かれます。recursive: trueで作られる途中のフォルダーにも同じmodeが使われます。 - macOS / Linux: 範囲の
*と**は.で始まる名前に一致しないので、.cacheのような隠しフォルダーを範囲の指定で作るなら、範囲にも.から書きます。 - 共通:
mkdir()は作ったフォルダーのパスを返しません。後で使うパスは自分で組み立てるか、Rust 版のように返します。作ったフォルダーの名前の変更や移動は ファイル名を変更する・別の場所へ移動する で扱います。
