枕を欹てて聴く

香炉峰の雪は簾を撥げて看る

Taberareloo + upload from cache

f:id:Constellation:20110411013824p:image

というわけでversion 2.0.13にて, Taberarelooにupload from cache相当を実装しました. 結果, ある種のreferer checkを行うserviceの画像がTumblrにpostできたり, なにより, screen captureの結果がHatena Fotolife以外のTumblrにpostできるようになりました!(上図) これで手軽にscreen captureを撮ってtumblrにuploadできますね.

Chrome Web Store - Taberareloo

upload from cacheは実装されないのかと言われてきましたが, なんというか技術的に可能になったので実装しました. 楽しいですね!
正確にはcacheからuploadしているわけではないのですが, 一応これでどうでしょうか? なんかダメなserviceとかあったら, githubのissueに知らせてください.
Issues · Constellation/taberareloo · GitHub
Chrome WebStoreのcommentはmailがくるわけではないのでなかなか見れませんので...

ここのところやたら難解な記事を書いていたので, 実用的なことをしてみました. さて, 以下実装概要. いかにして実装を行うのかに興味がある方はどうぞ.

upload

当たり前ですが, Chrome browserのcacheに触るAPIはありません. で, 実際には何をしているのかというと, Fileをdownloadしてきてそれをpostしています.
さて, Fileのupload, 旧来のXHRにおいては行うことができませんでしたが, XHR lv2 FormDataによりuploadを行うことができます.

var key = "file";
var value = ...;  // File APIのFile Object
var fd = new FormData();
fd.append(key, value);
...
xhr.send(fd);

FormDataについてはこちらが詳しいです. Firefox 4: easier JS form handling with FormData ✩ Mozilla Hacks – the Web developer blog 旧来のsendでPOSTするためのdataの代わりにFormDataを与えることができます. このFormDataのkey / valueはencodeURIComponentする必要もなく, 非常に直感的にPOSTのparamを構築することができます.
ただし, 注意として, boundaryを使ったmultipart形式でsendされるので, 一部のWeb APIによっては対応していない場合があります. よって, 普段は従来通りで, paramにFile Objectが含まれていた場合のみFormDataを利用するのがおすすめです. 自分の使うWeb APIが限られていて, すべて問題ないのであればこちらを利用したほうがずっと簡潔かつ直感的に書くことができます.

file

じゃあそのFileとやらはどこから取ってきているのかという話. これもXHR lv2が解決します. XHRのresponseTypeに"arraybuffer"を指定する事によりArrayBuffer形式で受け取ることができ, それをBlobBuilderを使うことでBlobに変換することができます.
ちなみにChrome ExtensionでFileSystem APIを触るにはpermissionsにunlimite_storageが必要なので注意

このへんの情報は id:os0x さんのこちらの資料が詳しいです. File API: Writer, Directories and System
また資料上のcreateObjectURLですが, 現在はwindowではなくURL以下になっています(Firefox4), ただ, WebKitの場合はwindow.webkitURL以下になっているので,

var getURLFromFile = (function() {
  var URL = window.URL || window.webkitURL;
  return function getURLFromFile(file) {
    return URL.createObjectURL(file)
  }
})();

といった感じでしょうか. URLがGlobalに定義されるであろう将来を考慮してclosureに含めました.

で, "Blobはわかった. ではFileはどうしてるんだ"という話ですが, 現行ChromeではFileを動的に構築することができます. それがFile System APIです.
Taberareloo該当codeで言うと, ここ周辺です. taberareloo/src/lib/utils.js at b9d601acdeb89cc1c1c533e4aca37a73dfab51e4 · Constellation/taberareloo · GitHub

// 例えば, 適当にfile entryを確保する
// Mochikit Deferredを利用しているとする
function getTempFile(ext) {
  ext || (ext = 'blob');
  var d = new Deferred();
  webkitRequestFileSystem(TEMPORARY, 1024 * 1024, function(fs) {
    fs.root.getDirectory('tmp', {
      create: true
    }, function (dir) {
      dir.getFile(Math.random().toString(36).slice(2) + '.' + ext, {
        create: true
      }, function(file) {
        d.callback(file);
      }, function(e) {
        d.errback(e);
      });
    }, function(e) {
      d.errback(e);
    });
  });
  return d;
}

といった感じでtemporary file entryを得ることができます. FileEntryを得るとそこの実体fileに書き込むことができます. まずはXHR lv2でdataを取ってきましょう.

function download(url, type) {
  return request(url, {
    responseType: 'arraybuffer'
  }).addCallback(function(res) {
    var buffer = res.response;
    return createFileEntryFromArrayBuffer(buffer, type);
  });
}

requestはTaberarelooの便利関数でXHRのwrapperです. xhrのresponseTypeを'arraybuffer'にすることでres.responseには(いつもはみなさんres.responseTextを触ると思いますが)ArrayBuffer Objectが入っています. ArrayBuffer - JavaScript typed arrays | MDN
url先を見るようにArrayBufferには変更するためのmethodがありませんが, ArrayBufferの中身をいろいろ変更したい場合は後ほど説明します.
そして無事に手に入れたbufferは以下の関数に渡されます.

