From 2f40f11c873b22881ad3ed21549253e53b6927ca Mon Sep 17 00:00:00 2001 From: Robin Appelman Date: Thu, 24 May 2012 18:22:33 +0200 Subject: [PATCH] smb filestorage backend --- 3rdparty/smb4php/smb.php | 439 +++++++++++++++++++++++++++ apps/files_external/appinfo/app.php | 2 + apps/files_external/lib/smb.php | 120 ++++++++ apps/files_external/tests/config.php | 7 + apps/files_external/tests/smb.php | 30 ++ 5 files changed, 598 insertions(+) create mode 100644 3rdparty/smb4php/smb.php create mode 100644 apps/files_external/lib/smb.php create mode 100644 apps/files_external/tests/smb.php diff --git a/3rdparty/smb4php/smb.php b/3rdparty/smb4php/smb.php new file mode 100644 index 0000000000..69fb2487bc --- /dev/null +++ b/3rdparty/smb4php/smb.php @@ -0,0 +1,439 @@ + +# +# This program is free software; you can redistribute it and/or +# modify it under the terms of the GNU General Public License +# as published by the Free Software Foundation; either version 2 +# of the License, or (at your option) any later version. +# +# This program is distributed in the hope that it will be useful, +# but WITHOUT ANY WARRANTY; without even the implied warranty of +# MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the +# GNU General Public License for more details. +# +################################################################### + +define ('SMB4PHP_VERSION', '0.8'); + +################################################################### +# CONFIGURATION SECTION - Change for your needs +################################################################### + +define ('SMB4PHP_SMBCLIENT', 'smbclient'); +define ('SMB4PHP_SMBOPTIONS', 'TCP_NODELAY IPTOS_LOWDELAY SO_KEEPALIVE SO_RCVBUF=8192 SO_SNDBUF=8192'); +define ('SMB4PHP_AUTHMODE', 'arg'); # set to 'env' to use USER enviroment variable + +################################################################### +# SMB - commands that does not need an instance +################################################################### + +$GLOBALS['__smb_cache'] = array ('stat' => array (), 'dir' => array ()); + +class smb { + + function parse_url ($url) { + $pu = parse_url (trim($url)); + foreach (array ('domain', 'user', 'pass', 'host', 'port', 'path') as $i) + if (! isset($pu[$i])) $pu[$i] = ''; + if (count ($userdomain = explode (';', urldecode ($pu['user']))) > 1) + @list ($pu['domain'], $pu['user']) = $userdomain; + $path = preg_replace (array ('/^\//', '/\/$/'), '', urldecode ($pu['path'])); + list ($pu['share'], $pu['path']) = (preg_match ('/^([^\/]+)\/(.*)/', $path, $regs)) + ? array ($regs[1], preg_replace ('/\//', '\\', $regs[2])) + : array ($path, ''); + $pu['type'] = $pu['path'] ? 'path' : ($pu['share'] ? 'share' : ($pu['host'] ? 'host' : '**error**')); + if (! ($pu['port'] = intval(@$pu['port']))) $pu['port'] = 139; + return $pu; + } + + + function look ($purl) { + return smb::client ('-L ' . escapeshellarg ($purl['host']), $purl); + } + + + function execute ($command, $purl) { + return smb::client ('-d 0 ' + . escapeshellarg ('//' . $purl['host'] . '/' . $purl['share']) + . ' -c ' . escapeshellarg ($command), $purl + ); + } + + function client ($params, $purl) { + + static $regexp = array ( + '^added interface ip=(.*) bcast=(.*) nmask=(.*)$' => 'skip', + 'Anonymous login successful' => 'skip', + '^Domain=\[(.*)\] OS=\[(.*)\] Server=\[(.*)\]$' => 'skip', + '^\tSharename[ ]+Type[ ]+Comment$' => 'shares', + '^\t---------[ ]+----[ ]+-------$' => 'skip', + '^\tServer [ ]+Comment$' => 'servers', + '^\t---------[ ]+-------$' => 'skip', + '^\tWorkgroup[ ]+Master$' => 'workg', + '^\t(.*)[ ]+(Disk|IPC)[ ]+IPC.*$' => 'skip', + '^\tIPC\\\$(.*)[ ]+IPC' => 'skip', + '^\t(.*)[ ]+(Disk)[ ]+(.*)$' => 'share', + '^\t(.*)[ ]+(Printer)[ ]+(.*)$' => 'skip', + '([0-9]+) blocks of size ([0-9]+)\. ([0-9]+) blocks available' => 'skip', + 'Got a positive name query response from ' => 'skip', + '^(session setup failed): (.*)$' => 'error', + '^(.*): ERRSRV - ERRbadpw' => 'error', + '^Error returning browse list: (.*)$' => 'error', + '^tree connect failed: (.*)$' => 'error', + '^(Connection to .* failed)$' => 'error', + '^NT_STATUS_(.*) ' => 'error', + '^NT_STATUS_(.*)\$' => 'error', + 'ERRDOS - ERRbadpath \((.*).\)' => 'error', + 'cd (.*): (.*)$' => 'error', + '^cd (.*): NT_STATUS_(.*)' => 'error', + '^\t(.*)$' => 'srvorwg', + '^([0-9]+)[ ]+([0-9]+)[ ]+(.*)$' => 'skip', + '^Job ([0-9]+) cancelled' => 'skip', + '^[ ]+(.*)[ ]+([0-9]+)[ ]+(Mon|Tue|Wed|Thu|Fri|Sat|Sun)[ ](Jan|Feb|Mar|Apr|May|Jun|Jul|Aug|Sep|Oct|Nov|Dec)[ ]+([0-9]+)[ ]+([0-9]{2}:[0-9]{2}:[0-9]{2})[ ]([0-9]{4})$' => 'files', + '^message start: ERRSRV - (ERRmsgoff)' => 'error' + ); + + if (SMB4PHP_AUTHMODE == 'env') { + putenv("USER={$purl['user']}%{$purl['pass']}"); + $auth = ''; + } else { + $auth = ($purl['user'] <> '' ? (' -U ' . escapeshellarg ($purl['user'] . '%' . $purl['pass'])) : ''); + } + if ($purl['domain'] <> '') { + $auth .= ' -W ' . escapeshellarg ($purl['domain']); + } + $port = ($purl['port'] <> 139 ? ' -p ' . escapeshellarg ($purl['port']) : ''); + $options = '-O ' . escapeshellarg(SMB4PHP_SMBOPTIONS); + $output = popen (SMB4PHP_SMBCLIENT." -N {$auth} {$options} {$port} {$options} {$params} 2>/dev/null", 'r'); + $info = array (); + $info['info']= array (); + while ($line = fgets ($output, 4096)) { + list ($tag, $regs, $i) = array ('skip', array (), array ()); + reset ($regexp); + foreach ($regexp as $r => $t) if (preg_match ('/'.$r.'/', $line, $regs)) { + $tag = $t; + break; + } + switch ($tag) { + case 'skip': continue; + case 'shares': $mode = 'shares'; break; + case 'servers': $mode = 'servers'; break; + case 'workg': $mode = 'workgroups'; break; + case 'share': + list($name, $type) = array ( + trim(substr($line, 1, 15)), + trim(strtolower(substr($line, 17, 10))) + ); + $i = ($type <> 'disk' && preg_match('/^(.*) Disk/', $line, $regs)) + ? array(trim($regs[1]), 'disk') + : array($name, 'disk'); + break; + case 'srvorwg': + list ($name, $master) = array ( + strtolower(trim(substr($line,1,21))), + strtolower(trim(substr($line, 22))) + ); + $i = ($mode == 'servers') ? array ($name, "server") : array ($name, "workgroup", $master); + break; + case 'files': + list ($attr, $name) = preg_match ("/^(.*)[ ]+([D|A|H|S|R]+)$/", trim ($regs[1]), $regs2) + ? array (trim ($regs2[2]), trim ($regs2[1])) + : array ('', trim ($regs[1])); + list ($his, $im) = array ( + explode(':', $regs[6]), 1 + strpos("JanFebMarAprMayJunJulAugSepOctNovDec", $regs[4]) / 3); + $i = ($name <> '.' && $name <> '..') + ? array ( + $name, + (strpos($attr,'D') === FALSE) ? 'file' : 'folder', + 'attr' => $attr, + 'size' => intval($regs[2]), + 'time' => mktime ($his[0], $his[1], $his[2], $im, $regs[5], $regs[7]) + ) + : array(); + break; + case 'error': + if(substr($regs[0],0,22)=='NT_STATUS_NO_SUCH_FILE'){ + return false; + }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_NAME_COLLISION'){ + return false; + }elseif(substr($regs[0],0,31)=='NT_STATUS_OBJECT_PATH_NOT_FOUND'){ + return false; + } + trigger_error($regs[0].' params('.$params.')', E_USER_ERROR); + } + if ($i) switch ($i[1]) { + case 'file': + case 'folder': $info['info'][$i[0]] = $i; + case 'disk': + case 'server': + case 'workgroup': $info[$i[1]][] = $i[0]; + } + } + pclose($output); + return $info; + } + + + # stats + + function url_stat ($url, $flags = STREAM_URL_STAT_LINK) { + if ($s = smb::getstatcache($url)) { return $s; } + list ($stat, $pu) = array (array (), smb::parse_url ($url)); + switch ($pu['type']) { + case 'host': + if ($o = smb::look ($pu)) + $stat = stat ("/tmp"); + else + trigger_error ("url_stat(): list failed for host '{$host}'", E_USER_WARNING); + break; + case 'share': + if ($o = smb::look ($pu)) { + $found = FALSE; + $lshare = strtolower ($pu['share']); # fix by Eric Leung + foreach ($o['disk'] as $s) if ($lshare == strtolower($s)) { + $found = TRUE; + $stat = stat ("/tmp"); + break; + } + if (! $found) + trigger_error ("url_stat(): disk resource '{$share}' not found in '{$host}'", E_USER_WARNING); + } + break; + case 'path': + if ($o = smb::execute ('dir "'.$pu['path'].'"', $pu)) { + $p = explode('\\', $pu['path']); + $name = $p[count($p)-1]; + if (isset ($o['info'][$name])) { + $stat = smb::addstatcache ($url, $o['info'][$name]); + } else { + trigger_error ("url_stat(): path '{$pu['path']}' not found", E_USER_WARNING); + } + } else { + return false; +// trigger_error ("url_stat(): dir failed for path '{$pu['path']}'", E_USER_WARNING); + } + break; + default: trigger_error ('error in URL', E_USER_ERROR); + } + return $stat; + } + + function addstatcache ($url, $info) { + global $__smb_cache; + $is_file = (strpos ($info['attr'],'D') === FALSE); + $s = ($is_file) ? stat ('/etc/passwd') : stat ('/tmp'); + $s[7] = $s['size'] = $info['size']; + $s[8] = $s[9] = $s[10] = $s['atime'] = $s['mtime'] = $s['ctime'] = $info['time']; + return $__smb_cache['stat'][$url] = $s; + } + + function getstatcache ($url) { + global $__smb_cache; + return isset ($__smb_cache['stat'][$url]) ? $__smb_cache['stat'][$url] : FALSE; + } + + function clearstatcache ($url='') { + global $__smb_cache; + if ($url == '') $__smb_cache['stat'] = array (); else unset ($__smb_cache['stat'][$url]); + } + + + # commands + + function unlink ($url) { + $pu = smb::parse_url($url); + if ($pu['type'] <> 'path') trigger_error('unlink(): error in URL', E_USER_ERROR); + smb::clearstatcache ($url); + smb_stream_wrapper::cleardircache (dirname($url)); + return smb::execute ('del "'.$pu['path'].'"', $pu); + } + + function rename ($url_from, $url_to) { + list ($from, $to) = array (smb::parse_url($url_from), smb::parse_url($url_to)); + if ($from['host'] <> $to['host'] || + $from['share'] <> $to['share'] || + $from['user'] <> $to['user'] || + $from['pass'] <> $to['pass'] || + $from['domain'] <> $to['domain']) { + trigger_error('rename(): FROM & TO must be in same server-share-user-pass-domain', E_USER_ERROR); + } + if ($from['type'] <> 'path' || $to['type'] <> 'path') { + trigger_error('rename(): error in URL', E_USER_ERROR); + } + smb::clearstatcache ($url_from); + return smb::execute ('rename "'.$from['path'].'" "'.$to['path'].'"', $to); + } + + function mkdir ($url, $mode, $options) { + $pu = smb::parse_url($url); + if ($pu['type'] <> 'path') trigger_error('mkdir(): error in URL', E_USER_ERROR); + return smb::execute ('mkdir "'.$pu['path'].'"', $pu)!==false; + } + + function rmdir ($url) { + $pu = smb::parse_url($url); + if ($pu['type'] <> 'path') trigger_error('rmdir(): error in URL', E_USER_ERROR); + smb::clearstatcache ($url); + smb_stream_wrapper::cleardircache (dirname($url)); + return smb::execute ('rmdir "'.$pu['path'].'"', $pu)!==false; + } + +} + +################################################################### +# SMB_STREAM_WRAPPER - class to be registered for smb:// URLs +################################################################### + +class smb_stream_wrapper extends smb { + + # variables + + private $stream, $url, $parsed_url = array (), $mode, $tmpfile; + private $need_flush = FALSE; + private $dir = array (), $dir_index = -1; + + + # directories + + function dir_opendir ($url, $options) { + if ($d = $this->getdircache ($url)) { + $this->dir = $d; + $this->dir_index = 0; + return TRUE; + } + $pu = smb::parse_url ($url); + switch ($pu['type']) { + case 'host': + if ($o = smb::look ($pu)) { + $this->dir = $o['disk']; + $this->dir_index = 0; + } else { + trigger_error ("dir_opendir(): list failed for host '{$pu['host']}'", E_USER_WARNING); + return false; + } + break; + case 'share': + case 'path': + if (is_array($o = smb::execute ('dir "'.$pu['path'].'\*"', $pu))) { + $this->dir = array_keys($o['info']); + $this->dir_index = 0; + $this->adddircache ($url, $this->dir); + foreach ($o['info'] as $name => $info) { + smb::addstatcache($url . '/' . urlencode($name), $info); + } + } else { + trigger_error ("dir_opendir(): dir failed for path '".$pu['path']."'", E_USER_WARNING); + return false; + } + break; + default: + trigger_error ('dir_opendir(): error in URL', E_USER_ERROR); + return false; + } + return TRUE; + } + + function dir_readdir () { + return ($this->dir_index < count($this->dir)) ? $this->dir[$this->dir_index++] : FALSE; + } + + function dir_rewinddir () { $this->dir_index = 0; } + + function dir_closedir () { $this->dir = array(); $this->dir_index = -1; return TRUE; } + + + # cache + + function adddircache ($url, $content) { + global $__smb_cache; + return $__smb_cache['dir'][$url] = $content; + } + + function getdircache ($url) { + global $__smb_cache; + return isset ($__smb_cache['dir'][$url]) ? $__smb_cache['dir'][$url] : FALSE; + } + + function cleardircache ($url='') { + global $__smb_cache; + if ($url == ''){ + $__smb_cache['dir'] = array (); + }else{ + unset ($__smb_cache['dir'][$url]); + } + } + + + # streams + + function stream_open ($url, $mode, $options, $opened_path) { + $this->url = $url; + $this->mode = $mode; + $this->parsed_url = $pu = smb::parse_url($url); + if ($pu['type'] <> 'path') trigger_error('stream_open(): error in URL', E_USER_ERROR); + switch ($mode) { + case 'r': + case 'r+': + case 'rb': + case 'a': + case 'a+': $this->tmpfile = tempnam('/tmp', 'smb.down.'); + smb::execute ('get "'.$pu['path'].'" "'.$this->tmpfile.'"', $pu); + break; + case 'w': + case 'w+': + case 'wb': + case 'x': + case 'x+': $this->cleardircache(); + $this->tmpfile = tempnam('/tmp', 'smb.up.'); + $this->need_flush=true; + } + $this->stream = fopen ($this->tmpfile, $mode); + return TRUE; + } + + function stream_close () { return fclose($this->stream); } + + function stream_read ($count) { return fread($this->stream, $count); } + + function stream_write ($data) { $this->need_flush = TRUE; return fwrite($this->stream, $data); } + + function stream_eof () { return feof($this->stream); } + + function stream_tell () { return ftell($this->stream); } + + function stream_seek ($offset, $whence=null) { return fseek($this->stream, $offset, $whence); } + + function stream_flush () { + if ($this->mode <> 'r' && $this->need_flush) { + smb::clearstatcache ($this->url); + smb::execute ('put "'.$this->tmpfile.'" "'.$this->parsed_url['path'].'"', $this->parsed_url); + $this->need_flush = FALSE; + } + } + + function stream_stat () { return smb::url_stat ($this->url); } + + function __destruct () { + if ($this->tmpfile <> '') { + if ($this->need_flush) $this->stream_flush (); + unlink ($this->tmpfile); + + } + } + +} + +################################################################### +# Register 'smb' protocol ! +################################################################### + +stream_wrapper_register('smb', 'smb_stream_wrapper') + or die ('Failed to register protocol'); diff --git a/apps/files_external/appinfo/app.php b/apps/files_external/appinfo/app.php index 2940a07025..9aef05b565 100644 --- a/apps/files_external/appinfo/app.php +++ b/apps/files_external/appinfo/app.php @@ -10,4 +10,6 @@ OC::$CLASSPATH['OC_Filestorage_FTP']='apps/files_external/lib/ftp.php'; OC::$CLASSPATH['OC_Filestorage_DAV']='apps/files_external/lib/webdav.php'; OC::$CLASSPATH['OC_Filestorage_Google']='apps/files_external/lib/google.php'; OC::$CLASSPATH['OC_Filestorage_SWIFT']='apps/files_external/lib/swift.php'; +OC::$CLASSPATH['OC_Filestorage_SMB']='apps/files_external/lib/smb.php'; + OCP\App::registerAdmin('files_external', 'settings'); diff --git a/apps/files_external/lib/smb.php b/apps/files_external/lib/smb.php new file mode 100644 index 0000000000..c769d29032 --- /dev/null +++ b/apps/files_external/lib/smb.php @@ -0,0 +1,120 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +require_once('smb4php/smb.php'); + +class OC_FileStorage_SMB extends OC_Filestorage_Common{ + private $password; + private $user; + private $host; + private $root; + + private static $tempFiles=array(); + + public function __construct($params){ + $this->host=$params['host']; + $this->user=$params['user']; + $this->password=$params['password']; + $this->root=isset($params['root'])?$params['root']:'/'; + + //create the root folder if necesary + $this->mkdir(''); + } + + public function constructUrl($path){ + if(substr($path,-1)=='/'){ + $path=substr($path,0,-1); + } + return 'smb://'.$this->user.':'.$this->password.'@'.$this->host.$this->root.$path; + + } + + public function mkdir($path){ + return mkdir($this->constructUrl($path)); + } + + public function rmdir($path){ + if($this->file_exists($path)){ + $succes=rmdir($this->constructUrl($path)); + clearstatcache(); + return $succes; + }else{ + return false; + } + } + + public function opendir($path){ + return opendir($this->constructUrl($path)); + } + + public function filetype($path){ + return filetype($this->constructUrl($path)); + } + + public function is_readable($path){ + return true;//not properly supported + } + + public function is_writable($path){ + return true;//not properly supported + } + + public function file_exists($path){ + return file_exists($this->constructUrl($path)); + } + + public function unlink($path){ + $succes=unlink($this->constructUrl($path)); + clearstatcache(); + return $succes; + } + + public function fopen($path,$mode){ + return fopen($this->constructUrl($path),$mode); + } + + public function writeBack($tmpFile){ + if(isset(self::$tempFiles[$tmpFile])){ + $this->uploadFile($tmpFile,self::$tempFiles[$tmpFile]); + unlink($tmpFile); + } + } + + public function free_space($path){ + return 0; + } + + public function touch($path,$mtime=null){ + if(is_null($mtime)){ + $fh=$this->fopen($path,'a'); + fwrite($fh,''); + fclose($fh); + }else{ + return false;//not supported + } + } + + public function getFile($path,$target){ + return copy($this->constructUrl($path),$target); + } + + public function uploadFile($path,$target){ + return copy($path,$this->constructUrl($target)); + } + + public function rename($path1,$path2){ + return rename($this->constructUrl($path1),$this->constructUrl($path2)); + } + + public function stat($path){ + return stat($this->constructUrl($path)); + } + + + +} diff --git a/apps/files_external/tests/config.php b/apps/files_external/tests/config.php index 5b6517755f..35b5fa0386 100644 --- a/apps/files_external/tests/config.php +++ b/apps/files_external/tests/config.php @@ -29,4 +29,11 @@ return array( 'host'=>'localhost:8080/auth', 'root'=>'/', ), + 'smb'=>array( + 'run'=>false, + 'user'=>'test', + 'password'=>'test', + 'host'=>'localhost', + 'root'=>'/test', + ), ); diff --git a/apps/files_external/tests/smb.php b/apps/files_external/tests/smb.php new file mode 100644 index 0000000000..52e1700b01 --- /dev/null +++ b/apps/files_external/tests/smb.php @@ -0,0 +1,30 @@ + + * This file is licensed under the Affero General Public License version 3 or + * later. + * See the COPYING-README file. + */ + +$config=include('apps/files_external/tests/config.php'); + +if(!is_array($config) or !isset($config['smb']) or !$config['smb']['run']){ + abstract class Test_Filestorage_SMB extends Test_FileStorage{} + return; +}else{ + class Test_Filestorage_SMB extends Test_FileStorage { + private $config; + private $id; + + public function setUp(){ + $id=uniqid(); + $this->config=include('apps/files_external/tests/config.php'); + $this->config['smb']['root'].='/'.$id;//make sure we have an new empty folder to work in + $this->instance=new OC_Filestorage_SMB($this->config['smb']); + } + + public function tearDown(){ + OCP\Files::rmdirr($this->instance->constructUrl('')); + } + } +}