2006年01月21日

圧縮! ただしNTFSだより

色々ごたごたしていて中々記事を書く暇もありません。

言い訳から始まる久しぶりっぷりです。

さて、今回は某掲示板で発せられた「ファイルの圧縮属性」についてのお話です。

NTFSでは、読み取り専用とかの属性に、新しく圧縮という属性が追加されました。その名の通り、この属性つきのファイルはファイルシステムが自動的に圧縮してディスクに保存してくれるようになります。読み取る側は別にこの属性の有無を把握する必要は無く、ごく普通のファイルとして読み書きできます。もちろん、入出力の際多少コストが増加するのは間違いないですが。また、最近はHDDが安くなったこと、ファイルサイズが大きくなるもの(画像、音声、動画など)は大抵圧縮機構が形式に組み込まれていることなどから、有効性は薄れているでしょう。

.NETではファイルの属性はSystem.IO.FileクラスのGetAttributes/SetAttributesメソッドなどを通して取得・設定するのがスタンダードです。これに使用するFileAttributes列挙体にはCompressedメンバが存在しているため、これを使えば話は早い。

はい、わざわざ記事にしている通り、早くありません。なんとこの属性、変更できないのです。例外も投げられず、ただ変更が無視されるだけ。どういうことかしらん、とSetAttributesの実装を見てみると、このメソッドはどうやらWin32APIのSetFileAttributesのごく薄いラッパです。今度はMSDNでSetFileAttributesを調べることになります。そこには、普通にファイルの圧縮状態を設定するには、FSCTL_SET_COMPRESSION の動作を指定して DeviceIoControl 関数を使ってください。とナチュラルに書いてありました。

ここではこういう思考の流れで正解に辿り着くよう書いていますが、実際に私がこの問題に突き当たったとき、何故かWin32APIのCreateFile関数を調べていました。何故CreateFileを調べようと思ったのかは自分でも本当に謎です。まあ、多分「属性はファイル作るときにも設定するよな、なら圧縮属性のことも書いてあるはず」ってことなんでしょうけど。とはいえFileStreamクラスのコンストラクタには別にFileAttributesを引数にとるオーバーロードがあるわけでもないのに。

ということで、DeviceIoControlです。デバイスと直接制御コードでやり取りする、かなり低レベルIO用のAPIなので、あまり使いたくは無い関数ですね。汎用性が高くなるように曖昧な型で書かれているので、そのままでは少々扱いにくい関数になっています。

設定、または取得は、ファイルのハンドルを通じて、定数FSCTL_SET_COMPRESSION、FSCTL_GET_COMPRESSIONを使用してDeviceIoControlを呼び出します。ちなみにこれらの定数は簡単なマクロで、アクセス許可やらがビット演算されているので移植はちょっと面倒です。

さて、基本骨子がわかったところで.NETでの実装に入りましょう。

まず、DeviceIoControlですが、前述の通り少々扱いにくいので、今回はDllImport属性のEntryPointフィールド、またはDeclare構文のAliasキーワードを利用し、同じ関数のオーバーロードとして圧縮属性取得・設定限定の別名定義を行うことにします。そうすればポインタの受け渡しもref/out(ByRef)で扱え、Marshalクラスのメソッドを使用する必要もなくなります。なにより見た目分かりやすくなります(と思ってるんですが)。ref/out/ByRefは便利な構文ですが、NULLを渡せないのが欠点ですね。

ファイルシステムがNTFSかどうかを確認しなければなりませんが、面倒なので省略。多分FAT32ならSetFileCompressModeは必ずfalseを、GetFileCompressModeは必ずNoneを返すはずです。多分。.NET 2.0ではSystem.IO名前空間にDriveInfoクラスが増えて、簡単に取得できるようになりましたが、1.0/1.1ではWMIかAPIか、さらにWMIはWMI自体が使用可能かどうかという問題もありますし。

.NET 2.0になって、CERなるものが使えるようになりました。詳しくはMSDNの解説に譲るとして、私の理解した範囲内では、指定したtry-catch-finally内で、そのブロック内の処理をスレッドが破棄されようがStackOverflowExceptionが出ようがアプリケーションドメインが破棄されようが確実に実行する手段、と言った感じでしょうか。トランザクション的と言えばいいのかな。

リンク先はMicrosoft Passportのサインインが必要で、サインインにはJavaScriptが必要なんですが、タブブラウザ使ってデフォルトではJavaScriptを切っている私としては面倒なことこの上ない。何が面倒かって、403のアドレスにリダイレクトしやがることです。403画面じゃなくて入力画面にリダイレクトしてnoscript要素を使えと。そうすればワンタッチでJavaScriptをオンにしてさっさとサインインできるのに。

Win32のハンドルを扱うのもこのCER内でやると安全になります。例えば

IntPtr handle = CreateMutex(IntPtr.Zero, true, null);