function createFileEntryFromArrayBuffer(buffer, type) {
  var d = new Deferred();
  var builder = new WebKitBlobBuilder();
  builder.append(buffer);
  getTempFile()
  .addCallback(function(entry) {
    return getWriter(entry)
    .addCallback(function(writer) {
      writer.onwrite = function onWrite(e) {
        d.callback(entry);
      };
      writer.onerror = function onError(e) {
        d.errback(e);
      };
      writer.write(builder.getBlob(type));
    })
    .addErrback(function(e) {
      d.errback(e);
    });
  });
  return d;
}

function getWriter(file) {
  var d = new Deferred();
  file.createWriter(function(writer) {
    d.callback(writer);
  });
  return d;
}

function getFileFromEntry(entry) {
  var d = new Deferred();
  entry.file(function(file) {
    d.callback(file);
  }, function onError(e) {
    d.errback(e);
  });
  return d;
}

(WebKit)BlobBuilderにArrayBufferをappendしたあと, builderからgetBlobを呼び出すことでBlobを手に入れることができます. typeは"image/jpeg"といったものです. そして, getTempFileからFileEntryを取ってきて, entry.createWriterで該当FileEntryへ書きこむFileWriterを手に入れます. そしてwriter.write(Blob)で書き込み, writerのonwriteで書き込み成功を待ち, そこから該当データが書き込まれたFileEntryを獲得するという流れです.
ちなみに下にあるgetFileFromEntryを見るとわかりますが, FileEntry#fileで該当FileEntryに対応するFile Objectを取得することができます. もちろんみなさん非同期です. 恐ろしいですね.

このようにしてFileを取得して書き込み, それをFormDataにappendすれば夢のFile uploadができます.

Base64?

では, このような場合はどうでしょうか? あなたはbase64されたdataを持っていて, これをuploadしたいと考える. このためにはdataをfileに変換しなければいけませんね.
具体的にはcanvas#toDataURLで取得したようなdataをpostしたい(canvasでいろいろ細工した画像をuploadしたい)というわけです.
Taberarelooにおける該当行はこれです. taberareloo/src/lib/background.js at 5eece23f3160298ddf8b6c7c1df4d2ecb1a63e43 · Constellation/taberareloo · GitHub

var cut = data.replace(/^.*?,/, ''); // base64 header cut
var binary = window.atob(cut);
var buffer = new ArrayBuffer(binary.length);
var view = new Uint8Array(buffer);
var fromCharCode = String.fromCharCode;
for (var i = 0, len = binary.length; i < len; ++i) {
  view[i] = binary.charCodeAt(i);
}
createFileEntryFromArrayBuffer(buffer, '').addCallback(function(entry) {
  return getFileFromEntry(entry).addCallback(function(file) {
    var key = getURLFromFile(file);
    GlobalFileEntryCache[key] = entry;
    return key;
  }).addCallbacks(function(url) {
    func(url);
  }, function(e) {
    func(e);
  });
});

まず最初のは, base64形式にある場合のある data:text/html;charset=utf-8 のようなものを削除しているだけです. そしてbase64なのですから素直にwindow.atobでbinary dataに変換します. このとき, JSのStringはUTF16, つまりuint16_tの配列になっていると考えられます. このままDOMStringとしてBlobBuilderにappendしてしまってはuint8_tごとに隙間ができてしまいます.
そこで, ArrayBufferを用意します. まずArrayBufferを長さ分で初期化. その後Uint8ArrayをArrayBufferを渡して作成します.
先ほどArrayBufferはいじれないと言いましたが, 実はまさにこれはbufferであり, これの中身をいじるviewとしてUint8ArrayなどTypeArrayが使われます. ArrayBufferView - JavaScript typed arrays | MDN これを用いることにより, 規定のformatでArrayBufferのbufferの中身をmodifyすることができるのです.
この場合byte dataとしてUint8Arrayを使い, もとのstringからそれぞれcharCodeAtでcodeを取得して埋め込んでいきます. XHRでhackyに取得したものではなくて, base64をdecodeしたものなので, 上位bitを吹き飛ばしたりとかそういう必要はありません.
そしてあとはbufferをつかって先ほどと同じようにFileを構築すればbase64からFile Objectを構築することができます.

まとめ

XHR lv2 は本当に素晴らしいですね.
File System API, XHR lv2と揃ってfile upload, そろそろいけることは分かっていましたが, 実際にTaberarelooに実装するのには一念発起が必要でした遅れてすみません.
というわけで, いまやChrome Extensionはより多くの表現力を得たので, File uploadとか余裕ですよふふんという心持ちで参りましょう!
TaberarelooがHTML5の個人的な実験場になっています...