FileSystemWatcherで作成ファイルを指定フォルダへ移動して上書きする方法(C#/.NET)

C#のFileSystemWatcherで「新規作成されたファイルを検知して、指定フォルダへ移動したい」という要件はよくあります。ただし、移動先に同名ファイルがあると例外で失敗したり、処理の順序を誤って“移動した直後のファイル自身を消す”事故が起きがちです。ここでは、上書き移動を正しく実装する方法と、現場で詰まりやすい注意点までまとめます。

目次

やりたいことを整理:Created を検知して「指定フォルダへ上書き移動」

要件をそのまま言語化すると、次の2点です。

  • FileSystemWatcher の Created で新規作成ファイルを検知し、毎回 C:\Users\sy\Documents\test22 に移動したい
  • 移動先に同名ファイルがある場合は入れ替え(=上書き)したい

そして、提示されがちな「失敗パターン」は次のロジック不整合です。

  • File.Move(source, target) を実行したあとで File.Exists(target) を見て File.Delete(target) をしてしまい、移動直後のファイル自身を削除してしまう
  • そもそも移動先に同名があると File.Move が例外になり得るため、削除→移動の順にすべきなのに順序が逆

つまり問題の本質は「上書きの扱い」と「順序」です。ここを .NET の機能で正しく解決します。

結論:File.Move の上書きオプションで一発解決

.NET(.NET Core / .NET 5+ / .NET 6+ など)では、File.Move に上書き指定(overwrite)を渡せます。これを使うと、同名ファイルが存在しても自動で置き換えて移動できます。

ポイントはシンプルです。

  • File.Delete(targetFile) は不要(むしろ入れると事故の原因になりやすい)
  • File.Move(source, dest, overwrite: true) を使う

要点だけの修正例は次のとおりです。

string targetFile = Path.Combine(TargetPath, Path.GetFileName(e.FullPath));

if (await IsFileReadyAsync(e.FullPath))
{
    File.Move(e.FullPath, targetFile, overwrite: true); // 既存なら上書きして移動
    listBox1.Invoke((Action)(() =>
        listBox1.Items.Add($"Moved: {e.FullPath} -> {targetFile}")));
}

これで「移動先に同名があって Move が失敗する」問題も、「Move のあとに Delete して自分を消す」問題も同時に解消します。

「そのファイルはどこへ行ったの?」をパスで説明する

上書き移動の動作が混乱の原因になりやすいので、処理結果をパスの観点で整理します。

  • 移動先は targetFile(= TargetPath + ファイル名)です
  • overwrite: true なので、移動先に同名があれば既存ファイルは置き換えられます
  • 移動が成功した時点で、元の場所(e.FullPath)にはファイルは残りません(移動なので消えます)
  • どこへ移動したかは、ログ(例:listBox1)に e.FullPath -> targetFile と出しておくと追跡が簡単です

つまり「上書き」は“コピーして両方残す”ではなく、“移動して置き換える”動きです。

上書き移動の実装で、ついでに直しておくべき落とし穴

File.Move(..., overwrite: true) はとても強力ですが、FileSystemWatcher 特有の落とし穴は別に存在します。ここを押さえておくと「動いたり動かなかったり」を減らせます。

落とし穴何が起きる?対策
Created が「書き込み途中」で来るファイルがロック中で Move できず例外、または不完全な状態で処理してしまうファイルが使用中でないことを確認してから Move(後述の IsFileReadyAsync)
同じファイルで複数回イベントが来ることがある二重移動・例外・ログが増えるキュー化/重複ガード(パス単位で処理中管理)/リトライ
監視対象に移動先フォルダが含まれている移動したファイルが再度 Created 扱いになり、無限ループや意図しない処理が起きる移動先を除外判定(パスが TargetPath 配下なら無視)
Created が「ファイル」だけでなく「ディレクトリ」でも来るディレクトリに対して Move しようとして失敗Directory.Exists(e.FullPath) などで弾く
アクセス権限(C:\配下)権限不足で読み取り・移動が失敗監視対象を絞る/実行権限を見直す/例外ログを残す
大量イベントでバッファオーバーフロー一部イベントが欠落するError イベントで検知し、再初期化/監視を絞る

特に3つ目の「移動先も監視してしまう」問題は、C:\配下を広く監視する構成だとかなりの頻度で踏みます。C:\Users\sy\Documents\test22 も C:\配下なので、IncludeSubdirectories = true の場合は対策が必須です。

実務向け:上書き移動を安定させるサンプル(除外・準備完了待ち・例外ログ)

「動く」だけでなく「事故りにくい」を重視した形です。ポイントは次の通りです。

  • 移動先フォルダは Directory.CreateDirectory で確実に作る(存在してもOK)
  • 移動先配下で発生したイベントは無視する(ループ防止)
  • ファイルがロック解除されるまで少し待つ(コピー中の Created 対策)
  • 例外は握りつぶさず、ログに残す(原因調査ができる)
private static readonly string TargetPath = @"C:\Users\sy\Documents\test22";

private async void Watcher_Created(object sender, FileSystemEventArgs e)
{
    try
    {
        // ディレクトリ作成イベント等を除外
        if (Directory.Exists(e.FullPath))
            return;

        // 移動先も監視している場合の無限ループを防ぐ
        if (e.FullPath.StartsWith(TargetPath, StringComparison.OrdinalIgnoreCase))
            return;

        Directory.CreateDirectory(TargetPath);

        string targetFile = Path.Combine(TargetPath, Path.GetFileName(e.FullPath));

        // 書き込み途中を避ける(一定時間待ってダメなら諦める/ログ)
        bool ready = await IsFileReadyAsync(e.FullPath, timeoutMs: 10_000, pollMs: 200);
        if (!ready)
        {
            // ここは要件により、リトライキューへ入れる等も検討
            Log($"Not ready: {e.FullPath}");
            return;
        }

        // ここが核心:上書き移動
        File.Move(e.FullPath, targetFile, overwrite: true);

        Log($"Moved: {e.FullPath} -> {targetFile}");
    }
    catch (UnauthorizedAccessException ex)
    {
        Log($"Access denied: {e.FullPath} ({ex.Message})");
    }
    catch (IOException ex)
    {
        Log($"IO error: {e.FullPath} ({ex.Message})");
    }
    catch (Exception ex)
    {
        Log($"Unexpected: {e.FullPath} ({ex})");
    }
}

private static async Task<bool> IsFileReadyAsync(string path, int timeoutMs, int pollMs)
{
    var start = Environment.TickCount;

    while (Environment.TickCount - start < timeoutMs)
    {
        try
        {
            // 排他で開ける = 他プロセスが掴んでいない可能性が高い
            using var stream = new FileStream(
                path,
                FileMode.Open,
                FileAccess.ReadWrite,
                FileShare.None);

            return true;
        }
        catch (IOException)
        {
            // コピー中・書き込み中など
        }
        catch (UnauthorizedAccessException)
        {
            // 権限不足・保護フォルダなど
        }

        await Task.Delay(pollMs);
    }

    return false;
}

private void Log(string message)
{
    // UI/ログ出力は環境に合わせて。WinFormsならInvokeが必要。
    listBox1.Invoke((Action)(() => listBox1.Items.Add(message)));
}

ここまでやって初めて「たまに失敗する」を潰しやすくなります。要件が「必ず移動したい」なら、失敗時に別フォルダへ退避したり、再試行キューに積むなどの運用寄りの設計も合わせて検討しましょう。

File.Delete を書きたくなる気持ちを、実装でどう抑えるか

上書き移動の実装で、つい次のようなコードを書きたくなります。

// やりがち(順序を間違えると事故)
File.Move(source, target);
if (File.Exists(target)) File.Delete(target);

しかし、この順序だと「Move に成功した直後の target は、まさに今移動してきたファイル」です。結果として移動したファイルを自分で消すことになります。

仮に順序を入れ替えても、次のような別の問題が残ります。

  • 削除の瞬間に、別のスレッド/別プロセスが同名ファイルを作る(競合)
  • 削除や移動の間で、監視イベントが追加で発生して処理が混線する
  • 削除できない(読み取り専用、権限、ロック)ケースが増えて例外が増える

そのため、「上書きする」ことを目的に Delete を自前で組み込むより、APIが提供する上書きオプションを使うほうが安全で、読みやすく、保守もしやすくなります。

.NET Framework だと File.Move(..., overwrite: true) が使えない場合の代替

プロジェクトによっては .NET Framework を使っていて、File.Move に上書きオプションが無いケースがあります。その場合は、代替案を用途別に選びます。

代替案やることメリット注意点
削除→移動if (File.Exists(dest)) File.Delete(dest); File.Move(src, dest);概念が分かりやすい競合・ロック・権限で失敗しやすい。順序ミスが事故の原因
コピー→削除File.Copy(src, dest, true); File.Delete(src);Copy の上書きは昔からある厳密には「移動」ではなく「コピーして消す」。大容量だと時間がかかる
File.Replace既存がある前提で置換(バックアップも可能)置換用途に強い存在しない場合の分岐が必要。仕様理解が必要

「どうしても .NET Framework で上書き移動したい」なら、まずは Copy→Delete が実装として安全なことが多いです。速度やディスク負荷が問題になる場合に、削除→移動や Replace を検討するとよいでしょう。

監視対象が「C:\配下」のときに知っておくべき現実的な制約

理想として「C:\ 配下で作成されたファイルを全部拾う」は魅力的ですが、実運用では制約も増えます。

  • イベント量が膨大:Windowsやアプリが細かくファイルを作るため、処理が追いつかないことがある
  • 権限の壁:システムフォルダや他ユーザー領域にアクセスできず、移動に失敗する
  • テンポラリファイルが多い:拡張子 .tmp、Office の ~$、アプリ固有の中間ファイルなど

このため、検索意図として「C:\配下全部」と書かれていても、実装では次のように絞ると成功率が上がります。

  • 監視するフォルダを「実際にファイルが置かれる場所」に限定する
  • Filter を拡張子で絞る(例:*.pdf、*.csv など)
  • 移動先フォルダは監視範囲から外す(またはイベント内で除外する)

「上書き移動のロジックが合っているのに不安定」というとき、原因が監視対象の広さにあることは少なくありません。

よくある質問:上書き移動が失敗するケースと対処

移動先ファイルが読み取り専用(ReadOnly)だとどうなる?

多くの場合、上書きは権限や属性の影響を受けます。読み取り専用・アクセス権不足・他プロセスロックのどれかがあると、overwrite: true でも例外になる可能性があります。対処は次のいずれかです。

  • 失敗したらログを残して、手動確認できる運用にする
  • 要件として「必ず置換」が必要なら、属性解除(ReadOnly を外す)や再試行の仕組みを入れる
  • 「置換できないなら別名で退避する」など、例外時の方針を決める

Created で拾ったのに「ファイルが見つかりません」になる

Created の発火タイミングと、実ファイルの生成・書き込みのタイミングがズレることがあります。特にネットワーク越しや、アプリが一時名で作って最後にリネームするタイプだと起きやすいです。

  • IsFileReadyAsync のような待ちを入れる
  • Created だけでなく Renamed を併用する(最終名で確実に拾う)

同じファイル名が頻繁に作られる(ログなど)場合は?

同名が頻発するなら、単純な上書き移動は「最新だけ残る」挙動になります。要件次第で、次の方針も検討すると事故が減ります。

  • 履歴を残す:タイムスタンプを付けて別名保存(例:log_20260103_120501.txt)
  • 上書きはするがバックアップも取る:置換前を別フォルダへ退避
  • 処理対象を限定:特定拡張子だけ、特定フォルダだけ

まとめ:上書き移動は「APIに任せて」Watcher特有の罠を潰す

FileSystemWatcher で検知したファイルを「同名なら上書きして移動」したいなら、まずはFile.Move の上書きオプションを使うのが最短で安全です。手動で File.Delete を挟むと、順序ミスで移動直後のファイルを削除するなど、意図しない事故に繋がります。

その上で、運用上の安定性を上げるには次の3点が効きます。

  • ファイルが書き込み途中ではないか(ロック解除待ち・リトライ)
  • 移動先フォルダが監視対象に含まれていないか(ループ防止)
  • 例外ログを残す(権限・ロック・パス問題の切り分け)

上書き移動自体は一行で済みますが、安定して動かすには「Watcherの癖」を理解して実装に織り込むことが重要です。この記事のサンプルをベースに、監視範囲と例外時の方針まで決めておくと、公開後のトラブルシュートが一気に楽になります。

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次