ファイル名を変更する・別の場所へ移動する

fs プラグインの rename()(権限 fs:allow-rename)で名前の変更と移動を行う。移動元と移動先の両方に要るスコープ、上書きの防ぎ方、別ドライブへ移すときの代わりの手順も示す。

ファイルシステム 対象: Tauri 2.x 更新日: 読了目安: 約10分 fs-009
目次
  1. 前提条件
  2. 1. フロントエンドから実装する (TypeScript)
  3. 名前を変える・移動する
  4. 上書きしないで移動する
  5. 別のドライブへも移せるようにする
  6. ユーザーが選んだファイルを、選んだ場所へ移す
  7. 2. バックエンドから実装する (Rust)
  8. 動作確認
  9. よくあるエラーと対処法
  10. OS ごとの違いと注意点
  11. 関連レシピ

名前の変更も別のフォルダーへの移動も「パスを付け替える」同じ操作なので、Tauri では File System プラグインの rename() 1 つで行います。移動元と移動先に別々の基準フォルダー(baseDir)を指定でき、フォルダーごとの改名や移動もできます。つまずきやすいのは、移動先に同じ名前のファイルがあると確認なしに上書きされること、移動元と移動先の両方がスコープ(許可するパスの範囲)に入っていないと失敗すること、別のドライブへは移せないことの 3 点です。Rust からは std::fs::rename() を使います。

前提条件

fs プラグインを追加します。ユーザーに選ばせて移す例ではダイアログプラグインも使います。

npm run tauri add fs
npm run tauri add dialog

rename() の権限 fs:allow-rename は fs:default に含まれないので追加します。範囲の判定は移動元と移動先のパスそれぞれに行われ、両方が範囲に入っている必要があります。fs:default はアプリ用のフォルダー($APPDATA など)とその中身を範囲に含むので、アプリ用フォルダーの中どうしなら "fs:allow-rename" だけで足ります。範囲の書き方と fs:default との関係は ファイルやディレクトリを削除する で詳しく説明しています。