みたいなコードにしても、CreateMutexの実行と取得したハンドルをhandleに代入するのは別々のアクションです。CreateMutexした直後、handleに代入する前に何らかの原因で実行が中断された場合、このMutexを解放する手段が失われてしまう、つまりリークしてしまいます。この処理をCER内で行うことで、実行が中断されようとしたときもその中断は遅延され、確実に処理が行われるようになるというわけです。

まだ理解が行き届いてませんのでコーディングも見よう見まねですけどねー。

さて、冒頭のきっかけとなった某掲示板の話ですが。MSDNでDeviceIoControlを知って、テスト用にざっと実装、動作を確認したうえでさあ答えよう、と見たらもう質問者がそれに辿り着いていました。切ない。

まあ折角調べてコード書いたので、清書してここに掲載する次第ですが。なお、C#版XMLドキュメントつきコードはここからどうぞ

C#版。

using System;
using System.ComponentModel;
using System.IO;
using System.Runtime.InteropServices;
#if !V10 && !V11
using System.Runtime.CompilerServices;
using Microsoft.Win32.SafeHandles;
#endif
public enum CompressMode : short {
  None    = 0,
  Default = 1,
  Lznt1   = 2,
}
public class FileCompressModeSetting {
  [DllImport("kernel32.dll", EntryPoint="DeviceIoControl")]
  public static extern bool SetCompressMode(
    IntPtr fileHandle, int code, ref CompressMode mode,
    int bufSize, IntPtr notUsed1, int notUsed2,
    out int returnedSize, IntPtr overlapped);
  [DllImport("kernel32.dll", EntryPoint="DeviceIoControl")]
  public static extern bool GetCompressMode(
    IntPtr fileHandle, int code, IntPtr notUsed1,
    int notUsed2, out CompressMode mode, int bufSize,
    out int returnedSize, IntPtr overlapped);
  public static bool SetFileCompressMode(string filePath,
                                         CompressMode mode) {
    const int setCompress = 639040;
    if (!Enum.IsDefined(typeof(CompressMode), mode))
      throw new InvalidEnumArgumentException(
        "mode", (int)mode, typeof(CompressMode));
    using (FileStream fs
           = new FileStream(filePath, FileMode.Open,
                            FileAccess.ReadWrite)) {
      int returned;
#if !V10 && !V11
      //.NET 2.0以降では、
      // ハンドルはより安全に扱い得るようになった。
      SafeHandle safeHandle = fs.SafeFileHandle;
      //SafeHandle.DangerousAddRefで加算した参照カウントを
      // Releaseしなければならないかどうか
      bool mustRelease = false;
      //続くtry-finallyをCERに指定。
      //CERは、ThreadAbortException等でも
      //問題なくfinally句を実行する効果を含む。
      RuntimeHelpers.PrepareConstrainedRegions();
      try {
        //SafeHandleの参照カウントをインクリメント。
        //trueが返る場合、DangerousReleaseするまで
        //ReleaseHandleが遅延される。
        safeHandle.DangerousAddRef(ref mustRelease);
        IntPtr handle = safeHandle.DangerousGetHandle();
        return SetCompressMode(
          handle, setCompress, ref mode, 2,
          IntPtr.Zero, 0, out returned, IntPtr.Zero);
      }
      finally {
        //必要な場合SafeHandleの参照カウントをデクリメント。
        if (mustRelease)
          safeHandle.DangerousRelease();
      }
#else
      return SetCompressMode(fs.Handle, setCompress, ref mode, 2,
                             IntPtr.Zero, 0, out returned, IntPtr.Zero);
#endif
    }
  }
  public static CompressMode GetFileCompressMode(string filePath) {
    const int getCompress = 589884;
    using (FileStream fs
             = new FileStream(filePath, FileMode.Open,
                              FileAccess.Read)) {
      CompressMode mode;
      int returned;
#if !V10 && !V11
      //SafeHandleについてはSetFileCompressMode参照。
      SafeHandle safeHandle = fs.SafeFileHandle;
      bool mustRelease = false;
      RuntimeHelpers.PrepareConstrainedRegions();
      try {
        safeHandle.DangerousAddRef(ref mustRelease);
        IntPtr handle = safeHandle.DangerousGetHandle();
        GetCompressMode(handle, getCompress, IntPtr.Zero, 0,
                        out mode, 2, out returned, IntPtr.Zero);
      }
      finally {
        if (mustRelease)
          safeHandle.DangerousRelease();
      }
#else
      GetCompressMode(fs.Handle, getCompress, IntPtr.Zero, 0,
                      out mode, 2, out returned, IntPtr.Zero);
#endif
      return mode;
    }
  }
}

VB.NET/VB2005版。そう言えば2005が出て、VB.NETで済ますわけにも行かなくなりましたねー。VB7以降、とか言う方がいいのかな。

Imports System
Imports System.ComponentModel
Imports System.IO
Imports System.Runtime.InteropServices
#If Not (V = 10 Or v = V11) Then
Imports System.Runtime.CompilerServices
Imports Microsoft.Win32.SafeHandles
#End If
Public Enum CompressMode As Short
  None = 0
  [Default] = 1
  Lznt1 = 2
