2009/02/23


WassrのAPIには、imageパラメータによる画像アップロードがあるのですが、これに困ってる人がいたので、人助け。
Wassr [お気軽メッセージングハブ・ワッサー]

質問です。Firefox の Live HTTP Headers で確認すると Web のフォームから画像をアップロードすると素のバイナリデータが送信されています。API を利用した POST でも素のバイナリデータで送信しなければならないのでしょうか?

http://wassr.jp/channel/wassr_api/messages/hNHcDCCtja

試しにvimperatorから出来ないか挑戦してたらハマってハマって...色々試してたんですが、どうも画像だけがアップロードされない。
送信データを以下の様な関数で保存して目で確認してみたけど、あってる。
function writeToFile (filePath, content) {
    try {
        netscape.security.PrivilegeManager.enablePrivilege ('UniversalXPConnect');
        var file = Components.classes["@mozilla.org/file/local;1"].createInstance (Components.interfaces.nsILocalFile);
        file.initWithPath (filePath);
        if (! file.exists ()) file.create (0, 0664);
        var out = Components.classes["@mozilla.org/network/file-output-stream;1"].createInstance (Components.interfaces.nsIFileOutputStream);
        out.init (file, 0x20 | 0x02, 00004, null);
        out.write(content, content.length);
        out.close();
    } catch (e) {
        throw e;
    }
}
で、最後の最後は自前でCGIを立ててデータを確認してみた所、なんとデータが欠けてる!!

もしかしてsendじゃ送れないんじゃないか?と思ったらXMLHttpRequestにsendAsBinaryなんてメソッドがあった。
XMLHttpRequest - MDC
This method was added in Firefox 3.

A variant of the send() method that sends binary data.

void sendAsBinary(
  in DOMString body
);
Parameters

body

The request body as a DOM string. This data is converted to a string of single-byte characters by truncation (removing the high-order byte of each character).

https://developer.mozilla.org/en/XMLHttpRequest#sendAsBinary%28%29
はよ言えや!

見事送信する事が出来ました。
function createMultipartData(data, boundary) {
    var ret = [], boundary = "BOUNDARY-" + (boundary||'')+Math.floor(0x1000000 + Math.random() * 0xffffff).toString(16);
    for (var item in data) {
        if (!data.hasOwnProperty(item)) continue;
        var obj = data[item];
        if (obj.filename) {
            ret.push(
                '--' + boundary,
                'Content-Type: '+obj.contentType,
                'Content-Length: '+obj.content.length,
                'Content-Disposition: form-data; name="'+item+'"; filename="'+obj.filename+'"',
                'Content-Transfer-Encoding: binary',
                '',
                obj.content
            );
        } else {
            ret.push(
                '--' + boundary,
                'Content-Disposition: form-data; name="'+item+'"',
                '',
                String(obj)
            );
        }
    }
    ret.push('--' + boundary + '--', '');
    return {
        contentType : "multipart/form-data; boundary=" + boundary,
        content: ret.join("\n")
    };
}


function getFileContents(aFilePath) {
    var file = Components.classes['@mozilla.org/file/local;1'].createInstance(Components.interfaces.nsILocalFile);
    file.initWithPath(aFilePath);
    var fstream = Components.classes['@mozilla.org/network/file-input-stream;1'].createInstance(Components.interfaces.nsIFileInputStream);
    fstream.init(file, 1, 1, Components.interfaces.nsIFileInputStream.CLOSE_ON_EOF);
    var bstream = Components.classes['@mozilla.org/network/buffered-input-stream;1'].createInstance(Components.interfaces.nsIBufferedInputStream);
    bstream.init(fstream, 4096);
    var binary = Components.classes["@mozilla.org/binaryinputstream;1"].createInstance(Components.interfaces.nsIBinaryInputStream);
    binary.setInputStream(fstream);

    var data = "", size = 0;
    while ((size = binary.available()) > 0) {
        data += binary.readBytes(size);
    }
    return data;
}

var sendData = createMultipartData({
    status: "image upload from vimperator!",
    source: "vimperator/wassr.js",
    image: {
        filename: "example.jpg",
        contentType: "application/octet-stream",
        content: getFileContents("C:\\example.png")
    }
});

var req = new XMLHttpRequest();
req.open("POST", "http://api.wassr.jp/statuses/update.json");
req.setRequestHeader("Content-Type", sendData.contentType);
req.setRequestHeader("Content-Length", sendData.content.length);
req.sendAsBinary(sendData.content);
こんなんでどうでしょうか?
てかFirefox2の時、どうしてたの?苦笑
Posted at by



2009/02/18


先ほど試していたtw2ですが、少しやる気になってoutputzの設定もすませて動かしてみました。
ただ、中で使っているreadlineはimportした瞬間にbuilt-inのraw_input()を乗っ取ってしまい、かつ完全にマルチバイトに対応しきれていない様で、入力後の文字が化け化けになってしまいました。
おそらく、入力をutf-8で想定した作りになっているんだと思いますが、これはおそらく解決までに時間が掛かりそうな気がしています。
なのでpatchを書いてa2cさんにpull requestしました。
まれにutf-8しかない文字をtwitter/wassrに書き込んでくれる人がいるので、良いテストになりました。
raw_input()の変わりに、sys.stdin.readline()を使うようにしてあります。
diff --git a/tw2.py b/tw2.py
index fb04179..98fe90a 100755
--- a/tw2.py
+++ b/tw2.py
@@ -73,6 +73,9 @@ if __name__ == "__main__":
   friends = set()
   friendsTimeLine = []
 
+  def _T(text):
+    return text.encode('mbcs', 'ignore')
+
   def complete(text, status):
     results = [x for x in friends if x.startswith(text)] + [None]
     return results[status]
