アプリ専用のデータ保存フォルダパスを取得する

appDataDir() と Rust の app_data_dir() でアプリ専用フォルダーを得る。identifier で決まる名前、OS ごとの場所、appConfig・appCache などとの使い分けを表で示す。

ファイルシステム 対象: Tauri 2.x 更新日: 読了目安: 約9分 fs-017
目次
  1. 前提条件
  2. フォルダー名は identifier で決まる
  3. OS ごとの場所と使い分け
  4. 1. フロントエンドから実装する (TypeScript)
  5. 2. バックエンドから実装する (Rust)
  6. 動作確認
  7. よくあるエラーと対処法
  8. OS ごとの違いと注意点
  9. 関連レシピ

データベースや利用者の作業データなど、アプリが作るファイルは OS がアプリごとに用意する場所に置きます。フロントエンドは @tauri-apps/api/path の appDataDir() など、Rust は app.path()(PathResolver)の app_data_dir() などで、その OS での正しいパスが得られます。フォルダー名は identifier で決まり、似た関数と同じ場所になるかどうかが OS で違うので、実際の場所と使い分けを表にまとめます。

前提条件

パスを得るだけならプラグインは不要で、path API の権限 core:path:default は core:default に含まれます。得たパスでファイルを読み書きするには File System プラグインを追加します。

npm run tauri add fs

fs:default はアプリ用フォルダーの中の読み取りとフォルダーの作成を許可し、書き込みは含みません。範囲(スコープ)は fs:default が覆っているので、fs:allow-write-text-file を足すだけで書けます。$APPDATA などの変数と関数の対応は ファイルやディレクトリを削除する を参照してください。

{
  "$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"
  ]
}

フォルダー名は identifier で決まる

どの関数も「OS の基準フォルダー + identifier」を返します(productName ではありません)。

{
  "productName": "My App",
  "identifier": "com.example.myapp"
}
  • 使えるのは英数字・ハイフン・ピリオドで、ほかのアプリと重ならない逆ドメイン形式にします。
  • 公開後に変えると別のフォルダーを見るため、保存済みのデータが消えたように見えます(古いフォルダーは残っています)。
  • tauri dev とインストールした本番版は同じフォルダーを使います。開発中のデータを分けるなら、開発時だけ identifier を変えた設定ファイルを重ねます。
{
  "identifier": "com.example.myapp.dev"
}
# 上の JSON を src-tauri/tauri.dev.conf.json として保存し、tauri.conf.json に重ねて起動する
npm run tauri dev -- --config src-tauri/tauri.dev.conf.json

OS ごとの場所と使い分け

<id> は identifier、~ はホームディレクトリです。Rust のメソッドは同じ名前のスネークケース(app_data_dir() など)で、同じ場所を返します。Linux は XDG_DATA_HOME・XDG_CONFIG_HOME・XDG_CACHE_HOME が設定されていればその下になります。

関数WindowsmacOSLinux
appDataDir()~\AppData\Roaming\<id>~/Library/Application Support/<id>~/.local/share/<id>
appLocalDataDir()~\AppData\Local\<id>~/Library/Application Support/<id>~/.local/share/<id>
appConfigDir()~\AppData\Roaming\<id>~/Library/Application Support/<id>~/.config/<id>
appCacheDir()~\AppData\Local\<id>~/Library/Caches/<id>~/.cache/<id>
appLogDir()~\AppData\Local\<id>\logs~/Library/Logs/<id>~/.local/share/<id>/logs
tempDir()~\AppData\Local\Temp/var/folders/…/T/tmp
homeDir()C:\Users\<ユーザー名>/Users/<ユーザー名>/home/<ユーザー名>
置くもの関数
利用者のデータ(Windows の移動プロファイルでは一緒に移る)appDataDir()
大きい・この PC 専用のデータ(DB など)appLocalDataDir()
設定ファイルappConfigDir()
消えても作り直せるキャッシュappCacheDir()
ログ(Log プラグインの LogDir もここ)appLogDir()
作業中だけ使う一時ファイルtempDir()

表のとおり、OS によっては別の関数が同じフォルダーを指します。同じ名前のファイルを置くとその OS でだけ上書きされるので、db・cache のようなサブフォルダーで分けます。Store プラグインの保存先も appDataDir() です。

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

どの関数も Promise<string> で絶対パスを返します。返るのはパスだけで、フォルダーがあるとは限りません。

import { appCacheDir, appConfigDir, appDataDir, appLocalDataDir, appLogDir } from '@tauri-apps/api/path';

// 5 つのアプリ用フォルダーの場所を並べて出す
export async function logAppDirs(): Promise<void> {
  const [data, local, config, cache, log] = await Promise.all([
    appDataDir(), appLocalDataDir(), appConfigDir(), appCacheDir(), appLogDir(),
  ]);
  for (const [name, dir] of Object.entries({ data, local, config, cache, log })) {
    console.log(name.padEnd(6), dir);
  }
}

