しょんぼり技術メモ

まいにちがしょんぼり

Dokan .netでファイルシステムを作って遊ぶ - Simple HashtableFS -

Hello world! だけじゃつまらない。というか役に立たないので、もう少し実用的なものを作ってみよう。

Simple Hashtable FileSystem

System.Collections.Hashtable のデータをファイルシステムにしてみよう、というもの。
キーがファイル名で、値がそのファイルの中身になるものを作る。

つまり、

  hash["hello.txt"] = "Hello world!\r\n";

なんてやることで、X:\hello.txt の中身が "Hello world!\r\n" になるようなものを作ってみる。

エントリポイントとスレッド

今回のように、プログラムの実行中に動的に中身が変わるようなものをファイルシステムに見せかけたい、ということを考えると、Dokanの処理は別スレッドで走らせる必要がある。
これは、DokanNet.DokanMain()関数がブロックされるからである。

今回は手抜き実装なので、オプションはグローバル変数に置き、DokanMain()関数のDokanOperationsインタフェース実装をParameterizedThreadStartdelegateのパラメタとして使うことにする。

エントリポイントを含む、Program.cs は次の通り。

using System;
using System.Collections.Generic;
using System.Text;
using Dokan;

namespace DokanTest
{
    class Program
    {
        // デフォルトのドライブレター
        const string default_mount_drive = "x";

        // Dokanオプション
        static DokanOptions opt;

        // ハッシュテーブル
        static System.Collections.Hashtable ht = null;

        /// <summary>
        /// エントリポイント
        /// </summary>
        static void Main(string[] args)
        {
            // arg[0]: マウントされるドライブのドライブレター

            // オプション設定
            opt = new DokanOptions();
            opt.DebugMode = true;
            opt.DriveLetter = (args.Length == 1) ? args[0].ToCharArray()[0] : default_mount_drive.ToCharArray()[0];
            //opt.ThreadCount = 1;
            opt.ThreadCount = 0;
            opt.VolumeLabel = "DokanTest";

            Console.WriteLine("Mounting Dokan...");


            // 見せるハッシュテーブルを初期化、設定
            ht = new System.Collections.Hashtable();
            ht["test.txt"] = "this is a HashtableFS test file.\r\n";
            ht["int.txt"] = 67;
            ht["bool.txt"] = true;

            // 100個のデータをハッシュに登録
            for (int i = 0; i < 100; i++)
            {
                string s = i.ToString();
                string name = s + ".txt";
                ht[name] = s;
            }


            // マウント実行@スレッド
            System.Threading.Thread dokanMain = null;
            try
            {
                // スレッド作成
                dokanMain = new System.Threading.Thread(new System.Threading.ParameterizedThreadStart(DokanStart));
                dokanMain.Name = "Dokan HashtableFS DokanMain thread";
                // スレッド実行
                dokanMain.Start(new HashtableFS(ref ht));

                // この間にHashtableに操作を加えると反映されるはず
                
                // 1秒ごとにdynamic_xxx.txt を追加していく
                for (int i = 0; i < 1000; i++)
                {
                    string s = i.ToString();
                    string name = "Dynamic_" + s + ".txt";
                    ht[name] = "dynamic_" + s;

                    // 1秒待つ
                    System.Threading.Thread.Sleep(1000);
                }


                // 終了待ち合わせ
                dokanMain.Join();
            }
            catch
            {
                // do nothing...
            }
            finally
            {
                // お片付け
                if (dokanMain != null)
                {
                    dokanMain.Abort();
                }
            }

        } // end of Main()

        /// <summary>
        /// パラメタ付きスレッドスタートデリゲートのためのラッピング関数
        /// </summary>
        static void DokanStart(object op)
        {
            DokanNet.DokanMain(opt, (DokanOperations)op);
        }

    }
}