次の例は、ダウンロードフォルダーの直下($DOWNLOAD/*)のファイルをアプリのフォルダーへ取り込むための設定です。別のドライブへ移すときの「コピーして元を消す」に備えて、コピーと削除も同じ範囲で許可しています。

{
  "$schema": "../gen/schemas/desktop-schema.json",
  "identifier": "default",
  "description": "Capability for the main window",
  "windows": ["main"],
  "permissions": [
    "core:default",
    "fs:default",
    "dialog:default",
    { "identifier": "fs:allow-rename", "allow": [{ "path": "$DOWNLOAD/*" }] },
    { "identifier": "fs:allow-copy-file", "allow": [{ "path": "$DOWNLOAD/*" }] },
    { "identifier": "fs:allow-remove", "allow": [{ "path": "$DOWNLOAD/*" }] }
  ]
}

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

名前を変える・移動する

oldPathBaseDir と newPathBaseDir は別々のオプションです。片方を省くと、そちらのパスは基準フォルダーの無い相対パスとして扱われ、たいてい範囲の外として失敗します。同じフォルダーの中でも両方に指定します。

import { mkdir, rename, BaseDirectory } from '@tauri-apps/plugin-fs';

const opts = { oldPathBaseDir: BaseDirectory.AppData, newPathBaseDir: BaseDirectory.AppData };

// 名前の変更
await rename('notes/draft.md', 'notes/2026-09-12.md', opts);

// 別のフォルダーへ移動。移動先のフォルダーが無いと失敗するので先に作る
await mkdir('archive', { baseDir: BaseDirectory.AppData, recursive: true });
await rename('notes/2026-09-12.md', 'archive/2026-09-12.md', opts);

// フォルダーごと改名する
await rename('archive', 'archive-2026', opts);

mkdir() はアプリ用フォルダーの中なら fs:default だけで使えます(新しいディレクトリ(フォルダ)を作成する)。

上書きしないで移動する

移動先に同じ名前のファイルがあると、rename() は確認なしに置き換え、元からあったファイルは失われます。残したいときは exists() で空いている名前を探してから移します。確かめてから移すまでの間に別の処理が同じ名前を作る可能性は残るので、同時に書き込む処理があるなら Rust 側でまとめて行います。

import { basename, join } from '@tauri-apps/api/path';
import { exists, rename, BaseDirectory } from '@tauri-apps/plugin-fs';

// toDir に同名があれば「名前 (2).拡張子」のように番号を付けて移し、移動先を返す
export async function moveKeepingBoth(from: string, toDir: string, baseDir: BaseDirectory): Promise<string> {
  const name = await basename(from);
  const dot = name.lastIndexOf('.');
  const [stem, ext] = dot > 0 ? [name.slice(0, dot), name.slice(dot)] : [name, ''];
  let target = await join(toDir, name);
  for (let n = 2; await exists(target, { baseDir }); n++) {
    target = await join(toDir, `${stem} (${n})${ext}`);
  }
  await rename(from, target, { oldPathBaseDir: baseDir, newPathBaseDir: baseDir });
  return target;
}

別のドライブへも移せるようにする

rename() は OS の「名前の付け替え」なので、別のドライブやファイルシステムへは移せず、「failed to rename old path: … to new path: … with error: …」で始まるエラーになります。ダウンロードフォルダーを別のドライブに移している Windows の環境などで起きます。その場合はコピーしてから元を消します。

import { join } from '@tauri-apps/api/path';
import { copyFile, mkdir, remove, rename, BaseDirectory } from '@tauri-apps/plugin-fs';

// ダウンロードフォルダー直下のファイルを、アプリデータの inbox に取り込む
export async function importFromDownloads(name: string): Promise<void> {
  await mkdir('inbox', { baseDir: BaseDirectory.AppData, recursive: true });
  const to = await join('inbox', name);
  try {
    await rename(name, to, { oldPathBaseDir: BaseDirectory.Download, newPathBaseDir: BaseDirectory.AppData });
  } catch (e) {
    console.warn('rename に失敗したのでコピーに切り替えます:', e);
    // コピーが成功したときだけ元を消す(コピーの失敗はそのまま呼び出し元へ)
    await copyFile(name, to, { fromPathBaseDir: BaseDirectory.Download, toPathBaseDir: BaseDirectory.AppData });
    await remove(name, { baseDir: BaseDirectory.Download });
  }
}

ユーザーが選んだファイルを、選んだ場所へ移す

ダイアログの open() と save() で選ばれたパスは実行中だけ自動で範囲に入るので、fs:allow-rename があれば allow を書かずに移せます。

import { open, save } from '@tauri-apps/plugin-dialog';
import { rename } from '@tauri-apps/plugin-fs';

export async function moveChosenFile(): Promise<string | null> {
  const from = await open({ multiple: false, directory: false, title: '移動するファイル' });
  if (!from) return null;
  const to = await save({ title: '移動先と名前', defaultPath: from });
  if (!to || to === from) return null;
  await rename(from, to); // どちらもダイアログで選ばれたので範囲に入っている
  return to;
}

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

std::fs::rename() は capability とスコープの外で動くので、受け取った名前を確かめてからパスを組み立てます。別のドライブへ移すときの失敗は ErrorKind::CrossesDevices(Rust 1.85 以降)で見分けられるので、そのときだけ「コピーして元を消す」に切り替えられます。

use std::io::ErrorKind;
use std::path::{Component, Path};
use tauri::Manager;

/// `..` や区切りを含まない、名前 1 つだけか
fn is_plain_name(name: &str) -> bool {
    let mut parts = Path::new(name).components();
    matches!((parts.next(), parts.next()), (Some(Component::Normal(_)), None))
}

/// ファイルを移す。別のドライブなら「コピーして元を消す」に切り替える
fn move_file(from: &Path, to: &Path) -> std::io::Result<()> {
    match std::fs::rename(from, to) {
        Err(e) if e.kind() == ErrorKind::CrossesDevices => {
            std::fs::copy(from, to)?;
            std::fs::remove_file(from)
        }
        result => result,
    }
}

/// ダウンロードフォルダーのファイルをアプリデータの inbox に取り込み、移動先を返す
#[tauri::command]
fn import_download(app: tauri::AppHandle, name: String) -> Result<String, String> {
    if !is_plain_name(&name) {
        return Err(format!("invalid file name: {name}"));
    }
    let from = app.path().download_dir().map_err(|e| e.to_string())?.join(&name);
    let dir = app.path().app_data_dir().map_err(|e| e.to_string())?.join("inbox");
    std::fs::create_dir_all(&dir).map_err(|e| e.to_string())?;
    let to = dir.join(&name);
    if to.exists() {
        return Err(format!("already exists: {}", to.display()));
    }
    move_file(&from, &to).map_err(|e| format!("{} -> {}: {e}", from.display(), to.display()))?;
    Ok(to.display().to_string())
}

/// アプリデータの notes フォルダーの中で名前を変える
#[tauri::command]
fn rename_note(app: tauri::AppHandle, old_name: String, new_name: String) -> Result<(), String> {
    if !is_plain_name(&old_name) || !is_plain_name(&new_name) {
        return Err("invalid file name".into());
    }
    let dir = app.path().app_data_dir().map_err(|e| e.to_string())?.join("notes");
    let to = dir.join(&new_name);
    // 大文字小文字だけの変更は、同じファイルが「ある」と判定されることがあるので除く
    if to.exists() && !old_name.eq_ignore_ascii_case(&new_name) {
        return Err(format!("already exists: {new_name}"));
    }
    std::fs::rename(dir.join(&old_name), to).map_err(|e| e.to_string())
}

#[cfg_attr(mobile, tauri::mobile_entry_point)]
pub fn run() {
    tauri::Builder::default()
        .plugin(tauri_plugin_fs::init())
        .plugin(tauri_plugin_dialog::init())
        .invoke_handler(tauri::generate_handler![import_download, rename_note])
        .run(tauri::generate_context!())
        .expect("error while running tauri application");
}

JS からは、Rust の old_name を oldName のように camelCase で渡します。

import { invoke } from '@tauri-apps/api/core';

const saved = await invoke<string>('import_download', { name: 'report.pdf' });
console.log(saved);
await invoke('rename_note', { oldName: 'draft.md', newName: '2026-09-12.md' });

動作確認

npm run tauri dev で起動し、ダウンロードフォルダーに report.pdf を置いてから importFromDownloads('report.pdf') を呼ぶと、ダウンロードフォルダーから消えてアプリデータの inbox に移ります。Rust の import_download では、Windows なら次のようなパスが返ります。同じ名前でもう一度呼ぶと「already exists: …」で止まり、上書きしません。

C:\Users\me\AppData\Roaming\com.example.app\inbox\report.pdf

moveKeepingBoth() を同じファイル名で続けて呼ぶと、archive\report.pdf、archive\report (2).pdf のように番号が付きます。

よくあるエラーと対処法

  • 「fs.rename not allowed. Permissions associated with this command: fs:allow-app-write, …」: fs:allow-rename の追加漏れです(リリースビルドでは「Command plugin:fs|rename not allowed by ACL」)。追加して tauri dev を再起動します。
  • 「forbidden path: <パス>, maybe it is not allowed on the scope for allow-rename permission in your capability file」: 表示されたパス(移動元か移動先)が範囲の外です。allow に足すか、oldPathBaseDir / newPathBaseDir の指定漏れを直します。
  • 「failed to rename old path: … to new path: … with error: …」: 移動元が無い、移動先のフォルダーが無い、別のドライブへ移そうとした、移動先が中身のあるフォルダー、のいずれかです。続く OS のメッセージで見分けます。
  • 移動先にあったファイルが消えた: rename() は同名のファイルを確認なしに置き換えます。moveKeepingBoth() のように先に確かめます。

OS ごとの違いと注意点

  • 移動先に同名のフォルダーがある場合: macOS / Linux では、移動元もフォルダーで移動先のフォルダーが空のときだけ置き換わり、それ以外は失敗します。Windows でも多くの環境で同じですが、ファイルシステムによっては移動先がフォルダーなら常に失敗します。
  • macOS / Linux: シンボリックリンクはたどらず、リンクそのものが改名・移動されます。範囲の * と ** は . で始まる名前に一致しないので、隠しファイルを移すなら範囲を別に書きます。
  • 共通: 「コピーして元を消す」は一度に切り替わる移動と違い、元の削除だけが失敗すると両方に残ります。フォルダーを別のドライブへ移すには中身を 1 つずつコピーする処理が要ります(ファイルをコピー・複製する)。名前の付け替えに使うパスの加工は パスを操作する(結合・正規化・絶対パス化・拡張子の取得) を参照してください。

関連レシピ

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

Web Ninja

この記事を書いた人

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

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

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

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