End Enum
Public Class FileCompressModeSetting
  Public Declare Auto Function SetCompressMode _
    Lib "Kernel32.dll" _ Alias "DeviceIoControl" ( _
    ByVal fileHandle As IntPtr, ByVal code As Integer, _
    ByRef mode As CompressMode, ByVal bufSize As Integer, _
    ByVal notUsed1 As IntPtr, ByVal notUsed2 As Integer, _
    ByRef returnedSize As Integer, ByVal overlapped As IntPtr) _
    As Boolean
  Public Declare Auto Function GetCompressMode _
    Lib "Kernel32.dll" Alias "DeviceIoControl" ( _
    ByVal fileHandle As IntPtr, ByVal code As Integer, _
    ByVal notUsed1 As IntPtr, ByVal notUsed2 As Integer, _
    ByRef mode As CompressMode, ByVal bufSize As Integer, _
    ByRef returnedSize As Integer, ByVal overlapped As IntPtr) _
    As Boolean

  Public Shared Function SetFileCompressMode( _
      ByVal filePath As String, ByVal mode As CompressMode) As Boolean
    Const setCompress As Integer = 639040
    If Not ([Enum].IsDefined(GetType(CompressMode), mode)) Then
    Throw New InvalidEnumArgumentException( _
      "mode", CInt(mode), GetType(CompressMode))
    End If
    Dim fs As New FileStream( _
      filePath, FileMode.Open, FileAccess.ReadWrite)
    Try
      Dim returned As Integer = 0
#If Not (V = 10 Or V = 11) Then
      '.NET 2.0以降では、ハンドルはより安全に扱い得るようになった。
      Dim safeHandle As SafeHandle = fs.SafeFileHandle
      'SafeHandle.DangerousAddRefで加算した参照カウントを
      'Releaseしなければならないかどうか
      Dim mustRelease As Boolean = False
      '続くtry-finallyをCERに指定。
      'CERは、ThreadAbortException等でも問題なく
      'finally句を実行する効果を含む。
      RuntimeHelpers.PrepareConstrainedRegions()
      Try
        'SafeHandleの参照カウントをインクリメント。
        'trueが返る場合、
        'DangerousReleaseするまでReleaseHandleが遅延される。
        safeHandle.DangerousAddRef(mustRelease)
        Dim handle As IntPtr = safeHandle.DangerousGetHandle()
        Return SetCompressMode( _
          handle, setCompress, mode, 2, _
          IntPtr.Zero, 0, returned, IntPtr.Zero)
      Finally
          '(必要な場合は)SafeHandleの参照カウントをデクリメント。
          If mustRelease Then safeHandle.DangerousRelease()
      End Try
#Else
      Return SetCompressMode( _
        fs.Handle, setCompress, mode, 2, _
        IntPtr.Zero, 0, returned, IntPtr.Zero)
#End If
    Finally
      If Not (fs Is Nothing) Then fs.Close()
    End Try
  End Function

  Public Shared Function GetFileCompressMode( _
      ByVal filePath As String) As CompressMode
    Const getCompress As Integer = 589884
    Dim fs As New FileStream( _
      filePath, FileMode.Open, FileAccess.Read)
    Try
      Dim mode As CompressMode
      Dim returned As Integer
#If Not (V = 10 Or V = 11) Then
      'SafeHandleについてはSetFileCompressMode参照。
      Dim safeHandle As SafeHandle = fs.SafeFileHandle
      Dim mustRelease As Boolean = False
      RuntimeHelpers.PrepareConstrainedRegions()
      Try
        safeHandle.DangerousAddRef(mustRelease)
        Dim handle As IntPtr _
          = safeHandle.DangerousGetHandle()
        GetCompressMode( _
          handle, getCompress, IntPtr.Zero, 0, _
          mode, 2, returned, IntPtr.Zero)
      Finally
        If mustRelease Then safeHandle.DangerousRelease()
      End Try
#Else
      GetCompressMode( _
        fs.Handle, getCompress, IntPtr.Zero, 0, _
        mode, 2, returned, IntPtr.Zero)
#End If
      Return mode
    Finally
      If Not (fs Is Nothing) Then fs.Close()
    End Try
  End Function
End Class
posted by Hongliang at 18:54| Comment(0) | TrackBack(0) | .NET | このブログの読者になる | 更新情報をチェックする
この記事へのコメント
コメントを書く
お名前:

メールアドレス:

ホームページアドレス:

コメント:

認証コード: [必須入力]


※画像の中の文字を半角で入力してください。

この記事へのトラックバック

ここ(hongliang.seesaa.net)で公開しているものについて、利用は自由に行って頂いて構いません。改変、再頒布もお好きになさって下さい。利用に対しこちらが何かを要求することはありません。

ただし、公開するものを使用、または参考したことによって何らかの損害等が生じた場合でも、私はいかなる責任も負いません。

あ、こんなのに使ったってコメントを頂ければ嬉しいです。

×

この広告は1年以上新しい記事の投稿がないブログに表示されております。