一時ファイル用フォルダパスを取得する

tempDir() と Rust の temp_dir() でシステムの一時フォルダーを得る。全アプリ共用なので、専用のサブフォルダーに重ならない名前で作る方法、$TEMP の権限の絞り方、古いファイルの掃除を示す。

ファイルシステム 対象: Tauri 2.x 更新日: 読了目安: 約9分 fs-019
目次
  1. 前提条件
  2. 一時フォルダーの性質
  3. 1. フロントエンドから実装する (TypeScript)
  4. 専用のサブフォルダーに、重ならない名前で作る
  5. 起動時に古いファイルを掃除する
  6. 2. バックエンドから実装する (Rust)
  7. 動作確認
  8. よくあるエラーと対処法
  9. OS ごとの違いと注意点
  10. 関連レシピ

変換途中の画像、ほかのアプリで開くために書き出すファイル、ダウンロードの途中経過など、作業が終われば要らないファイルは一時フォルダーに置きます。フロントエンドの tempDir()、Rust の app.path().temp_dir() は OS の一時フォルダーを返します。appDataDir() などと違ってアプリ専用の場所ではなく、identifier も付かない、すべてのアプリが共用するフォルダーです。そのため、専用のサブフォルダーを作る、重ならない名前で作る、自分で掃除する、の 3 つをセットで扱います。

前提条件

tempDir() は core:default に含まれる core:path:default で呼べます。ファイルを作るには File System プラグインを追加します。

npm run tauri add fs

fs:default の範囲はアプリ用フォルダーだけで、一時フォルダーは入っていません。公式の fs:allow-temp-write-recursive は一時フォルダー全体(ほかのアプリのファイルを含む)の書き換えと削除を許してしまうので、自分のサブフォルダー($TEMP/<identifier>)だけを範囲にします。fs:scope は範囲だけを足す権限で、fs のすべてのコマンドに効きます。コマンドは、フォルダーの作成と一覧が fs:default に含まれるので、書き込み・日時の取得・削除を足します。

{
  "$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",
    "fs:allow-remove",
    {
      "identifier": "fs:scope",
      "allow": [
        { "path": "$TEMP/com.example.myapp" },
        { "path": "$TEMP/com.example.myapp/**" }
      ]
    }
  ]
}

com.example.myapp の部分には tauri.conf.json の identifier と同じ文字列を書きます。BaseDirectory.Temp が $TEMP に当たります(変数の一覧は ファイルやディレクトリを削除する)。

一時フォルダーの性質

OS ごとの場所は アプリ専用のデータ保存フォルダパスを取得する の表にあります。どの OS でも環境変数で変わり、次の性質があります。

  • ほかのアプリと共用: output.txt のような固定の名前で書くと、別のアプリや、同時に起動した自分のアプリと上書きし合います。
  • いつ消えるか決まっていない: OS や掃除ツールに消されることも、ずっと残ることもあります。後でまた使うものは appCacheDir()、残すものは appDataDir() に置きます。
  • ほかの利用者から見えることがある: パスワードや個人情報は書かず、書くなら自分だけが読めるようにします。

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

専用のサブフォルダーに、重ならない名前で作る

サブフォルダー名は getIdentifier() で取ると、capability に書いた範囲と一致します(権限は core:default に含まれます)。ファイル名は乱数で作り、createNew: true で「同名があれば上書きせずに失敗」させます。mode は macOS と Linux で自分だけが読み書きできるようにする指定で、Windows では無視されます。

import { getIdentifier } from '@tauri-apps/api/app';
import { join, tempDir } from '@tauri-apps/api/path';
import { BaseDirectory, mkdir, writeTextFile } from '@tauri-apps/plugin-fs';

function randomName(ext: string): string {
  const bytes = crypto.getRandomValues(new Uint8Array(8));
  return Array.from(bytes, (b) => b.toString(16).padStart(2, '0')).join('') + '.' + ext;
}

// 一時ファイルを作り、ほかのアプリに渡せる絶対パスを返す
export async function createTempFile(ext: string, text: string): Promise<string> {
  const baseDir = BaseDirectory.Temp;
  const sub = await getIdentifier(); // 例: com.example.myapp
  await mkdir(sub, { baseDir, recursive: true, mode: 0o700 });
  const rel = `${sub}/${randomName(ext)}`;
  await writeTextFile(rel, text, { baseDir, createNew: true, mode: 0o600 });
  return join(await tempDir(), rel);
}

起動時に古いファイルを掃除する

終了時にまとめて消す方法では、強制終了したときに残るうえ、同時に起動している 2 つ目のインスタンスや、ファイルを渡した先のアプリがまだ使っているファイルまで消してしまいます。起動時に「一定時間より古いもの」だけを消すと、どちらの問題も避けられます。

import { getIdentifier } from '@tauri-apps/api/app';
import { BaseDirectory, readDir, remove, stat } from '@tauri-apps/plugin-fs';

// maxAgeMs より古い一時ファイルを消し、消した数を返す(起動時に 1 回呼ぶ)
export async function cleanTempFiles(maxAgeMs = 24 * 60 * 60 * 1000): Promise<number> {
  const baseDir = BaseDirectory.Temp;
  const sub = await getIdentifier();
  const entries = await readDir(sub, { baseDir }).catch(() => []); // フォルダーがまだ無い
  let removed = 0;
  for (const e of entries) {
    if (!e.isFile) continue;
    const path = `${sub}/${e.name}`;
    const { mtime } = await stat(path, { baseDir });
    if (mtime && Date.now() - mtime.getTime() > maxAgeMs) {
      await remove(path, { baseDir }).then(() => removed++, () => {}); // 使用中なら次回に回す
    }
  }
  return removed;
}

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

