C# の ZipFile.CreateFromDirectory を使っているときに「別のプロセスが使用中です」という IOException が出て、なかなか原因が分からない……というケースはとてもよくあります。この記事では、典型例である C:\Users\User\Desktop\Ajib\ を ZIP 化するシナリオを題材に、原因の切り分けから具体的なコード例、Azure Functions での実践的な対処まで、実運用を意識した対策をまとめます。
エラーの概要と再現パターン
今回の代表的なケースは次のようなものです。
- やりたいこと:
C:\Users\User\Desktop\Ajib\配下のファイルを、C:\Users\User\Desktop\Cojib\NM 1.zipとしてまとめて ZIP 化したい。 - 発生している例外:
System.IO.IOException: The process cannot access the file
'C:\Users\User\Desktop\Ajib\NM 1.zip' because it is being used by another process.
エラーメッセージをよく見ると、パスが Desktop\Ajib\NM 1.zip を指していることに気づきます。つまり、
- 圧縮元フォルダ
Ajibの中にもNM 1.zipが存在している - その ZIP が「どこかのプロセスで開かれたまま」になっている
という状況が非常に疑われます。
さらに、Azure Functions や Web API で「一時フォルダを ZIP にして HTTP レスポンスで返す」といった処理を書くと、サーバー側でも同様の現象が起きることがあります。これは根本原因が同じ「ファイルロック」であるためです。
最短で試すべき対処ポイント
現場でまず試すべき「手っ取り早い解決策」は次の3つです。
- 圧縮元フォルダ内・出力先フォルダの「開きっぱなしのファイル」を全て閉じる
例:エクスプローラーのプレビュー、ZIP を開いているエクスプローラーや解凍ソフト、エディタなど。 - 生成する ZIP ファイルを圧縮元フォルダの外に出す
Ajibを圧縮するなら、Ajibの外側(今回はCojib)にNM 1.zipを作成する。 - 既存の同名 ZIP があれば削除してから新しく作り直す
多くのケースでは、この3つを徹底するだけでエラーが解消します。
なぜ「別のプロセスが使用中」になるのか
Windows のファイルは、「誰が」「どのモードで」開いているかによって、他プロセスからのアクセス可否が変わります。C# からファイルを開くとき、内部では FileShare という共有モードが使われており、これが「排他ロック」の正体です。
よくある原因を整理すると、次のようになります。
| 原因パターン | 典型的な症状 | 主な対策 |
|---|---|---|
| 出力先 ZIP が圧縮元の内側にある | 自分で作成中の ZIP を再帰的に読み込もうとして例外 | ZIP の出力先を圧縮元の外側に移動する |
| 圧縮元内のファイルがロックされている | 特定ファイルだけでエラーになる、ウイルス対策ソフトなどが原因のことも | 該当アプリを閉じる/あとからリトライ処理を入れる |
| 既存の出力先 ZIP が別プロセスで開かれている | 前回生成した ZIP を開いたまま、同じ場所に再生成しようとして失敗 | Zip を開いているアプリを閉じる/事前に File.Delete する |
| 自前コードで FileStream を閉じていない | 自分のプログラムを実行するたびに再現する | using で確実に Dispose する |
出力先 ZIP が圧縮元フォルダ内にある問題
もっともありがちな罠がこれです。
- 圧縮元:
C:\Users\User\Desktop\Ajib - 出力先:
C:\Users\User\Desktop\Ajib\NM 1.zip
という構成にしてしまうと、
ZipFile.CreateFromDirectoryがAjibを列挙しはじめる- 処理の途中で自分が今まさに作っている
NM 1.zipをファイル一覧の中に見つける - その ZIP を読み込もうとして、自分自身と競合し
IOExceptionが発生
という自己再帰のような状態になります。基本的に「圧縮元フォルダの中に出力 ZIP を置く」のは NG です。
圧縮元ファイル/ZIP が他プロセスにロックされている
もう一つのよくあるパターンは、「ファイルを開いたまま ZIP を作ろうとしている」ケースです。
- エクスプローラーのプレビューウィンドウで PDF や画像を表示している
- Office・テキストエディタ・画像編集ソフトが該当ファイルを開いている
- ウイルス対策ソフトやインデックスサービスがアクセス中
- 自前の C# コードで開いた
FileStreamをDisposeしていない
特に自分のコードでありがちなのは、次のような書き方です。
// よくない例:Dispose を忘れている
var stream = new FileStream(path, FileMode.Create);
var writer = new StreamWriter(stream);
// writer.Close() も Dispose も呼ばずに終了…
これだと FileStream が開きっぱなしになり、そのファイルを含む ZIP を作ろうとした時点でエラーが出ます。必ず using で囲む癖を付けましょう。
基本の解決策:ZipFile を素直に使う
まずは、標準 API を素直に使った最小構成の例を押さえておきます。
using System.IO;
using System.IO.Compression;
var source = @"C:\Users\User\Desktop\Ajib";
var dest = @"C:\Users\User\Desktop\Cojib\NM 1.zip";
// 既存の出力先 ZIP は削除してから作成
if (File.Exists(dest))
{
File.Delete(dest);
}
// 圧縮元と出力先が「内包関係」になっていないことを確認した上で実行
ZipFile.CreateFromDirectory(
source,
dest,
CompressionLevel.Fastest,
includeBaseDirectory: true
);
ポイントは次の通りです。
- 出力先 ZIP は圧縮元フォルダの外に置く
今回はAjibの外側であるCojibフォルダに出力しています。 - 同名 ZIP が存在する場合は事前に削除
別プロセスで開かれているとFile.Delete自体が失敗するので、その時点で「他で開かれている」ことに気づけます。 ZipFile.CreateFromDirectory後にClose()は不要
メソッドから戻るタイミングで ZIP はクローズされています。ここを疑っても解決しません。
不要なファイルを除外して ZIP を作成する
フォルダの中にすでに ZIP ファイルが存在していて、それを含めたくない/含めると自己参照になってしまう場合は、「列挙してから除外する」方法が安全です。
using System;
using System.IO;
using System.IO.Compression;
var source = @"C:\Users\User\Desktop\Ajib";
var dest = @"C:\Users\User\Desktop\Cojib\NM 1.zip";
if (File.Exists(dest))
{
File.Delete(dest);
}
using (var archive = ZipFile.Open(dest, ZipArchiveMode.Create))
{
foreach (var path in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{
// 例:既存の .zip ファイルは除外する
if (string.Equals(Path.GetExtension(path), ".zip", StringComparison.OrdinalIgnoreCase))
{
continue;
}
var relativePath = Path.GetRelativePath(source, path);
archive.CreateEntryFromFile(
path,
relativePath,
CompressionLevel.Fastest
);
}
}
このように自分でファイル列挙~追加を制御すると、
- 特定の拡張子だけ除外・含める
- 特定のフォルダ以下を除外する
- エラー時のログを詳細に出す
といったカスタマイズがやりやすくなり、トラブルシュートもしやすくなります。
ロックされているファイルの簡易チェック
「どのファイルがロックされているのか分からない」という場合、小さなユーティリティメソッドで簡易チェックを行うのも手です。
using System;
using System.IO;
static bool IsFileLocked(string path)
{
try
{
// FileShare.None で排他的に開けるかチェック
using (var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.None))
{
}
return false; // 正常に開けた=ロックされていない
}
catch (IOException)
{
return true; // 何かに掴まれている
}
}
開発環境では、これを使って怪しそうなファイルだけチェックし、ログ出力しておくと原因に近づきやすくなります。さらに突き詰めたいときは、Windows 標準の「リソース モニター」や Sysinternals の Process Explorer / Handle などでロック元プロセスを特定します。
Azure Functions・Web API での ZIP 生成時の注意点
Azure Functions や ASP.NET Core Web API など、サーバーサイドで ZIP を返す場合も、基本の考え方は同じです。ただし環境の特性上、より堅牢な作りにしておく必要があります。
前処理のファイルは必ず閉じる(using の徹底)
ZIP を作る前段で、テンポラリファイルを生成しているケースが多いはずです。例えば CSV を出力してから ZIP にまとめる、HTML をレンダリングしてからまとめる、などです。
- テンポラリファイルを作る
StreamWriter - 画像変換のために使う
FileStream - PDF 生成ライブラリが返す
Stream
これらを using で確実に閉じないと、本番環境では「ときどき ZIP 生成に失敗する」「リクエストが積み上がる」といった不安定な挙動になります。
// 良い例:書き込み後は必ず閉じる
var tempFile = Path.Combine(Path.GetTempPath(), $"{Guid.NewGuid():N}.csv");
using (var writer = new StreamWriter(tempFile))
{
// CSV を出力
}
「ZIP のコードは合っているのに、前処理のストリームが閉じられていなかった」というパターンは非常に多いので、まずここを疑いましょう。
ディスクを使わずメモリ上で ZIP を作って返す
ファイルロック問題をかなりの割合で避けられる方法が、「ディスクに ZIP を書かず、メモリ上で ZIP を構成してそのまま返す」やり方です。Azure Functions(HTTP トリガー)を想定した例を示します。
using System.IO;
using System.IO.Compression;
using Microsoft.AspNetCore.Mvc;
// sourceDir: 一時ファイルを並べておいたフォルダパス
public static IActionResult BuildZipResult(string sourceDir)
{
using var ms = new MemoryStream();
// leaveOpen: true にしてメモリストリームを閉じない
using (var archive = new ZipArchive(ms, ZipArchiveMode.Create, leaveOpen: true))
{
foreach (var file in Directory.EnumerateFiles(sourceDir, "*", SearchOption.AllDirectories))
{
var relativePath = Path.GetRelativePath(sourceDir, file);
archive.CreateEntryFromFile(
file,
relativePath,
CompressionLevel.Fastest
);
}
}
ms.Position = 0;
return new FileContentResult(ms.ToArray(), "application/zip")
{
FileDownloadName = "output.zip"
};
}
この方法のメリットは以下の通りです。
- 「出力先 ZIP ファイル」という実体がディスク上に存在しないため、Zip 自体のファイルロックは発生しない
- 一時ファイルの掃除(削除)だけを考えればよい
- ストレージ I/O が減るため、軽量な ZIP であれば高速
注意点としては、大きな ZIP(数百 MB~)になるとメモリ使用量が増えるので、サイズに応じてディスク経由の方法と使い分けるとよいでしょう。
どうしてもディスク経由で ZIP を返す場合
ZIP が大きい場合や、他プロセスと ZIP を共有する必要がある場合は、「一時フォルダに作ってから読み出し、最後に削除する」という流れが一般的です。その際、ウイルス対策ソフト等の影響で、生成直後に読み出そうとすると一瞬だけ IOException が出ることがあります。
そのような揮発的なエラーは、軽いリトライで吸収してしまうのが実運用上は楽です。
using System;
using System.IO;
using System.IO.Compression;
using System.Threading;
using Microsoft.AspNetCore.Mvc;
public static IActionResult BuildZipResultViaDisk(string outputPath)
{
// 一意な ZIP ファイルパスを作成
var tempZip = Path.Combine(
Path.GetTempPath(),
$"out-{Guid.NewGuid():N}.zip"
);
// フォルダ outputPath を ZIP にする
ZipFile.CreateFromDirectory(
outputPath,
tempZip,
CompressionLevel.Fastest,
includeBaseDirectory: false
);
byte[] bytes = null;
// 最大 5 回まで読み込みをリトライ
for (int i = 0; i < 5; i++)
{
try
{
bytes = File.ReadAllBytes(tempZip);
break;
}
catch (IOException)
{
// 150ms 待ってから再試行
Thread.Sleep(150);
}
}
try
{
return new FileContentResult(bytes ?? Array.Empty<byte>(), "application/zip")
{
FileDownloadName = "output.zip"
};
}
finally
{
// 後始末は失敗しても致命的ではないので握りつぶす
try
{
if (File.Exists(tempZip))
{
File.Delete(tempZip);
}
}
catch
{
}
}
}
ここでのポイントは次の通りです。
- 一意なファイル名:
Guidなどを使い、複数リクエストで衝突しないようにする - 小さなリトライ:エラーが一瞬だけ発生するようなケースを吸収する
- 最後に必ず削除:一時ファイルが溜まり続けないようにする
ZipFile.CreateFromDirectory の仕様上のポイント
トラブルシュート時によく誤解されるポイントを整理しておきます。
| 項目 | 誤解されがちな点 | 正しい理解 |
|---|---|---|
| ZIP のクローズタイミング | Close() しないと ZIP が閉じられない | メソッドから戻る時点で ZIP は閉じられる。追加の Close() は不要。 |
includeBaseDirectory | 真偽によってロックの挙動が変わる | ZIP 内のパス構造が変わるだけで、ファイルロックとは無関係。 |
| 出力先パス | 圧縮元と同じフォルダでも問題ない | 「圧縮元フォルダの内側」はNG。自己参照でエラーになりやすい。 |
| IOException の原因 | ZipFile 側のバグ | ほとんどが「どこかでファイルが開きっぱなし」の問題。 |
「別のプロセスが使用中」エラーを防ぐ設計のコツ
一度問題に当たってしまうと、再発しないように設計や実装のレベルで対策しておきたくなります。以下は、実運用を考えたときにおすすめできる設計上のポイントです。
ファイル生成処理を 1 メソッドに閉じ込める
テンポラリファイルの生成・書き込み・クローズを分散させると、「どこで閉じ忘れたか」がすぐには分からなくなります。可能であれば、次のような構成にして、1 メソッドの中で完結させるのが望ましいです。
public string GenerateTempFiles()
{
var dir = Path.Combine(Path.GetTempPath(), $"work-{Guid.NewGuid():N}");
Directory.CreateDirectory(dir);
// 1. using を徹底しながらテンポラリファイルを生成
// 2. 必要であればここでログも残す
return dir; // ZIP の元になるフォルダパスだけ返す
}
こうしておけば、「このメソッドから戻るときには、少なくともロックは残っていない」という前提で ZipFile.CreateFromDirectory を呼べます。
UI アプリでは「ZIP 完了まで開かない」誘導をする
デスクトップアプリや WinForms・WPF などでユーザーに ZIP を生成させる場合、
- ZIP 生成中は「完了するまで ZIP を開かないでください」と明示的に伝える
- 必要なら ZIP 完了後に自動でエクスプローラーを開く
といった UX の工夫も有効です。ユーザーが ZIP をダブルクリックして開いている状態で再生成しようとすると、自動テストでは再現しないのに本番だけエラー、という「あるある」な状況になりがちです。
ログには「どのファイルで失敗したか」を必ず残す
エラーログの粒度を上げることも、後からの調査を楽にします。例えば自前でファイルを列挙して ZIP に追加している場合、次のようにログを残しておくと便利です。
foreach (var path in Directory.EnumerateFiles(source, "*", SearchOption.AllDirectories))
{
try
{
var rel = Path.GetRelativePath(source, path);
archive.CreateEntryFromFile(path, rel, CompressionLevel.Fastest);
}
catch (IOException ex)
{
logger.LogError(ex, "ZIP 追加中にエラー: {FilePath}", path);
throw;
}
}
これで、「どのファイルを追加しようとしたときにエラーになったか」が一目で分かり、原因特定のスピードが一気に上がります。
チェックリスト:これを見直せば大体直る
最後に、「別のプロセスが使用中」エラーに遭遇したときに確認したいポイントをチェックリストとしてまとめます。実際の現場では、このリストを上から順に潰していくだけで解決することがほとんどです。
- 出力先 ZIP は圧縮元フォルダの外側にあるか
- 既存の出力先 ZIP を
File.Deleteしてから作成しているか - 圧縮元フォルダ内の ZIP やその他ファイルを、エクスプローラーやアプリが開いたままになっていないか
- 自前コードの
FileStream/StreamWriter/Streamを全てusingで閉じているか - ウイルス対策ソフトやインデックスサービスによる一時的なロックに対して、短いリトライを入れているか
- サーバーサイドでは、可能な限りメモリ上で ZIP を作るようにしているか
- どうしてもディスク経由にする場合、一意な一時ファイル名+後始末(削除)が実装されているか
これらを意識して設計・実装しておくことで、「なぜか本番だけ ZIP 生成が失敗する」「環境によって再現したりしなかったりする」といった不安定なトラブルをかなりの割合で防止できます。
「別のプロセスが使用中」エラーは、一見すると .NET や ZipFile の問題に見えますが、実際には ファイルを開いたままにしている何かがいる だけです。この記事のポイントを押さえておけば、原因の切り分けと対策がスムーズに進むはずです。

コメント