From e71608d8ccf42a59aecde32023e98aa703235172 Mon Sep 17 00:00:00 2001 From: Ralf Becker Date: Mon, 18 Feb 2008 06:52:07 +0000 Subject: [PATCH] new sqlfs stream wrapper, as replacement for the old vfs class (it uses the PDO extension, as PDO allows to access BLOBs as streams), the update create a new egw_sqlfs table and fills it with the content of the old vfs (egw_vfs table), BOTH use the same files in the filesystem, so beware if you delete something in one or the other, this is definitly NOT for production systems --- .../inc/class.sqlfs_stream_wrapper.inc.php | 910 ++++++++++++++++++ phpgwapi/setup/setup.inc.php | 4 +- phpgwapi/setup/tables_current.inc.php | 23 + phpgwapi/setup/tables_update.inc.php | 124 +++ 4 files changed, 1060 insertions(+), 1 deletion(-) create mode 100644 phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php diff --git a/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php new file mode 100644 index 0000000000..2950689255 --- /dev/null +++ b/phpgwapi/inc/class.sqlfs_stream_wrapper.inc.php @@ -0,0 +1,910 @@ + + * @copyright (c) 2008 by Ralf Becker + * @version $Id$ + */ + +/** + * eGroupWare API: VFS - new DB based VFS stream wrapper + * + * The sqlfs stream wrapper has 3 operation modi: + * - content of files is stored in the filesystem (eGW's files_dir) (default) + * - content of files is stored as BLOB in the DB + * - content of files is versioned (and stored in the DB) NOT YET IMPLEMENTED + * In the future it will be possible to activate eg. versioning in parts of the filesystem, via mount options in the vfs + * + * I use the PDO DB interface, as it allows to access BLOB's as streams (avoiding to hold them complete in memory). + * + * The interface is according to the docu on php.net + * + * @link http://de.php.net/manual/de/function.stream-wrapper-register.php + * @ToDo compare (and optimize) performance with old vfs system (eg. via webdav) + * @ToDo pass $operation parameter via context from vfs stream-wrappers mount table, to eg. allow to mount parts with(out) versioning + * @ToDo versioning + */ +class sqlfs_stream_wrapper implements iface_stream_wrapper +{ + /** + * If this class should do the operations direct in the filesystem, instead of going through the vfs + */ + const USE_FILESYSTEM_DIRECT = true; + /** + * Mime type of directories, the old vfs uses 'Directory', while eg. WebDAV uses 'httpd/unix-directory' + */ + const DIR_MIME_TYPE = 'httpd/unix-directory'; + /** + * Scheme / protocoll used for this stream-wrapper + */ + const SCHEME = 'sqlfs'; + /** + * Does url_stat returns a mime type, or has it to be determined otherwise (string with attribute name) + */ + const STAT_RETURN_MIME_TYPE = 'mime'; + /** + * Our tablename + */ + const TABLE = 'egw_sqlfs'; + /** + * mode-bits, which have to be set for files + */ + const MODE_FILE = 0100000; + /** + * mode-bits, which have to be set for directories + */ + const MODE_DIR = 040000; + /** + * How much should be logged to the apache error-log + * + * 0 = Nothing + * 1 = only errors + * 2 = all function calls and errors (contains passwords too!) + */ + const LOG_LEVEL = 1; + + /** + * We do versioning AND store the content in the db, NOT YET IMPLEMENTED + */ + const VERSIONING = 0; + /** + * We store the content in the DB (no versioning) + */ + const STORE2DB = 1; + /** + * We store the content in the filesystem (egw_info/server/files_dir) (no versioning) + */ + const STORE2FS = 2; + /** + * default for operation, change that if you want to test with STORE2DB atm + */ + const DEFAULT_OPERATION = 2; + + var $operation = self::DEFAULT_OPERATION; + + /** + * optional context param when opening the stream, null if no context passed + * + * @var mixed + */ + var $context; + + /** + * Path off the file opened by stream_open + * + * @var string + */ + protected $opened_path; + /** + * Mode of the file opened by stream_open + * + * @var int + */ + protected $opened_mode; + /** + * Stream of the opened file, either from the DB via PDO or the filesystem + * + * @var resource + */ + protected $opened_stream; + /** + * fs_id of opened file + * + * @var int + */ + protected $opened_fs_id; + /** + * Directory vfs::ls() of dir opened with dir_opendir() + * + * This static var gets overwritten by each new dir_opendir, it helps to not read the entries twice. + * + * @var array $path => info-array pairs + */ + static private $stat_cache; + /** + * Reference to the PDO object we use + * + * @var PDO + */ + static private $pdo; + /** + * Array with filenames of dir opened with dir_opendir + * + * @var array + */ + protected $opened_dir; + + /** + * This method is called immediately after your stream object is created. + * + * @param string $url URL that was passed to fopen() and that this object is expected to retrieve + * @param string $mode mode used to open the file, as detailed for fopen() + * @param int $options additional flags set by the streams API (or'ed together): + * - STREAM_USE_PATH If path is relative, search for the resource using the include_path. + * - STREAM_REPORT_ERRORS If this flag is set, you are responsible for raising errors using trigger_error() during opening of the stream. + * If this flag is not set, you should not raise any errors. + * @param string $opened_path full path of the file/resource, if the open was successfull and STREAM_USE_PATH was set + * @return boolean true if the ressource was opened successful, otherwise false + */ + function stream_open ( $url, $mode, $options, &$opened_path ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); + + $path = parse_url($url,PHP_URL_PATH); + + $this->opened_path = $path; + $this->opened_mode = $mode; + $this->opened_stream = null; + + $stat = self::url_stat($url,0); + + if (!($stat = self::url_stat($path,0)) || $mode[0] == 'x') // file not found or file should NOT exist + { + if ($mode[0] == 'r' || // does $mode require the file to exist (r,r+) + $mode[0] == 'x' || // or file should not exist, but does + !egw_vfs::check_access(($dir_stat = self::url_stat(dirname($path),0)),egw_vfs::WRITABLE)) // or we are not allowed to create it + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file does not exist or can not be created!"); + if (!($options & STREAM_URL_STAT_QUIET)) + { + trigger_error(__METHOD__."($url,$mode,$options) file does not exist or can not be created!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + // new file --> create it in the DB + if ($this->operation == self::STORE2FS) + { + $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator)'); + } + else + { + $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_created,fs_modified,fs_creator,fs_content'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_created,:fs_modified,:fs_creator,:fs_content)'); + $stmt->bindParam(':fs_content',$this->open_stream,PDO::PARAM_LOB); + } + $values = array( + 'fs_name' => basename($path), + 'fs_dir' => $dir_stat['ino'], + 'fs_mode' => $dir_stat['mode'] & 0666, + 'fs_uid' => $dir_stat['uid'], + 'fs_gid' => $dir_stat['gid'], + 'fs_created' => self::_pdo_timestamp(time()), + 'fs_modified' => self::_pdo_timestamp(time()), + 'fs_creator' => $GLOBALS['egw_info']['user']['account_id'], + ); + foreach($values as $name => &$val) + { + $stmt->bindParam(':'.$name,$val); + } + $stmt->execute(); + $this->opened_fs_id = self::$pdo->lastInsertId('fs_id'); + } + else + { + if ($mode != 'r' && !egw_vfs::check_access($stat,egw_vfs::WRITABLE)) // we are not allowed to edit it + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$mode,$options) file can not be edited!"); + if (!($options & STREAM_URL_STAT_QUIET)) + { + trigger_error(__METHOD__."($url,$mode,$options) file can not be edited!",E_USER_WARNING); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = null; + return false; + } + $this->opened_fs_id = $stat['fs_id']; + } + // do we operate directly on the filesystem + if ($this->operation == self::STORE2FS) + { + $this->opened_stream = fopen(self::_fs_path($path),$mode); + } + if ($mode[0] == 'a') // append modes: a, a+ + { + $this->stream_seek(0,SEEK_END); + } + return is_resource($this->opened_stream); + } + + /** + * This method is called when the stream is closed, using fclose(). + * + * You must release any resources that were locked or allocated by the stream. + */ + function stream_close ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_null($this->opened_path) || !is_resource($this->opened_stream)) + { + return false; + } + + if ($this->opened_mode != 'r') + { + $this->stream_seek(0,SEEK_END); + + static $mime_magic; + if (is_null($mime_magic)) + { + $mime_magic = new mime_magic(); + } + + // we need to update the mime-type and size + $values = array( + ':fs_size' => $this->stream_tell(), + // todo: analyse the file for the mime-type + ':fs_mime' => $mime_magic->filename2mime($this->opened_path), + ':fs_id' => $this->opened_fs_id, + ); + $ret = fclose($this->opened_stream); + + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_size=:fs_size,fs_mime=:fs_mime WHERE fs_id=:fs_id'); + $stmt->execute($values); + } + else + { + $ret = fclose($this->opened_stream); + } + $this->opened_stream = $this->opened_path = $this->opened_mode = $this->opend_fs_id = null; + + return $ret; + } + + /** + * This method is called in response to fread() and fgets() calls on the stream. + * + * You must return up-to count bytes of data from the current read/write position as a string. + * If there are less than count bytes available, return as many as are available. + * If no more data is available, return either FALSE or an empty string. + * You must also update the read/write position of the stream by the number of bytes that were successfully read. + * + * @param int $count + * @return string/false up to count bytes read or false on EOF + */ + function stream_read ( $count ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($count) pos=$this->opened_pos"); + + if (is_resource($this->opened_stream)) + { + return fread($this->opened_stream,$count); + } + return false; + } + + /** + * This method is called in response to fwrite() calls on the stream. + * + * You should store data into the underlying storage used by your stream. + * If there is not enough room, try to store as many bytes as possible. + * You should return the number of bytes that were successfully stored in the stream, or 0 if none could be stored. + * You must also update the read/write position of the stream by the number of bytes that were successfully written. + * + * @param string $data + * @return integer + */ + function stream_write ( $data ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($data)"); + + if (is_resource($this->opened_stream)) + { + return fwrite($this->opened_stream,$data); + } + return false; + } + + /** + * This method is called in response to feof() calls on the stream. + * + * Important: PHP 5.0 introduced a bug that wasn't fixed until 5.1: the return value has to be the oposite! + * + * if(version_compare(PHP_VERSION,'5.0','>=') && version_compare(PHP_VERSION,'5.1','<')) + * { + * $eof = !$eof; + * } + * + * @return boolean true if the read/write position is at the end of the stream and no more data availible, false otherwise + */ + function stream_eof ( ) + { + if (is_resource($this->opened_stream)) + { + return feof($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to ftell() calls on the stream. + * + * @return integer current read/write position of the stream + */ + function stream_tell ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_resource($this->opened_stream)) + { + return ftell($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to fseek() calls on the stream. + * + * You should update the read/write position of the stream according to offset and whence. + * See fseek() for more information about these parameters. + * + * @param integer $offset + * @param integer $whence SEEK_SET - Set position equal to offset bytes + * SEEK_CUR - Set position to current location plus offset. + * SEEK_END - Set position to end-of-file plus offset. (To move to a position before the end-of-file, you need to pass a negative value in offset.) + * @return boolean TRUE if the position was updated, FALSE otherwise. + */ + function stream_seek ( $offset, $whence ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($offset,$whence)"); + + if (is_resource($this->opened_stream)) + { + return fseek($this->opened_stream,$offset,$whence); + } + return false; + } + + /** + * This method is called in response to fflush() calls on the stream. + * + * If you have cached data in your stream but not yet stored it into the underlying storage, you should do so now. + * + * @return booelan TRUE if the cached data was successfully stored (or if there was no data to store), or FALSE if the data could not be stored. + */ + function stream_flush ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."()"); + + if (is_resource($this->opened_stream)) + { + return fflush($this->opened_stream); + } + return false; + } + + /** + * This method is called in response to fstat() calls on the stream. + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @return array containing the same values as appropriate for the stream. + */ + function stream_stat ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($this->opened_path)"); + + return $this->url_stat($this->opened_path,0); + } + + /** + * This method is called in response to unlink() calls on URL paths associated with the wrapper. + * + * It should attempt to delete the item specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support unlinking! + * + * @param string $url + * @return boolean TRUE on success or FALSE on failure + */ + static function unlink ( $url,$operation=self::DEFAULT_OPERATION ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); + + $path = parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,0)) || !($dir_stat = self::url_stat(dirname($path),0)) || + !egw_vfs::check_access($dir_stat,egw_vfs::WRITABLE)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url) (type=$type) permission denied!"); + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=:fs_id'); + unset(self::$stat_cache[$path]); + + if (($ret = $stmt->execute(array(':fs_id' => $stat['ino']))) && $operation == self::STORE2FS) + { + unlink(self::_fs_path($path)); + } + return $ret; + } + + /** + * This method is called in response to rename() calls on URL paths associated with the wrapper. + * + * It should attempt to rename the item specified by path_from to the specification given by path_to. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support renaming. + * + * The regular filesystem stream-wrapper returns an error, if $url_from and $url_to are not either both files or both dirs! + * + * @param string $url_from + * @param string $url_to + * @return boolean TRUE on success or FALSE on failure + */ + static function rename ( $url_from, $url_to, $operation=self::DEFAULT_OPERATION ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url_from,$url_to)"); + + $path_from = parse_url($url_from,PHP_URL_PATH); + $path_to = parse_url($url_to,PHP_URL_PATH); + + if (!($from_stat = self::url_stat($path_from,0)) || + !($from_dir_stat = self::url_stat(dirname($path_from),0)) || !egw_vfs::check_access($from_dir_stat,egw_vfs::WRITABLE)) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_from permission denied!"); + return false; // no permission or file does not exist + } + if (!($to_dir_stat = self::url_stat(dirname($path_to),0)) || !egw_vfs::check_access($to_dir_stat,egw_vfs::WRITABLE)) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + if (self::LOG_LEVEL) error_log(__METHOD__."($url_from,$url_to): $path_to permission denied!"); + return false; // no permission or parent-dir does not exist + } + // the filesystem stream-wrapper does NOT allow to rename files to directories, as this makes problems + // for our vfs too, we abort here with an error, like the filesystem one does + if (($to_stat = self::url_stat($path_to,0)) && + ($to_stat['mime'] === self::DIR_MIME_TYPE) !== ($from_stat['mime'] === self::DIR_MIME_TYPE)) + { + self::_remove_password($url_from); + self::_remove_password($url_to); + $is_dir = $to_stat['mime'] === self::DIR_MIME_TYPE ? 'a' : 'no'; + if (self::LOG_LEVEL) error_log(__METHOD__."($url_to,$url_from) $path_to is $is_dir directory!"); + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_dir=:fs_dir,fs_name=:fs_name WHERE fs_id=:fs_id'); + if (($ret = $stmt->execute(array( + ':fs_dir' => $to_dir_stat['ino'], + ':fs_name' => basename($path_to), + ':fs_id' => $from_stat['ino'], + ))) && $operation == self::STORE2FS) + { + rename(self::_fs_path($path_from),self::_fs_path($path_to)); + } + unset(self::$stat_cache[$path_from]); + + return $ret; + } + + /** + * This method is called in response to mkdir() calls on URL paths associated with the wrapper. + * + * It should attempt to create the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support creating directories. + * + * @param string $url + * @param int $mode + * @param int $options Posible values include STREAM_REPORT_ERRORS and STREAM_MKDIR_RECURSIVE + * @return boolean TRUE on success or FALSE on failure + */ + static function mkdir ( $url, $mode, $options, $operation=self::DEFAULT_OPERATION ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url,$mode,$options)"); + + $path = parse_url($url,PHP_URL_PATH); + + $parent = self::url_stat(dirname($path),0); + + // check if we should also create all non-existing path components and our parent does not exist, + // if yes call ourself recursive with the parent directory + if (($options & STREAM_MKDIR_RECURSIVE) && $path != '/' && !$parent) + { + if (!self::mkdir(dirname($path),$mode,$options)) + { + return false; + } + $parent = self::url_stat(dirname($path),0); + } + if (!$parent || !egw_vfs::check_access($parent,egw_vfs::WRITABLE)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url) permission denied!"); + if (!($options & STREAM_URL_STAT_QUIET)) + { + trigger_error(__METHOD__."('$url',$mode,$options) permission denied!",E_USER_WARNING); + } + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('INSERT INTO '.self::TABLE.' (fs_name,fs_dir,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified,fs_creator'. + ') VALUES (:fs_name,:fs_dir,:fs_mode,:fs_uid,:fs_gid,:fs_size,:fs_mime,:fs_created,:fs_modified,:fs_creator)'); + if (($ret = $stmt->execute(array( + ':fs_name' => basename($path), + ':fs_dir' => $parent['ino'], + ':fs_mode' => $parent['mode'], + ':fs_uid' => $parent['uid'], + ':fs_gid' => $parent['gid'], + ':fs_size' => 0, + ':fs_mime' => self::DIR_MIME_TYPE, + ':fs_created' => self::_pdo_timestamp(time()), + ':fs_modified' => self::_pdo_timestamp(time()), + ':fs_creator' => $GLOBALS['egw_info']['user']['account_id'], + ))) && $operation == self::STORE2FS) + { + mkdir(self::_fs_path($path)); + } + return $ret; + } + + /** + * This method is called in response to rmdir() calls on URL paths associated with the wrapper. + * + * It should attempt to remove the directory specified by path. + * In order for the appropriate error message to be returned, do not define this method if your wrapper does not support removing directories. + * + * @param string $url + * @param int $options Possible values include STREAM_REPORT_ERRORS. + * @return boolean TRUE on success or FALSE on failure. + */ + static function rmdir ( $url, $options, $operation=self::DEFAULT_OPERATION ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); + + $path = parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,0)) || $stat['mime'] != self::DIR_MIME_TYPE || + !($parent = self::url_stat(dirname($path),0)) || !egw_vfs::check_access($parent,egw_vfs::WRITABLE)) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) (type=$type) permission denied!"); + if (!($options & STREAM_URL_STAT_QUIET)) + { + trigger_error(__METHOD__."('$url',$options) (type=$type) permission denied!",E_USER_WARNING); + } + return false; // no permission or file does not exist + } + $stmt = self::$pdo->prepare('SELECT COUNT(*) FROM '.self::TABLE.' WHERE fs_dir=?'); + $stmt->execute(array($stat['ino'])); + if ($stmt->fetchColumn()) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."($url,$options) dir is not empty!"); + if (!($options & STREAM_URL_STAT_QUIET)) + { + trigger_error(__METHOD__."('$url',$options) dir is not empty!",E_USER_WARNING); + } + return false; + } + unset(self::$stat_cache[$path]); + + $stmt = self::$pdo->prepare('DELETE FROM '.self::TABLE.' WHERE fs_id=?'); + if (($ret = $stmt->execute(array($stat['ino']))) && $operation == self::STORE2FS) + { + rmdir(self::_fs_path($path)); + } + return $ret; + } + + /** + * This is not (yet) a stream-wrapper function, but it's necessary and can be used static + * + * @param string $url + * @param int $time=null modification time (unix timestamp), default null = current time + * @param int $atime=null access time (unix timestamp), default null = current time, not implemented in the vfs! + */ + static function touch($url,$time=null,$atime=null) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."($url)"); + + $path = parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($path,0))) + { + return false; + } + $stmt = self::$pdo->prepare('UPDATE '.self::TABLE.' SET fs_modified=:fs_modified WHERE fs_id=:fs_id'); + + return $stmt->execute(array( + ':fs_modified' => self::_pdo_timestamp($time ? $time : time()), + ':fs_id' => $stat['ino'], + )); + } + + /** + * This method is called immediately when your stream object is created for examining directory contents with opendir(). + * + * @param string $path URL that was passed to opendir() and that this object is expected to explore. + * @param $options + * @return booelan + */ + function dir_opendir ( $url, $options ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$options)"); + + $this->opened_dir = null; + + $path = parse_url($url,PHP_URL_PATH); + + if (!($stat = self::url_stat($url,0)) || // dir not found + $stat['mime'] != self::DIR_MIME_TYPE || // no dir + !egw_vfs::check_access($stat,egw_vfs::EXECUTABLE)) // no access + { + self::_remove_password($url); + $msg = $stat['mime'] != self::DIR_MIME_TYPE ? "$url is no directory" : 'permission denied'; + if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$options) $msg!"); + $this->opened_dir = null; + return false; + } + self::$stat_cache = $this->opened_dir = array(); + $stmt = self::$pdo->prepare('SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified FROM '.self::TABLE.' WHERE fs_dir=?'); + $stmt->setFetchMode(PDO::FETCH_ASSOC); + if ($stmt->execute(array($stat['ino']))) + { + foreach($stmt as $file) + { + $this->opened_dir[] = $file['fs_name']; + self::$stat_cache[$path.'/'.$file['fs_name']] = $file; + } + } + //print_r($this->opened_dir); + reset($this->opened_dir); + + return true; + } + + /** + * This method is called in response to stat() calls on the URL paths associated with the wrapper. + * + * It should return as many elements in common with the system function as possible. + * Unknown or unavailable values should be set to a rational value (usually 0). + * + * If you plan to use your wrapper in a require_once you need to define stream_stat(). + * If you plan to allow any other tests like is_file()/is_dir(), you have to define url_stat(). + * stream_stat() must define the size of the file, or it will never be included. + * url_stat() must define mode, or is_file()/is_dir()/is_executable(), and any of those functions affected by clearstatcache() simply won't work. + * It's not documented, but directories must be a mode like 040777 (octal), and files a mode like 0100666. + * If you wish the file to be executable, use 7s instead of 6s. + * The last 3 digits are exactly the same thing as what you pass to chmod. + * 040000 defines a directory, and 0100000 defines a file. + * + * @param string $path + * @param int $flags holds additional flags set by the streams API. It can hold one or more of the following values OR'd together: + * - STREAM_URL_STAT_LINK For resources with the ability to link to other resource (such as an HTTP Location: forward, + * or a filesystem symlink). This flag specified that only information about the link itself should be returned, + * not the resource pointed to by the link. + * This flag is set in response to calls to lstat(), is_link(), or filetype(). + * - STREAM_URL_STAT_QUIET If this flag is set, your wrapper should not raise any errors. If this flag is not set, + * you are responsible for reporting errors using the trigger_error() function during stating of the path. + * stat triggers it's own warning anyway, so it makes no sense to trigger one by our stream-wrapper! + * @return array + */ + static function url_stat ( $url, $flags ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."('$url',$flags)"); + + $path = parse_url($url,PHP_URL_PATH); + + // webdav adds a trailing slash to dirs, which causes url_stat to NOT find the file otherwise + if ($path != '/' && substr($path,-1) == '/') + { + $path = substr($path,0,-1); + } + // check if we already have the info from the last dir_open call, as the old vfs reads it anyway from the db + if (self::$stat_cache && isset(self::$stat_cache[$path])) + { + return self::_vfsinfo2stat(self::$stat_cache[$path]); + } + + if (!is_object(self::$pdo)) + { + self::_pdo(); + } + $base_query = 'SELECT fs_id,fs_name,fs_mode,fs_uid,fs_gid,fs_size,fs_mime,fs_created,fs_modified FROM '.self::TABLE.' WHERE fs_name=? AND fs_dir='; + $parts = explode('/',$path); + foreach($parts as $n => $name) + { + if ($n == 0) + { + $query = 1; // / always has fs_id == 1, no need to query it + } + elseif ($n < count($parts)-1) + { + $query = 'SELECT fs_id FROM '.self::TABLE.' WHERE fs_dir=('.$query.') AND fs_name='.self::$pdo->quote($name); + } + else + { + $query = str_replace('fs_name=?','fs_name='.self::$pdo->quote($name),$base_query).'('.$query.')'; + } + } + //echo "query=$query\n"; + + if (!($result = self::$pdo->query($query)) || !($info = $result->fetch(PDO::FETCH_ASSOC))) + { + self::_remove_password($url); + if (self::LOG_LEVEL) error_log(__METHOD__."('$url',$flags) file or directory not found!"); + return false; + } + self::$stat_cache[$path] = $info; + + return self::_vfsinfo2stat($info); + } + + /** + * This method is called in response to readdir(). + * + * It should return a string representing the next filename in the location opened by dir_opendir(). + * + * @return string + */ + function dir_readdir ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); + + if (!is_array($this->opened_dir)) return false; + + $file = current($this->opened_dir); next($this->opened_dir); + + return $file ? $file : false; + } + + /** + * This method is called in response to rewinddir(). + * + * It should reset the output generated by dir_readdir(). i.e.: + * The next call to dir_readdir() should return the first entry in the location returned by dir_opendir(). + * + * @return boolean + */ + function dir_rewinddir ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); + + if (!is_array($this->opened_dir)) return false; + + reset($this->opened_dir); + + return true; + } + + /** + * This method is called in response to closedir(). + * + * You should release any resources which were locked or allocated during the opening and use of the directory stream. + * + * @return boolean + */ + function dir_closedir ( ) + { + if (self::LOG_LEVEL > 1) error_log(__METHOD__."( )"); + + if (!is_array($this->opened_dir)) return false; + + $this->opened_dir = null; + + return true; + } + + /** + * Convert a sqlfs-file-info into a stat array + * + * @param array $info + * @return array + */ + static private function _vfsinfo2stat($info) + { + $stat = array( + 'ino' => $info['fs_id'], + 'name' => $info['fs_name'], + 'mode' => $info['fs_mode'] | + ($info['fs_mime'] == self::DIR_MIME_TYPE ? self::MODE_DIR : self::MODE_FILE), // required by the stream wrapper + 'size' => $info['fs_size'], + 'uid' => $info['fs_uid'], + 'gid' => $info['fs_gid'], + 'mtime' => strtotime($info['fs_modified']), + 'ctime' => strtotime($info['fs_created']), + 'nlink' => $info['fs_mime'] == self::DIR_MIME_TYPE ? 2 : 1, + // eGW addition to return the mime type + 'mime' => $info['fs_mime'], + ); + //error_log(__METHOD__."($info[name]) = ".print_r($stat,true)); + return $stat; + } + + /** + * Create pdo object / connection, as long as pdo is not generally used in eGW + * + * @return PDO + */ + static private function _pdo() + { + $server =& $GLOBALS['egw_info']['server']; + + switch($server['db_type']) + { + default: + $dsn = $server['db_type'].':host='.$server['db_host'].';dbname='.$server['db_name']; + break; + } + return self::$pdo = new PDO($dsn,$server['db_user'],$server['db_pass']); + } + + /** + * Just a little abstration 'til I know how to organise stuff like that with PDO + * + * @param mixed $time + * @return string Y-m-d H:i:s + */ + static private function _pdo_timestamp($time) + { + if (is_numeric($time)) + { + $time = date('Y-m-d H:i:s',$time); + } + return $time; + } + + /** + * Return the path of the stored content of a file if $this->operation == self::STORE2FS + * + * @param string $path + * @return string + */ + static private function _fs_path($path) + { + return $GLOBALS['egw_info']['server']['files_dir'].$path; + } + + /** + * Replace the password of an url with '...' for error messages + * + * @param string &$url + */ + static private function _remove_password(&$url) + { + $parts = parse_url($url); + + if ($parts['pass'] || $parts['scheme']) + { + $url = $parts['scheme'].'://'.($parts['user'] ? $parts['user'].($parts['pass']?':...':'').'@' : ''). + $parts['host'].$parts['path']; + } + } +} + +stream_register_wrapper(sqlfs_stream_wrapper::SCHEME ,'sqlfs_stream_wrapper'); diff --git a/phpgwapi/setup/setup.inc.php b/phpgwapi/setup/setup.inc.php index 2060371695..d81c984eb2 100755 --- a/phpgwapi/setup/setup.inc.php +++ b/phpgwapi/setup/setup.inc.php @@ -14,7 +14,7 @@ /* Basic information about this app */ $setup_info['phpgwapi']['name'] = 'phpgwapi'; $setup_info['phpgwapi']['title'] = 'eGroupWare API'; - $setup_info['phpgwapi']['version'] = '1.5.002'; + $setup_info['phpgwapi']['version'] = '1.5.004'; $setup_info['phpgwapi']['versions']['current_header'] = '1.28'; $setup_info['phpgwapi']['enable'] = 3; $setup_info['phpgwapi']['app_order'] = 1; @@ -53,6 +53,7 @@ $setup_info['phpgwapi']['tables'][] = 'egw_addressbook_extra'; $setup_info['phpgwapi']['tables'][] = 'egw_addressbook_lists'; $setup_info['phpgwapi']['tables'][] = 'egw_addressbook2list'; + $setup_info['phpgwapi']['tables'][] = 'egw_sqlfs'; // hooks used by vfs_home to manage user- and group-directories $setup_info['phpgwapi']['hooks']['addaccount'] = 'phpgwapi.vfs_home.addAccount'; @@ -73,3 +74,4 @@ + diff --git a/phpgwapi/setup/tables_current.inc.php b/phpgwapi/setup/tables_current.inc.php index 3fce8f85f1..c5a347b89b 100644 --- a/phpgwapi/setup/tables_current.inc.php +++ b/phpgwapi/setup/tables_current.inc.php @@ -548,5 +548,28 @@ 'fk' => array(), 'ix' => array(), 'uc' => array() + ), + 'egw_sqlfs' => array( + 'fd' => array( + 'fs_id' => array('type' => 'auto','nullable' => False), + 'fs_dir' => array('type' => 'int','precision' => '4','nullable' => False), + 'fs_name' => array('type' => 'varchar','precision' => '200','nullable' => False), + 'fs_mode' => array('type' => 'int','precision' => '2','nullable' => False), + 'fs_uid' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0'), + 'fs_gid' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0'), + 'fs_created' => array('type' => 'timestamp','precision' => '8','nullable' => False,'default' => 'current_timestamp'), + 'fs_modified' => array('type' => 'timestamp','precision' => '8','nullable' => False), + 'fs_mime' => array('type' => 'varchar','precision' => '64','nullable' => False), + 'fs_size' => array('type' => 'int','precision' => '8','nullable' => False), + 'fs_creator' => array('type' => 'int','precision' => '4','nullable' => False), + 'fs_modifier' => array('type' => 'int','precision' => '4'), + 'fs_active' => array('type' => 'bool','nullable' => False,'default' => 't'), + 'fs_comment' => array('type' => 'varchar','precision' => '255'), + 'fs_content' => array('type' => 'blob') + ), + 'pk' => array('fs_id'), + 'fk' => array(), + 'ix' => array(array('fs_dir','fs_active','fs_name')), + 'uc' => array() ) ); diff --git a/phpgwapi/setup/tables_update.inc.php b/phpgwapi/setup/tables_update.inc.php index d15754deba..b1278f4ae0 100644 --- a/phpgwapi/setup/tables_update.inc.php +++ b/phpgwapi/setup/tables_update.inc.php @@ -54,4 +54,128 @@ return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '1.5.002'; } + $test[] = '1.5.002'; + function phpgwapi_upgrade1_5_002() + { + $GLOBALS['egw_setup']->oProc->CreateTable('egw_sqlfs',array( + 'fd' => array( + 'fs_id' => array('type' => 'auto','nullable' => False), + 'fs_dir' => array('type' => 'int','precision' => '4','nullable' => False), + 'fs_name' => array('type' => 'varchar','precision' => '200','nullable' => False), + 'fs_mode' => array('type' => 'int','precision' => '2','nullable' => False), + 'fs_uid' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0'), + 'fs_gid' => array('type' => 'int','precision' => '4','nullable' => False,'default' => '0'), + 'fs_created' => array('type' => 'timestamp','precision' => '8','nullable' => False,'default' => 'current_timestamp'), + 'fs_modified' => array('type' => 'timestamp','precision' => '8','nullable' => False), + 'fs_mime' => array('type' => 'varchar','precision' => '64','nullable' => False), + 'fs_size' => array('type' => 'int','precision' => '8','nullable' => False), + 'fs_creator' => array('type' => 'int','precision' => '4','nullable' => False), + 'fs_modifier' => array('type' => 'int','precision' => '4'), + 'fs_active' => array('type' => 'bool','nullable' => False,'default' => 't'), + 'fs_comment' => array('type' => 'varchar','precision' => '255'), + 'fs_content' => array('type' => 'blob') + ), + 'pk' => array('fs_id'), + 'fk' => array(), + 'ix' => array(array('fs_dir','fs_active','fs_name')), + 'uc' => array() + )); + return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '1.5.003'; + } + + $test[] = '1.5.003'; + function phpgwapi_upgrade1_5_003() + { + // import the current egw_vfs into egw_sqlfs + // ToDo: moving /infolog and /infolog/$app to /apps in the files dir!!! + $debug = true; + + $query = $GLOBALS['egw_setup']->db->select('egw_vfs','*',"vfs_mime_type != 'journal' AND vfs_mime_type != 'journal-deleted'",__LINE__,__FILE__,false,'ORDER BY length(vfs_directory) ASC','phpgwapi'); + if ($debug) echo "rows=
\n";
+
+		$dirs = array();
+		foreach($query as $row)
+		{
+			// rename the /infolog dir to /apps/infolog and /infolog/$app /apps/$app
+			if (substr($row['vfs_directory'],0,8) == '/infolog')
+			{
+				$parts = explode('/',$row['vfs_directory']);	// 0 = '', 1 = 'infolog', 2 = app or info_id
+				//$parts[1] = is_numeric($parts[2]) ? 'apps/infolog' : 'apps';
+				$parts[1] = $row['vfs_directory']=='/infolog' && is_numeric($row['vfs_name']) ? 'apps/infolog' : 'apps';
+				$row['vfs_directory'] = implode('/',$parts);
+			}
+			$nrow = array(
+				'fs_dir'  => $dirs[$row['vfs_directory']],
+				'fs_name' => $row['vfs_name'],
+				'fs_mode' => $row['vfs_owner_id'] > 0 ? 
+					($row['vfs_mime_type'] == 'Directory' ? 0700 : 0600) :
+					($row['vfs_mime_type'] == 'Directory' ? 0070 : 0060),
+				'fs_uid' => $row['vfs_owner_id'] > 0 ? $row['vfs_owner_id'] : 0,
+				'fs_gid' => $row['vfs_owner_id'] < 0 ? -$row['vfs_owner_id'] : 0,
+				'fs_created' => $row['vfs_created'],
+				'fs_modified' => $row['vfs_modified'] ? $row['vfs_modified'] : $row['vfs_created'],
+				'fs_mime' => $row['vfs_mime_type'] == 'Directory' ? 'httpd/unix-directory' : 
+					($row['vfs_mime_type'] ? $row['vfs_mime_type'] : 'application/octet-stream'),
+				'fs_size' => $row['vfs_size'],
+				'fs_creator' => $row['vfs_createdby_id'],
+				'fs_modifier' => $row['vfs_modifedby_id'],
+				'fs_comment' => $row['vfs_comment'] ? $row['vfs_comment'] : null,
+				'fs_content' => $row['vfs_content'],
+			);
+			// rename the /infolog dir to /apps/infolog (create /apps)
+			if ($nrow['fs_dir'] == 1 && $nrow['fs_name'] == 'infolog')
+			{
+				$nrow['fs_name'] = 'apps';
+				$GLOBALS['egw_setup']->db->insert('egw_sqlfs',$nrow,false,__LINE__,__FILE__,'phpgwapi');
+				$dir = '/apps';
+				$nrow['fs_dir'] = $dirs[$dir] = $GLOBALS['egw_setup']->db->get_last_insert_id('egw_sqlfs','fs_id');
+				if ($debug) echo "$dir = {$dirs[$dir]}\n";
+				$nrow['fs_name'] = 'infolog';
+				$row['vfs_directory'] = '/apps';
+			}
+			if ($debug)
+			{
+				foreach($row as $key => $val)
+				{
+					if (is_numeric($key)) unset($row[$key]);
+				}
+				print_r($row);
+				print_r($nrow);
+			}
+			if ($row['vfs_mime_type'] == 'Directory')
+			{
+				$dir = ($row['vfs_directory'] == '/' ? '' : $row['vfs_directory']).'/'.$row['vfs_name'];
+
+				if (!isset($dirs[$dir]))	// ignoring doublicate dirs, my devel box has somehow many of them specially /home
+				{
+					// fix some common perms
+					if(in_array($dir,array('/','/home')))
+					{
+						$nrow['fs_mode'] = 05;	// everyone (other rights) can read the / and /home
+						$nrow['uid'] = $nrow['gid'] = 0;	// owner root != any eGW user
+					}
+					$GLOBALS['egw_setup']->db->insert('egw_sqlfs',$nrow,false,__LINE__,__FILE__,'phpgwapi');
+					$dirs[$dir] = $GLOBALS['egw_setup']->db->get_last_insert_id('egw_sqlfs','fs_id');
+					if ($debug) echo "$dir = {$dirs[$dir]}\n";
+				}
+				elseif ($debug)
+				{
+					echo "ignoring doublicate directory '$dir'!\n";
+				}
+			}
+			else
+			{
+				$GLOBALS['egw_setup']->db->insert('egw_sqlfs',$nrow,false,__LINE__,__FILE__,'phpgwapi');
+			}
+
+		}
+		if ($debug)
+		{
+			echo "dirs=";
+			print_r($dirs);
+			echo "
\n"; + } + return $GLOBALS['setup_info']['phpgwapi']['currentver'] = '1.5.004'; + } +?>