--- 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ファイルシステムを作ってみた。
RESTfs.png

(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, {&#39;Content-type&#39;=>&#39;application/x-www-form-urlencoded&#39;})
          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