C# Tips | ファイル・ディレクトリ操作:再帰的フォルダ削除

C sharp C#
スポンサーリンク

はじめに なぜ「再帰的フォルダ削除」は危険で、だからこそ重要なのか

業務システムでは、「一時作業フォルダを丸ごと消す」「古いバックアップフォルダをまとめて削除する」「処理失敗時に作業領域をクリーンアップする」といった、「フォルダごと中身を全部消す」場面が必ず出てきます。
このときに使うのが「再帰的フォルダ削除」です。

ただし、再帰的フォルダ削除は、正しく設計しないと非常に危険です。
一つパスを間違えただけで、「必要なデータを丸ごと削除してしまった」「アプリケーション本体のフォルダを消してしまった」といった致命的な事故につながります。
だからこそ、「どう書くか」だけでなく、「どう制限するか」「どうログを残すか」まで含めて設計することが、実務ではとても大切になります。

ここでは、C# の Directory.Delete をベースに、「再帰的フォルダ削除」を初心者向けにかみ砕きつつ、業務でそのまま使えるユーティリティの形まで落とし込んでいきます。


基本のメソッド Directory.Delete と recursive 引数

Directory.Delete の基本挙動を押さえる

まずは、Directory.Delete の基本的な使い方から確認します。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string folderPath = @"C:\temp\work";

        Directory.Delete(folderPath);

        Console.WriteLine("ディレクトリ削除を実行しました。");
    }
}
C#

このコードは、「C:\temp\work というディレクトリを削除する」という意味です。
ここで重要なのは、この形の Directory.Delete は「中身が空のディレクトリでないと削除できない」という点です。
中にファイルやサブフォルダが残っていると、IOException が発生して削除に失敗します。

つまり、「フォルダの中身も含めて全部消したい」場合には、このままでは不十分です。

recursive: true で中身ごと削除する

フォルダの中身も含めて丸ごと削除したいときは、Directory.Delete の第二引数 recursivetrue にします。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string folderPath = @"C:\temp\work";

        Directory.Delete(folderPath, recursive: true);

        Console.WriteLine("ディレクトリとその中身をすべて削除しました。");
    }
}
C#

recursive: true を指定すると、そのフォルダ内のファイル、サブフォルダ、そのまた中のファイル…というように、階層をたどりながらすべて削除してくれます。
つまり、「再帰的フォルダ削除」を、.NET が内部でやってくれているイメージです。

ここで深掘りしたいのは、「この一行が非常に強力であると同時に、とても危険でもある」ということです。
パスを一文字間違えただけで、「消すつもりのなかったフォルダ」を丸ごと削除してしまう可能性があります。


再帰的フォルダ削除をユーティリティとして包む

存在チェックとログを組み合わせた基本ユーティリティ

業務コードのあちこちで、いきなり Directory.Delete(path, true); と書くのは危険です。
そこで、「存在チェック」「ログ出力」を含めたユーティリティメソッドとして包んでおくと、安全性と読みやすさがぐっと上がります。

using System;
using System.IO;

public static class RecursiveDirectoryDeleteUtil
{
    public static bool DeleteDirectoryIfExists(string folderPath)
    {
        if (!Directory.Exists(folderPath))
        {
            Console.WriteLine($"削除対象ディレクトリが存在しません: {folderPath}");
            return false;
        }

        Directory.Delete(folderPath, recursive: true);
        Console.WriteLine($"ディレクトリを再帰的に削除しました: {folderPath}");
        return true;
    }
}
C#

使い方の例は次の通りです。

class Program
{
    static void Main()
    {
        string folderPath = @"C:\temp\work";

        bool deleted = RecursiveDirectoryDeleteUtil.DeleteDirectoryIfExists(folderPath);

        if (deleted)
        {
            Console.WriteLine("削除済みとして後続処理を進めます。");
        }
        else
        {
            Console.WriteLine("そもそも存在しなかったので、削除は行われていません。");
        }
    }
}
C#

ここでのポイントは、「存在しない場合は例外ではなく情報として扱い、戻り値で削除の有無を返している」ことです。
業務によっては、「存在しない=問題なし」とみなすほうが自然な場合が多く、その考え方をコードに反映しています。

ログ出力先を差し替えられるようにする

実務では、コンソールではなく、ファイルログや監視システムにメッセージを送りたいことが多いです。
そこで、ログ出力をデリゲートとして受け取る形にしておくと、再利用性が高くなります。

using System;
using System.IO;

