アプリの設定保存フォルダパスを取得する

appConfigDir() と Rust の app_config_dir() で設定ファイルの場所を得て読み書きする。Windows と macOS ではデータ用と同じフォルダーになる点、壊れた設定の扱いも示す。

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

テーマや文字の大きさ、最後に開いたフォルダーといった設定は、appConfigDir()(Rust は app_config_dir())が返すフォルダーに保存します。フォルダー名は tauri.conf.json の identifier で、場所は OS の慣習に従います。注意したいのは、Linux ではデータ用とは別に ~/.config の下に作られる一方、Windows と macOS ではデータ用の appDataDir() と同じフォルダーになることです。ここでは、初回起動や壊れたファイルに強い設定ファイルの読み書きと、画面を出す前に Rust で設定を読む方法を示します。

前提条件

パスの取得は core:default に含まれる core:path:default で行えます。ファイルの読み書きには File System プラグインを追加します。

npm run tauri add fs

fs:default だけで、設定フォルダーの作成と読み取り(mkdir()・exists()・readTextFile())ができます。保存に使う fs:allow-write-text-file を足します。範囲は fs:default がアプリ用フォルダーを覆っているので書かなくて構いません(範囲と $APPCONFIG などの変数は ファイルやディレクトリを削除する)。2 章の Rust だけで読み書きする方式なら、fs プラグインもこの権限も要りません。

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

設定フォルダーを使うときの注意

各 OS の実際の場所と、ほかのフォルダーとの使い分けは アプリ専用のデータ保存フォルダパスを取得する の表にまとめています。設定ファイルを置くうえでは、次の点を押さえます。

  • データ用と同じフォルダーになる OS がある: Windows と macOS では appConfigDir() と appDataDir() が同じ場所です。設定とデータに同じファイル名を使うと上書きし合います。
  • フォルダーは自動では作られない: 初回起動では、設定ファイルもフォルダーも無い前提で書きます。
  • 初期化はファイル単位で: 「設定をリセット」でフォルダーごと消すと、Windows と macOS ではデータも消えます。
  • Store プラグインの既定は別の場所: Store プラグイン は相対パスを appDataDir() 基準で保存します。設定フォルダーに置くなら、appConfigDir() と join() で作った絶対パスを渡します。

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

読み込みでは、初回起動でファイルが無い、手で編集されたり保存中に強制終了したりして JSON が壊れている、古い版で保存されて新しい項目が無い、の 3 つに備えます。既定値に保存した値を重ねれば、足りない項目は既定値で補われます。壊れていたときは既定値で起動しますが、次の保存で上書きされる前に中身を別名で残しておくと、利用者が手で直せます。一方、ファイルを読めないこと自体(権限など)は既定値で隠さず、例外のまま返します。

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

export type Settings = { theme: 'system' | 'light' | 'dark'; fontSize: number; lastFolder: string | null };
export const DEFAULTS: Settings = { theme: 'system', fontSize: 14, lastFolder: null };

const FILE = 'settings.json';
const opts = { baseDir: BaseDirectory.AppConfig };

export async function loadSettings(): Promise<Settings> {
  if (!(await exists(FILE, opts))) return { ...DEFAULTS }; // 初回起動
  const text = await readTextFile(FILE, opts); // 読めないときは例外のまま
  try {
    return { ...DEFAULTS, ...(JSON.parse(text) as Partial<Settings>) }; // 無い項目は既定値
  } catch {
    await writeTextFile('settings.broken.json', text, opts); // 壊れた中身を残しておく
    return { ...DEFAULTS };
  }
}

export async function saveSettings(s: Settings): Promise<void> {
  await mkdir(await appConfigDir(), { recursive: true }); // 初回はフォルダー自体が無い
  await writeTextFile(FILE, JSON.stringify(s, null, 2), opts);
}

JSON.stringify(s, null, 2) で整形しておくと、利用者やサポートが中身を読みやすくなります。保存中の強制終了でも壊れないようにするなら、一時ファイルに書いてから名前を変える手順(テキストファイルに書き込む)にします。

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

ウィンドウの大きさやテーマのように画面を出す前に反映したい設定は、Rust の setup で読みます。読んだ値を State に置き、JS からはコマンドで取得・保存する形にすると、ファイルを触るのが Rust だけになり、JS と Rust がそれぞれ保存して食い違うこともありません。#[serde(default)] を付けると、ファイルに無い項目は Default の値になります。

