ファイルのメタデータ(サイズ・作成日時・更新日時)を取得する

fs プラグインの stat()(権限 fs:allow-stat)でサイズや更新日時を読む。日時が null になる場合、フォルダーの合計サイズ、ほかのアプリによる変更の見分け方、Rust 版も示す。

ファイルシステム 対象: Tauri 2.x 更新日: 読了目安: 約10分 fs-015
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 返ってくる値
  4. 読みやすく表示する
  5. ほかのアプリによる変更を見分ける
  6. フォルダーの合計サイズとシンボリックリンク
  7. 2. バックエンドから実装する (Rust)
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

ファイルの情報画面にサイズや日付を出す、開いているファイルがほかのアプリに書き換えられたかを見分ける、といった場面では 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
fileAttributesWindows の属性現在は値が届かない(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-stat permission 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 で「最近使ったファイル」を判断しないようにします。
  • 共通: 更新日時はアプリ以外の操作でも変わります。コピーしたファイルの日時が元と同じになるとは限らない点は ファイルをコピー・複製する を参照してください。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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