--- /dev/null
- if(is_dir($filesfoldername.'/'.$filename)){\r
+<?php\r
+/**\r
+ * Copyright (c) 2012 Frank Karlitschek <frank@owncloud.org>\r
+ * This file is licensed under the Affero General Public License version 3 or\r
+ * later.\r
+ * See the COPYING-README file.\r
+ */\r
+\r
+/**\r
+ * Versions\r
+ *\r
+ * A class to handle the versioning of files.\r
+ */\r
+\r
+namespace OCA_Versions;\r
+\r
+class Storage {\r
+\r
+\r
+ // config.php configuration:\r
+ // - files_versions\r
+ // - files_versionsfolder\r
+ // - files_versionsblacklist\r
+ // - files_versionsmaxfilesize\r
+ // - files_versionsinterval \r
+ // - files_versionmaxversions \r
+ //\r
+ // todo:\r
+ // - finish porting to OC_FilesystemView to enable network transparency\r
+ // - add transparent compression. first test if it´s worth it.\r
+\r
+ const DEFAULTENABLED=true; \r
+ const DEFAULTFOLDER='versions'; \r
+ const DEFAULTBLACKLIST='avi mp3 mpg mp4 ctmp'; \r
+ const DEFAULTMAXFILESIZE=1048576; // 10MB \r
+ const DEFAULTMININTERVAL=1; // 2 min\r
+ const DEFAULTMAXVERSIONS=50;\r
+ \r
+ private $view;\r
+ \r
+ function __construct( $view ) {\r
+ \r
+ $this->view = $view;\r
+ \r
+ }\r
+\r
+ /**\r
+ * init the versioning and create the versions folder.\r
+ */\r
+ public static function init() {\r
+ if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {\r
+ // create versions folder\r
+ $foldername=\OCP\Config::getSystemValue('datadirectory').'/'. \OCP\USER::getUser() .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+ if(!is_dir($foldername)){\r
+ mkdir($foldername);\r
+ }\r
+ }\r
+ }\r
+\r
+\r
+ /**\r
+ * listen to write event.\r
+ */\r
+ public static function write_hook($params) {\r
+ if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {\r
+ $path = $params[\OC_Filesystem::signal_param_path];\r
+ if($path<>'') $this->store($path);\r
+ }\r
+ }\r
+\r
+\r
+\r
+ /**\r
+ * store a new version of a file.\r
+ */\r
+ public function store($filename) {\r
+ if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {\r
++ \r
++ $files_view = \OCP\Files::getStorage("files");\r
++ $users_view = \OCP\Files::getStorage("files_versions");\r
++ $users_view->chroot(\OCP\User::getUser().'/');\r
++ \r
+ if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {\r
+ $pos = strpos($source, '/files', 1);\r
+ $uid = substr($source, 1, $pos - 1);\r
+ $filename = substr($source, $pos + 6);\r
+ } else {\r
+ $uid = \OCP\User::getUser();\r
+ }\r
+ $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+ $filesfoldername=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/files';\r
+ Storage::init();\r
+\r
++ //check if source file already exist as version to avoid recursions.\r
++ if ($users_view->file_exists($filename)) {\r
++ return false;\r
++ }\r
++ \r
+ // check if filename is a directory\r
- if(filesize($filesfoldername.'/'.$filename)>\OCP\Config::getSystemValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)){\r
++ if($files_view->is_dir($filename)){\r
+ return false;\r
+ }\r
+\r
+ // check filetype blacklist\r
+ $blacklist=explode(' ',\OCP\Config::getSystemValue('files_versionsblacklist', Storage::DEFAULTBLACKLIST));\r
+ foreach($blacklist as $bl) {\r
+ $parts=explode('.', $filename);\r
+ $ext=end($parts);\r
+ if(strtolower($ext)==$bl) {\r
+ return false;\r
+ }\r
+ }\r
+ \r
+ // check filesize\r
- copy($filesfoldername.'/'.$filename,$versionsFolderName.'/'.$filename.'.v'.time());\r
++ if($files_view->filesize($filename)>\OCP\Config::getSystemValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)){\r
+ return false;\r
+ }\r
+\r
+\r
+ // check mininterval if the file is being modified by the owner (all shared files should be versioned despite mininterval)\r
+ if ($uid == \OCP\User::getUser()) {\r
+ $matches=glob($versionsFolderName.'/'.$filename.'.v*');\r
+ sort($matches);\r
+ $parts=explode('.v',end($matches));\r
+ if((end($parts)+Storage::DEFAULTMININTERVAL)>time()){\r
+ return false;\r
+ }\r
+ }\r
+\r
+\r
+ // create all parent folders\r
+ $info=pathinfo($filename); \r
+ if(!file_exists($versionsFolderName.'/'.$info['dirname'])) mkdir($versionsFolderName.'/'.$info['dirname'],0700,true); \r
+\r
+ // store a new version of a file\r
- $this->expire($filename);\r
++ @$users_view->copy('files'.$filename, 'versions'.$filename.'.v'.time());\r
+ \r
+ // expire old revisions if necessary\r
- public function rollback( $filename, $revision ) {\r
++ Storage::expire($filename);\r
+ }\r
+ }\r
+\r
+\r
+ /**\r
+ * rollback to an old version of a file.\r
+ */\r
- if ( copy( $versionsFolderName.'/'.$filename.'.v'.$revision, $filesfoldername.'/'.$filename ) ) {\r
++ public static function rollback($filename,$revision) {\r
+ \r
+ if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {\r
+ if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {\r
+ $pos = strpos($source, '/files', 1);\r
+ $uid = substr($source, 1, $pos - 1);\r
+ $filename = substr($source, $pos + 6);\r
+ } else {\r
+ $uid = \OCP\User::getUser();\r
+ }\r
+ $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'.$uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+ \r
+ $filesfoldername=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/files';\r
+ \r
+ // rollback\r
- } else {\r
++ if ( @copy($versionsFolderName.'/'.$filename.'.v'.$revision,$filesfoldername.'/'.$filename) ) {\r
+ \r
+ return true;\r
+ \r
- public function expire( $filename ) {\r
++ }else{\r
+ \r
+ return false;\r
+ \r
+ }\r
+ \r
+ }\r
+ \r
+ }\r
+\r
+ /**\r
+ * check if old versions of a file exist.\r
+ */\r
+ public static function isversioned($filename) {\r
+ if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {\r
+ if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {\r
+ $pos = strpos($source, '/files', 1);\r
+ $uid = substr($source, 1, $pos - 1);\r
+ $filename = substr($source, $pos + 6);\r
+ } else {\r
+ $uid = \OCP\User::getUser();\r
+ }\r
+ $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+\r
+ // check for old versions\r
+ $matches=glob($versionsFolderName.'/'.$filename.'.v*');\r
+ if(count($matches)>1){\r
+ return true;\r
+ }else{\r
+ return false;\r
+ }\r
+ }else{\r
+ return(false);\r
+ }\r
+ }\r
+\r
+\r
+ \r
+ /**\r
+ * @brief get a list of all available versions of a file in descending chronological order\r
+ * @param $filename file to find versions of, relative to the user files dir\r
+ * @param $count number of versions to return\r
+ * @returns array\r
+ */\r
+ public static function getVersions( $filename, $count = 0 ) {\r
+ \r
+ if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) {\r
+ \r
+ if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {\r
+ $pos = strpos($source, '/files', 1);\r
+ $uid = substr($source, 1, $pos - 1);\r
+ $filename = substr($source, $pos + 6);\r
+ } else {\r
+ $uid = \OCP\User::getUser();\r
+ }\r
+ $versionsFolderName = \OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+ $versions = array(); \r
+ \r
+ // fetch for old versions\r
+ $matches = glob( $versionsFolderName.'/'.$filename.'.v*' );\r
+ \r
+ sort( $matches );\r
+ \r
+ $i = 0;\r
+ \r
+ foreach( $matches as $ma ) {\r
+ \r
+ $i++;\r
+ $versions[$i]['cur'] = 0;\r
+ $parts = explode( '.v', $ma );\r
+ $versions[$i]['version'] = ( end( $parts ) );\r
+ \r
+ // if file with modified date exists, flag it in array as currently enabled version\r
+ $curFile['fileName'] = basename( $parts[0] );\r
+ $curFile['filePath'] = \OCP\Config::getSystemValue('datadirectory').\OC_Filesystem::getRoot().'/'.$curFile['fileName'];\r
+ \r
+ ( \md5_file( $ma ) == \md5_file( $curFile['filePath'] ) ? $versions[$i]['fileMatch'] = 1 : $versions[$i]['fileMatch'] = 0 );\r
+ \r
+ }\r
+ \r
+ $versions = array_reverse( $versions );\r
+ \r
+ foreach( $versions as $key => $value ) {\r
+ \r
+ // flag the first matched file in array (which will have latest modification date) as current version\r
+ if ( $versions[$key]['fileMatch'] ) {\r
+ \r
+ $versions[$key]['cur'] = 1;\r
+ break;\r
+ \r
+ }\r
+ \r
+ }\r
+ \r
+ $versions = array_reverse( $versions );\r
+ \r
+ // only show the newest commits\r
+ if( $count != 0 and ( count( $versions )>$count ) ) {\r
+ \r
+ $versions = array_slice( $versions, count( $versions ) - $count );\r
+ \r
+ }\r
+ \r
+ return( $versions );\r
+\r
+\r
+ } else {\r
+ \r
+ // if versioning isn't enabled then return an empty array\r
+ return( array() );\r
+ \r
+ }\r
+ \r
+ }\r
+\r
+ /**\r
+ * @brief Erase a file's versions which exceed the set quota\r
+ */\r
- $this->view->unlink( $versionsFolderName.'/'.$filename.'.v'.$de );\r
++ public static function expire($filename) {\r
+ if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {\r
+\r
+ if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {\r
+ $pos = strpos($source, '/files', 1);\r
+ $uid = substr($source, 1, $pos - 1);\r
+ $filename = substr($source, $pos + 6);\r
+ } else {\r
+ $uid = \OCP\User::getUser();\r
+ }\r
+ $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+\r
+ // check for old versions\r
+ $matches = glob( $versionsFolderName.'/'.$filename.'.v*' );\r
+ \r
+ if( count( $matches ) > \OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ) ) {\r
+ \r
+ $numberToDelete = count( $matches-\OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ) );\r
+\r
+ // delete old versions of a file\r
+ $deleteItems = array_slice( $matches, 0, $numberToDelete );\r
+ \r
+ foreach( $deleteItems as $de ) {\r
+ \r
- $this->view->unlink( $abs_path . $v['version'] );\r
++ unlink( $versionsFolderName.'/'.$filename.'.v'.$de );\r
+ \r
+ }\r
+ }\r
+ }\r
+ }\r
+\r
+ /**\r
+ * @brief Erase all old versions of all user files\r
+ * @return true/false\r
+ */\r
+ public function expireAll() {\r
+ \r
+ $dir = \OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);\r
+ \r
+ return $this->view->deleteAll( $dir, true );\r
+ \r
+ }\r
+\r
+ /**\r
+ * @brief Erase versions of deleted file\r
+ * @param array\r
+ * \r
+ * This function is connected to the delete signal of OC_Filesystem\r
+ * cleanup the versions directory if the actual file gets deleted\r
+ */\r
+ public static function removeVersions($params) {\r
+ $rel_path = $params['path'];\r
+ $abs_path = \OCP\Config::getSystemValue('datadirectory').'/'.\OCP\User::getUser()."/versions".$rel_path.'.v';\r
+ if(Storage::isversioned($rel_path)) {\r
+ $versions = Storage::getVersions($rel_path);\r
+ foreach ($versions as $v){\r
++ unlink($abs_path . $v['version']);\r
+ }\r
+ }\r
+ }\r
+ \r
+ /**\r
+ * @brief rename/move versions of renamed/moved files\r
+ * @param array with oldpath and newpath\r
+ * \r
+ * This function is connected to the rename signal of OC_Filesystem and adjust the name and location\r
+ * of the stored versions along the actual file\r
+ */\r
+ public static function renameVersions($params) {\r
+ $rel_oldpath = $params['oldpath'];\r
+ $abs_oldpath = \OCP\Config::getSystemValue('datadirectory').'/'.\OCP\User::getUser()."/versions".$rel_oldpath.'.v';\r
+ $abs_newpath = \OCP\Config::getSystemValue('datadirectory').'/'.\OCP\User::getUser()."/versions".$params['newpath'].'.v';\r
+ if(Storage::isversioned($rel_oldpath)) {\r
+ $versions = Storage::getVersions($rel_oldpath);\r
+ foreach ($versions as $v){\r
+ rename($abs_oldpath.$v['version'], $abs_newpath.$v['version']);\r
+ }\r
+ }\r
+ }\r
+}\r