app.path().temp_dir() は Rust 標準の std::env::temp_dir() と同じ場所を返し、失敗しません(戻り値は Result ですが常に Ok)。Rust の std::fs には capability の範囲が効かないので、JS から受け取るのは中身と拡張子だけにし、パスは Rust の中で決めます。create_new(true) は同名のファイルがあると AlreadyExists で失敗するので、名前を変えてやり直します。

use std::fs::{self, OpenOptions};
use std::io::{ErrorKind, Write};
use std::path::PathBuf;
use std::sync::atomic::{AtomicU64, Ordering};
use std::time::{Duration, SystemTime, UNIX_EPOCH};
use tauri::Manager;

static COUNTER: AtomicU64 = AtomicU64::new(0);

/// <一時フォルダー>/<identifier> を作って返す
fn app_temp_dir(app: &tauri::AppHandle) -> tauri::Result<PathBuf> {
    let dir = app.path().temp_dir()?.join(&app.config().identifier);
    fs::create_dir_all(&dir)?;
    Ok(dir)
}

/// 重ならない名前で新しいファイルを作り、そのパスを返す
#[tauri::command]
fn write_temp_file(app: tauri::AppHandle, ext: String, contents: String) -> Result<String, String> {
    if ext.is_empty() || !ext.chars().all(|c| c.is_ascii_alphanumeric()) {
        return Err(format!("invalid extension: {ext}"));
    }
    let dir = app_temp_dir(&app).map_err(|e| e.to_string())?;
    for _ in 0..10 {
        let nanos = SystemTime::now().duration_since(UNIX_EPOCH).map(|d| d.as_nanos()).unwrap_or(0);
        let n = COUNTER.fetch_add(1, Ordering::Relaxed);
        let path = dir.join(format!("{}-{nanos}-{n}.{ext}", std::process::id()));
        match OpenOptions::new().write(true).create_new(true).open(&path) {
            Ok(mut file) => {
                file.write_all(contents.as_bytes()).map_err(|e| e.to_string())?;
                return Ok(path.display().to_string());
            }
            Err(e) if e.kind() == ErrorKind::AlreadyExists => continue, // 同名があった
            Err(e) => return Err(format!("{}: {e}", path.display())),
        }
    }
    Err("could not create a unique temp file".into())
}

/// max_age より古いファイルを消す。消せないもの(使用中など)は次回に回す
fn clean_old_temp_files(app: &tauri::AppHandle, max_age: Duration) {
    let Ok(dir) = app_temp_dir(app) else { return };
    let Ok(entries) = fs::read_dir(&dir) else { return };
    for entry in entries.flatten() {
        let old = entry
            .metadata()
            .and_then(|m| m.modified())
            .ok()
            .and_then(|t| t.elapsed().ok())
            .is_some_and(|age| age > max_age);
        if old {
            let _ = fs::remove_file(entry.path());
        }
    }
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .setup(|app| {
            clean_old_temp_files(app.handle(), Duration::from_secs(24 * 60 * 60));
            Ok(())
        })
        .invoke_handler(tauri::generate_handler![write_temp_file])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';

const path = await invoke<string>('write_temp_file', { ext: 'csv', contents: 'id,name\n1,foo\n' });
console.log(path);

動作確認

npm run tauri dev で起動し、createTempFile('txt', 'hello') を呼ぶと、Windows では次のようなパスが返ります(名前は毎回変わります)。続けて cleanTempFiles(0) を呼ぶと、今作ったファイルも「古い」と判定されて消え、1 が返ります。

C:\Users\me\AppData\Local\Temp\com.example.myapp\3f9c0a1b2d4e5f60.txt

Rust 版の write_temp_file は、同じフォルダーに「プロセス ID-時刻-連番」の 12345-1789212345678901234-0.csv のような名前でファイルを作ります。

よくあるエラーと対処法

  • 「forbidden path: <パス>, maybe it is not allowed on the scope for allow-write-text-file permission in your capability file」: 範囲の外です。capability の identifier の綴り、baseDir: BaseDirectory.Temp の付け忘れを確かめます。開発時だけ identifier を変えている場合は、その名前も範囲に書きます。
  • 「failed to open file at path: <パス> with error: …」: createNew: true で同名のファイルがあったか、サブフォルダーがありません。続く OS のメッセージで見分けます。
  • 「fs.stat not allowed. Permissions associated with this command: …」: stat() の権限は fs:default に含まれないので、fs:allow-stat を足します。
  • 掃除で消えないファイルがある: Windows では、ほかのアプリが開いているファイルは消せません。エラーを無視して次回の掃除に回します。

OS ごとの違いと注意点

  • Windows: 環境変数 TMP(無ければ TEMP)の場所で、通常はユーザーごとの ~\AppData\Local\Temp です。
  • macOS: 環境変数 TMPDIR の場所で、ユーザーごとの /var/folders/…/T です。
  • Linux: TMPDIR が無ければ /tmp で、全ユーザー共用です。ファイル名はほかの利用者から見えるので、mode で中身を読めないようにします。ディストリビューションによっては再起動で空になります。
  • 共通: 一時フォルダーの名前だけでは、どのアプリのファイルか区別できません。サブフォルダーに identifier を使うのは、ほかと重ならず、capability にも同じ文字列で書けるからです。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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