C#「NTFSのADSでファイルにコメントをつける」

コンピュータ

実行環境構築

プロジェクトの作成

mkdir プロジェクト名
cd プロジェクト名
dotnet new winforms
code .

ソースプログラム

ファイル名:Form1.cs

using System.Runtime.InteropServices;
using System.Runtime.ConstrainedExecution;
using System.Security;
using System.Diagnostics;

namespace EditComment;

public partial class Form1 : Form
{
    // CreateFileW
    [DllImport("kernel32.dll", CharSet = CharSet.Unicode, SetLastError = true)]
    static extern IntPtr CreateFileW(
        [MarshalAs(UnmanagedType.LPWStr)] string filename,
        [MarshalAs(UnmanagedType.U4)] FileAccess access,
        [MarshalAs(UnmanagedType.U4)] FileShare share,
        IntPtr securityAttributes,
        [MarshalAs(UnmanagedType.U4)] FileMode creationDisposition,
        [MarshalAs(UnmanagedType.U4)] FileAttributes flagsAndAttributes,
        IntPtr templateFile);
    
    // CloseHandle
    [DllImport("kernel32.dll", SetLastError=true)]
    [SuppressUnmanagedCodeSecurity]
    [return: MarshalAs(UnmanagedType.Bool)]
    static extern bool CloseHandle(IntPtr hObject);

    // WriteFile
    [DllImport("kernel32.dll")]
    static extern bool WriteFile(IntPtr hFile, byte [] lpBuffer,
        uint nNumberOfBytesToWrite, out uint lpNumberOfBytesWritten,
        [In] ref System.Threading.NativeOverlapped lpOverlapped);

    // ReadFile
    [DllImport("kernel32.dll", SetLastError = true)]
    static extern bool ReadFile(IntPtr hFile, [Out] byte[] lpBuffer,
        uint nNumberOfBytesToRead, out uint lpNumberOfBytesRead, IntPtr lpOverlapped);
    
    // GetFileSizeEx
    [DllImport("kernel32.dll")]
    static extern bool GetFileSizeEx(IntPtr hFile, out long lpFileSize);

    public Form1()
    {
        InitializeComponent();
        
        string[] args = System.Environment.GetCommandLineArgs();

        if (2 > args.Length) return;

        string file = args[1];
        if (!File.Exists(file)) return;

        string msg = String.Format("「{0}」のコメント", file);
        string comment = "";

        System.Text.Encoding.RegisterProvider(System.Text.CodePagesEncodingProvider.Instance);

        var handle = CreateFileW(file+":Comment", FileAccess.ReadWrite, FileShare.Read, IntPtr.Zero, FileMode.OpenOrCreate, FileAttributes.Normal, IntPtr.Zero);
        if (handle != IntPtr.Zero)
        {
            Debug.Print("Success");

            long len = 0;
            GetFileSizeEx(handle, out len);
            if (len > 0)
            {
                UInt32 sz = 0;
                UInt32 l = (UInt32)len;
                byte[] buf = new byte[len];
                ReadFile(handle, buf, l, out sz, IntPtr.Zero);
                comment = (System.Text.Encoding.GetEncoding("Shift_JIS")).GetString(buf);
            }
        }
        else
        {
            Debug.Print("Failed");
            return;
        }


        Size = new Size(640, 360);
        Text = "コメント";

        var panel = new TableLayoutPanel
        {
            Dock = DockStyle.Fill,
            Padding = new Padding(16),
        };

        var label = new Label
        {
            Dock = DockStyle.Fill,
            Margin = new Padding(16),
            Text = msg,
        };
        panel.Controls.Add(label, 0, 0);

        var tbox = new TextBox
        {
            Dock = DockStyle.Fill,
            Margin = new Padding(16),
            Text = comment,
        };
        panel.Controls.Add(tbox, 0, 1);

        var panel2 = new FlowLayoutPanel
        {
            Margin = new Padding(16),
            Anchor = AnchorStyles.None,
            Dock = DockStyle.Fill,
        };
        panel.Controls.Add(panel2, 0, 2);

        var okBtn = new Button
        {
            Margin = new Padding(16),
            Size = new Size(100, 40),
            Text = "OK",
        };
        panel2.Controls.Add(okBtn);

        var cancelBtn = new Button
        {
            Margin = new Padding(16),
            Size = new Size(100, 40),
            Text = "Cancel",
        };
        panel2.Controls.Add(cancelBtn);

        Controls.Add(panel);
        Action<Form> okAction = (Form form) =>
        {
            string str = tbox.Text;
            byte[] bytes = (System.Text.Encoding.GetEncoding("Shift_JIS")).GetBytes(str.ToCharArray());
            UInt32 length = (UInt32)bytes.Length;
            UInt32 sz = 0;
            System.Threading.NativeOverlapped lpOverlapped = new();
            WriteFile(handle, bytes, length, out sz, ref lpOverlapped);

            CloseHandle(handle);
            form.Close();
        };
        Action<Form> cancelAction = (Form form) =>
        {
            CloseHandle(handle);
            form.Close();
        };
        okBtn.Click += (s, e) => okAction(this);
        tbox.KeyDown += (s, e) =>
        {
            if (e.KeyCode == Keys.Return)
            {
                okAction(this);
            }
        };
        cancelBtn.Click += (s, e) => cancelAction(this);
    }
}

使い方

コマンドライン引数にコメントをつけたいファイルのパスをセットして起動します。
エクスプローラーの「送る」で使うことを想定しています。

ファイルのプロパティぽい使い方になりますが、エクスプローラーではコメントの一覧が表示できないので使い勝手は今一つ。
文字コードはShift_JISにしていますが何が正解かは理解していません。

追記20240503

調べたら.NetでもADS(代替ストリーム)にアクセスできるようです。
使い方も簡単で普通のテキストファイルを読みこむFile.ReadAllTextメソッドで、読み込むファイルのパスを指定する際、ファイル名の後に「:代替ストリーム名」をセットすると代替ストリームを読み出すことが出来ました。ファイルの有無を調べるSystem.IO.File.Existsで同様なパスの書式を試した所、代替ストリーム名の有無を返してくれました。書き込みなども同じ方法で実行することが出来るでしょう。

そうなりますと、NTFSでないSambaのファイルサーバーが代替ストリームが対応してくれると個人的には完璧なのでそのあたりも調べてみました。
実際実施されている方の記事がありましたので、そちらを試しました。
『Sambaと代替データストリーム』
<環境>CentOS 6.6Samba 4.1.14Windowsのファイルには代替データストリームが付いていることがあるので、Sambaでも代替データストリ…

自分の環境ではそれだけではうまく行かず、さらに以下のページの設定を施し増し。
vfs_streams_xattr

そうしたところsambaで公開している共有フォルダ上のファイルにも代替ストリームが使えるようになりました。

ファイルにコメントをつける方法としての代替ストリームですが、使えるようなめどが立ってきました。
あとはエクスプローラーのようなGUIでファイルとコメントを同時に一覧表示してくれるアプリケーションとコメントに対してキーワードを含むファイルを検索する機能が欲しいところです。

コメント