versions.php 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390
  1. <?php
  2. /**
  3. * Copyright (c) 2012 Frank Karlitschek <frank@owncloud.org>
  4. * This file is licensed under the Affero General Public License version 3 or
  5. * later.
  6. * See the COPYING-README file.
  7. */
  8. /**
  9. * Versions
  10. *
  11. * A class to handle the versioning of files.
  12. */
  13. namespace OCA_Versions;
  14. class Storage {
  15. // config.php configuration:
  16. // - files_versions
  17. // - files_versionsfolder
  18. // - files_versionsblacklist
  19. // - files_versionsmaxfilesize
  20. // - files_versionsinterval
  21. // - files_versionmaxversions
  22. //
  23. // todo:
  24. // - port to oc_filesystem to enable network transparency
  25. // - implement expire all function. And find a place to call it ;-)
  26. // - add transparent compression. first test if it´s worth it.
  27. const DEFAULTENABLED=true;
  28. const DEFAULTFOLDER='versions';
  29. const DEFAULTBLACKLIST='avi mp3 mpg mp4 ctmp';
  30. const DEFAULTMAXFILESIZE=1048576; // 10MB
  31. const DEFAULTMININTERVAL=1; // 2 min
  32. const DEFAULTMAXVERSIONS=50;
  33. /**
  34. * init the versioning and create the versions folder.
  35. */
  36. public static function init() {
  37. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  38. // create versions folder
  39. $foldername=\OCP\Config::getSystemValue('datadirectory').'/'. \OCP\USER::getUser() .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  40. if(!is_dir($foldername)){
  41. mkdir($foldername);
  42. }
  43. }
  44. }
  45. /**
  46. * listen to write event.
  47. */
  48. public static function write_hook($params) {
  49. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  50. $path = $params[\OC_Filesystem::signal_param_path];
  51. if($path<>'') Storage::store($path);
  52. }
  53. }
  54. /**
  55. * store a new version of a file.
  56. */
  57. public static function store($filename) {
  58. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  59. if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {
  60. $pos = strpos($source, '/files', 1);
  61. $uid = substr($source, 1, $pos - 1);
  62. $filename = substr($source, $pos + 6);
  63. } else {
  64. $uid = \OCP\User::getUser();
  65. }
  66. $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  67. $filesfoldername=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/files';
  68. Storage::init();
  69. // check if filename is a directory
  70. if(is_dir($filesfoldername.'/'.$filename)){
  71. return false;
  72. }
  73. // check filetype blacklist
  74. $blacklist=explode(' ',\OCP\Config::getSystemValue('files_versionsblacklist', Storage::DEFAULTBLACKLIST));
  75. foreach($blacklist as $bl) {
  76. $parts=explode('.', $filename);
  77. $ext=end($parts);
  78. if(strtolower($ext)==$bl) {
  79. return false;
  80. }
  81. }
  82. // check filesize
  83. if(filesize($filesfoldername.'/'.$filename)>\OCP\Config::getSystemValue('files_versionsmaxfilesize', Storage::DEFAULTMAXFILESIZE)){
  84. return false;
  85. }
  86. // check mininterval if the file is being modified by the owner (all shared files should be versioned despite mininterval)
  87. if ($uid == \OCP\User::getUser()) {
  88. $matches=glob($versionsFolderName.'/'.$filename.'.v*');
  89. sort($matches);
  90. $parts=explode('.v',end($matches));
  91. if((end($parts)+Storage::DEFAULTMININTERVAL)>time()){
  92. return false;
  93. }
  94. }
  95. // create all parent folders
  96. $info=pathinfo($filename);
  97. if(!file_exists($versionsFolderName.'/'.$info['dirname'])) mkdir($versionsFolderName.'/'.$info['dirname'],0700,true);
  98. // store a new version of a file
  99. copy($filesfoldername.'/'.$filename,$versionsFolderName.'/'.$filename.'.v'.time());
  100. // expire old revisions if necessary
  101. Storage::expire($filename);
  102. }
  103. }
  104. /**
  105. * rollback to an old version of a file.
  106. */
  107. public static function rollback($filename,$revision) {
  108. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  109. if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {
  110. $pos = strpos($source, '/files', 1);
  111. $uid = substr($source, 1, $pos - 1);
  112. $filename = substr($source, $pos + 6);
  113. } else {
  114. $uid = \OCP\User::getUser();
  115. }
  116. $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'.$uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  117. $filesfoldername=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/files';
  118. // rollback
  119. if ( @copy($versionsFolderName.'/'.$filename.'.v'.$revision,$filesfoldername.'/'.$filename) ) {
  120. return true;
  121. }else{
  122. return false;
  123. }
  124. }
  125. }
  126. /**
  127. * check if old versions of a file exist.
  128. */
  129. public static function isversioned($filename) {
  130. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  131. if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {
  132. $pos = strpos($source, '/files', 1);
  133. $uid = substr($source, 1, $pos - 1);
  134. $filename = substr($source, $pos + 6);
  135. } else {
  136. $uid = \OCP\User::getUser();
  137. }
  138. $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  139. // check for old versions
  140. $matches=glob($versionsFolderName.'/'.$filename.'.v*');
  141. if(count($matches)>1){
  142. return true;
  143. }else{
  144. return false;
  145. }
  146. }else{
  147. return(false);
  148. }
  149. }
  150. /**
  151. * @brief get a list of all available versions of a file in descending chronological order
  152. * @param $filename file to find versions of, relative to the user files dir
  153. * @param $count number of versions to return
  154. * @returns array
  155. */
  156. public static function getVersions( $filename, $count = 0 ) {
  157. if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) {
  158. if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {
  159. $pos = strpos($source, '/files', 1);
  160. $uid = substr($source, 1, $pos - 1);
  161. $filename = substr($source, $pos + 6);
  162. } else {
  163. $uid = \OCP\User::getUser();
  164. }
  165. $versionsFolderName = \OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  166. $versions = array();
  167. // fetch for old versions
  168. $matches = glob( $versionsFolderName.'/'.$filename.'.v*' );
  169. sort( $matches );
  170. $i = 0;
  171. foreach( $matches as $ma ) {
  172. $i++;
  173. $versions[$i]['cur'] = 0;
  174. $parts = explode( '.v', $ma );
  175. $versions[$i]['version'] = ( end( $parts ) );
  176. // if file with modified date exists, flag it in array as currently enabled version
  177. $curFile['fileName'] = basename( $parts[0] );
  178. $curFile['filePath'] = \OCP\Config::getSystemValue('datadirectory').\OC_Filesystem::getRoot().'/'.$curFile['fileName'];
  179. ( \md5_file( $ma ) == \md5_file( $curFile['filePath'] ) ? $versions[$i]['fileMatch'] = 1 : $versions[$i]['fileMatch'] = 0 );
  180. }
  181. $versions = array_reverse( $versions );
  182. foreach( $versions as $key => $value ) {
  183. // flag the first matched file in array (which will have latest modification date) as current version
  184. if ( $versions[$key]['fileMatch'] ) {
  185. $versions[$key]['cur'] = 1;
  186. break;
  187. }
  188. }
  189. $versions = array_reverse( $versions );
  190. // only show the newest commits
  191. if( $count != 0 and ( count( $versions )>$count ) ) {
  192. $versions = array_slice( $versions, count( $versions ) - $count );
  193. }
  194. return( $versions );
  195. } else {
  196. // if versioning isn't enabled then return an empty array
  197. return( array() );
  198. }
  199. }
  200. /**
  201. * expire old versions of a file.
  202. */
  203. public static function expire($filename) {
  204. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  205. if (\OCP\App::isEnabled('files_sharing') && $source = \OC_Share::getSource('/'.\OCP\User::getUser().'/files'.$filename)) {
  206. $pos = strpos($source, '/files', 1);
  207. $uid = substr($source, 1, $pos - 1);
  208. $filename = substr($source, $pos + 6);
  209. } else {
  210. $uid = \OCP\User::getUser();
  211. }
  212. $versionsFolderName=\OCP\Config::getSystemValue('datadirectory').'/'. $uid .'/'.\OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  213. // check for old versions
  214. $matches = glob( $versionsFolderName.'/'.$filename.'.v*' );
  215. if( count( $matches ) > \OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ) ) {
  216. $numberToDelete = count( $matches-\OCP\Config::getSystemValue( 'files_versionmaxversions', Storage::DEFAULTMAXVERSIONS ) );
  217. // delete old versions of a file
  218. $deleteItems = array_slice( $matches, 0, $numberToDelete );
  219. foreach( $deleteItems as $de ) {
  220. unlink( $versionsFolderName.'/'.$filename.'.v'.$de );
  221. }
  222. }
  223. }
  224. }
  225. /**
  226. * @brief erase all old versions of all user files
  227. * @return
  228. */
  229. public static function expireAll() {
  230. function deleteAll( $directory, $empty = false ) {
  231. // strip leading slash
  232. if( substr( $directory, 0, 1 ) == "/" ) {
  233. $directory = substr( $directory, 1 );
  234. }
  235. // strip trailing slash
  236. if( substr( $directory, -1) == "/" ) {
  237. $directory = substr( $directory, 0, -1 );
  238. }
  239. $view = new \OC_FilesystemView('');
  240. if ( !$view->file_exists( $directory ) || !$view->is_dir( $directory ) ) {
  241. return false;
  242. } elseif( !$view->is_readable( $directory ) ) {
  243. return false;
  244. } else {
  245. $foldername = \OCP\Config::getSystemValue('datadirectory') .'/' . \OCP\USER::getUser() .'/' . $directory; // have to set an absolute path for use with PHP's opendir as OC version doesn't work
  246. $directoryHandle = $view->opendir( \OCP\USER::getUser() . '/' . $directory );
  247. while ( $contents = readdir( $directoryHandle ) ) {
  248. if ( $contents != '.' && $contents != '..') {
  249. $path = $directory . "/" . $contents;
  250. if ( $view->is_dir( $path ) ) {
  251. deleteAll( $path );
  252. } else {
  253. $view->unlink( \OCP\USER::getUser() .'/' . $path ); // TODO: make unlink use same system path as is_dir
  254. }
  255. }
  256. }
  257. //$view->closedir( $directoryHandle ); // TODO: implement closedir in OC_FSV
  258. if ( $empty == false ) {
  259. if ( !$view->rmdir( $directory ) ) {
  260. return false;
  261. }
  262. }
  263. return true;
  264. }
  265. }
  266. $dir = \OCP\Config::getSystemValue('files_versionsfolder', Storage::DEFAULTFOLDER);
  267. if ( deleteAll( $dir, true ) ) {
  268. return true;
  269. } else {
  270. return false;
  271. }
  272. }
  273. }