C# ZipFileで「別のプロセスが使用中」エラーが出る原因と対処法まとめ【Azure Functions対応】

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

という構成にしてしまうと、

  1. ZipFile.CreateFromDirectory が Ajib を列挙しはじめる
  2. 処理の途中で自分が今まさに作っている NM 1.zip をファイル一覧の中に見つける
  3. その 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&gt(), "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 の問題に見えますが、実際には ファイルを開いたままにしている何かがいる だけです。この記事のポイントを押さえておけば、原因の切り分けと対策がスムーズに進むはずです。

この記事を書いた人

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

コメント

コメントする

目次