このブログの更新は Twitterアカウント @m_hiyama で通知されます。
Follow @m_hiyama

メールでのご連絡は hiyama{at}chimaira{dot}org まで。

はじめてのメールはスパムと判定されることがあります。最初は、信頼されているドメインから差し障りのない文面を送っていただけると、スパムと判定されにくいと思います。

参照用 記事

最小抽象ファイルシステムの仕様案 その3

これらの続きです。

内容:

ファイル名、だいたいのところ

ファイル名に関しては:

ちゃんと考えると、ものすごくめんどくさいですよね。なので、今は決めないことにします。

もう少し決めておくことにします。まず、名前文字という概念に関して次の規約をします。

  1. 文字番号32未満の文字は名前文字ではない。
  2. '/'(スラッシュ), '\'(バックスラッシュ), ':'(コロン)は名前文字ではない。
  3. 他に禁止する文字があってもいいが、それは環境に依存する。

これじゃ曖昧な点が残ってますが、名前文字がなんであるかが確定すれば、次のBNFで「名前」という構文要素を定義できます。

名前 ::= 名前文字+

上の定義に合致する文字列でも、さらに若干は除外すべきものが出てくるかもしれません。例えば、"." と ".." は通常の意味では使わない(後述)ので、誤解を招かないために禁止したほうがいいでしょう。

mafsで使うパス名は常に絶対パスです。

絶対パス ::= 接頭辞? '/' パス主要部
パス主要部 ::= 空文字列 | 名前 ('/' 名前)* '/'?
接頭辞 ::= 名前

接頭辞は、ドライブ、ボリューム、バーティション、あるいは Amazon S3 のバケットのような単位を表すための識別子です。接頭辞はなくてもいいので、絶対パスは '/' からはじまることもあります(むしろ、それが多い)。

ルートディレクトリ、親ディレクトリ、拡張子などの概念は通常のファイルシステムと同じです。ただし、カレントディレクトリを表す「.」や親ディレクトリを表す「..」は一切使いません。必要なら、ファイルシステムではなくてアプリケーション側でサポートします。

ファイル名(完全パス名)の最後が'/'のときは、ディレクトリであることを明示的に主張していると解釈します。/foo/bar/baz/ と書いたとき、/foo/bar/baz というファイルがあったとしても、そのファイルを指すわけではなく、あくまでもディレクトリと解釈します。最後が'/'でないときは、そのパスがファイルかディレクトリかの判断はできません。パスにより、明示的に通常ファイル(regular file)を指定することも不可能です。ディレクトリではなくてファイルを指定すべきときに、最後が'/'である名前を使うとエラー(einval)になります。

これらの概念を区別する用語法が難しいよなー。例えば:

  • (広義の)ファイル名、パス名、パス -- 上記の構文にマッチする文字列
  • ディレクトリ名、ディレクトリパス名、ディレクトリパス -- 最後が'/'で終わるパス
  • (狭義の)ファイル名、ファイルパス名、ファイルパス -- 最後が'/'で終わらないパス

ウーーーン、広義、狭義とか付いてるし、ファイルパスが実際はディレクトリである可能性が残っているし、、、ダメだなー。でも、伝統的にここらへんは曖昧だったんだよなー。僕はこの曖昧さがかなり嫌なんで、APIの関数名で対処します。

アクション設計の方針

mafsでは、ファイルシステムとそのクライアント(APIの呼び出し側)が離れている状況も想定しています。つまり、RPCとかRESTとかで要求を伝え/結果を戻すかも知れないわけです。

例えば、ディレクトリ内の全てのファイルを調べたいとき、多くのファイル関係ライブラリでは、名前の一覧を取得した後で isDir, canWrite, getSize みたいな関数/メソッドを使います。これだと、実質1ビットの情報を得るために関数呼び出しが発生します。リモート呼び出しではうれしくありません。

