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の癖」を理解して実装に織り込むことが重要です。この記事のサンプルをベースに、監視範囲と例外時の方針まで決めておくと、公開後のトラブルシュートが一気に楽になります。

コメント