public static class RecursiveDirectoryDeleteUtil
{
    public static bool DeleteDirectoryIfExists(
        string folderPath,
        Action<string>? log = null)
    {
        if (!Directory.Exists(folderPath))
        {
            log?.Invoke($"[INFO] 削除対象ディレクトリが存在しません: {folderPath}");
            return false;
        }

        Directory.Delete(folderPath, recursive: true);
        log?.Invoke($"[INFO] ディレクトリを再帰的に削除しました: {folderPath}");
        return true;
    }
}
C#

使い方の例は次の通りです。

class Program
{
    static void Main()
    {
        string folderPath = @"C:\temp\work";

        RecursiveDirectoryDeleteUtil.DeleteDirectoryIfExists(
            folderPath,
            message => Console.WriteLine(message)
        );
    }
}
C#

こうしておくと、将来「ログをファイルに書きたい」「外部のロガーライブラリを使いたい」となったときも、ユーティリティ本体を変えずに対応できます。


「消してよい範囲」を制限するルートチェック

一番怖いのは「消しすぎる」こと

再帰的フォルダ削除で一番怖いのは、「消すつもりのなかった場所まで消してしまう」ことです。
たとえば、本当は C:\app\work\temp を消したかったのに、設定ミスで C:\app を渡してしまった、というようなケースです。

このリスクを減らすために、「このルート配下だけ削除を許可する」という制限をユーティリティに組み込むのが、実務的な防御策になります。

ルート配下かどうかをチェックするユーティリティ

次のようなユーティリティを考えてみます。

using System;
using System.IO;

public static class SafeRecursiveDirectoryDeleteUtil
{
    public static bool DeleteDirectoryUnderRoot(
        string targetFolder,
        string allowedRoot,
        Action<string>? log = null)
    {
        string fullTarget = Path.GetFullPath(targetFolder);
        string fullRoot   = Path.GetFullPath(allowedRoot);

        if (!fullTarget.StartsWith(fullRoot, StringComparison.OrdinalIgnoreCase))
        {
            string message =
                $"[ERROR] 許可されていない場所の削除が要求されました: {fullTarget} / ルート: {fullRoot}";
            log?.Invoke(message);
            throw new InvalidOperationException(message);
        }

        if (!Directory.Exists(fullTarget))
        {
            log?.Invoke($"[INFO] 削除対象ディレクトリが存在しません: {fullTarget}");
            return false;
        }

        Directory.Delete(fullTarget, recursive: true);
        log?.Invoke($"[INFO] ディレクトリを再帰的に削除しました: {fullTarget}");
        return true;
    }
}
C#

使い方の例は次の通りです。

class Program
{
    static void Main()
    {
        string root        = @"C:\app\work";
        string targetFolder = @"C:\app\work\temp1";

        SafeRecursiveDirectoryDeleteUtil.DeleteDirectoryUnderRoot(
            targetFolder,
            root,
            message => Console.WriteLine(message)
        );
    }
}
C#

ここで深掘りしたいポイントは、「削除してよい範囲をコードで明示している」ことです。
Path.GetFullPath で正規化したうえで StartsWith でチェックすることで、「ルート配下かどうか」を判定しています。
これにより、設定ミスやバグで「アプリの作業領域の外」を消してしまうリスクを大きく減らせます。


一時ディレクトリ・作業領域のライフサイクル管理

「作る→使う→丸ごと消す」を一つの流れとして設計する

バッチ処理やファイル変換処理では、「処理ごとに一時フォルダを作り、その中で作業して、最後に丸ごと削除する」というパターンがよくあります。
このとき、ディレクトリ作成と再帰削除をセットでユーティリティ化しておくと、コードがとても分かりやすくなります。

using System;
using System.IO;

public static class WorkDirectoryManager
{
    public static string CreateWorkDirectory(string baseFolder, Action<string>? log = null)
    {
        Directory.CreateDirectory(baseFolder);

        string workFolder = Path.Combine(
            baseFolder,
            "work_" + DateTime.Now.ToString("yyyyMMdd_HHmmss_fff")
        );

        Directory.CreateDirectory(workFolder);
        log?.Invoke?.Invoke($"[INFO] 作業ディレクトリを作成しました: {workFolder}");
        return workFolder;
    }

    public static void CleanupWorkDirectory(string workFolder, Action<string>? log = null)
    {
        if (!Directory.Exists(workFolder))
        {
            log?.Invoke($"[INFO] 作業ディレクトリが既に存在しません: {workFolder}");
            return;
        }

        Directory.Delete(workFolder, recursive: true);
        log?.Invoke($"[INFO] 作業ディレクトリを再帰的に削除しました: {workFolder}");
    }
}
C#

使い方のイメージは次の通りです。