そこで、1回のAPI関数呼び出しで、できるだけ多くの情報をまとめて得る方針にします。直前の例で言えば、ディレクトリを読み出すと、名前とメタデータのひと揃い全部が一度に入手できます。isDir, canWrite, getSize なんかが必要なら便利関数として作ってね、というスタンス。こういう方針でも、ファイルシステムとクライアントが近いときに目立った弊害はありません(たぶんね)。

エンティティの分類

mafsファイルシステム内に存在していて名前で識別できるモノ(エンティティ)は、通常ファイル(regular file)とディレクトリだけです。通常ファイルを単に「ファイル」と呼び、フィイルかディレクトリか分かんないとき、あるいはどっちかを気にしないときは「エンティティ」と呼ぶことにします。

                 ファイル(通常フィアル)
              /
  エンティティ 
              \
                 ディレクトリ

ファイルを表す記号'File'、ディレクトリを表す記号'Directory'、不明を表す記号'-'とパス名を組み合わせたペアを考え、その意味を解釈しておきます。特に、パス名のお尻のスラッシュに注目。

  • (File, "foo/bar") -- これはファイル
  • (File, "foo/bar/") -- これは矛盾している、エラー
  • (Directory, "foo/bar") -- これはディレクトリ
  • (Directory, "foo/bar/") -- これはディレクトリ
  • (-, "foo/bar") -- これは不明
  • (-, "foo/bar/") -- これはディレクトリ

この解釈を、APIのネーミングの原則にします。例えば、readFile(authori_token, "foo/bar/") は、実行するまでもなく引数が矛盾しているので einval (invalid argument)エラーです。readFile(authori_token, "foo/bar") においては、"foo/bar"がファイルかディレクトリかは、実際にファイルシステムにアクセスして調べるまで分かりません。もし、"foo/bar"がディレクトリなら、eisdir (illegal operation on a directory)エラーです。

アクション動詞の分類

操作の対象物を分類したので、次は操作そのもの、つまり動詞(verb)を列挙します。基本的にはCRUD(Create Read Update Delete)ですが、アクションが働く対象物によって、動詞を若干変えます。以下の表は、動詞とその動詞が働く対象物の一覧です。△については後述。

動詞 ファイル ディレクトリ ファイルメタデータ ディレクトリメタデータ
create
delete - -
read - -
write - - -
get - -
set - -
update - -

動詞createにより、ファイルまたはディレクトリを生成できます。そのとき、メタデータも指定できるし、当然にメタデータも一緒に生成されます。それで△を付けておきました。read/writeは内容データ(コンテント)に対するIOです。setとupdateの違いは、setはメタデータを丸ごと置き換え、updateは一部だけ書き換えます。

API関数のネーミング

先の「動詞 対象物」の表(マトリックス)には、13個のマルが付いています。マル1個に対して1個の関数を作れば機能的にはOKです。が、使い勝手を考えて少し変形をします。それは次の2点です。

  1. メタデータの操作は、ファイルとディレクトリの区別をしない。例えば、getFileMetadata と getDirectoryMetadata は集約して、getMetadata とする。
  2. deleteFile, deleteDirectory 以外に、ファイルであってもディレクトリであっても削除できる delte を付け加える。

すると、関数の名前「動詞 (+ 対象物)?」は次の11個になります。

  1. createFile
  2. createDirectory
  3. deleteFile
  4. deleteDirectory
  5. delete
  6. readFile
  7. readDirectory
  8. writeFile
  9. getMetadata
  10. setMetadata
  11. updateMetadata

API関数群の仕様

引数/戻り値に必要なデータ型をもう一度確認しておきます。

  1. AuthoriToken -- 認可情報(アクセス権)を表すナニカ
  2. Path -- 任意のパス文字列
  3. Binary -- 生のバイト配列
  4. TimeStamp -- 秒までは表せる時刻値
  5. Void -- 値がないのではなくて、常に同じ1つの値を意味する。unit型。HTTPで言えば、200 OK。

メタデータに関しては次のスキーマ定義。

// remarkはプログラムには理解できない、人間向けの注釈
// その他のスキーマ属性は json-schema.org 参照

