wasavi 0.5.342 released

リリースした。Google Drive への対応と ex コマンド入力時の各種補完がメインになっている。

ファイルをアップロードするために Google Drive が提供する API を呼び出す際、Exponential Backoff なる方式にするといいよ、と google 先生は言う。

なにやら難しい専門用語である。exponential とは指数のことだ。しかし内容はわりと明快で、ファイルをアップロードした際に何らかのエラーが発生した場合のリトライの待機時間をべき乗則にするといいんじゃね? ということである。

wasavi ではこれは実装していない。書き込みが失敗したらエラーだよ♪ と表示されてそれだけだ。ファイルへの書き込みはユーザーが明示的に指定するので、エラーが発生したらユーザーが再度指定すればいいのである(このインターフェースには異論はあるかもしれない)。

Google Drive #6

ex コマンド filesystem を実装した。最短は files。最初の引数がコマンドであり、以下の種類がある:

  • default: ドライブ名を省略した場合に使用されるデフォルトファイルシステムを取得・設定する。
    取得
    :filesystem default
    設定の例
    :filesystem default gdrive
  • status: 現在使用できるファイルシステムの一覧を表示する。
    :filesystem status
    *** available file systems ***
    * dropbox /
    gdrive /foo/
  • reset: 指定のファイルシステムのアクセストークンを破棄する

また、ex コマンド pwd、chdir、cd も実装した。機能は読んで字の通り。

Google Drive #5

dropbox、google drive と、wasavi から複数のオンラインストレージを読み書きできるようになる。その場合、ストレージの種別を区別させる必要が出てくる。そのため、wasavi 上でのファイルパスは接頭辞を付けることができる。[cci]dropbox:/path/to/file[/cci] とか [cci]gdrive:/path/to/file[/cci] というような感じだ。

このようにファイルパスを操作するとき、ストレージ・プリフィクスは省略可能である。ということは省略したときはよきに計らってくれるデフォルト値が必要ということであり、すでに内部ではそれを持っている。ただし今のところ dropbox 固定であり、変更する手段がない。どういう変更手段を設けるべきか?

ex コマンドを新設し、[cci]:filesystem default dropbox[/cci] などと指定できるようにするか? あるいは、そうではなく、単にオプションを増やして、[cci]:set filesystem=dropbox[/cci] とさせるか。

うーん、後者が自然かな。ただし一方で、デフォルトのファイルシステムの指定、のようなメタな設定は他に

  • 取得したアクセストークンの破棄
  • 現在有効なアクセストークンに関連付けられたアカウントの情報の表示

のような機能はいずれ必要になる。そうすると、「ファイルシステムのメタデータを管理する ex コマンド」に一本化したほうが長い目で見ればいいのか?

省略可能といえば、パス自体も常に絶対パスで指定しなければならないわけではない。つまり仮想的なカレントディレクトリもまた内部に持っておき、相対パスが入力されたらカレントディレクトリと結合する必要がある。というわけで、カレントディレクトリも内部で持っているのだが、これもまた wasavi 上から変更する手段がまだない。どうするか?

vim であれば、vim 上のカレントディレクトリを参照するには [cci]:pwd[/cci]、変更するには [cci]:chdir path[/cci] である。

こちらは、素直に ex コマンド新設コースかもしれない。pwd とか cd コマンドがあまりにもポピュラーすぎてそこから一線を画す理由がない。[cci]:set cwd=/[/cci] なんてなんだか奇妙だ。特に、相対パスでカレントディレクトリを変更する場合の [cci]:set cwd=../foo[/cci] のようなものは実際の動作とも食い違う(cwd が代入した値そのものになるわけではない)ので奇妙以上におかしい。