読み書きでは、絶対パスをつなぐより BaseDirectory.AppData と相対パスを渡す方が簡単です(絶対パスの組み立ては パスを操作する)。アプリ用フォルダーは初回起動では無いことがあるので、書く前に recursive: true で作ります(新しいディレクトリ(フォルダ)を作成する)。

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

const baseDir = BaseDirectory.AppData;

export async function saveNotes(notes: string[]): Promise<void> {
  await mkdir('notes', { baseDir, recursive: true }); // $APPDATA 自体が無くても親ごと作る
  await writeTextFile('notes/index.json', JSON.stringify(notes), { baseDir });
}

export async function loadNotes(): Promise<string[]> {
  if (!(await exists('notes/index.json', { baseDir }))) return []; // 初回起動
  return JSON.parse(await readTextFile('notes/index.json', { baseDir })) as string[];
}

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

app.path() を使うには tauri::Manager の use が必要で、各メソッドは tauri::Result<PathBuf> を返します。setup で必要なフォルダーを作ってパスを State に入れておくと、コマンドごとに組み立てずに済みます。

use std::path::PathBuf;
use tauri::Manager;

/// 設定画面などに出すための、アプリ用フォルダーの一覧
#[derive(serde::Serialize)]
#[serde(rename_all = "camelCase")]
struct AppDirs {
    data: PathBuf,
    local_data: PathBuf,
    config: PathBuf,
    cache: PathBuf,
    log: PathBuf,
}

fn collect_dirs(app: &tauri::AppHandle) -> tauri::Result<AppDirs> {
    let p = app.path();
    Ok(AppDirs {
        data: p.app_data_dir()?,
        local_data: p.app_local_data_dir()?,
        config: p.app_config_dir()?,
        cache: p.app_cache_dir()?,
        log: p.app_log_dir()?,
    })
}

#[tauri::command]
fn app_dirs(app: tauri::AppHandle) -> Result<AppDirs, String> {
    collect_dirs(&app).map_err(|e| e.to_string())
}

/// 起動時に決めた DB ファイルのパス
struct DbPath(PathBuf);

#[tauri::command]
fn db_path(db: tauri::State<'_, DbPath>) -> String {
    db.0.display().to_string()
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .setup(|app| {
            // この PC 専用の大きいデータなので LocalData に置く。フォルダーは自分で作る
            let dir = app.path().app_local_data_dir()?.join("db");
            std::fs::create_dir_all(&dir)?;
            app.manage(DbPath(dir.join("main.sqlite")));
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![app_dirs, db_path])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

type AppDirs = { data: string; localData: string; config: string; cache: string; log: string };

const dirs = await invoke<AppDirs>('app_dirs');
console.log(dirs.localData, await invoke<string>('db_path'));

動作確認

npm run tauri dev で logAppDirs() を呼ぶと、identifier が com.example.myapp の Windows では次のように出ます(tauri.dev.conf.json を重ねると末尾が .dev 付きになります)。

data   C:\Users\me\AppData\Roaming\com.example.myapp
local  C:\Users\me\AppData\Local\com.example.myapp
config C:\Users\me\AppData\Roaming\com.example.myapp
cache  C:\Users\me\AppData\Local\com.example.myapp
log    C:\Users\me\AppData\Local\com.example.myapp\logs

saveNotes(['a']) を呼ぶと、Roaming 側の com.example.myapp\notes\index.json ができます。

よくあるエラーと対処法

  • 「failed to open file at path: <パス> with error: …」: 書き込み先のフォルダーがありません。recursive: true の mkdir() で作ってから書きます。
  • 「fs.write_text_file not allowed. Permissions associated with this command: …」: 書き込み権限の追加漏れです(リリースビルドでは「Command plugin:fs|write_text_file not allowed by ACL」)。
  • EBWebView の中で「forbidden path: <パス>」になる: Windows の WebView のデータ(localStorage や Cookie)で、fs:default が拒否しています。自分のファイルは別のサブフォルダーに置きます。
  • 更新後にデータが消えた: identifier を変えていないか確かめます。移行するなら、Rust の setup で app.path().data_dir()?.join("旧 identifier") の中身を移します。

OS ごとの違いと注意点

  • Windows: AppData は隠しフォルダーです。自分で開くときは、パスをエクスプローラーのアドレスバーに貼り付けます。
  • macOS: Finder では ~/Library が隠れています。データ・ローカルデータ・設定は同じフォルダーです。
  • Linux: ローカルデータのフォルダーに、ログと WebView のデータも入ります。
  • 共通: アプリ用フォルダーを丸ごと消すと、同じ場所を指す別の用途のファイルや WebView のデータまで消えます。消すのはサブフォルダーの単位にします。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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