ファイルの情報画面にサイズや日付を出す、開いているファイルがほかのアプリに書き換えられたかを見分ける、といった場面では File System プラグインの stat() を使います。1 回の呼び出しで、種類・サイズ・更新日時・作成日時・読み取り専用かどうかがまとめて返ります。つまずきやすいのは、日時が Date | null で作成日時は取れない環境があること、フォルダーの size は中身の合計ではないこと、stat() の権限が fs:default に含まれないことです。Rust では std::fs::metadata() を使います。
前提条件
fs プラグインを追加します。
npm run tauri add fs
stat()(fs:allow-stat)、シンボリックリンク自体を調べる lstat()(fs:allow-lstat)、フォルダーの合計サイズを出す size()(fs:allow-size)は、どれも fs:default に含まれないので追加します。範囲(スコープ)は fs:default のアプリ用フォルダー($APPDATA など)がそのまま使われ、ダイアログで選ばれたファイルも実行中は範囲に入ります。ほかの場所は範囲を付けて足します(ファイルやディレクトリを削除する)。アプリ用フォルダーだけなら、この 3 つと exists()・readDir() をまとめて許可する fs:allow-app-meta-recursive 1 つでも足ります。
{
"$schema": "../gen/schemas/desktop-schema.json",
"identifier": "default",
"description": "Capability for the main window",
"windows": ["main"],
"permissions": [
"core:default",
"fs:default",
"fs:allow-stat",
"fs:allow-lstat",
"fs:allow-size"
]
}
あるかどうかだけなら、fs:default に含まれる exists() で足ります(ファイルやフォルダが存在するか確認する)。
1. フロントエンドから実装する (TypeScript)
返ってくる値
| 項目 | 内容 | 注意 |
|---|---|---|
isFile / isDirectory / isSymlink | 種類 | isSymlink が true になるのは lstat() だけ |
size | バイト数 | フォルダーでは中身の合計ではない |
mtime | 更新日時 | Date か null |
birthtime | 作成日時 | 取れない OS・ファイルシステムでは null |
atime | 最後に読まれた日時 | 更新しない設定のことがある |
readonly | 書き込み禁止か | 変え方は fs-016 |
mode・uid・ino など | Unix の情報 | Windows では null |
fileAttributes | Windows の属性 | 現在は値が届かない(2 章) |
読みやすく表示する
日時は Date か null なので、info.mtime.getTime() のように直接呼ぶと strict モードの型チェックで「'info.mtime' is possibly 'null'.」になります。null のときの表示を先に決めておきます。Date は時刻そのものなので、Intl.DateTimeFormat で表示すれば利用者の地域の時刻と書式になります。
import { stat, BaseDirectory } from '@tauri-apps/plugin-fs';
const fmt = new Intl.DateTimeFormat('ja-JP', { dateStyle: 'medium', timeStyle: 'short' });
// 1536 → "1.5 KB"(1024 単位)
export function formatBytes(bytes: number): string {
const units = ['B', 'KB', 'MB', 'GB', 'TB'];
let value = bytes;
let i = 0;
while (value >= 1024 && i < units.length - 1) {
value /= 1024;
i++;
}
return i === 0 ? `${value} B` : `${value.toFixed(1)} ${units[i]}`;
}
// 日時は Date か null
const when = (d: Date | null) => (d ? fmt.format(d) : '取得できません');
export async function describeFile(path: string, baseDir: BaseDirectory): Promise<string[]> {
const info = await stat(path, { baseDir }); // fs:allow-stat が必要
return [
`種類: ${info.isDirectory ? 'フォルダー' : info.isFile ? 'ファイル' : 'その他'}`,
`サイズ: ${info.isFile ? formatBytes(info.size) : '-'}`, // フォルダーの size は中身の合計ではない
`更新: ${when(info.mtime)}`,
`作成: ${when(info.birthtime)}`,
`読み取り専用: ${info.readonly ? 'はい' : 'いいえ'}`,
];
}
ほかのアプリによる変更を見分ける
エディターのように、開いたファイルを後で上書きするアプリでは、開いたときの更新日時とサイズを控えておき、保存の直前に比べます。違っていればほかのアプリが書き換えたので、上書きしてよいかを尋ねます。更新日時の刻みが粗いファイルシステムもあるため、サイズも一緒に比べます。保存したら控えを取り直します。開いている間ずっと見張るなら ファイルの変更をリアルタイムで監視する を使います。
import { stat } from '@tauri-apps/plugin-fs';
export type Snapshot = { mtime: number | null; size: number };
export async function takeSnapshot(path: string): Promise<Snapshot> {
const info = await stat(path);
return { mtime: info.mtime?.getTime() ?? null, size: info.size };
}
// 開いたときの控えと比べて、ほかのアプリが書き換えたかを返す
export async function changedSince(path: string, before: Snapshot): Promise<boolean> {
const now = await takeSnapshot(path);
return now.mtime !== before.mtime || now.size !== before.size;
}
フォルダーの合計サイズとシンボリックリンク
size() はファイルならそのサイズを、フォルダーなら中身を全部たどった合計を返します。件数が多いと時間がかかるので、必要なときだけ呼びます。size() は baseDir を受け取らないため、appCacheDir() などで絶対パスを作って渡します。stat() はシンボリックリンクの先を調べるので、リンク自体かどうかは lstat() で見ます。
import { appCacheDir, join } from '@tauri-apps/api/path';
import { lstat, size, BaseDirectory } from '@tauri-apps/plugin-fs';
// キャッシュの thumbnails フォルダーの合計バイト数
export async function thumbnailCacheSize(): Promise<number> {
return size(await join(await appCacheDir(), 'thumbnails')); // fs:allow-size が必要
}
export async function isSymlink(path: string): Promise<boolean> {
return (await lstat(path, { baseDir: BaseDirectory.AppData })).isSymlink; // fs:allow-lstat が必要
}
2. バックエンドから実装する (Rust)
std::fs::metadata() の modified() と created() は Result<SystemTime> を返し、取れない環境では Err になります。JS へ渡すときは UNIX エポックからのミリ秒にし、取れなければ None(JS では null)にします。Windows の隠し属性などを表す fileAttributes は、現在のプラグイン(2.5 系)では JS に値が届かず undefined になるので、必要なら Rust の MetadataExt::file_attributes() で読みます。Rust の std::fs にはスコープが効かないので、受け取った名前は確かめてから使います。多数のファイルの情報を 1 回で返す例は フォルダ内のファイル一覧を取得する にあります。
use serde::Serialize;
use std::path::{Component, Path};
use std::time::{SystemTime, UNIX_EPOCH};
use tauri::Manager;
#[derive(Serialize)]
#[serde(rename_all = "camelCase")]
struct FileMeta {
size: u64,
is_dir: bool,
readonly: bool,
hidden: bool,
modified_ms: Option<u64>, // 取れなければ null
created_ms: Option<u64>,
}
/// UNIX エポックからのミリ秒にする。取れない・エポックより前なら None
fn to_ms(time: std::io::Result<SystemTime>) -> Option<u64> {
let d = time.ok()?.duration_since(UNIX_EPOCH).ok()?;
Some(d.as_millis() as u64)
}
/// アプリデータの中の、名前 1 つだけのファイルの情報を返す
#[tauri::command]
fn file_meta(app: tauri::AppHandle, name: String) -> Result<FileMeta, String> {
let mut parts = Path::new(&name).components();
if !matches!((parts.next(), parts.next()), (Some(Component::Normal(_)), None)) {
return Err(format!("invalid file name: {name}"));
}
let path = app.path().app_data_dir().map_err(|e| e.to_string())?.join(&name);
let meta = std::fs::metadata(&path).map_err(|e| format!("{}: {e}", path.display()))?;
#[cfg(windows)]
let hidden = {
use std::os::windows::fs::MetadataExt;
(meta.file_attributes() & 0x2) != 0 // 0x2 は隠し属性
};
#[cfg(not(windows))]
let hidden = name.starts_with('.'); // . で始まる名前を隠しファイルとして扱う
Ok(FileMeta {
size: meta.len(),
is_dir: meta.is_dir(),
readonly: meta.permissions().readonly(),
hidden,
modified_ms: to_ms(meta.modified()),
created_ms: to_ms(meta.created()),
})
}
#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
tauri::Builder::default()
.plugin(tauri_plugin_fs::init())
.invoke_handler(tauri::generate_handler![file_meta])
.run(tauri::generate_context!())
.expect("error while running tauri application");
}
import { invoke } from '@tauri-apps/api/core';
type FileMeta = {
size: number;
isDir: boolean;
readonly: boolean;
hidden: boolean;
modifiedMs: number | null;
createdMs: number | null;
};
const meta = await invoke<FileMeta>('file_meta', { name: 'settings.json' });
const created = meta.createdMs === null ? '取得できません' : new Date(meta.createdMs).toLocaleString();
console.log(`${meta.size} bytes / 隠し: ${meta.hidden} / 作成: ${created}`);
動作確認
npm run tauri dev で起動し、アプリデータに settings.json を置いてから describeFile('settings.json', BaseDirectory.AppData) の結果を表示すると、次のようになります。
種類: ファイル
サイズ: 1.5 KB
更新: 2026/09/12 10:30
作成: 2026/09/01 9:05
読み取り専用: いいえ
takeSnapshot() を取った後にテキストエディターで同じファイルを保存すると、changedSince() が true を返します。Rust の file_meta は、Windows でファイルのプロパティから「隠しファイル」を付けると hidden が true になります。
よくあるエラーと対処法
- 「fs.stat not allowed. Permissions associated with this command: fs:allow-app-meta, …」:
fs:allow-statの追加漏れです(リリースビルドでは「Command plugin:fs|stat not allowed by ACL」)。lstat()とsize()も同じ形のエラーになり、それぞれfs:allow-lstat・fs:allow-sizeを足します。 - 「failed to get metadata of path: <パス> with error: …」: パスが無いか、
stat()でリンク先の無いシンボリックリンクを調べました。 - 「forbidden path: <パス>, maybe it is not allowed on the scope for
allow-statpermission in your capability file」: パスが範囲の外です。size()に相対パスを渡したときも、基準フォルダーが付かないまま判定されて同じ形のエラーになります。 - 「'info.mtime' is possibly 'null'.」: TypeScript の型チェックです。
info.mtime?.getTime() ?? nullのようにnullの場合を書きます。
OS ごとの違いと注意点
- Windows:
mode・uid・inoなど Unix の項目はnullです。fileAttributesも上記のとおり値が届かないので、属性は Rust で読みます。 - macOS / Linux:
modeには種類を表すビットも入っており、パーミッションはmode & 0o777で取り出します。readonlyは書き込みのビットが 1 つも無いことを示すだけで、falseでも今のユーザーが書けるとは限りません(ファイルのパーミッション(権限)を変更する)。 - 作成日時:
birthtimeは取れない OS やファイルシステムではnullです。「新しい順」の並べ替えなどには更新日時を使う方が確実です。 - アクセス日時: Windows には更新を止める設定があり、Linux にも更新しない
noatimeの設定があるので、atimeで「最近使ったファイル」を判断しないようにします。 - 共通: 更新日時はアプリ以外の操作でも変わります。コピーしたファイルの日時が元と同じになるとは限らない点は ファイルをコピー・複製する を参照してください。
