versions.php 19 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408409410411412413414415416417418419420421422423424425426427428429430431432433434435436437438439440441442443444445446447448449450451452453454455456457458459460461462463464465466467468469470471472473474475476477478479480481482483484485486487488489490491492493494495496497498499500501502503504505506507508509510511512513514515516517518519520521522523524525526527528529530531532533534535536537538539540541542543544545546547548549550551552553554555556557558559560561562563564565566567568569570571572
  1. <?php
  2. /**
  3. * Copyright (c) 2012 Frank Karlitschek <frank@owncloud.org>
  4. * 2013 Bjoern Schiessle <schiessle@owncloud.com>
  5. * This file is licensed under the Affero General Public License version 3 or
  6. * later.
  7. * See the COPYING-README file.
  8. */
  9. /**
  10. * Versions
  11. *
  12. * A class to handle the versioning of files.
  13. */
  14. namespace OCA\Files_Versions;
  15. class Storage {
  16. const DEFAULTENABLED=true;
  17. const DEFAULTMAXSIZE=50; // unit: percentage; 50% of available disk space/quota
  18. private static $max_versions_per_interval = array(
  19. //first 10sec, one version every 2sec
  20. 1 => array('intervalEndsAfter' => 10, 'step' => 2),
  21. //next minute, one version every 10sec
  22. 2 => array('intervalEndsAfter' => 60, 'step' => 10),
  23. //next hour, one version every minute
  24. 3 => array('intervalEndsAfter' => 3600, 'step' => 60),
  25. //next 24h, one version every hour
  26. 4 => array('intervalEndsAfter' => 86400, 'step' => 3600),
  27. //next 30days, one version per day
  28. 5 => array('intervalEndsAfter' => 2592000, 'step' => 86400),
  29. //until the end one version per week
  30. 6 => array('intervalEndsAfter' => -1, 'step' => 604800),
  31. );
  32. public static function getUidAndFilename($filename) {
  33. $uid = \OC\Files\Filesystem::getOwner($filename);
  34. \OC\Files\Filesystem::initMountPoints($uid);
  35. if ( $uid != \OCP\User::getUser() ) {
  36. $info = \OC\Files\Filesystem::getFileInfo($filename);
  37. $ownerView = new \OC\Files\View('/'.$uid.'/files');
  38. $filename = $ownerView->getPath($info['fileid']);
  39. }
  40. return array($uid, $filename);
  41. }
  42. /**
  43. * get current size of all versions from a given user
  44. *
  45. * @param $user user who owns the versions
  46. * @return mixed versions size or false if no versions size is stored
  47. */
  48. private static function getVersionsSize($user) {
  49. $query = \OC_DB::prepare('SELECT `size` FROM `*PREFIX*files_versions` WHERE `user`=?');
  50. $result = $query->execute(array($user))->fetchAll();
  51. if ($result) {
  52. return $result[0]['size'];
  53. }
  54. return false;
  55. }
  56. /**
  57. * write to the database how much space is in use for versions
  58. *
  59. * @param $user owner of the versions
  60. * @param $size size of the versions
  61. */
  62. private static function setVersionsSize($user, $size) {
  63. if ( self::getVersionsSize($user) === false) {
  64. $query = \OC_DB::prepare('INSERT INTO `*PREFIX*files_versions` (`size`, `user`) VALUES (?, ?)');
  65. }else {
  66. $query = \OC_DB::prepare('UPDATE `*PREFIX*files_versions` SET `size`=? WHERE `user`=?');
  67. }
  68. $query->execute(array($size, $user));
  69. }
  70. /**
  71. * store a new version of a file.
  72. */
  73. public static function store($filename) {
  74. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  75. // if the file gets streamed we need to remove the .part extension
  76. // to get the right target
  77. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  78. if ($ext === 'part') {
  79. $filename = substr($filename, 0, strlen($filename)-5);
  80. }
  81. list($uid, $filename) = self::getUidAndFilename($filename);
  82. $files_view = new \OC\Files\View('/'.$uid .'/files');
  83. $users_view = new \OC\Files\View('/'.$uid);
  84. $versions_view = new \OC\Files\View('/'.$uid.'/files_versions');
  85. // check if filename is a directory
  86. if($files_view->is_dir($filename)) {
  87. return false;
  88. }
  89. // we should have a source file to work with, and the file shouldn't
  90. // be empty
  91. $fileExists = $files_view->file_exists($filename);
  92. $fileSize = $files_view->filesize($filename);
  93. if ($fileExists === false || $fileSize === 0) {
  94. return false;
  95. }
  96. // create all parent folders
  97. $info=pathinfo($filename);
  98. $versionsFolderName=$versions_view->getLocalFolder('');
  99. if(!file_exists($versionsFolderName.'/'.$info['dirname'])) {
  100. mkdir($versionsFolderName.'/'.$info['dirname'], 0750, true);
  101. }
  102. $versionsSize = self::getVersionsSize($uid);
  103. if ( $versionsSize === false || $versionsSize < 0 ) {
  104. $versionsSize = self::calculateSize($uid);
  105. }
  106. // assumption: we need filesize($filename) for the new version +
  107. // some more free space for the modified file which might be
  108. // 1.5 times as large as the current version -> 2.5
  109. $neededSpace = $files_view->filesize($filename) * 2.5;
  110. $versionsSize = self::expire($filename, $versionsSize, $neededSpace);
  111. // disable proxy to prevent multiple fopen calls
  112. $proxyStatus = \OC_FileProxy::$enabled;
  113. \OC_FileProxy::$enabled = false;
  114. // store a new version of a file
  115. $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
  116. // reset proxy state
  117. \OC_FileProxy::$enabled = $proxyStatus;
  118. $versionsSize += $users_view->filesize('files'.$filename);
  119. self::setVersionsSize($uid, $versionsSize);
  120. }
  121. }
  122. /**
  123. * Delete versions of a file
  124. */
  125. public static function delete($filename) {
  126. list($uid, $filename) = self::getUidAndFilename($filename);
  127. $versions_fileview = new \OC\Files\View('/'.$uid .'/files_versions');
  128. $abs_path = $versions_fileview->getLocalFile($filename.'.v');
  129. if( ($versions = self::getVersions($uid, $filename)) ) {
  130. $versionsSize = self::getVersionsSize($uid);
  131. if ( $versionsSize === false || $versionsSize < 0 ) {
  132. $versionsSize = self::calculateSize($uid);
  133. }
  134. foreach ($versions as $v) {
  135. unlink($abs_path . $v['version']);
  136. $versionsSize -= $v['size'];
  137. }
  138. self::setVersionsSize($uid, $versionsSize);
  139. }
  140. }
  141. /**
  142. * rename versions of a file
  143. */
  144. public static function rename($old_path, $new_path) {
  145. list($uid, $oldpath) = self::getUidAndFilename($old_path);
  146. list($uidn, $newpath) = self::getUidAndFilename($new_path);
  147. $versions_view = new \OC\Files\View('/'.$uid .'/files_versions');
  148. $files_view = new \OC\Files\View('/'.$uid .'/files');
  149. // if the file already exists than it was a upload of a existing file
  150. // over the web interface -> store() is the right function we need here
  151. if ($files_view->file_exists($newpath)) {
  152. return self::store($new_path);
  153. }
  154. self::expire($newpath);
  155. $abs_newpath = $versions_view->getLocalFile($newpath);
  156. if ( $files_view->is_dir($oldpath) && $versions_view->is_dir($oldpath) ) {
  157. $versions_view->rename($oldpath, $newpath);
  158. } else if ( ($versions = Storage::getVersions($uid, $oldpath)) ) {
  159. $info=pathinfo($abs_newpath);
  160. if(!file_exists($info['dirname'])) mkdir($info['dirname'], 0750, true);
  161. foreach ($versions as $v) {
  162. $versions_view->rename($oldpath.'.v'.$v['version'], $newpath.'.v'.$v['version']);
  163. }
  164. }
  165. }
  166. /**
  167. * rollback to an old version of a file.
  168. */
  169. public static function rollback($file, $revision) {
  170. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  171. list($uid, $filename) = self::getUidAndFilename($file);
  172. $users_view = new \OC\Files\View('/'.$uid);
  173. $files_view = new \OC\Files\View('/'.\OCP\User::getUser().'/files');
  174. $versionCreated = false;
  175. //first create a new version
  176. $version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename);
  177. if ( !$users_view->file_exists($version)) {
  178. // disable proxy to prevent multiple fopen calls
  179. $proxyStatus = \OC_FileProxy::$enabled;
  180. \OC_FileProxy::$enabled = false;
  181. $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename));
  182. // reset proxy state
  183. \OC_FileProxy::$enabled = $proxyStatus;
  184. $versionCreated = true;
  185. }
  186. // rollback
  187. if( @$users_view->rename('files_versions'.$filename.'.v'.$revision, 'files'.$filename) ) {
  188. $files_view->touch($file, $revision);
  189. Storage::expire($file);
  190. return true;
  191. }else if ( $versionCreated ) {
  192. $users_view->unlink($version);
  193. }
  194. }
  195. return false;
  196. }
  197. /**
  198. * @brief get a list of all available versions of a file in descending chronological order
  199. * @param $uid user id from the owner of the file
  200. * @param $filename file to find versions of, relative to the user files dir
  201. * @returns array
  202. */
  203. public static function getVersions($uid, $filename ) {
  204. if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) {
  205. $versions_fileview = new \OC\Files\View('/' . $uid . '/files_versions');
  206. $versionsName = $versions_fileview->getLocalFile($filename).'.v';
  207. $escapedVersionName = preg_replace('/(\*|\?|\[)/', '[$1]', $versionsName);
  208. $versions = array();
  209. // fetch for old versions
  210. $matches = glob($escapedVersionName.'*');
  211. if ( !$matches ) {
  212. return $versions;
  213. }
  214. sort( $matches );
  215. $files_view = new \OC\Files\View('/'.$uid.'/files');
  216. $local_file = $files_view->getLocalFile($filename);
  217. $local_file_md5 = \md5_file( $local_file );
  218. foreach( $matches as $ma ) {
  219. $parts = explode( '.v', $ma );
  220. $version = ( end( $parts ) );
  221. $key = $version.'#'.$filename;
  222. $versions[$key]['cur'] = 0;
  223. $versions[$key]['version'] = $version;
  224. $versions[$key]['humanReadableTimestamp'] = self::getHumanReadableTimestamp($version);
  225. $versions[$key]['path'] = $filename;
  226. $versions[$key]['size'] = $versions_fileview->filesize($filename.'.v'.$version);
  227. // if file with modified date exists, flag it in array as currently enabled version
  228. ( \md5_file( $ma ) == $local_file_md5 ? $versions[$key]['fileMatch'] = 1 : $versions[$key]['fileMatch'] = 0 );
  229. }
  230. // newest versions first
  231. $versions = array_reverse( $versions );
  232. foreach( $versions as $key => $value ) {
  233. // flag the first matched file in array (which will have latest modification date) as current version
  234. if ( $value['fileMatch'] ) {
  235. $value['cur'] = 1;
  236. break;
  237. }
  238. }
  239. return( $versions );
  240. } else {
  241. // if versioning isn't enabled then return an empty array
  242. return( array() );
  243. }
  244. }
  245. /**
  246. * @brief translate a timestamp into a string like "5 days ago"
  247. * @param int $timestamp
  248. * @return string for example "5 days ago"
  249. */
  250. private static function getHumanReadableTimestamp($timestamp) {
  251. $diff = time() - $timestamp;
  252. if ($diff < 60) { // first minute
  253. return $diff . " seconds ago";
  254. } elseif ($diff < 3600) { //first hour
  255. return round($diff / 60) . " minutes ago";
  256. } elseif ($diff < 86400) { // first day
  257. return round($diff / 3600) . " hours ago";
  258. } elseif ($diff < 604800) { //first week
  259. return round($diff / 86400) . " days ago";
  260. } elseif ($diff < 2419200) { //first month
  261. return round($diff / 604800) . " weeks ago";
  262. } elseif ($diff < 29030400) { // first year
  263. return round($diff / 2419200) . " months ago";
  264. } else {
  265. return round($diff / 29030400) . " years ago";
  266. }
  267. }
  268. /**
  269. * @brief deletes used space for files versions in db if user was deleted
  270. *
  271. * @param type $uid id of deleted user
  272. * @return result of db delete operation
  273. */
  274. public static function deleteUser($uid) {
  275. $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_versions` WHERE `user`=?');
  276. return $query->execute(array($uid));
  277. }
  278. /**
  279. * @brief get the size of all stored versions from a given user
  280. * @param $uid id from the user
  281. * @return size of vesions
  282. */
  283. private static function calculateSize($uid) {
  284. if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) {
  285. $versions_fileview = new \OC\Files\View('/'.$uid.'/files_versions');
  286. $versionsRoot = $versions_fileview->getLocalFolder('');
  287. $iterator = new \RecursiveIteratorIterator(
  288. new \RecursiveDirectoryIterator($versionsRoot),
  289. \RecursiveIteratorIterator::CHILD_FIRST
  290. );
  291. $size = 0;
  292. foreach ($iterator as $path) {
  293. if ( preg_match('/^.+\.v(\d+)$/', $path, $match) ) {
  294. $relpath = substr($path, strlen($versionsRoot)-1);
  295. $size += $versions_fileview->filesize($relpath);
  296. }
  297. }
  298. return $size;
  299. }
  300. }
  301. /**
  302. * @brief returns all stored file versions from a given user
  303. * @param $uid id to the user
  304. * @return array with contains two arrays 'all' which contains all versions sorted by age and 'by_file' which contains all versions sorted by filename
  305. */
  306. private static function getAllVersions($uid) {
  307. if( \OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true' ) {
  308. $versions_fileview = new \OC\Files\View('/'.$uid.'/files_versions');
  309. $versionsRoot = $versions_fileview->getLocalFolder('');
  310. $iterator = new \RecursiveIteratorIterator(
  311. new \RecursiveDirectoryIterator($versionsRoot),
  312. \RecursiveIteratorIterator::CHILD_FIRST
  313. );
  314. $versions = array();
  315. foreach ($iterator as $path) {
  316. if ( preg_match('/^.+\.v(\d+)$/', $path, $match) ) {
  317. $relpath = substr($path, strlen($versionsRoot)-1);
  318. $versions[$match[1].'#'.$relpath] = array('path' => $relpath, 'timestamp' => $match[1]);
  319. }
  320. }
  321. ksort($versions);
  322. $i = 0;
  323. $result = array();
  324. foreach( $versions as $key => $value ) {
  325. $i++;
  326. $size = $versions_fileview->filesize($value['path']);
  327. $filename = substr($value['path'], 0, -strlen($value['timestamp'])-2);
  328. $result['all'][$key]['version'] = $value['timestamp'];
  329. $result['all'][$key]['path'] = $filename;
  330. $result['all'][$key]['size'] = $size;
  331. $filename = substr($value['path'], 0, -strlen($value['timestamp'])-2);
  332. $result['by_file'][$filename][$key]['version'] = $value['timestamp'];
  333. $result['by_file'][$filename][$key]['path'] = $filename;
  334. $result['by_file'][$filename][$key]['size'] = $size;
  335. }
  336. return $result;
  337. }
  338. }
  339. /**
  340. * @brief Erase a file's versions which exceed the set quota
  341. */
  342. private static function expire($filename, $versionsSize = null, $offset = 0) {
  343. if(\OCP\Config::getSystemValue('files_versions', Storage::DEFAULTENABLED)=='true') {
  344. list($uid, $filename) = self::getUidAndFilename($filename);
  345. $versionsFileview = new \OC\Files\View('/'.$uid.'/files_versions');
  346. // get available disk space for user
  347. $softQuota = true;
  348. $quota = \OC_Preferences::getValue($uid, 'files', 'quota');
  349. if ( $quota === null || $quota === 'default') {
  350. $quota = \OC_Appconfig::getValue('files', 'default_quota');
  351. }
  352. if ( $quota === null || $quota === 'none' ) {
  353. $quota = \OC\Files\Filesystem::free_space('/');
  354. $softQuota = false;
  355. } else {
  356. $quota = \OCP\Util::computerFileSize($quota);
  357. }
  358. // make sure that we have the current size of the version history
  359. if ( $versionsSize === null ) {
  360. $versionsSize = self::getVersionsSize($uid);
  361. if ( $versionsSize === false || $versionsSize < 0 ) {
  362. $versionsSize = self::calculateSize($uid);
  363. }
  364. }
  365. // calculate available space for version history
  366. // subtract size of files and current versions size from quota
  367. if ($softQuota) {
  368. $files_view = new \OC\Files\View('/'.$uid.'/files');
  369. $rootInfo = $files_view->getFileInfo('/');
  370. $free = $quota-$rootInfo['size']; // remaining free space for user
  371. if ( $free > 0 ) {
  372. $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - ($versionsSize + $offset); // how much space can be used for versions
  373. } else {
  374. $availableSpace = $free - $versionsSize - $offset;
  375. }
  376. } else {
  377. $availableSpace = $quota - $offset;
  378. }
  379. // with the probability of 0.1% we reduce the number of all versions not only for the current file
  380. $random = rand(0, 1000);
  381. if ($random == 0) {
  382. $allFiles = true;
  383. } else {
  384. $allFiles = false;
  385. }
  386. $allVersions = Storage::getVersions($uid, $filename);
  387. $versionsByFile[$filename] = $allVersions;
  388. $sizeOfDeletedVersions = self::delOldVersions($versionsByFile, $allVersions, $versionsFileview);
  389. $availableSpace = $availableSpace + $sizeOfDeletedVersions;
  390. $versionsSize = $versionsSize - $sizeOfDeletedVersions;
  391. // if still not enough free space we rearrange the versions from all files
  392. if ($availableSpace <= 0 || $allFiles) {
  393. $result = Storage::getAllVersions($uid);
  394. $versionsByFile = $result['by_file'];
  395. $allVersions = $result['all'];
  396. $sizeOfDeletedVersions = self::delOldVersions($versionsByFile, $allVersions, $versionsFileview);
  397. $availableSpace = $availableSpace + $sizeOfDeletedVersions;
  398. $versionsSize = $versionsSize - $sizeOfDeletedVersions;
  399. }
  400. // Check if enough space is available after versions are rearranged.
  401. // If not we delete the oldest versions until we meet the size limit for versions,
  402. // but always keep the two latest versions
  403. $numOfVersions = count($allVersions) -2 ;
  404. $i = 0;
  405. while ($availableSpace < 0 && $i < $numOfVersions) {
  406. $version = current($allVersions);
  407. $versionsFileview->unlink($version['path'].'.v'.$version['version']);
  408. $versionsSize -= $version['size'];
  409. $availableSpace += $version['size'];
  410. next($allVersions);
  411. $i++;
  412. }
  413. return $versionsSize; // finally return the new size of the version history
  414. }
  415. return false;
  416. }
  417. /**
  418. * @brief delete old version from a given list of versions
  419. *
  420. * @param array $versionsByFile list of versions ordered by files
  421. * @param array $allVversions all versions accross multiple files
  422. * @param $versionsFileview OC\Files\View on data/user/files_versions
  423. * @return size of releted versions
  424. */
  425. private static function delOldVersions($versionsByFile, &$allVersions, $versionsFileview) {
  426. $time = time();
  427. $size = 0;
  428. // delete old versions for every given file
  429. foreach ($versionsByFile as $versions) {
  430. $versions = array_reverse($versions); // newest version first
  431. $interval = 1;
  432. $step = Storage::$max_versions_per_interval[$interval]['step'];
  433. if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] == -1) {
  434. $nextInterval = -1;
  435. } else {
  436. $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
  437. }
  438. $firstVersion = reset($versions);
  439. $firstKey = key($versions);
  440. $prevTimestamp = $firstVersion['version'];
  441. $nextVersion = $firstVersion['version'] - $step;
  442. unset($versions[$firstKey]);
  443. foreach ($versions as $key => $version) {
  444. $newInterval = true;
  445. while ($newInterval) {
  446. if ($nextInterval == -1 || $version['version'] >= $nextInterval) {
  447. if ($version['version'] > $nextVersion) {
  448. //distance between two version too small, delete version
  449. $versionsFileview->unlink($version['path'] . '.v' . $version['version']);
  450. $size += $version['size'];
  451. unset($allVersions[$key]); // update array with all versions
  452. } else {
  453. $nextVersion = $version['version'] - $step;
  454. }
  455. $newInterval = false; // version checked so we can move to the next one
  456. } else { // time to move on to the next interval
  457. $interval++;
  458. $step = Storage::$max_versions_per_interval[$interval]['step'];
  459. $nextVersion = $prevTimestamp - $step;
  460. if (Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'] == -1) {
  461. $nextInterval = -1;
  462. } else {
  463. $nextInterval = $time - Storage::$max_versions_per_interval[$interval]['intervalEndsAfter'];
  464. }
  465. $newInterval = true; // we changed the interval -> check same version with new interval
  466. }
  467. }
  468. $prevTimestamp = $version['version'];
  469. }
  470. }
  471. return $size;
  472. }
  473. }