"// この間にHashtableに操作を加えると反映されるはず" の行から下が、動的にハッシュに変更を加える処理である。
今回は、1秒ごとに1エントリ追加していく処理を書いた。

処理の本体

DokanOperationsインタフェースの実装は全てこちらの HashtableFS.cs に書いた。

今回はHello worldよりも若干複雑になっている。注意すべきなのは、スレッドセーフであることを心がけること。

HashtableFSでは、コンストラクタの引数として対象となるハッシュテーブルを受け取る。その際、IsSynchronizedプロパティをチェックし、falseであればHashtable.Synchronized()でラッパを取得するようにしている。
今回は読み込みしか対応していないが、書き込みを実装する際にはロック処理が必要になるだろう。

本実装の仕様として、たとえキーに"somedir\somefile.ext"と書いたとしても、ディレクトリ構造は作られない。めんどくさいし…
そういう意味でもあまり実用的ではないが、習作と言うことでご容赦を…

ソースコードは次の通り。

using System;
using System.Collections;
using System.Collections.Generic;
using System.Text;
using System.IO;
using Dokan;

namespace DokanTest
{
    class HashtableFS : DokanOperations
    {
        // コンソールにデバッグ文字列を流すか
        bool DEBUGMODE = true;
        // →デバッグ文字列をファイルにも吐き出すか
        bool DEBUG_WRITE_TO_FILE = false;
        // →→ログファイルのパス
        const string debug_log_file = "c:\\dokan.log";


        // 対象となるハッシュテーブル
        Hashtable ht = null;


        #region debugout
        /// <summary>
        /// デバッグ出力
        /// </summary>
        private void DebugOut(string mes)
        {
            // コンソールへのデバッグ出力(時刻表示付き)
            if (DEBUGMODE)
            {
                string message = string.Format("[{0}] {1}", System.DateTime.Now.ToLongTimeString(), mes);
                Console.WriteLine(message);
                
                // ファイル出力
                if (DEBUG_WRITE_TO_FILE)
                {
                    // 面倒なので毎回開いて追記して閉じる。恐らくスループットがた落ちのはず
                    using (System.IO.StreamWriter sw = new System.IO.StreamWriter(debug_log_file, true, Encoding.Default))
                    {
                        sw.WriteLine(message);
                    }
                }
            }
        }
        #endregion

        /// <summary>
        /// コンストラクタ
        /// </summary>
        /// <param name="targetHT">対象となるハッシュテーブル</param>
        public HashtableFS(ref Hashtable targetHT)
        {
            if (targetHT.IsSynchronized == false)
            {
                this.ht = Hashtable.Synchronized(targetHT);
            }
            else
            {
                this.ht = targetHT;
            }
        }


        #region DokanOperations メンバ