class Program
{
    static void Main()
    {
        string baseFolder = @"C:\app\work";

        string workFolder = WorkDirectoryManager.CreateWorkDirectory(
            baseFolder,
            message => Console.WriteLine(message)
        );

        try
        {
            Console.WriteLine("ここで作業フォルダ内で処理を行うイメージです。");
        }
        finally
        {
            WorkDirectoryManager.CleanupWorkDirectory(
                workFolder,
                message => Console.WriteLine(message)
            );
        }
    }
}
C#

ここでの重要ポイントは、「作業フォルダのライフサイクルをコードで明確にしている」ことです。
どこに作るか、どんな名前にするか、いつ消すかをユーティリティに閉じ込めることで、業務ロジック側は「作る→使う→片付ける」という流れに集中できます。


例外とエラー処理を意識した再帰削除

どんな例外が起こり得るかを知っておく

再帰的フォルダ削除では、次のような理由で例外が発生する可能性があります。

権限がなくて削除できない。
中のファイルが別プロセスにロックされている。
読み取り専用属性が付いている。
パスが不正、またはディレクトリではなくファイルだった。
ディスクやファイルシステムに問題がある。

これらをすべて細かくハンドリングするのは大変ですが、「少なくともログには残す」という姿勢が大事です。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string folderPath = @"C:\temp\work";

        try
        {
            Directory.Delete(folderPath, recursive: true);
            Console.WriteLine("ディレクトリ削除に成功しました。");
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine("権限エラーが発生しました: " + ex.Message);
        }
        catch (IOException ex)
        {
            Console.WriteLine("入出力エラーが発生しました: " + ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("想定外のエラーが発生しました: " + ex.Message);
        }
    }
}
C#

実務では、これらのメッセージをファイルログや監視システムに記録しておくことで、「どのフォルダの削除に、どんな理由で失敗したか」を後から追跡できます。
特に権限やロックの問題は、アプリ側だけでは解決できないことも多いため、運用担当者が原因を判断できる情報を残しておくことが重要です。


「消しすぎない」ための設計のコツ

パスの組み立ては必ず Path.Combine を使う

削除系の処理では、「パスの組み立てミス」が致命傷になりやすいです。
文字列連結で baseFolder + "\\" + subFolder のように書くと、区切り文字の重複や抜けで、意図しないパスになることがあります。

C# では Path.Combine を使うことで、OS に合わせて安全にパスを組み立てられます。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string baseFolder = @"C:\app\work";
        string subFolder  = "temp";

        string targetFolder = Path.Combine(baseFolder, subFolder);

        Console.WriteLine("削除対象パス: " + targetFolder);

        RecursiveDirectoryDeleteUtil.DeleteDirectoryIfExists(targetFolder);
    }
}
C#

再帰的フォルダ削除のような危険度の高い操作では、「パスの組み立てを安全に行う」ことを徹底するだけでも、事故の確率をかなり下げられます。

「どこまで消すか」をコードで固定しておく

実務では、「このフォルダ配下は自由に消してよいが、それより上は絶対に消さない」というルールを決めておくと安全です。
先ほどのルート制限ユーティリティは、そのルールをコードに落とし込んだものです。

さらに、次のような工夫も有効です。

削除対象のパスを必ずログに残す。
本番環境では、設定ファイルで「削除を許可するルート」を限定する。
テスト環境と本番環境で、ルートパスを明確に分ける。

削除は「やりすぎたら終わり」なので、「やりすぎないための仕組み」を先に作っておく、という発想がとても大事です。


まとめ 実務で使える再帰的フォルダ削除の考え方

再帰的フォルダ削除は、「フォルダとその中身を一気に片付ける」ための強力な手段であり、業務システムでは一時領域のクリーンアップや古いデータの整理などで頻繁に登場します。
その一方で、「一歩間違えると取り返しがつかない」という危険も常に抱えています。

Directory.Delete(path, recursive: true) が「中身ごと削除する」強力な操作であることを正しく理解すること。
存在チェックやログ出力を組み合わせたユーティリティで包み、あちこちに生の Directory.Delete を書かないようにすること。
ルート制限やパスの組み立てルールをコードに組み込み、「消してよい範囲」を明確にすること。
一時ディレクトリや作業領域のライフサイクルを「作る→使う→丸ごと消す」として設計し、再帰削除をその一部として位置づけること。
例外やエラーの原因をログに残し、運用側が原因を判断できるようにしておくこと。

ここまで押さえておけば、「作業フォルダが掃除されずにディスクがいっぱいになった」「逆に、消してはいけないフォルダを丸ごと消してしまった」といった、現場でよくあるトラブルをかなり減らせます。

タイトルとURLをコピーしました