Google Drive #4

  • 前回の記事で Google Drive 上のファイルを読み出すために 3 回のアクセスが必要と書いたが、パスの構築時に basename も含めることでこれを 2 回に抑えることができた。
  • あるファイルがその親ディレクトリを複数持てるというのは、そのままの通りの意味のようだ。ハードリンクみたいな感じか。ちょっとこれは面倒なのでちゃんと対応するのは後回し。現状では、パスの構築時は親は 1 つだけの前提にしている。
  • ファイルの実体を読み出す際、ファイルのメタデータに含まれる downloadUrl が指すアドレス(たいてい https://*.googleuserconent.com/*)を参照することになる。一方で、OAuth 2.0 による承認を行う際、スコープと呼ばれる識別子を送る。[cci]https://www.googleapis.com/auth/drive[/cci] が最も強い権限、[cci]~/drive.file[/cci] がファイルの操作、[cci]~/drive.readonly.metadata[/cci] がメタデータの読み込み、みたいな。

    このうち最も強い権限を持つ drive スコープはほんとうに必要な場合のみ使ってね! とドキュメントにも明示してあるのだが、どうも downloadUrl からの読み込みは drive スコープじゃないと行えないような。drive.file だと 404 が帰ってくる。これは……バグじゃないのかな。というわけで drive スコープを使わざるを得ない。そんなような問いと答えが stackoverflow にもあった。

残るのはファイルの書き込み。

Google Drive #3

Google Drive 上の、あるファイルへのパスが与えられたときそれを読み込む手順は以下のとおりとした:

  • パスを dirname と basename に分割する
  • dirname をディレクトリ名ごとに分割し、各フラグメントを [cci]title=’ディレクトリ名'[/cci] に変換し、[cci]or[/cci] で結合してクエリ文字列を得る。[cci]/foo/bar[/cci] は [cci]title=’foo’ or title=’bar'[/cci] となる
  • File:list API を呼び出す。このとき q パラメータは上のクエリ文字列を与える
  • API の結果から、目的のパスに合致するメタデータの並びを得る。上の例で言うと、まずディレクトリ bar があり、その親 ID を持ち、かつタイトルが foo であるものを抜き出す。その処理をルートに向かって再帰的に行い、ルートに達した(分割したディレクトリの個数 == 抜き出したメタデータの個数となった)らパスが決定する
  • File:list API を呼び出す。このとき q パラメータは dirname の最後のフラグメントの fileID を親に持つファイル、とする。つまり [cci]’fileID’ in parents[/cci] とする。これは Children:list API でもよい気がするが、Children:list は指定のファイルの子供の ID しか返さない。
  • API の結果から、basename に一致するものがあれば、その downloadUrl を用いてファイルの内容を読み込む。なければ新しいファイルの編集とみなし、空文字列を wasavi へ返す

つまり 1 つのファイルを読み込むのに 3 回のリクエストが必要になる……それにしてもイケてないなー! もっとうまい方法はないのかしら。

あと、ひとつ気になる点がある。あるファイルのメタデータ内に、その親ファイルの ID が格納されているのだが、名前が parents で、型は配列なのである。あるファイルが複数の親を持つことができる? どうやって?

Google Drive #2

そんなわけで、OAuth 2.0 で認証したりする Google Drive 対応部を書いている。だいたいここここを見ればすべきことは書いてある。

ところで API のリファレンスを眺めてみると、なんか任意のファイルへアクセスするのにパス形式の文字列を使用する機能がないような? つまり、My Drive -> folder A -> folder B -> folder C -> foo.txt みたいな位置にあるファイルを読み書きするためには、dropbox の API であれば
GET /1/files/dropbox/folder_a/folder_b/folder_c/foo.txt
みたいなリクエストを素直に投げればいいのだが、Google Drive だとルートフォルダから 1 階層ごとに
GET /drive/v2/files/root/children/
みたいなリクエストを投げて、フォルダの中身のメタデータを見て掘り進んでいくようなことをしなければいけない。当然、パス階層が深ければその分リクエストの回数も多くなる。

えー……? 天下の Google 様がそんなアホみたいな仕様にしてるなんて嘘でしょ? なんか方法があるんでしょ? と思ったけど特にないようだ。

どういうことなの。なにか見落としてるんだろうか。

* * *

Files:list において、q パラメータに対して title=’folder A’ OR title=’folder B’ OR title=’folder C’ OR title=’foo.txt’ みたいな物を与えて、帰ってきたデータを元にルートから目的のファイルまでのパスを自前で構築し、目的のファイルの ID を特定すれば、とりあえず読む分においては、リクエスト回数は常にパス情報の取得 + 読み込みの 2 回で済むことに気がついた。

なんかイケてないなー。

Google Drive

0.5.320 をリリースした

ところで wasavi が対応しているオンラインストレージは今のところ dropbox だけであるが、Google Drive と Microsoft Skydrive に対応する気がないこともない。対応しないままずるずる来ているのは、つまり 1 年ほど前、これらのサイトは試験的に、認証・承認に OAuth 2.0 を使っていたわけなのだが、その当時は OAuth 2.0 はまだ仕様が流動的だったのだ。

現在、OAuth 2.0 自体の規格はまだ正式に固まっているわけではないみたいだけど、google drive、skydrive ともに OAuth 2.0 での認証・承認が普通に行われるようになって久しいようなので、ぼちぼち対応し始めてもいい頃合いかもしれない。

OAuth 1.0 では、jsOAuth というライブラリを使わせていただいたのだが、2.0 はもうちょっとシンプルな仕様になっている。そこで、もっと薄いライブラリを自前で用意するか、あるいは極端に言えば、直接 xhr をいじる方向でもいけるような気がする。

ちなみに dropbox はいつ OAuth 2.0 に対応するのだろうか。1 年前から「将来は 2.0 に対応するかも!」みたいなアナウンスはあったけど未だに実際の行動がなされていないまま今に至っているような。

completion #4

ファイル名の補完もできるようにした。ファイル名は dropbox へのネットワークアクセスが発生するため、「間」が発生するのは避けられない。補完が非同期になる場合はこんな感じで「補完してますよー」的なものが出る:
completion1
補完が完了すると何項目のうちの何番目ですよ的なものが出る:
completion2

completion #3

set コマンドについて、オプション名を補完できるようにもした。また、オプション名の略語を展開するようにもした。つまり set ai と打って tab を押すと、set autoindent に展開される。また、shift+tab で補完リストを逆方向に辿れるようにした。

これで、補完対象となる 3 種のうち 2 つまでを作り終えた。残るのは dropbox のようなストレージ上のファイル名の補完だ。

この手の補完は、他の 2 つとは決定的に違う点があって、つまり補完リストを得るためにネットワークアクセスが発生するのである。そうすると、アクセスの頻度とか、遅延とか、ワーストケースを考えつつ組まなければいけない。

まずアクセス頻度を抑えるために補完リストは最低数分間はキャッシュするようにしよう。また遅延は、たとえばアクセスして 15 秒経過しても反応がない場合は中断するような監視タイマを設け、中断時は古いキャッシュを使いまわすような仕組みにすればいいんじゃないかな!

completion #2

まずは ex コマンド名の補完に絞って作ってみる。

基本的な考え方は、入力行が ^\s*([^”\S]*).*$ にマッチし、カーソルが \1 の中にある場合、つまり ex コマンド名の入力途中であるとみなされる場合、tab の押下により \1 の範囲内を補完し置き換えるようにすればいい。

しかしながら、これは本当に核の部分の基本的な仕組みであって、いろいろと考えることはある。

  • この正規表現はアドレスの指定を考慮していない。アドレス指定の文法はかなり複雑 なので、補完機能の中にパーサを入れるのはちょっと無駄だ。ex コマンド実行部のパーサを流用できるようにしたい。
  • ex コマンドは | で連結することができる。したがって、補完は行頭の場合のほか、| の直後にも行われないといけない。ただし、/ の引数やアドレス指定内のリテラル内での | 、および ” 以降のコメント内の | は ex コマンドの区切りではないので、補完の開始トリガーとみなしてはならない。これもやはり、ex コマンドパーサの結果を使用しないと正しい動作を行うことができない。

というわけで、ex コマンド実行部にまず拡張を施し、純粋にパースだけを行うことができるようにした。その結果を元に補完を行うかを判断することになる。

また、補完処理は補完の準備が整っていることを確認した後に非同期的に行うようにしないといけない。コマンド名のリストであれば同期的な処理で構わないが、dropbox 上のファイルパスの補完などを行うことを考えると非同期にならざるを得ない。

というわけで作ってみた。

  • 補完対象を置き換える範囲をどうするか? 上記の正規表現の \1 の部分を置き換えるようにすると、
    ver#foo
    みたいなのがあったとき(# はカーソルを示す)、補完後の文字列は例えば version とかになる。つまりカーソルの後ろの領域も置き換えられる。一方、vim では置き換えるのはカーソル位置までなのである。つまり versionfoo とかになる。どちらがいいか。