C# Tips | ファイル・ディレクトリ操作:ファイル読み取り専用判定

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

はじめに なぜ「読み取り専用判定」が業務で効いてくるのか

業務システムでファイルを扱うとき、「このファイル、書き込んでいいのか?」「上書きしようとしたら失敗した」「削除できないと思ったら読み取り専用だった」という場面、けっこうあります。
特に、他システムから受け取ったファイルや、ユーザーが手でコピーしてきたファイルは、「読み取り専用属性」が付いていることがあり、それを知らずに書き込みや削除をしようとしてエラーになる、というのは現場あるあるです。

だからこそ、「処理前に読み取り専用かどうかを判定する」「必要なら読み取り専用を解除してから処理する」といったユーティリティを持っておくと、エラーの原因がぐっと減ります。
ここでは、C# での「読み取り専用判定」の基本から、実務で使えるユーティリティ化、読み取り専用解除まで、初心者向けにかみ砕いて解説していきます。


基本の考え方 FileAttributes と ReadOnly フラグ

ファイル属性は「フラグの集合」

Windows のファイルには、「読み取り専用」「隠しファイル」「システムファイル」など、いくつかの「属性」が付いています。
C# では、これらを FileAttributes という列挙型(enum)で扱います。

ファイルの属性を取得する基本は File.GetAttributes です。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.csv";

        FileAttributes attrs = File.GetAttributes(path);

        Console.WriteLine(attrs);
    }
}
C#

attrs を表示すると、例えば Archive, ReadOnly のように、複数の属性がカンマ区切りで表示されることがあります。
これは、「フラグが組み合わさった値」になっているからです。

読み取り専用かどうかを判定する基本

「読み取り専用かどうか」を判定するには、この FileAttributesReadOnly フラグが含まれているかを調べます。
C# では、ビット演算(&)を使ってチェックします。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.csv";

        FileAttributes attrs = File.GetAttributes(path);

        bool isReadOnly = (attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;

        Console.WriteLine($"読み取り専用か: {isReadOnly}");
    }
}
C#

ここが最初の重要ポイントです。
attrs は「複数のフラグが OR された値」なので、== FileAttributes.ReadOnly のように単純比較すると、「読み取り専用だけが付いている場合」にしか true になりません。
実際には Archive, ReadOnly のように複数付いていることが多いので、& で「ReadOnly ビットが立っているか」を見る必要があります。


読み取り専用判定ユーティリティの基本形

単体ファイルの読み取り専用判定メソッド

毎回ビット演算を書くのは面倒なので、「読み取り専用かどうかを bool で返す」ユーティリティメソッドを用意しておくと便利です。

using System;
using System.IO;

public static class FileReadOnlyUtil
{
    public static bool IsReadOnly(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"ファイルが見つかりません: {filePath}", filePath);
        }

        FileAttributes attrs = File.GetAttributes(filePath);

        return (attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
    }
}
C#

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

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.csv";

        try
        {
            bool isReadOnly = FileReadOnlyUtil.IsReadOnly(path);

            if (isReadOnly)
            {
                Console.WriteLine("このファイルは読み取り専用です。");
            }
            else
            {
                Console.WriteLine("このファイルは書き込み可能です。");
            }
        }
        catch (FileNotFoundException ex)
        {
            Console.WriteLine("エラー: " + ex.Message);
        }
    }
}
C#

ここでの重要ポイントは二つです。
一つ目は、「存在チェックをしてから属性を取得している」こと。存在しないパスに対して属性を取得しようとすると例外になるので、ユーティリティ側で先にチェックしておくと呼び出し側が楽になります。
二つ目は、「ビット演算をユーティリティに隠してしまう」こと。呼び出し側は IsReadOnly(path) という自然な名前で意図を表現でき、読みやすさがぐっと上がります。

例外ではなく false を返すバージョン

場合によっては、「存在しないファイルは読み取り専用ではない(false)として扱いたい」ということもあります。
その場合は、例外ではなく false を返すバージョンを用意します。

