Google App Engineでストレージサービス作ってみた - yoshida_eth0の日記で作ったRESTful APIを、ファイルシステムとしてマウントする。
Flexでファイラも作ったし、知識がなくても簡単に使える、っていう点はクリアしてるけど、ブラウザを立ち上げなきゃいけない時点で面倒だった。
FUSE
FUSEをC言語以外で使えるようにしたものはいくつかあるっぽい。
Rubyが1番実績がありそう。
でもRubyはさっぱり解らないからPHPで。
PHP FUSEのリファレンスは充実してるけど、探してもこれを使った実績が全然ない。
とりあえず人柱に。
RubyForge: FuseFS: Project Info
http://rubyforge.org/projects/fusefs/
Python Package Index : fuse-python 0.2
http://pypi.python.org/pypi/fuse-python/
PHP FUSE - GREE Labs
http://labs.gree.jp/Top/OpenSource/Fuse.html
PHP FUSEインストール
FUSEインストール
yum -y install fuse fuse-devel dkms-fuse modprobe fuse yum -y install pkgconfig echo "export PKG_CONFIG_PATH=/usr/lib/pkgconfig" > /etc/profile.d/pkgconfig.sh source /etc/profile.d/pkgconfig.sh
cd /usr/local/src/ wget http://labs.gree.jp/data/source/php-fuse-0.9.2.tgz tar -C /tmp/ -zxvf php-fuse-0.9.2.tgz cd /tmp/php-fuse-0.9.2 phpize ./configure make make test make install echo extension=fuse.so > /etc/php.d/fuse.ini
書き込み出来ない
writeメソッドは用意されてるんだけど、なぜか呼び出されてない。
オプションにdebugをつけてマウントして、cpしてみると、SETATTRが実装されていないと怒られた。
unique: 2, opcode: LOOKUP (1), nodeid: 1, insize: 44 LOOKUP /aaa NODEID: 2 unique: 2, error: 0 (Success), outsize: 136 unique: 3, opcode: SETATTR (4), nodeid: 2, insize: 128 unique: 3, error: -38 (Function not implemented), outsize: 16
fuse.hのfunction一覧が書かれてるっぽい、fuse_operationsを見てもsetattrなんてなかった。
lowlevel…?
こういう時にC言語解らないと辛いなぁ。
# find /usr/include/fuse/ | xargs grep setattr /usr/include/fuse/fuse_lowlevel_compat.h: void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct stat *attr, /usr/include/fuse/fuse_lowlevel_compat.h: void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct stat *attr, /usr/include/fuse/fuse_lowlevel.h:/* 'to_set' flags in setattr */ /usr/include/fuse/fuse_lowlevel.h: * If the setattr was invoked from the ftruncate() system call /usr/include/fuse/fuse_lowlevel.h: void (*setattr) (fuse_req_t req, fuse_ino_t ino, struct stat *attr, /usr/include/fuse/fuse_lowlevel.h: * getattr, setattr
GStorageFS.php
読み取り専用としてなら動く。
http://oauth.googlecode.com/svn/code/php/OAuth.phpを同じディレクトリに置く。
ソース
<?php require_once(dirname(__FILE__).'/OAuth.php'); class Usage extends Exception { public function __construct() { $message = "Usage: php GStorage.php"; parent::__construct($message); } } class OAuthSignatureMethod_RSA_SHA1_Wrapper extends OAuthSignatureMethod_RSA_SHA1 { protected $privatekey; public function __construct($privatekey) { $this->privatekey = $privatekey; } protected function fetch_private_cert(&$request) { return $this->privatekey; } } abstract class GStorageIO { public $ssl = true; public $host; public $basepath = ''; public $username; public $password; public $privatekey; public function request($method, $path, $body=null) { $scheme = $this->ssl ? 'https' : 'http'; $url = sprintf('%s://%s%s%s', $scheme, $this->host, $this->basepath, $path); $param = array( 'format' => 'bin', 'method' => $method ); $req = $this->getRequestParam($url, $param, $body); $ch = curl_init($req['url']); curl_setopt($ch, CURLOPT_HEADER, false); curl_setopt($ch, CURLOPT_RETURNTRANSFER, true); curl_setopt($ch, CURLOPT_BINARYTRANSFER, true); curl_setopt($ch, CURLOPT_HTTPHEADER, $req['header']); if (!is_null($req['body'])) { curl_setopt($ch, CURLOPT_POSTFIELDS, $req['body']); } $res_body = curl_exec($ch); $res_info = curl_getinfo($ch); curl_close($ch); return array('info'=>$res_info, 'body'=>$res_body); } abstract public function init(); abstract protected function getRequestParam($path, $param, $body); } class GStorageIO_WSSE_a extends GStorageIO { public function init() { if (!isset($this->host, $this->username, $this->password)) { throw new Usage(); } } protected function getRequestParam($url, $params, $body) { $nonce = base64_encode(pack('H*', sha1(md5(time().rand().posix_getpid())))); $created = date('Y-m-d\TH:i:s\Z'); $digest = base64_encode(pack('H*', sha1($nonce . $created . $this->password))); $wsse_header = sprintf('X-WSSE: UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"', $this->username, $digest, $nonce, $created); $url = $url.'?'.http_build_query($params); return array( 'url' => $url, 'header' => array( $wsse_header ), 'body' => $body ); } } class GStorageIO_WSSE_b extends GStorageIO { public function init() { if (!isset($this->host, $this->username, $this->password)) { throw new Usage(); } } protected function getRequestParam($url, $params, $body) { $nonce = pack('H*', sha1(md5(time().rand().posix_getpid()))); $created = date('Y-m-d\TH:i:s\Z'); $digest = base64_encode(pack('H*', sha1($nonce . $created . $pass))); $wsse_header = sprintf('UsernameToken Username="%s", PasswordDigest="%s", Nonce="%s", Created="%s"', $user, $digest, base64_encode($nonce), $created); $url = $url.'?'.http_build_query($params); return array( 'url' => $url, 'header' => array( $wsse_header ), 'body' => $body ); } } class GStorageIO_OAuth_HMAC_SHA1 extends GStorageIO { protected $consumer; protected $signature_method; public function init() { if (!isset($this->host, $this->username, $this->password)) { throw new Usage(); } $this->consumer = new OAuthConsumer($this->username, $this->password, null); $this->signature_method = new OAuthSignatureMethod_HMAC_SHA1(); } protected function getRequestParam($url, $params, $body) { $method = is_null($body) ? 'GET' : 'POST'; $req = OAuthRequest::from_consumer_and_token($this->consumer, null, $method, $url, $params); $req->sign_request($this->signature_method, $this->consumer, null); return array( 'url' => $req->to_url(), 'header' => array( ), 'body' => $body ); } } class GStorageIO_OAuth_RSA_SHA1 extends GStorageIO { protected $consumer; protected $signature_method; public function init() { if (!isset($this->host, $this->username, $this->privatekey)) { throw new Usage(); } $this->consumer = new OAuthConsumer($this->username, null, null); $this->signature_method = new OAuthSignatureMethod_RSA_SHA1_Wrapper($this->privatekey); } protected function getRequestParam($url, $params, $body) { $method = is_null($body) ? 'GET' : 'POST'; $req = OAuthRequest::from_consumer_and_token($this->consumer, null, $method, $url, $params); $req->sign_request($this->signature_method, $this->consumer, null); return array( 'url' => $req->to_url(), 'header' => array( ), 'body' => $body ); } } class GStorageFS extends Fuse { protected $_io; protected $_cache = array(); protected $_f; public function __construct(GStorageIO $io) { $this->_io = $io; $this->debug("construct"); } public function checkmount($path, $option) { $this->debug("checkmount"); // authenticate check $this->_io->init(); $res = $this->_io->request('ls', '/'); if ($res['info']['http_code']!=200) { throw new Exception('mount error: '.$res['body']); } // mount $this->mount($path, $option); } // fuse method public function getdir($path, &$retval) { $this->debug("getdir {$path}"); $path = rtrim($path, '/'); $res = $this->_io->request('ls', $path); // response ok if ($res['info']['http_code']==200) { $retval['.'] = array('type' => FUSE_DT_DIR); $retval['..'] = array('type' => FUSE_DT_DIR); $list = explode("\n", $res['body']); foreach ($list as $item) { $item = explode("\t", $item, 3); if (count($item)==3) { $model = trim($item[0], "\r\n"); $name = trim($item[1], "\r\n"); $size = (int)trim($item[2], "\r\n"); $type = $model=='dir' ? FUSE_DT_DIR : FUSE_DT_REG; $retval[$name] = array( 'type' => $type ); $this->_cache[$path.'/'.$name] = array( 'model' => $model, 'size' => $size, 'time' => microtime(true) ); } } return 0; } $this->debug(print_r($res, 1)); return -FUSE_ENOENT; } public function getattr($path, &$st) { $this->debug("getattr {$path}"); $st['dev'] = 0; $st['ino'] = 0; $st['mode'] = 0; $st['nlink'] = 0; $st['uid'] = 0; $st['gid'] = 0; $st['rdev'] = 0; $st['size'] = 0; $st['atime'] = 0; $st['mtime'] = 0; $st['ctime'] = 0; $st['blksize'] = 0; $st['blocks'] = 0; $item = array( 'model' => '', 'size' => 0 ); // use cache if (isset($this->_cache[$path]) && microtime(true)-3<$this->_cache[$path]['time']) { $item = $this->_cache[$path]; // send request } elseif ($path!='/') { $retval; $this->debug(" ", false); $this->getdir(preg_replace('#/[^/]+$#', '', $path), $retval); if (isset($this->_cache[$path])) { $item = $this->_cache[$path]; } } if ($path=='/' || $item['model']=='dir') { $st['mode'] = FUSE_S_IFDIR | 0775; $st['nlink'] = 3; $st['size'] = 0; // } else { } elseif ($item['model']=='file') { $st['mode'] = FUSE_S_IFREG | 0664; $st['nlink'] = 1; $st['size'] = $item['size']; } return 0; } public function open($path, $mode) { $this->debug("open {$path}"); return 1; } public function read($path, $fh, $offset, $buf_len, &$buf) { $this->debug("read {$path}"); $res = $this->_io->request('cat', $path); $buf = ''; // response ok if ($res['info']['http_code']==200) { $buf = $res['body']; } return strlen($buf); } public function release($path, $fh) { $this->debug("release {$path}"); return 0; } public function mkdir($path, $mode) { $this->debug("mkdir {$path}"); $res = $this->_io->request('mkdir', $path); // response ok if ($res['info']['http_code']==200) { return 0; } return -1; } public function unlink($path) { $this->debug("unlink {$path}"); $res = $this->_io->request('rm', $path); // response ok if ($res['info']['http_code']==200) { return 0; } return -1; } public function rmdir($path) { $this->debug("rmdir {$path}"); $res = $this->_io->request('rm', $path); // response ok if ($res['info']['http_code']==200) { return 0; } return -1; } public function write($path, $fh, $offset, $buf) { $this->debug("write {$path} {$fh} {$offset}"); $res = $this->_io->request('post', $path, $buf); // response ok if ($res['info']['http_code']==200) { return strlen($buf); } return -1; } public function setattr($path, &$st) { $this->debug("setattr"); return 0; } // not implemented method /* public function readlink($path, &$retval) { $this->debug("readlink"); return -1; } public function mknod($path, $mode, $dev) { $this->debug("mknod"); return -1; } public function symlink($path_from, $path_to) { $this->debug("symlink"); return -1; } public function link($path_from, $path_to) { $this->debug("link"); return -1; } public function chmod($path, $mode) { $this->debug("chmod"); return -1; } public function chown($path, $uid, $gid) { $this->debug("chown"); return -1; } public function truncate($path, $offset) { $this->debug("truncate"); return -1; } public function utime($path, $atime, $mtime) { $this->debug("utime"); return -1; } public function statfs($path, $st) { $this->debug("statfs"); return -1; } public function flush($path, $fh) { $this->debug("flush"); return -1; } public function fsync($path, $fh, $mode) { $this->debug("fsync"); return -1; } public function setxattr($path, $name, $value, $mode) { $this->debug("setxattr"); return -1; } public function getxattr($path, $name, &$value) { $this->debug("getxattr"); return -1; } public function listxattr($path, &$name_list) { $this->debug("listxattr"); return -1; } public function removexattr($path, $name) { $this->debug("removexattr"); return -1; } */ // debug public function debug($txt, $eol=true) { if (!is_resource($this->_f)) { $this->_f = fopen('/tmp/fuse.log', 'a'); } $eol = $eol ? "\n" : ''; fwrite($this->_f, $txt.$eol); } } $io = new GStorageIO_WSSE_a(); //$io = new GStorageIO_WSSE_b(); //$io = new GStorageIO_OAuth_HMAC_SHA1(); //$io = new GStorageIO_OAuth_RSA_SHA1(); $io->ssl = false; $io->host = 'g-storage.appspot.com'; $io->basepath = '/storage'; $io->username = 'testuser'; $io->password = 'testpass'; //$io->privatekey = file_get_contents(dirname(__FILE__).'/private.key'); $fuse = new GStorageFS($io); try { $fuse->checkmount("/mnt", "allow_other"); // $fuse->checkmount("/mnt", "allow_other,debug"); } catch (Exception $e) { echo $e->getMessage()."\n"; }
使い方
# php GStorageFS.php
実行結果
[root@eth0 ~]# ls -l /mnt/ 合計 0 drwxrwxr-x 3 root root 0 1月 1 1970 opensocial drwxrwxr-x 3 root root 0 1月 1 1970 share -rw-rw-r-- 1 root root 5 1月 1 1970 test1.txt -rw-rw-r-- 1 root root 5 1月 1 1970 test2.txt -rw-rw-r-- 1 root root 5 1月 1 1970 test3.txt [root@eth0 ~]# cat /mnt/test1.txt aaaa