You can not select more than 25 topics Topics must start with a letter or number, can include dashes ('-') and can be up to 35 characters long.

Trashbin.php 33KB

8 years ago
8 years ago
8 years ago
11 years ago
7 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
11 years ago
1234567891011121314151617181920212223242526272829303132333435363738394041424344454647484950515253545556575859606162636465666768697071727374757677787980818283848586878889909192939495969798991001011021031041051061071081091101111121131141151161171181191201211221231241251261271281291301311321331341351361371381391401411421431441451461471481491501511521531541551561571581591601611621631641651661671681691701711721731741751761771781791801811821831841851861871881891901911921931941951961971981992002012022032042052062072082092102112122132142152162172182192202212222232242252262272282292302312322332342352362372382392402412422432442452462472482492502512522532542552562572582592602612622632642652662672682692702712722732742752762772782792802812822832842852862872882892902912922932942952962972982993003013023033043053063073083093103113123133143153163173183193203213223233243253263273283293303313323333343353363373383393403413423433443453463473483493503513523533543553563573583593603613623633643653663673683693703713723733743753763773783793803813823833843853863873883893903913923933943953963973983994004014024034044054064074084094104114124134144154164174184194204214224234244254264274284294304314324334344354364374384394404414424434444454464474484494504514524534544554564574584594604614624634644654664674684694704714724734744754764774784794804814824834844854864874884894904914924934944954964974984995005015025035045055065075085095105115125135145155165175185195205215225235245255265275285295305315325335345355365375385395405415425435445455465475485495505515525535545555565575585595605615625635645655665675685695705715725735745755765775785795805815825835845855865875885895905915925935945955965975985996006016026036046056066076086096106116126136146156166176186196206216226236246256266276286296306316326336346356366376386396406416426436446456466476486496506516526536546556566576586596606616626636646656666676686696706716726736746756766776786796806816826836846856866876886896906916926936946956966976986997007017027037047057067077087097107117127137147157167177187197207217227237247257267277287297307317327337347357367377387397407417427437447457467477487497507517527537547557567577587597607617627637647657667677687697707717727737747757767777787797807817827837847857867877887897907917927937947957967977987998008018028038048058068078088098108118128138148158168178188198208218228238248258268278288298308318328338348358368378388398408418428438448458468478488498508518528538548558568578588598608618628638648658668678688698708718728738748758768778788798808818828838848858868878888898908918928938948958968978988999009019029039049059069079089099109119129139149159169179189199209219229239249259269279289299309319329339349359369379389399409419429439449459469479489499509519529539549559569579589599609619629639649659669679689699709719729739749759769779789799809819829839849859869879889899909919929939949959969979989991000100110021003100410051006100710081009101010111012101310141015101610171018101910201021102210231024102510261027102810291030103110321033103410351036103710381039104010411042104310441045104610471048
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bart Visscher <bartv@thisnet.nl>
  6. * @author Bastien Ho <bastienho@urbancube.fr>
  7. * @author Bjoern Schiessle <bjoern@schiessle.org>
  8. * @author Björn Schießle <bjoern@schiessle.org>
  9. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  10. * @author Florin Peter <github@florin-peter.de>
  11. * @author Georg Ehrke <oc.list@georgehrke.com>
  12. * @author Joas Schilling <coding@schilljs.com>
  13. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  14. * @author Juan Pablo Villafáñez <jvillafanez@solidgear.es>
  15. * @author Lars Knickrehm <mail@lars-sh.de>
  16. * @author Lukas Reschke <lukas@statuscode.ch>
  17. * @author Morris Jobke <hey@morrisjobke.de>
  18. * @author Qingping Hou <dave2008713@gmail.com>
  19. * @author Robin Appelman <robin@icewind.nl>
  20. * @author Robin McCorkell <robin@mccorkell.me.uk>
  21. * @author Roeland Jago Douma <roeland@famdouma.nl>
  22. * @author Sjors van der Pluijm <sjors@desjors.nl>
  23. * @author Steven Bühner <buehner@me.com>
  24. * @author Thomas Müller <thomas.mueller@tmit.eu>
  25. * @author Victor Dubiniuk <dubiniuk@owncloud.com>
  26. * @author Vincent Petry <pvince81@owncloud.com>
  27. *
  28. * @license AGPL-3.0
  29. *
  30. * This code is free software: you can redistribute it and/or modify
  31. * it under the terms of the GNU Affero General Public License, version 3,
  32. * as published by the Free Software Foundation.
  33. *
  34. * This program is distributed in the hope that it will be useful,
  35. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  36. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  37. * GNU Affero General Public License for more details.
  38. *
  39. * You should have received a copy of the GNU Affero General Public License, version 3,
  40. * along with this program. If not, see <http://www.gnu.org/licenses/>
  41. *
  42. */
  43. namespace OCA\Files_Trashbin;
  44. use OC\Files\Filesystem;
  45. use OC\Files\ObjectStore\ObjectStoreStorage;
  46. use OC\Files\View;
  47. use OCA\Files_Trashbin\AppInfo\Application;
  48. use OCA\Files_Trashbin\Command\Expire;
  49. use OCP\AppFramework\Utility\ITimeFactory;
  50. use OCP\Files\File;
  51. use OCP\Files\Folder;
  52. use OCP\Files\NotFoundException;
  53. use OCP\Files\NotPermittedException;
  54. use OCP\Lock\ILockingProvider;
  55. use OCP\Lock\LockedException;
  56. use OCP\User;
  57. class Trashbin {
  58. // unit: percentage; 50% of available disk space/quota
  59. public const DEFAULTMAXSIZE = 50;
  60. /**
  61. * Whether versions have already be rescanned during this PHP request
  62. *
  63. * @var bool
  64. */
  65. private static $scannedVersions = false;
  66. /**
  67. * Ensure we don't need to scan the file during the move to trash
  68. * by triggering the scan in the pre-hook
  69. *
  70. * @param array $params
  71. */
  72. public static function ensureFileScannedHook($params) {
  73. try {
  74. self::getUidAndFilename($params['path']);
  75. } catch (NotFoundException $e) {
  76. // nothing to scan for non existing files
  77. }
  78. }
  79. /**
  80. * get the UID of the owner of the file and the path to the file relative to
  81. * owners files folder
  82. *
  83. * @param string $filename
  84. * @return array
  85. * @throws \OC\User\NoUserException
  86. */
  87. public static function getUidAndFilename($filename) {
  88. $uid = Filesystem::getOwner($filename);
  89. $userManager = \OC::$server->getUserManager();
  90. // if the user with the UID doesn't exists, e.g. because the UID points
  91. // to a remote user with a federated cloud ID we use the current logged-in
  92. // user. We need a valid local user to move the file to the right trash bin
  93. if (!$userManager->userExists($uid)) {
  94. $uid = User::getUser();
  95. }
  96. if (!$uid) {
  97. // no owner, usually because of share link from ext storage
  98. return [null, null];
  99. }
  100. Filesystem::initMountPoints($uid);
  101. if ($uid !== User::getUser()) {
  102. $info = Filesystem::getFileInfo($filename);
  103. $ownerView = new View('/' . $uid . '/files');
  104. try {
  105. $filename = $ownerView->getPath($info['fileid']);
  106. } catch (NotFoundException $e) {
  107. $filename = null;
  108. }
  109. }
  110. return [$uid, $filename];
  111. }
  112. /**
  113. * get original location of files for user
  114. *
  115. * @param string $user
  116. * @return array (filename => array (timestamp => original location))
  117. */
  118. public static function getLocations($user) {
  119. $query = \OC_DB::prepare('SELECT `id`, `timestamp`, `location`'
  120. . ' FROM `*PREFIX*files_trash` WHERE `user`=?');
  121. $result = $query->execute([$user]);
  122. $array = [];
  123. while ($row = $result->fetchRow()) {
  124. if (isset($array[$row['id']])) {
  125. $array[$row['id']][$row['timestamp']] = $row['location'];
  126. } else {
  127. $array[$row['id']] = [$row['timestamp'] => $row['location']];
  128. }
  129. }
  130. return $array;
  131. }
  132. /**
  133. * get original location of file
  134. *
  135. * @param string $user
  136. * @param string $filename
  137. * @param string $timestamp
  138. * @return string original location
  139. */
  140. public static function getLocation($user, $filename, $timestamp) {
  141. $query = \OC_DB::prepare('SELECT `location` FROM `*PREFIX*files_trash`'
  142. . ' WHERE `user`=? AND `id`=? AND `timestamp`=?');
  143. $result = $query->execute([$user, $filename, $timestamp])->fetchAll();
  144. if (isset($result[0]['location'])) {
  145. return $result[0]['location'];
  146. } else {
  147. return false;
  148. }
  149. }
  150. private static function setUpTrash($user) {
  151. $view = new View('/' . $user);
  152. if (!$view->is_dir('files_trashbin')) {
  153. $view->mkdir('files_trashbin');
  154. }
  155. if (!$view->is_dir('files_trashbin/files')) {
  156. $view->mkdir('files_trashbin/files');
  157. }
  158. if (!$view->is_dir('files_trashbin/versions')) {
  159. $view->mkdir('files_trashbin/versions');
  160. }
  161. if (!$view->is_dir('files_trashbin/keys')) {
  162. $view->mkdir('files_trashbin/keys');
  163. }
  164. }
  165. /**
  166. * copy file to owners trash
  167. *
  168. * @param string $sourcePath
  169. * @param string $owner
  170. * @param string $targetPath
  171. * @param $user
  172. * @param integer $timestamp
  173. */
  174. private static function copyFilesToUser($sourcePath, $owner, $targetPath, $user, $timestamp) {
  175. self::setUpTrash($owner);
  176. $targetFilename = basename($targetPath);
  177. $targetLocation = dirname($targetPath);
  178. $sourceFilename = basename($sourcePath);
  179. $view = new View('/');
  180. $target = $user . '/files_trashbin/files/' . $targetFilename . '.d' . $timestamp;
  181. $source = $owner . '/files_trashbin/files/' . $sourceFilename . '.d' . $timestamp;
  182. $free = $view->free_space($target);
  183. $isUnknownOrUnlimitedFreeSpace = $free < 0;
  184. $isEnoughFreeSpaceLeft = $view->filesize($source) < $free;
  185. if ($isUnknownOrUnlimitedFreeSpace || $isEnoughFreeSpaceLeft) {
  186. self::copy_recursive($source, $target, $view);
  187. }
  188. if ($view->file_exists($target)) {
  189. $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
  190. $result = $query->execute([$targetFilename, $timestamp, $targetLocation, $user]);
  191. if (!$result) {
  192. \OC::$server->getLogger()->error('trash bin database couldn\'t be updated for the files owner', ['app' => 'files_trashbin']);
  193. }
  194. }
  195. }
  196. /**
  197. * move file to the trash bin
  198. *
  199. * @param string $file_path path to the deleted file/directory relative to the files root directory
  200. * @param bool $ownerOnly delete for owner only (if file gets moved out of a shared folder)
  201. *
  202. * @return bool
  203. */
  204. public static function move2trash($file_path, $ownerOnly = false) {
  205. // get the user for which the filesystem is setup
  206. $root = Filesystem::getRoot();
  207. [, $user] = explode('/', $root);
  208. [$owner, $ownerPath] = self::getUidAndFilename($file_path);
  209. // if no owner found (ex: ext storage + share link), will use the current user's trashbin then
  210. if (is_null($owner)) {
  211. $owner = $user;
  212. $ownerPath = $file_path;
  213. }
  214. $ownerView = new View('/' . $owner);
  215. // file has been deleted in between
  216. if (is_null($ownerPath) || $ownerPath === '' || !$ownerView->file_exists('/files/' . $ownerPath)) {
  217. return true;
  218. }
  219. self::setUpTrash($user);
  220. if ($owner !== $user) {
  221. // also setup for owner
  222. self::setUpTrash($owner);
  223. }
  224. $path_parts = pathinfo($ownerPath);
  225. $filename = $path_parts['basename'];
  226. $location = $path_parts['dirname'];
  227. /** @var ITimeFactory $timeFactory */
  228. $timeFactory = \OC::$server->query(ITimeFactory::class);
  229. $timestamp = $timeFactory->getTime();
  230. $lockingProvider = \OC::$server->getLockingProvider();
  231. // disable proxy to prevent recursive calls
  232. $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
  233. $gotLock = false;
  234. while (!$gotLock) {
  235. try {
  236. /** @var \OC\Files\Storage\Storage $trashStorage */
  237. [$trashStorage, $trashInternalPath] = $ownerView->resolvePath($trashPath);
  238. $trashStorage->acquireLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
  239. $gotLock = true;
  240. } catch (LockedException $e) {
  241. // a file with the same name is being deleted concurrently
  242. // nudge the timestamp a bit to resolve the conflict
  243. $timestamp = $timestamp + 1;
  244. $trashPath = '/files_trashbin/files/' . $filename . '.d' . $timestamp;
  245. }
  246. }
  247. /** @var \OC\Files\Storage\Storage $sourceStorage */
  248. [$sourceStorage, $sourceInternalPath] = $ownerView->resolvePath('/files/' . $ownerPath);
  249. if ($trashStorage->file_exists($trashInternalPath)) {
  250. $trashStorage->unlink($trashInternalPath);
  251. }
  252. $connection = \OC::$server->getDatabaseConnection();
  253. $connection->beginTransaction();
  254. $trashStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
  255. try {
  256. $moveSuccessful = true;
  257. // when moving within the same object store, the cache update done above is enough to move the file
  258. if (!($trashStorage->instanceOfStorage(ObjectStoreStorage::class) && $trashStorage->getId() === $sourceStorage->getId())) {
  259. $trashStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $trashInternalPath);
  260. }
  261. } catch (\OCA\Files_Trashbin\Exceptions\CopyRecursiveException $e) {
  262. $moveSuccessful = false;
  263. if ($trashStorage->file_exists($trashInternalPath)) {
  264. $trashStorage->unlink($trashInternalPath);
  265. }
  266. \OC::$server->getLogger()->error('Couldn\'t move ' . $file_path . ' to the trash bin', ['app' => 'files_trashbin']);
  267. }
  268. if ($sourceStorage->file_exists($sourceInternalPath)) { // failed to delete the original file, abort
  269. if ($sourceStorage->is_dir($sourceInternalPath)) {
  270. $sourceStorage->rmdir($sourceInternalPath);
  271. } else {
  272. $sourceStorage->unlink($sourceInternalPath);
  273. }
  274. $connection->rollBack();
  275. return false;
  276. }
  277. $connection->commit();
  278. if ($moveSuccessful) {
  279. $query = \OC_DB::prepare("INSERT INTO `*PREFIX*files_trash` (`id`,`timestamp`,`location`,`user`) VALUES (?,?,?,?)");
  280. $result = $query->execute([$filename, $timestamp, $location, $owner]);
  281. if (!$result) {
  282. \OC::$server->getLogger()->error('trash bin database couldn\'t be updated', ['app' => 'files_trashbin']);
  283. }
  284. \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_moveToTrash', ['filePath' => Filesystem::normalizePath($file_path),
  285. 'trashPath' => Filesystem::normalizePath($filename . '.d' . $timestamp)]);
  286. self::retainVersions($filename, $owner, $ownerPath, $timestamp);
  287. // if owner !== user we need to also add a copy to the users trash
  288. if ($user !== $owner && $ownerOnly === false) {
  289. self::copyFilesToUser($ownerPath, $owner, $file_path, $user, $timestamp);
  290. }
  291. }
  292. $trashStorage->releaseLock($trashInternalPath, ILockingProvider::LOCK_EXCLUSIVE, $lockingProvider);
  293. self::scheduleExpire($user);
  294. // if owner !== user we also need to update the owners trash size
  295. if ($owner !== $user) {
  296. self::scheduleExpire($owner);
  297. }
  298. return $moveSuccessful;
  299. }
  300. /**
  301. * Move file versions to trash so that they can be restored later
  302. *
  303. * @param string $filename of deleted file
  304. * @param string $owner owner user id
  305. * @param string $ownerPath path relative to the owner's home storage
  306. * @param integer $timestamp when the file was deleted
  307. */
  308. private static function retainVersions($filename, $owner, $ownerPath, $timestamp) {
  309. if (\OCP\App::isEnabled('files_versions') && !empty($ownerPath)) {
  310. $user = User::getUser();
  311. $rootView = new View('/');
  312. if ($rootView->is_dir($owner . '/files_versions/' . $ownerPath)) {
  313. if ($owner !== $user) {
  314. self::copy_recursive($owner . '/files_versions/' . $ownerPath, $owner . '/files_trashbin/versions/' . basename($ownerPath) . '.d' . $timestamp, $rootView);
  315. }
  316. self::move($rootView, $owner . '/files_versions/' . $ownerPath, $user . '/files_trashbin/versions/' . $filename . '.d' . $timestamp);
  317. } elseif ($versions = \OCA\Files_Versions\Storage::getVersions($owner, $ownerPath)) {
  318. foreach ($versions as $v) {
  319. if ($owner !== $user) {
  320. self::copy($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $owner . '/files_trashbin/versions/' . $v['name'] . '.v' . $v['version'] . '.d' . $timestamp);
  321. }
  322. self::move($rootView, $owner . '/files_versions' . $v['path'] . '.v' . $v['version'], $user . '/files_trashbin/versions/' . $filename . '.v' . $v['version'] . '.d' . $timestamp);
  323. }
  324. }
  325. }
  326. }
  327. /**
  328. * Move a file or folder on storage level
  329. *
  330. * @param View $view
  331. * @param string $source
  332. * @param string $target
  333. * @return bool
  334. */
  335. private static function move(View $view, $source, $target) {
  336. /** @var \OC\Files\Storage\Storage $sourceStorage */
  337. [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
  338. /** @var \OC\Files\Storage\Storage $targetStorage */
  339. [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
  340. /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
  341. $result = $targetStorage->moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  342. if ($result) {
  343. $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  344. }
  345. return $result;
  346. }
  347. /**
  348. * Copy a file or folder on storage level
  349. *
  350. * @param View $view
  351. * @param string $source
  352. * @param string $target
  353. * @return bool
  354. */
  355. private static function copy(View $view, $source, $target) {
  356. /** @var \OC\Files\Storage\Storage $sourceStorage */
  357. [$sourceStorage, $sourceInternalPath] = $view->resolvePath($source);
  358. /** @var \OC\Files\Storage\Storage $targetStorage */
  359. [$targetStorage, $targetInternalPath] = $view->resolvePath($target);
  360. /** @var \OC\Files\Storage\Storage $ownerTrashStorage */
  361. $result = $targetStorage->copyFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  362. if ($result) {
  363. $targetStorage->getUpdater()->update($targetInternalPath);
  364. }
  365. return $result;
  366. }
  367. /**
  368. * Restore a file or folder from trash bin
  369. *
  370. * @param string $file path to the deleted file/folder relative to "files_trashbin/files/",
  371. * including the timestamp suffix ".d12345678"
  372. * @param string $filename name of the file/folder
  373. * @param int $timestamp time when the file/folder was deleted
  374. *
  375. * @return bool true on success, false otherwise
  376. */
  377. public static function restore($file, $filename, $timestamp) {
  378. $user = User::getUser();
  379. $view = new View('/' . $user);
  380. $location = '';
  381. if ($timestamp) {
  382. $location = self::getLocation($user, $filename, $timestamp);
  383. if ($location === false) {
  384. \OC::$server->getLogger()->error('trash bin database inconsistent! ($user: ' . $user . ' $filename: ' . $filename . ', $timestamp: ' . $timestamp . ')', ['app' => 'files_trashbin']);
  385. } else {
  386. // if location no longer exists, restore file in the root directory
  387. if ($location !== '/' &&
  388. (!$view->is_dir('files/' . $location) ||
  389. !$view->isCreatable('files/' . $location))
  390. ) {
  391. $location = '';
  392. }
  393. }
  394. }
  395. // we need a extension in case a file/dir with the same name already exists
  396. $uniqueFilename = self::getUniqueFilename($location, $filename, $view);
  397. $source = Filesystem::normalizePath('files_trashbin/files/' . $file);
  398. $target = Filesystem::normalizePath('files/' . $location . '/' . $uniqueFilename);
  399. if (!$view->file_exists($source)) {
  400. return false;
  401. }
  402. $mtime = $view->filemtime($source);
  403. // restore file
  404. if (!$view->isCreatable(dirname($target))) {
  405. throw new NotPermittedException("Can't restore trash item because the target folder is not writable");
  406. }
  407. $restoreResult = $view->rename($source, $target);
  408. // handle the restore result
  409. if ($restoreResult) {
  410. $fakeRoot = $view->getRoot();
  411. $view->chroot('/' . $user . '/files');
  412. $view->touch('/' . $location . '/' . $uniqueFilename, $mtime);
  413. $view->chroot($fakeRoot);
  414. \OCP\Util::emitHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', ['filePath' => Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename),
  415. 'trashPath' => Filesystem::normalizePath($file)]);
  416. self::restoreVersions($view, $file, $filename, $uniqueFilename, $location, $timestamp);
  417. if ($timestamp) {
  418. $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
  419. $query->execute([$user, $filename, $timestamp]);
  420. }
  421. return true;
  422. }
  423. return false;
  424. }
  425. /**
  426. * restore versions from trash bin
  427. *
  428. * @param View $view file view
  429. * @param string $file complete path to file
  430. * @param string $filename name of file once it was deleted
  431. * @param string $uniqueFilename new file name to restore the file without overwriting existing files
  432. * @param string $location location if file
  433. * @param int $timestamp deletion time
  434. * @return false|null
  435. */
  436. private static function restoreVersions(View $view, $file, $filename, $uniqueFilename, $location, $timestamp) {
  437. if (\OCP\App::isEnabled('files_versions')) {
  438. $user = User::getUser();
  439. $rootView = new View('/');
  440. $target = Filesystem::normalizePath('/' . $location . '/' . $uniqueFilename);
  441. [$owner, $ownerPath] = self::getUidAndFilename($target);
  442. // file has been deleted in between
  443. if (empty($ownerPath)) {
  444. return false;
  445. }
  446. if ($timestamp) {
  447. $versionedFile = $filename;
  448. } else {
  449. $versionedFile = $file;
  450. }
  451. if ($view->is_dir('/files_trashbin/versions/' . $file)) {
  452. $rootView->rename(Filesystem::normalizePath($user . '/files_trashbin/versions/' . $file), Filesystem::normalizePath($owner . '/files_versions/' . $ownerPath));
  453. } elseif ($versions = self::getVersionsFromTrash($versionedFile, $timestamp, $user)) {
  454. foreach ($versions as $v) {
  455. if ($timestamp) {
  456. $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v . '.d' . $timestamp, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
  457. } else {
  458. $rootView->rename($user . '/files_trashbin/versions/' . $versionedFile . '.v' . $v, $owner . '/files_versions/' . $ownerPath . '.v' . $v);
  459. }
  460. }
  461. }
  462. }
  463. }
  464. /**
  465. * delete all files from the trash
  466. */
  467. public static function deleteAll() {
  468. $user = User::getUser();
  469. $userRoot = \OC::$server->getUserFolder($user)->getParent();
  470. $view = new View('/' . $user);
  471. $fileInfos = $view->getDirectoryContent('files_trashbin/files');
  472. try {
  473. $trash = $userRoot->get('files_trashbin');
  474. } catch (NotFoundException $e) {
  475. return false;
  476. }
  477. // Array to store the relative path in (after the file is deleted, the view won't be able to relativise the path anymore)
  478. $filePaths = [];
  479. foreach ($fileInfos as $fileInfo) {
  480. $filePaths[] = $view->getRelativePath($fileInfo->getPath());
  481. }
  482. unset($fileInfos); // save memory
  483. // Bulk PreDelete-Hook
  484. \OC_Hook::emit('\OCP\Trashbin', 'preDeleteAll', ['paths' => $filePaths]);
  485. // Single-File Hooks
  486. foreach ($filePaths as $path) {
  487. self::emitTrashbinPreDelete($path);
  488. }
  489. // actual file deletion
  490. $trash->delete();
  491. $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
  492. $query->execute([$user]);
  493. // Bulk PostDelete-Hook
  494. \OC_Hook::emit('\OCP\Trashbin', 'deleteAll', ['paths' => $filePaths]);
  495. // Single-File Hooks
  496. foreach ($filePaths as $path) {
  497. self::emitTrashbinPostDelete($path);
  498. }
  499. $trash = $userRoot->newFolder('files_trashbin');
  500. $trash->newFolder('files');
  501. return true;
  502. }
  503. /**
  504. * wrapper function to emit the 'preDelete' hook of \OCP\Trashbin before a file is deleted
  505. *
  506. * @param string $path
  507. */
  508. protected static function emitTrashbinPreDelete($path) {
  509. \OC_Hook::emit('\OCP\Trashbin', 'preDelete', ['path' => $path]);
  510. }
  511. /**
  512. * wrapper function to emit the 'delete' hook of \OCP\Trashbin after a file has been deleted
  513. *
  514. * @param string $path
  515. */
  516. protected static function emitTrashbinPostDelete($path) {
  517. \OC_Hook::emit('\OCP\Trashbin', 'delete', ['path' => $path]);
  518. }
  519. /**
  520. * delete file from trash bin permanently
  521. *
  522. * @param string $filename path to the file
  523. * @param string $user
  524. * @param int $timestamp of deletion time
  525. *
  526. * @return int size of deleted files
  527. */
  528. public static function delete($filename, $user, $timestamp = null) {
  529. $userRoot = \OC::$server->getUserFolder($user)->getParent();
  530. $view = new View('/' . $user);
  531. $size = 0;
  532. if ($timestamp) {
  533. $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=? AND `id`=? AND `timestamp`=?');
  534. $query->execute([$user, $filename, $timestamp]);
  535. $file = $filename . '.d' . $timestamp;
  536. } else {
  537. $file = $filename;
  538. }
  539. $size += self::deleteVersions($view, $file, $filename, $timestamp, $user);
  540. try {
  541. $node = $userRoot->get('/files_trashbin/files/' . $file);
  542. } catch (NotFoundException $e) {
  543. return $size;
  544. }
  545. if ($node instanceof Folder) {
  546. $size += self::calculateSize(new View('/' . $user . '/files_trashbin/files/' . $file));
  547. } elseif ($node instanceof File) {
  548. $size += $view->filesize('/files_trashbin/files/' . $file);
  549. }
  550. self::emitTrashbinPreDelete('/files_trashbin/files/' . $file);
  551. $node->delete();
  552. self::emitTrashbinPostDelete('/files_trashbin/files/' . $file);
  553. return $size;
  554. }
  555. /**
  556. * @param View $view
  557. * @param string $file
  558. * @param string $filename
  559. * @param integer|null $timestamp
  560. * @param string $user
  561. * @return int
  562. */
  563. private static function deleteVersions(View $view, $file, $filename, $timestamp, $user) {
  564. $size = 0;
  565. if (\OCP\App::isEnabled('files_versions')) {
  566. if ($view->is_dir('files_trashbin/versions/' . $file)) {
  567. $size += self::calculateSize(new View('/' . $user . '/files_trashbin/versions/' . $file));
  568. $view->unlink('files_trashbin/versions/' . $file);
  569. } elseif ($versions = self::getVersionsFromTrash($filename, $timestamp, $user)) {
  570. foreach ($versions as $v) {
  571. if ($timestamp) {
  572. $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
  573. $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v . '.d' . $timestamp);
  574. } else {
  575. $size += $view->filesize('/files_trashbin/versions/' . $filename . '.v' . $v);
  576. $view->unlink('/files_trashbin/versions/' . $filename . '.v' . $v);
  577. }
  578. }
  579. }
  580. }
  581. return $size;
  582. }
  583. /**
  584. * check to see whether a file exists in trashbin
  585. *
  586. * @param string $filename path to the file
  587. * @param int $timestamp of deletion time
  588. * @return bool true if file exists, otherwise false
  589. */
  590. public static function file_exists($filename, $timestamp = null) {
  591. $user = User::getUser();
  592. $view = new View('/' . $user);
  593. if ($timestamp) {
  594. $filename = $filename . '.d' . $timestamp;
  595. }
  596. $target = Filesystem::normalizePath('files_trashbin/files/' . $filename);
  597. return $view->file_exists($target);
  598. }
  599. /**
  600. * deletes used space for trash bin in db if user was deleted
  601. *
  602. * @param string $uid id of deleted user
  603. * @return bool result of db delete operation
  604. */
  605. public static function deleteUser($uid) {
  606. $query = \OC_DB::prepare('DELETE FROM `*PREFIX*files_trash` WHERE `user`=?');
  607. return $query->execute([$uid]);
  608. }
  609. /**
  610. * calculate remaining free space for trash bin
  611. *
  612. * @param integer $trashbinSize current size of the trash bin
  613. * @param string $user
  614. * @return int available free space for trash bin
  615. */
  616. private static function calculateFreeSpace($trashbinSize, $user) {
  617. $config = \OC::$server->getConfig();
  618. $systemTrashbinSize = (int)$config->getAppValue('files_trashbin', 'trashbin_size', '-1');
  619. $userTrashbinSize = (int)$config->getUserValue($user, 'files_trashbin', 'trashbin_size', '-1');
  620. $configuredTrashbinSize = ($userTrashbinSize < 0) ? $systemTrashbinSize : $userTrashbinSize;
  621. if ($configuredTrashbinSize) {
  622. return $configuredTrashbinSize - $trashbinSize;
  623. }
  624. $softQuota = true;
  625. $userObject = \OC::$server->getUserManager()->get($user);
  626. if (is_null($userObject)) {
  627. return 0;
  628. }
  629. $quota = $userObject->getQuota();
  630. if ($quota === null || $quota === 'none') {
  631. $quota = Filesystem::free_space('/');
  632. $softQuota = false;
  633. // inf or unknown free space
  634. if ($quota < 0) {
  635. $quota = PHP_INT_MAX;
  636. }
  637. } else {
  638. $quota = \OCP\Util::computerFileSize($quota);
  639. }
  640. // calculate available space for trash bin
  641. // subtract size of files and current trash bin size from quota
  642. if ($softQuota) {
  643. $userFolder = \OC::$server->getUserFolder($user);
  644. if (is_null($userFolder)) {
  645. return 0;
  646. }
  647. $free = $quota - $userFolder->getSize(false); // remaining free space for user
  648. if ($free > 0) {
  649. $availableSpace = ($free * self::DEFAULTMAXSIZE / 100) - $trashbinSize; // how much space can be used for versions
  650. } else {
  651. $availableSpace = $free - $trashbinSize;
  652. }
  653. } else {
  654. $availableSpace = $quota;
  655. }
  656. return $availableSpace;
  657. }
  658. /**
  659. * resize trash bin if necessary after a new file was added to Nextcloud
  660. *
  661. * @param string $user user id
  662. */
  663. public static function resizeTrash($user) {
  664. $size = self::getTrashbinSize($user);
  665. $freeSpace = self::calculateFreeSpace($size, $user);
  666. if ($freeSpace < 0) {
  667. self::scheduleExpire($user);
  668. }
  669. }
  670. /**
  671. * clean up the trash bin
  672. *
  673. * @param string $user
  674. */
  675. public static function expire($user) {
  676. $trashBinSize = self::getTrashbinSize($user);
  677. $availableSpace = self::calculateFreeSpace($trashBinSize, $user);
  678. $dirContent = Helper::getTrashFiles('/', $user, 'mtime');
  679. // delete all files older then $retention_obligation
  680. [$delSize, $count] = self::deleteExpiredFiles($dirContent, $user);
  681. $availableSpace += $delSize;
  682. // delete files from trash until we meet the trash bin size limit again
  683. self::deleteFiles(array_slice($dirContent, $count), $user, $availableSpace);
  684. }
  685. /**
  686. * @param string $user
  687. */
  688. private static function scheduleExpire($user) {
  689. // let the admin disable auto expire
  690. /** @var Application $application */
  691. $application = \OC::$server->query(Application::class);
  692. $expiration = $application->getContainer()->query('Expiration');
  693. if ($expiration->isEnabled()) {
  694. \OC::$server->getCommandBus()->push(new Expire($user));
  695. }
  696. }
  697. /**
  698. * if the size limit for the trash bin is reached, we delete the oldest
  699. * files in the trash bin until we meet the limit again
  700. *
  701. * @param array $files
  702. * @param string $user
  703. * @param int $availableSpace available disc space
  704. * @return int size of deleted files
  705. */
  706. protected static function deleteFiles($files, $user, $availableSpace) {
  707. /** @var Application $application */
  708. $application = \OC::$server->query(Application::class);
  709. $expiration = $application->getContainer()->query('Expiration');
  710. $size = 0;
  711. if ($availableSpace < 0) {
  712. foreach ($files as $file) {
  713. if ($availableSpace < 0 && $expiration->isExpired($file['mtime'], true)) {
  714. $tmp = self::delete($file['name'], $user, $file['mtime']);
  715. \OC::$server->getLogger()->info('remove "' . $file['name'] . '" (' . $tmp . 'B) to meet the limit of trash bin size (50% of available quota)', ['app' => 'files_trashbin']);
  716. $availableSpace += $tmp;
  717. $size += $tmp;
  718. } else {
  719. break;
  720. }
  721. }
  722. }
  723. return $size;
  724. }
  725. /**
  726. * delete files older then max storage time
  727. *
  728. * @param array $files list of files sorted by mtime
  729. * @param string $user
  730. * @return integer[] size of deleted files and number of deleted files
  731. */
  732. public static function deleteExpiredFiles($files, $user) {
  733. /** @var Expiration $expiration */
  734. $expiration = \OC::$server->query(Expiration::class);
  735. $size = 0;
  736. $count = 0;
  737. foreach ($files as $file) {
  738. $timestamp = $file['mtime'];
  739. $filename = $file['name'];
  740. if ($expiration->isExpired($timestamp)) {
  741. try {
  742. $size += self::delete($filename, $user, $timestamp);
  743. $count++;
  744. } catch (\OCP\Files\NotPermittedException $e) {
  745. \OC::$server->getLogger()->logException($e, ['app' => 'files_trashbin', 'level' => \OCP\ILogger::WARN, 'message' => 'Removing "' . $filename . '" from trashbin failed.']);
  746. }
  747. \OC::$server->getLogger()->info(
  748. 'Remove "' . $filename . '" from trashbin because it exceeds max retention obligation term.',
  749. ['app' => 'files_trashbin']
  750. );
  751. } else {
  752. break;
  753. }
  754. }
  755. return [$size, $count];
  756. }
  757. /**
  758. * recursive copy to copy a whole directory
  759. *
  760. * @param string $source source path, relative to the users files directory
  761. * @param string $destination destination path relative to the users root directoy
  762. * @param View $view file view for the users root directory
  763. * @return int
  764. * @throws Exceptions\CopyRecursiveException
  765. */
  766. private static function copy_recursive($source, $destination, View $view) {
  767. $size = 0;
  768. if ($view->is_dir($source)) {
  769. $view->mkdir($destination);
  770. $view->touch($destination, $view->filemtime($source));
  771. foreach ($view->getDirectoryContent($source) as $i) {
  772. $pathDir = $source . '/' . $i['name'];
  773. if ($view->is_dir($pathDir)) {
  774. $size += self::copy_recursive($pathDir, $destination . '/' . $i['name'], $view);
  775. } else {
  776. $size += $view->filesize($pathDir);
  777. $result = $view->copy($pathDir, $destination . '/' . $i['name']);
  778. if (!$result) {
  779. throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
  780. }
  781. $view->touch($destination . '/' . $i['name'], $view->filemtime($pathDir));
  782. }
  783. }
  784. } else {
  785. $size += $view->filesize($source);
  786. $result = $view->copy($source, $destination);
  787. if (!$result) {
  788. throw new \OCA\Files_Trashbin\Exceptions\CopyRecursiveException();
  789. }
  790. $view->touch($destination, $view->filemtime($source));
  791. }
  792. return $size;
  793. }
  794. /**
  795. * find all versions which belong to the file we want to restore
  796. *
  797. * @param string $filename name of the file which should be restored
  798. * @param int $timestamp timestamp when the file was deleted
  799. * @return array
  800. */
  801. private static function getVersionsFromTrash($filename, $timestamp, $user) {
  802. $view = new View('/' . $user . '/files_trashbin/versions');
  803. $versions = [];
  804. //force rescan of versions, local storage may not have updated the cache
  805. if (!self::$scannedVersions) {
  806. /** @var \OC\Files\Storage\Storage $storage */
  807. [$storage,] = $view->resolvePath('/');
  808. $storage->getScanner()->scan('files_trashbin/versions');
  809. self::$scannedVersions = true;
  810. }
  811. if ($timestamp) {
  812. // fetch for old versions
  813. $matches = $view->searchRaw($filename . '.v%.d' . $timestamp);
  814. $offset = -strlen($timestamp) - 2;
  815. } else {
  816. $matches = $view->searchRaw($filename . '.v%');
  817. }
  818. if (is_array($matches)) {
  819. foreach ($matches as $ma) {
  820. if ($timestamp) {
  821. $parts = explode('.v', substr($ma['path'], 0, $offset));
  822. $versions[] = end($parts);
  823. } else {
  824. $parts = explode('.v', $ma);
  825. $versions[] = end($parts);
  826. }
  827. }
  828. }
  829. return $versions;
  830. }
  831. /**
  832. * find unique extension for restored file if a file with the same name already exists
  833. *
  834. * @param string $location where the file should be restored
  835. * @param string $filename name of the file
  836. * @param View $view filesystem view relative to users root directory
  837. * @return string with unique extension
  838. */
  839. private static function getUniqueFilename($location, $filename, View $view) {
  840. $ext = pathinfo($filename, PATHINFO_EXTENSION);
  841. $name = pathinfo($filename, PATHINFO_FILENAME);
  842. $l = \OC::$server->getL10N('files_trashbin');
  843. $location = '/' . trim($location, '/');
  844. // if extension is not empty we set a dot in front of it
  845. if ($ext !== '') {
  846. $ext = '.' . $ext;
  847. }
  848. if ($view->file_exists('files' . $location . '/' . $filename)) {
  849. $i = 2;
  850. $uniqueName = $name . " (" . $l->t("restored") . ")" . $ext;
  851. while ($view->file_exists('files' . $location . '/' . $uniqueName)) {
  852. $uniqueName = $name . " (" . $l->t("restored") . " " . $i . ")" . $ext;
  853. $i++;
  854. }
  855. return $uniqueName;
  856. }
  857. return $filename;
  858. }
  859. /**
  860. * get the size from a given root folder
  861. *
  862. * @param View $view file view on the root folder
  863. * @return integer size of the folder
  864. */
  865. private static function calculateSize($view) {
  866. $root = \OC::$server->getConfig()->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data') . $view->getAbsolutePath('');
  867. if (!file_exists($root)) {
  868. return 0;
  869. }
  870. $iterator = new \RecursiveIteratorIterator(new \RecursiveDirectoryIterator($root), \RecursiveIteratorIterator::CHILD_FIRST);
  871. $size = 0;
  872. /**
  873. * RecursiveDirectoryIterator on an NFS path isn't iterable with foreach
  874. * This bug is fixed in PHP 5.5.9 or before
  875. * See #8376
  876. */
  877. $iterator->rewind();
  878. while ($iterator->valid()) {
  879. $path = $iterator->current();
  880. $relpath = substr($path, strlen($root) - 1);
  881. if (!$view->is_dir($relpath)) {
  882. $size += $view->filesize($relpath);
  883. }
  884. $iterator->next();
  885. }
  886. return $size;
  887. }
  888. /**
  889. * get current size of trash bin from a given user
  890. *
  891. * @param string $user user who owns the trash bin
  892. * @return integer trash bin size
  893. */
  894. private static function getTrashbinSize($user) {
  895. $view = new View('/' . $user);
  896. $fileInfo = $view->getFileInfo('/files_trashbin');
  897. return isset($fileInfo['size']) ? $fileInfo['size'] : 0;
  898. }
  899. /**
  900. * check if trash bin is empty for a given user
  901. *
  902. * @param string $user
  903. * @return bool
  904. */
  905. public static function isEmpty($user) {
  906. $view = new View('/' . $user . '/files_trashbin');
  907. if ($view->is_dir('/files') && $dh = $view->opendir('/files')) {
  908. while ($file = readdir($dh)) {
  909. if (!Filesystem::isIgnoredDir($file)) {
  910. return false;
  911. }
  912. }
  913. }
  914. return true;
  915. }
  916. /**
  917. * @param $path
  918. * @return string
  919. */
  920. public static function preview_icon($path) {
  921. return \OC::$server->getURLGenerator()->linkToRoute('core_ajax_trashbin_preview', ['x' => 32, 'y' => 32, 'file' => $path]);
  922. }
  923. }