public static class FileReadOnlyUtil
{
    public static bool IsReadOnlyOrFalseIfNotExists(string filePath)
    {
        if (!File.Exists(filePath))
        {
            return false;
        }

        FileAttributes attrs = File.GetAttributes(filePath);

        return (attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly;
    }
}
C#

「例外で止めるか」「false で返すか」をメソッドごとに分けておくと、業務ロジックに合わせて選びやすくなります。


読み取り専用解除と組み合わせた実務ユーティリティ

読み取り専用を解除する方法

読み取り専用ファイルに書き込みたい・削除したい、という場面では、「判定」だけでなく「解除」も必要になります。
解除は、File.SetAttributes を使って ReadOnly フラグを外すことで行います。

using System;
using System.IO;

public static class FileReadOnlyUtil
{
    public static void RemoveReadOnly(string filePath)
    {
        if (!File.Exists(filePath))
        {
            throw new FileNotFoundException($"ファイルが見つかりません: {filePath}", filePath);
        }

        FileAttributes attrs = File.GetAttributes(filePath);

        if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
        {
            attrs &= ~FileAttributes.ReadOnly;
            File.SetAttributes(filePath, attrs);
        }
    }
}
C#

ここでの深掘りポイントは、attrs &= ~FileAttributes.ReadOnly; の部分です。
これは、「ReadOnly ビットだけを 0 にする(外す)」というビット演算です。

~FileAttributes.ReadOnly は「ReadOnly 以外のビットが 1 のマスク」で、
attrs & マスク にすることで、「ReadOnly ビットだけを落とした新しい属性値」が得られます。

「必要なら解除してから処理する」パターン

実務では、「読み取り専用なら解除してから削除する」「読み取り専用なら解除してから上書きする」といったパターンがよくあります。
例えば、削除ユーティリティと組み合わせると次のようになります。

using System;
using System.IO;

public static class SafeFileDeleteUtil
{
    public static void DeleteFileForce(string filePath)
    {
        if (!File.Exists(filePath))
        {
            return;
        }

        FileAttributes attrs = File.GetAttributes(filePath);

        if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
        {
            attrs &= ~FileAttributes.ReadOnly;
            File.SetAttributes(filePath, attrs);
        }

        File.Delete(filePath);
    }
}
C#

このようにしておくと、「読み取り専用だから削除できなかった」というエラーを避けられます。
ただし、「読み取り専用を勝手に外してよいかどうか」は業務ルール次第なので、本番運用では「本当にそれでいいか」をチームで決めてから使うのが大事です。


フォルダ内の読み取り専用ファイルを一括チェック

読み取り専用ファイルの一覧を取得する

運用の現場では、「このフォルダの中で読み取り専用になっているファイルを洗い出したい」ということもあります。
その場合は、フォルダ内のファイルを走査して、読み取り専用のものだけを集めるユーティリティを作れます。

using System;
using System.IO;
using System.Collections.Generic;

public static class FileReadOnlySearchUtil
{
    public static string[] GetReadOnlyFiles(string directoryPath, string searchPattern = "*.*")
    {
        if (!Directory.Exists(directoryPath))
        {
            throw new DirectoryNotFoundException($"ディレクトリが見つかりません: {directoryPath}");
        }

        string[] files = Directory.GetFiles(directoryPath, searchPattern);

        var result = new List<string>();

        foreach (string file in files)
        {
            FileAttributes attrs = File.GetAttributes(file);

            if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
            {
                result.Add(file);
            }
        }

        return result.ToArray();
    }
}
C#

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

class Program
{
    static void Main()
    {
        string dir = @"C:\data\import";

        string[] readOnlyFiles = FileReadOnlySearchUtil.GetReadOnlyFiles(dir, "*.csv");

        Console.WriteLine("読み取り専用のファイル一覧:");
        foreach (string file in readOnlyFiles)
        {
            Console.WriteLine(file);
        }
    }
}
C#

これにより、「なぜか一部のファイルだけ更新できない」といったトラブルの原因調査がしやすくなります。

再帰的にサブフォルダも含めて調べる

サブフォルダも含めて全体を調べたい場合は、再帰的にディレクトリをたどるようにします。

public static class FileReadOnlySearchUtil
{
    public static string[] GetReadOnlyFilesRecursive(string directoryPath, string searchPattern = "*.*")
    {
        if (!Directory.Exists(directoryPath))
        {
            throw new DirectoryNotFoundException($"ディレクトリが見つかりません: {directoryPath}");
        }

        var result = new List<string>();
        AddReadOnlyFiles(directoryPath, searchPattern, result);
        return result.ToArray();
    }