@@ -90,13 +93,13 @@ if __name__ == "__main__":
     # get twitter friends Replies
     print '\twassr replies\t'
     for data in reversed(wassr.getReplies()):
-      print '%-12s : %s' % (data['user_login_id'] ,data['text'])
+      print _T('%-12s : %s' % (data['user_login_id'] ,data['text']))
       friends.add(data['user_login_id'])
  
     # get twitter friends Replies        
     print '\n\twassr replies\t'
     for data in reversed(twitter.GetReplies()):
-      print '%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText())
+      print _T('%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText()))
       friends.add(data.GetUser().GetScreenName())
     return 0
 
@@ -105,7 +108,7 @@ if __name__ == "__main__":
     # Get FriendsTimeLine
     print ' -----  wassr Friends Time Line  -----'
     for data in reversed(wassr. getTimeline()):
-      print "%-12s: %s" % (data['user_login_id'] ,data['text'])
+      print _T("%-12s: %s" % (data['user_login_id'] ,data['text']))
       twit = "wr[]%-12s: %s" % (data['user_login_id'] ,data['text'])
       if twit in friendsTimeLine:
         pass
@@ -114,7 +117,7 @@ if __name__ == "__main__":
         friendsTimeLine.append(twit)
     print '\n  -----  twitter Friends Time Line  -----'
     for data in reversed(twitter.GetFriendsTimeline()):
-      print '%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText())
+      print _T('%-12s : %s' % (data.GetUser().GetScreenName() ,data.GetText()))
       # append log
       twit  = "tw[%s]%-12s: %s" % (data.GetCreatedAt(), data.GetUser().GetScreenName(), data.GetText())
       if twit in friendsTimeLine:
@@ -127,7 +130,8 @@ if __name__ == "__main__":
 
   prompt = '\n cmd: Friendstimeline[f] eXit[xx] \n> '
   while True:
-    input = raw_input(prompt).split(" ")
+    print prompt,
+    input = sys.stdin.readline().decode('mbcs').encode('utf-8').strip().split(" ")
     if input[0] != '':
       if len(input) == 1:
         if len(input[0]) > 2:
Macで動かなかったら御免なさい。
Posted at by




a2cさんが作ったtwitter client「tw2」を動かして見ようと思ったのですが、エラーも出るしpitが起動するエディタに何もテンプレートが出力されないのでコードを追ってみた。
調べた結果tw2ではなくpit側に原因がある事が分かった。
Bug #325139 in web.py: “web.profiler does not work on Windows”

tempfile.NamedTemporaryFile() to generate a temp file. On Windows the file cannot be opened a second time. Using tempfile.mkstemp() instead works but you lose the guaranteed cleanup of the temp file.

https://bugs.edge.launchpad.net/webpy/+bug/325139
どうやらNamedTemporaryFile()を使っていて、かつNamedTemporaryFile()はclose()時にファイルを削除してくれるのだけれど、内部でmkstempを使いfdを貰っているのにそのfdをclose()してくれていないものだからWindowsのハンドルが残りっぱなしになりwrite()もflush()もremove()出来ないと言う物だった。
Windowsでpit.pyを使ったプログラムを起動された事がある方は、一度テンポラリディレクトリの中を綺麗にした方が良いかもしれません。
    (fd, name) = _mkstemp_inner(dir, prefix, suffix, flags)
    file = _os.fdopen(fd, mode, bufsize)
    return _TemporaryFileWrapper(file, name)
つまり import os, yaml, tempfile

t = tempfile.NamedTemporaryFile()
t.write("foo")
t.flush()
path = os.path.abspath(t.name)
result = open(path).read()
t.close()
print result
の様にclose()前だと、fdが開放されていないので"Permission Denited"、また import os, yaml, tempfile

t = tempfile.NamedTemporaryFile()
t.write("foo")
t.flush()
path = os.path.abspath(t.name)
t.close()
result = open(path).read()
print result
の様にclose()後だと、ファイルは消えてしまっているのでopen()出来ない...という事。なにそれ苦笑

上記リンクにある通り、NamedTemporaryFile()の変わりにmkstempを使い、かつremove()前にclose()してやれば動いた。
--- pit.py.orig 2008-06-28 16:58:42.000000000 +0900
+++ pit.py  2009-02-18 17:22:30.734375000 +0900
@@ -18,13 +18,21 @@
         else:
             if not os.environ.has_key('EDITOR'):
                 return {}
-            t = tempfile.NamedTemporaryFile()
             c = yaml.dump(opts['config'] if opts.has_key('config') else Pit.get(name) ,default_flow_style=False)
+
+            fd, path = tempfile.mkstemp()
+            os.close(fd)
+            t = open(path, "w+b")
             t.write(c)
             t.flush()
-            path = os.path.abspath(t.name)
+
             Popen([os.environ['EDITOR'],path]).communicate()
-            result = open(path).read()
+
+            t = open(path)
+            result = t.read()
+            t.close()
+            os.remove(path)
+
             if result == c:
                 print 'No Changes'
                 return profile[name]
@@ -44,7 +52,7 @@
         if opts.has_key('require'):
             flg = False
             keys = set(opts['require'].keys()) - set(ret.keys())
-            if keys:
+            if len(keys):
                 for key in keys:
                     ret[key] = opts['require'][key]
                 ret = Pit.set(name,{'config' : ret})
で、tw2については、outputzのIDがいるらしく面倒臭くなったのでヤメ。また今度試します。

ところでこのパッチ、NamedTemporaryFile()を修正するパッチにすべきか、はたまたpit.pyを修正してyoshioriさんに送ればよいのか...どうしましょうねぇ
Posted at by