変換途中の画像、ほかのアプリで開くために書き出すファイル、ダウンロードの途中経過など、作業が終われば要らないファイルは一時フォルダーに置きます。フロントエンドの 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-filepermission 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 にも同じ文字列で書けるからです。