    private static void AddReadOnlyFiles(string directoryPath, string searchPattern, List<string> result)
    {
        foreach (string file in Directory.GetFiles(directoryPath, searchPattern))
        {
            FileAttributes attrs = File.GetAttributes(file);

            if ((attrs & FileAttributes.ReadOnly) == FileAttributes.ReadOnly)
            {
                result.Add(file);
            }
        }

        foreach (string subDir in Directory.GetDirectories(directoryPath))
        {
            AddReadOnlyFiles(subDir, searchPattern, result);
        }
    }
}
C#

このようなユーティリティを持っておくと、「特定の領域で読み取り専用が紛れ込んでいないか」を定期的にチェックするバッチなども簡単に作れます。


実務での注意点 読み取り専用と「書けない理由」は別物

読み取り専用以外にも「書けない理由」はたくさんある

ここで一つ大事なことを押さえておきましょう。
「ファイルに書き込めない理由」は、読み取り専用だけではありません。

例えば、次のようなケースがあります。

他のプロセスがファイルを開きっぱなしでロックしている。
フォルダやファイルに対する OS のアクセス権限が足りない。
ディスクがいっぱいで書き込めない。
ネットワークドライブの接続が切れている。

つまり、「読み取り専用ではないから絶対に書ける」とは限りません。
読み取り専用判定はあくまで「書けない理由の一つを事前に潰す」ものであり、最終的には実際に書き込みを試みて、例外をキャッチして判断する必要があります。

「読み取り専用だから書けない」を分かりやすくする

とはいえ、読み取り専用判定をしておくと、「なぜ書けなかったのか」をユーザーやログに分かりやすく伝えられます。

例えば、次のようなメッセージを出せます。

「ファイルに書き込めませんでした(読み取り専用属性が付いています)。」
「ファイルに書き込めませんでした(読み取り専用ではありませんが、別の理由で失敗しました)。」

これにより、運用担当者が原因を切り分けやすくなります。
実務では、「エラーの原因が分かるログを残す」ことがとても大事なので、読み取り専用判定はその一部として役に立ちます。


例外とエラー処理を意識した読み取り専用判定

どんな例外が起こり得るか

読み取り専用判定や属性操作では、次のような理由で例外が発生する可能性があります。

ファイルが存在しない。
権限がなくて属性を取得・変更できない。
パスが不正または長すぎる。
ファイルシステムに問題がある。

呼び出し側での例外処理の例を見てみます。

using System;
using System.IO;

class Program
{
    static void Main()
    {
        string path = @"C:\data\report.csv";

        try
        {
            bool isReadOnly = FileReadOnlyUtil.IsReadOnly(path);
            Console.WriteLine($"読み取り専用か: {isReadOnly}");
        }
        catch (FileNotFoundException ex)
        {
            Console.WriteLine("ファイルが見つかりません: " + ex.Message);
        }
        catch (UnauthorizedAccessException ex)
        {
            Console.WriteLine("権限エラーが発生しました: " + ex.Message);
        }
        catch (IOException ex)
        {
            Console.WriteLine("入出力エラーが発生しました: " + ex.Message);
        }
        catch (Exception ex)
        {
            Console.WriteLine("想定外のエラーが発生しました: " + ex.Message);
        }
    }
}
C#

実務では、これらのメッセージをログに残しておくことで、「どのファイルの属性取得・変更に、どんな理由で失敗したか」を後から追跡できます。


まとめ 実務で使える「ファイル読み取り専用判定」ユーティリティの考え方

読み取り専用判定は、一見地味ですが、「書けない・消せない」系トラブルの原因を減らすための大事なピースです。
だからこそ、「属性を直接いじる生コード」をあちこちに書くのではなく、ユーティリティとして整理しておく価値があります。

File.GetAttributesFileAttributes.ReadOnly を使い、ビット演算で「読み取り専用フラグが立っているか」を正しく判定すること。
存在チェック込みのユーティリティ(例外を投げる版と、false を返す版)を用意し、呼び出し側の意図をコードに表現すること。
File.SetAttributes とビット演算(attrs &= ~FileAttributes.ReadOnly)で、必要に応じて読み取り専用を解除するユーティリティを用意すること。
フォルダ内・サブフォルダ内の読み取り専用ファイルを一括で洗い出すユーティリティを作り、運用トラブルの調査や定期チェックに活かすこと。
読み取り専用は「書けない理由の一つ」に過ぎないことを理解しつつ、「読み取り専用だから書けない」という情報をログやメッセージで分かりやすく伝えること。

ここまで押さえておけば、「なぜか書けない・消せないファイル」に振り回されることがかなり減ります。

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