FileMetadata = object {
  "contentLength" : integer(minimum = 0),
  "contentType" : string(optional = true, 
                    remark = "MIMEメディアタイプ"),
  "created" : TimeStamp(optional = true),
  "lastModified" : TimeStamp,
  "readOnly" : boolean,
  "isText" : boolean(optional = true),
  "textEncoding" : string(optional = true, requires = "isText"
                     remark = "文字エンコーディングスキーム名"),
  "hidden" : boolean(optional = true, default = false)
  "executable" : boolean(optional = true, default = false)
};

DirectoryMetadata = object {
  "created" : TimeStamp(optional = true),
  "lastModified" : TimeStamp,
  "readOnly" : boolean,
  "hidden" : boolean(optional = true, default = false)
};

// unionは弁別子付きユニオン、file, dirが弁別子の値(タイプタグ)
// 弁別子の実現方法は、今は規定しない(そのうち述べます)
Metadata = union {
  file => FileMetadata,
  dir => DirectoryMetadata
};

DirectoryEntry = object {
  "name" : string(remark = "ファイルのベース名"),
  "metadata" : Metadata
};

DirectoryEntryList = array [ DirectoryEntry* ];

これ以外に、メタデータのプロパティをすべてオプショナルにしたデータ型として次を導入します。

  1. FileMetadataSetting
  2. DirectoryMetadataSetting
  3. MetadataSetting
関数呼び出し形式 戻り値
createFile(AuthoriToken, Path, FileMetadataSetting) Void
createDirectory(AuthoriToken, Path, DirectoryMetadataSetting) Void
deleteFile(AuthoriToken, Path) Void
deleteDirectory(AuthoriToken, Path) Void
delete(AuthoriToken, Path) Void
readFile(AuthoriToken, Path) Binary
readDirectory(AuthoriToken, Path) DirectoryEntryList
writeFile(AuthoriToken, Path, Binary) Void
getMetadata(AuthoriToken, Path) Metadata
setMetadata(AuthoriToken, Path, MetadataSetting) Void
updateMetadata(AuthoriToken, Path, MetadataSetting) Void

まだ続く

これで、最小なファイルシステムの輪郭は一通り描けたと思います。しかし、まだ曖昧であったり迷っていたり、記述が不十分な点があります。順不同で列挙します。

  1. APIのネーミング; updateMetadata より changeMetadata がいいかなー。readFile より readFileContent のほうが事実を表しているかも … 等。
  2. create関数の引数; (AuthoriToken, Path, Name, FileMetadataSetting) のほうがいいかも。Pathは親ディレクトリで、Nameはベース名。
  3. API関数が引き起こすエラーに関して述べてません。HTTPステータスコードへのマッピングも確定してないし。
  4. アクセス制御ももう少し詳しく述べるべきです。

また、次のような要望はすぐさま出てきそうです。

  1. ファイルがテキストファイルのとき、バイナリじゃなくて(プログラミング言語固有の)テキストデータとしてIOが出来たほうが便利でしょ。
  2. ファイルシステムなんだから、moveとcopyはできて当然では。
  3. シンボリックリンクがないのは辛すぎる。
  4. /dev/* や /proc/* みたいな特殊ファイルも認めたほうがいいんじゃないの。

mafsは、各種ファイルシステムの公約数を考えたものです。 -- この方針からは逸脱するのですが、JSONデータだけはえこひいきして懇ろ<ねんごろ>に扱おうかと思っています。具体的には、JSONファイルだけは部分(配列項目、オブジェクトプロパティ値)を読み出せるようにしようかと。

mafsは、ファイルをストリームとして読むことも、シークして一部分(ブロック、チャンク、レンジ)を読み出すこともサポートしてません。各フィイルが小さいか、大きなファイルを読み切るまでの待ち時間が問題にならない状況を想定しています。そうでない状況ではたぶん困るでしょう。JSONデータへの部分アクセスは、この困難を緩和するのに役立つと思います。