use serde::{Deserialize, Serialize};
use std::path::{Path, PathBuf};
use std::sync::Mutex;
use tauri::{Manager, State};

#[derive(Serialize, Deserialize, Clone)]
#[serde(default, rename_all = "camelCase")]
struct Settings {
    theme: String,
    font_size: u32,
    last_folder: Option<String>,
}

impl Default for Settings {
    fn default() -> Self {
        Self { theme: "system".into(), font_size: 14, last_folder: None }
    }
}

struct SettingsState {
    path: PathBuf,
    current: Mutex<Settings>,
}

/// 無ければ既定値。壊れていれば settings.broken.json に退避して既定値
fn load_settings(path: &Path) -> std::io::Result<Settings> {
    let text = match std::fs::read_to_string(path) {
        Ok(t) => t,
        Err(e) if e.kind() == std::io::ErrorKind::NotFound => return Ok(Settings::default()),
        Err(e) => return Err(e),
    };
    Ok(serde_json::from_str(&text).unwrap_or_else(|_| {
        let _ = std::fs::rename(path, path.with_extension("broken.json"));
        Settings::default()
    }))
}

#[tauri::command]
fn get_settings(state: State<'_, SettingsState>) -> Settings {
    state.current.lock().unwrap().clone()
}

#[tauri::command]
fn save_settings(state: State<'_, SettingsState>, settings: Settings) -> Result<(), String> {
    let text = serde_json::to_string_pretty(&settings).map_err(|e| e.to_string())?;
    let tmp = state.path.with_extension("json.tmp");
    std::fs::write(&tmp, text).map_err(|e| e.to_string())?;
    std::fs::rename(&tmp, &state.path).map_err(|e| e.to_string())?; // 書き終えてから置き換える
    *state.current.lock().unwrap() = settings;
    Ok(())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .setup(|app| {
            let dir = app.path().app_config_dir()?;
            std::fs::create_dir_all(&dir)?;
            let path = dir.join("settings.json");
            let current = Mutex::new(load_settings(&path)?);
            // ここで読んだテーマなどをウィンドウに反映してから表示できる
            app.manage(SettingsState { path, current });
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![get_settings, save_settings])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

type Settings = { theme: string; fontSize: number; lastFolder: string | null };

const current = await invoke<Settings>('get_settings');
await invoke('save_settings', { settings: { ...current, fontSize: 16 } });

動作確認

npm run tauri dev で起動して saveSettings({ ...DEFAULTS, theme: 'dark' }) を呼ぶと、Windows では C:\Users\me\AppData\Roaming\com.example.myapp\settings.json、Linux では ~/.config/com.example.myapp/settings.json に次の内容が保存されます。

{
  "theme": "dark",
  "fontSize": 14,
  "lastFolder": null
}

このファイルの最後の } を消して再起動すると、loadSettings() は既定値を返し、同じフォルダーに settings.broken.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」)。
  • 更新後に設定が初期化される: #[serde(default)] が無いと、古い設定ファイルに無い項目があるだけで読み込みが「missing field」を含むエラーになり、上の load_settings() では壊れたファイルとして退避されます。構造体に #[serde(default)] を付けます。
  • 保存したはずの設定が戻る: JS と Rust の両方が同じファイルを書いていないか確かめます。State に読み込んだ後で JS がファイルを書き換えても、State は古いままです。

OS ごとの違いと注意点

  • Windows: Roaming の下です。移動プロファイルの環境ではサインインのたびにほかの PC と同期されるので、大きなファイルは置かず appLocalDataDir() に回します。
  • macOS: データ用・ローカルデータ用と同じ ~/Library/Application Support/<id> です。
  • Linux: XDG_CONFIG_HOME があればその下、無ければ ~/.config/<id> です。データ用(~/.local/share/<id>)とは別の場所なので、設定とデータが同じフォルダーにある前提で相対パスを組むと、Linux でだけ失敗します。
  • 共通: 利用者がファイルを直接編集することもあります。型の違う値("fontSize": "14" など)まで防ぐなら、読み込んだ後に値を確かめます。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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