        public int CreateFile(string filename, System.IO.FileAccess access, System.IO.FileShare share, System.IO.FileMode mode, System.IO.FileOptions options, DokanFileInfo info)
        {
            DebugOut("CreateFile() called with filename: " + filename);

            // ハッシュキーには \ をつけないのでハッシュ処理用に\を削除しておく
            string fname_key = filename.Replace("\\", "");

            // ルートディレクトリを特別扱い
            if (filename == "\\")
            {
                info.IsDirectory = true;
                return DokanNet.DOKAN_SUCCESS;
            }
            // そのファイル名を持つキーがあれば処理する
            else if (this.ht.ContainsKey(fname_key))
            {
                return DokanNet.DOKAN_SUCCESS;
            }

            // ほかはFILE_NOT_FOUND
            return -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int OpenDirectory(string filename, DokanFileInfo info)
        {
            DebugOut("OpenDirectory() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int CreateDirectory(string filename, DokanFileInfo info)
        {
            DebugOut("CreateDirectory() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int Cleanup(string filename, DokanFileInfo info)
        {
            DebugOut("Cleanup() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int CloseFile(string filename, DokanFileInfo info)
        {
            DebugOut("CloseFile() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int ReadFile(string filename, byte[] buffer, ref uint readBytes, long offset, DokanFileInfo info)
        {
            DebugOut("ReadFile() called with filename:    " + filename);
            DebugOut("                  with buffer size: " + buffer.Length.ToString());
            DebugOut("                  with offset     : " + offset.ToString());

            // ハッシュキーには \ をつけないのでハッシュ処理用に\を削除しておく
            string fname_key = filename.Replace("\\", "");

            // そのファイル名を持つキーがあれば処理する
            if (this.ht.ContainsKey(fname_key))
            {
                try
                {
                    // バイナリ列
                    byte[] raw = getBinary(fname_key);
                    if (raw == null)
                    {
                        return DokanNet.DOKAN_SUCCESS;
                    }

                    // 超過チェック
                    if (offset >= raw.Length)
                    {
                        readBytes = 0;
                        return -DokanNet.DOKAN_ERROR;
                    }

                    // offsetから読み込むバイト数
                    long read_try_byte = (raw.Length - offset < buffer.Length) ? raw.Length - offset : buffer.Length;

                    // コピー
                    int i = 0;
                    for (i = 0; i < read_try_byte; i++)
                    {
                        buffer[i] = raw[offset + i];
                    }

                    readBytes = (uint)i;

                    return DokanNet.DOKAN_SUCCESS;
                }
                catch(Exception e)
                {
                    Console.Write(e);
                    return -DokanNet.DOKAN_ERROR;
                }
            }


            return -DokanNet.ERROR_FILE_NOT_FOUND;
        }

        public int WriteFile(string filename, byte[] buffer, ref uint writtenBytes, long offset, DokanFileInfo info)
        {
            DebugOut("WriteFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int FlushFileBuffers(string filename, DokanFileInfo info)
        {
            DebugOut("FlushFileBuffers() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int GetFileInformation(string filename, FileInformation fileinfo, DokanFileInfo info)
        {
            DebugOut("GetFileInformation() called with filename: " + filename);

            // ハッシュキーには \ をつけないのでハッシュ処理用に\を削除しておく
            string fname_key = filename.Replace("\\", "");

            // ルートディレクトリは特別扱い
            if (filename == "\\")
            {
                // ルートディレクトリ情報を適当に設定する
                fileinfo.Attributes = System.IO.FileAttributes.Directory;
                fileinfo.CreationTime = System.DateTime.Now;
                fileinfo.LastAccessTime = System.DateTime.Now;
                fileinfo.LastWriteTime = System.DateTime.Now;
                fileinfo.Length = 0;

                return DokanNet.DOKAN_SUCCESS;
            }
            // そのファイル名を持つキーがあれば処理する
            else if (this.ht.ContainsKey(fname_key))
            {
                return getFileInfoFromKey(fname_key, ref fileinfo);
            }

            return -DokanNet.DOKAN_ERROR;
        }

        public int FindFiles(string filename, System.Collections.ArrayList files, DokanFileInfo info)
        {
            DebugOut("FindFiles() called with filename: " + filename);

            // ハッシュテーブルについてループ
            foreach (DictionaryEntry de in this.ht)
            {
                // ファイルの情報をセット
                FileInformation fi = new FileInformation();
                if (getFileInfoFromKey((string)de.Key, ref fi) == DokanNet.DOKAN_SUCCESS)
                {
                    // リストに追加
                    files.Add(fi);
                }
                else
                {
                    // do nothing.
                }
            }

            return DokanNet.DOKAN_SUCCESS;
        }

        public int SetFileAttributes(string filename, System.IO.FileAttributes attr, DokanFileInfo info)
        {
            DebugOut("SetFileAttributes() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int SetFileTime(string filename, DateTime ctime, DateTime atime, DateTime mtime, DokanFileInfo info)
        {
            DebugOut("SetFileTime() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int DeleteFile(string filename, DokanFileInfo info)
        {
            DebugOut("DeleteFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int DeleteDirectory(string filename, DokanFileInfo info)
        {
            DebugOut("DeleteDirectory() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int MoveFile(string filename, string newname, bool replace, DokanFileInfo info)
        {
            DebugOut("MoveFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int SetEndOfFile(string filename, long length, DokanFileInfo info)
        {
            DebugOut("SetEndOfFile() called with filename: " + filename);

            return -DokanNet.DOKAN_ERROR;
        }

        public int LockFile(string filename, long offset, long length, DokanFileInfo info)
        {
            DebugOut("LockFile() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int UnlockFile(string filename, long offset, long length, DokanFileInfo info)
        {
            DebugOut("UnlockFile() called with filename: " + filename);

            return DokanNet.DOKAN_SUCCESS;
        }

        public int GetDiskFreeSpace(ref ulong freeBytesAvailable, ref ulong totalBytes, ref ulong totalFreeBytes, DokanFileInfo info)
        {
            DebugOut("GetDiskFreeSpace() called.");

            freeBytesAvailable = 0;
            totalBytes = 0;
            
            totalFreeBytes = 0;

            return DokanNet.DOKAN_SUCCESS;
        }

        public int Unmount(DokanFileInfo info)
        {
            DebugOut("Unmount() called.");

            return DokanNet.DOKAN_SUCCESS;
        }

        #endregion



        // 非Dokan関数 ///////////////////////////////////////////////////////////////////////////////


        /// <summary>
        /// キーからFileInfo構造体を設定する
        /// </summary>
        private int getFileInfoFromKey(string key, ref FileInformation fileinfo)
        {
            fileinfo.Attributes = FileAttributes.Normal;
            fileinfo.CreationTime = System.DateTime.Now;
            fileinfo.LastAccessTime = System.DateTime.Now;
            fileinfo.LastWriteTime = System.DateTime.Now;
            fileinfo.FileName = key;

            // 長さ
            byte[] raw = getBinary(key);
            if (raw == null)
                return -DokanNet.DOKAN_ERROR;

            fileinfo.Length = raw.Length;

            return DokanNet.DOKAN_SUCCESS;
        }


        /// <summary>
        /// キーが示す値をできる限りbyte[]にして返す
        /// </summary>
        private byte[] getBinary(string key)
        {
            if (this.ht == null)
                return null;

            // とりあえずvalとして取り出す
            object val = this.ht[key];

            // 型をチェックしてbyte[]に変換して返す
            if (val is byte[])
            {
                return (byte[])val;
            }
            else if (val is string)
            {
                return Encoding.Default.GetBytes((string)val);
            }
            else if (val is bool)
            {
                return BitConverter.GetBytes((bool)val);
            }
            else if (val is char)
            {
                return BitConverter.GetBytes((char)val);
            }
            else if (val is double)
            {
                return BitConverter.GetBytes((double)val);
            }
            else if (val is float)
            {
                return BitConverter.GetBytes((float)val);
            }
            else if (val is int)
            {
                return BitConverter.GetBytes((int)val);
            }
            else if (val is long)
            {
                return BitConverter.GetBytes((long)val);
            }
            else if (val is ushort)
            {
                return BitConverter.GetBytes((ushort)val);
            }
            else if (val is uint)
            {
                return BitConverter.GetBytes((uint)val);
            }
            else if (val is ulong)
            {
                return BitConverter.GetBytes((ulong)val);
            }
            else if (val is ushort)
            {
                return BitConverter.GetBytes((ushort)val);
            }

            // お手上げ
            return null;
        }

    }
}

intやdouble,boolなどに関しては

Encoding.Default.GetBytes(x.ToString())

の方が良いかもしれないが、そこはまぁ必要に応じて。

それにしても、Hashtableのインスタンスの大きさってどうにかして取得できないだろうか。書き込みを実装していないので今のところ使用ディスク容量が見えなくても気にならないが…