PHP FUSEでG-Storageをマウント出来るようにしてみた

Google App Engineでストレージサービス作ってみた - yoshida_eth0の日記で作ったRESTful APIを、ファイルシステムとしてマウントする。
Flexでファイラも作ったし、知識がなくても簡単に使える、っていう点はクリアしてるけど、ブラウザを立ち上げなきゃいけない時点で面倒だった。
 

FUSE

FUSEC言語以外で使えるようにしたものはいくつかあるっぽい。
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

 
PHP FUSEインストール

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