--- layout: old_post title: RESTなWebサービスをマウントするRESTファイルシステム、FUSEで作ってみた permalink: /tatsuya/show/352-rest-web-rest-fuse ---
FUSE用のRubyライブラリで、FuseFSてのがあるのを最近知った
RubyのFuseFS使ってtwitter file systemを作ってみた
Rubyで手軽にファイルシステムを構築できるそうな。面白そうなので、ひとつ試しにRESTなWebサービスをローカルにマウントするRESTファイルシステムを作ってみた。
(http://localhost:3000/books/3.xml へアクセスして中身を表示)
あと外部Webサービスをローカルにマウント!てのがやりたかったので、TwitterとTumblrのAPIをマウントしてみた。
$ cat ~/restfs/TwitterStatus/user_timeline/117011742/text > ~/restfs/TumblrAPI/write
こんな感じで普通にファイル操作をすると、Twitter http://twitter.com/tkmr/statuses/117011742 から Tumblr http://tkmr.tumblr.com/post/4104854 へポスト。
サーバ準備
手始めに手元にあったこんなクラスへアクセスしてみる、Railsでさくっと。
ruby script/generate scaffold_resource Book title:string rate:int review:string
//タイトル(title)とレビュー(review)を文字列でレート(rate)をIntで持つBookクラス
ruby script/srever start で待ち受けておく
開発環境の準備
自分のPCはMacなので、MacFUSEをインストール
http://code.google.com/p/macfuse/
今回は MacFUSE-Core-0.4.0.dmg をダウンロード、普通にインストール
FUSEへのRubyブリッジFuseFS、MacOSX対応した物が↓らしいので
http://www.datanoise.com/articles/2007/3/9/macfuse-and-ruby-fusefs-extension
svn co http://svn.datanoise.com/fusefs-osx
cd fusefs-osx
make
sudo make install
インストール、すんなり上手くいった
あとActiveResourceを単体で使いたいので
sudo gem install activeresource --source http://gems.rubyonrails.org
gemから入れておいた
CRUDを実装
RubyでFuseFS::MetaDirを継承したクラスを実装していく、適当に以下のようにしてみました。富豪的プログラミングだなー
require 'fusefs' class RESTfs < FuseFS::MetaDir def initialize resource @resource=resource end #カレントディレクトリーのファイル一覧を返す - ls dirname/ def contents path action, id, key = scan_path path unless action then return @resource.actions end unless id then resources = action=="all" ? @resource.find(:all) : @resource.find(action) return resources.map{|r| r.id.to_s } end @resource.find(id).attributes.keys end #ディレクトリーとファイルの判定 def directory? path action, id, key = scan_path path key ? false : true end def file? path !directory?(path) end #ファイルの中身を返す - cat filename def read_file path action, id, key = scan_path path item = @resource.find(id) item.attributes[key].to_s + "\n" end def size(path) read_file(path).size end #ファイルへ書き込み - echo "hoge" > filename def can_write? path file?(path) end def write_to path, body action, id, key = scan_path path if key then item = @resource.find(id) item[key] = body item.save end end #ファイル削除 - rm filename def can_delete? path file?(path) end def delete path action, id, key = scan_path path @resource.find(id).destroy if id end #フォルダ作成 - mkdir newdir def can_mkdir? path false end def mkdir path end #フォルダ削除 - rmdir dirname def can_rmdir? path action, id, key = scan_path path return false if key id ? true : false end def rmdir path action, id, key = scan_path path @resource.find(id).destroy if id end end
ざっくりとCRUD一通り書いたので使ってみる
require 'restfs' require 'rubygems' gem 'activeresource' require 'active_resource' class Book < ActiveResource::Base self.site = "http://localhost:3000/" self.logger = Logger.new($stderr) def self.actions ["all"] end end if (File.basename($0) == File.basename(__FILE__)) root = FuseFS::MetaDir.new root.mkdir("/Book", RESTfs.new(Book)) FuseFS.set_root(root) FuseFS.mount_under(ARGV[0]) FuseFS.run end
これをターミナルで立ち上げる ruby bookfs.rb ~/restfs/(ホームへrestfsという空ディレクトリを作成しておく)
~ tatsuya$ cd restfs/Book ~/restfs/Book tatsuya$ ls all ~/restfs/Book tatsuya$ cd all ~/restfs/Book/all tatsuya$ ls 1 11 13 15 17 19 20 22 24 26 28 3 31 33 35 37 8 10 12 14 16 18 2 21 23 25 27 29 30 32 34 36 7 9 ~/restfs/Book/all tatsuya$ cd 3 ~/restfs/Book/all/3 tatsuya$ ls id rate review title ~/restfs/Book/all/3 tatsuya$ cat title ~/restfs/Book/all/3 tatsuya$ ~/restfs/Book/all/3 tatsuya$ cat title てすとタイトル ~/restfs/Book/all/3 tatsuya$ ls -ltr >> title ~/restfs/Book/all/3 tatsuya$ cat title てすとタイトル total 4 -rw-rw-rw- 1 tatsuya tatsuya 16 6 23 16:02 title -rw-rw-rw- 1 tatsuya tatsuya 13 6 23 16:02 review -rw-rw-rw- 1 tatsuya tatsuya 2 6 23 16:02 rate -rw-rw-rw- 1 tatsuya tatsuya 2 6 23 16:02 id ~/restfs/Book/all/3 tatsuya$
普通にローカルファイルのように操作できる。お手軽で楽しいな〜、サーバ側のログを見ると
GET http://localhost:3000/books.xml --> 200 OK (4775b 0.37s) GET http://localhost:3000/books/3.xml --> 200 OK (224b 0.32s) GET http://localhost:3000/books/3.xml --> 200 OK (224b 0.33s) ・・・・・・・・・・・・・・ PUT http://localhost:3000/books/3.xml --> 200 OK (1b 0.32s)
GETアクセスが大変なことに!!富豪的だが気にしない
次は外部サービスをマウントしてみる、TwitterAPIとTumblrAPIをマウントしてTwitterから取ってきた発言をTumblrへポスト。まずTwitterから
module Twitter USER = "Twitterアカウント" PASSWORD = "パスワード" class Status < ActiveResource::Base self.site = "http://#{Twitter::USER}:#{Twitter::PASSWORD}@twitter.com/" self.logger = Logger.new($stderr) def self.actions ["public_timeline","user_timeline","friend_timeline"] end def self.find *args if args[0].to_s.to_i.to_s.size == args[0].to_s.size then super("show/#{args[0]}") else self.get(args).map {|r| self.new(r) } end end end end
とりあえずReadだけ。Tumblrも同じように書こう思ったけどAPIとActiveResourceが相性悪そうだったので普通にNet::HTTPでアクセスした、ソースは長くなったので一応最後に貼り付けときます。それではマウントして試してみる
root = FuseFS::MetaDir.new root.mkdir("/TwitterStatus", RESTfs.new(Twitter::Status)) root.mkdir("/TumblrAPI", Tumblr::ApiFS.new) FuseFS.set_root(root) FuseFS.mount_under(ARGV[0]) FuseFS.run
これを実行してマウント、次に別のターミナルを立ち上げて普通にシェルからファイル操作してみる
#ローカルファイルとしてアクセスすれば ~/restfs/TwitterStatus/public_timeline tatsuya$ ls 117011742 created_at id text user ~/restfs/TwitterStatus/public_timeline tatsuya$ cat 117011742/text なんかさっきtwitter落ちてた? #TwitterAPIへアクセスしている GET http://twitter.com:80/statuses/show/117011742.xml --> 200 OK (589b 0.43s) #Twitterの発言をTumblrへポスト ~/restfs/TwitterStatus/public_timeline tatsuya$ cat 117011742/text > ~/restfs/TumblrAPI/write
OKぽい。
$ cat ~/restfs/TwitterStatus/user_timeline/117011742/text > ~/restfs/TumblrAPI/write
で、Twitter http://twitter.com/tkmr/statuses/117011742 から Tumblr http://tkmr.tumblr.com/post/4104854 へポスト。
本当は $cat ~/restfs/TwittreStatus/user_timeline/* | grep `date +%Y%m%d` | ~/restfs/TumblrAPI/write とかシェルスクリプトに書いて、cronで自動更新にして。本物のUNIXパイプでWebサービスをマッシュアップ! Web1.0世代のPlagger (w みたいなネタを書きたかったけど、時間が無くなったのでそれはまた今度。
しかし面白いね〜FuseFS。RSSfsなんて作ると面白いかも、またはAtomPPfsとか。ただMacのFinderで外部サービスのフォルダを開くとかなり激しいアタックがWebサービスへ行くのでw やっぱ実用的では無いな、LAN内にあるサーバへ繋ぐならなんとかなるかも。あとちゃんとキャッシュの仕組みを作ればまた別だろうけど。
それにしてもweb上のリソース とローカル上のリソースが混在する環境って楽しいね。ネットOSというか、ネット上のリソースをローカルリソースと同じ意識で扱う(またはネット上に全てのリソースを置く)ことが実用的になったら、その用途に最適化したOSが出てくるのかな。そうすると通信速度の進化はまだまだ続く必要があるな、今のローカルHDDがSerialATA接続で 1.2Gbps/秒 と考えれば家庭用の光ファイバーでも単純な速度は何とかなりそう?むしろサーバの負荷やネットのハブになるエクスチェンジポイントの負荷が問題なのかも。データセンタ内でサーバを分散しても、回線の負荷は下がらないので、全体でみるとデータセンタの分散が良いのかな。
あと回線速度が十分高速になっても、日本からアメリカのWebサービスにアクセスするならレイテンシが問題になるのか、、どんな技術でも光速が限界になるから1秒で地球7周半、ls コマンド打って結果が返るまで最速0.1秒は遅いか早いか。いくら回線速度が早くてもレイテンシが高いとスーファミのスト2ターボみたいになるのかな?w そうするとやっぱデータセンタの分散やWinnyみたいな情報のキャッシュを分散するモデルが有効になってくるのかな、光速の限界を超えるためにプロセスを微細化する最近のCPUみたい。速度が伸びないなら距離を縮めよう、みたいな。
じゃあ究極の負荷分散はP2Pでクライアントをサーバにして、負荷の中心を無くしてしまうのが良いのかな?Google GearでブラウザがストレージとWebサーバを持ったから、あとはクロスサイトアクセスの制限を取っ払っちゃえばw どうだろ。データの同期・整合性は取り難くなるのでサーバ型とのハイブリットとか、例えばニコニコ動画のコメントは軽いのでサーバで同期して、映像はBittorrent的なモデルで配信するとか・・・・・
うーんなんだろ。まあ結論としては、FUSEfs面白い:)ということで
===============================================
一応TumblrFSのソースを↓に貼っておきます、適当ですが。
module Tumblr USER = "ユーザ名" SITE = "http://#{USER}.tumblr.com/api" EMAIL = "mailアドレス" PASS = "パスワード" class ApiFS < FuseFS::MetaDir def contents path ["read","write"] end def directory? path false end def file? path !directory?(path) end def read_file path action, hoge = scan_path(path) return "Please insert a text!!\n" if action=="write" result = "" open("#{SITE}/#{action}") do |f| result += f.read end result end def size path read_file(path).size end def can_write? path scan_path(path)[0]=="write" end def write_to path, text if can_write?(path) then begin title = text body = text param = "" {"email"=>EMAIL,"password"=>PASS,"type"=>"regular","title"=>title,"body"=>body}.each do |key,value| param += "#{key}=#{URI.encode(value)}&" end param.chomp!("&") res = Net::HTTP.start("www.tumblr.com", 80) do |http| http.post("/api/write", param, {'Content-type'=>'application/x-www-form-urlencoded'}) end rescue p "Email or Password error!! : tumblr" end end end def can_delete? path can_write? path end def delete path end def can_mkdir? path false end def mkdir path end def can_rmdir? path false end def rmdir path end end end