Storage.php 5.8 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202
  1. <?php
  2. /**
  3. * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
  4. * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
  5. * SPDX-License-Identifier: AGPL-3.0-only
  6. */
  7. namespace OCA\Files_Trashbin;
  8. use OC\Files\Filesystem;
  9. use OC\Files\Storage\Wrapper\Wrapper;
  10. use OCA\Files_Trashbin\Events\MoveToTrashEvent;
  11. use OCA\Files_Trashbin\Trash\ITrashManager;
  12. use OCP\Encryption\Exceptions\GenericEncryptionException;
  13. use OCP\EventDispatcher\IEventDispatcher;
  14. use OCP\Files\IRootFolder;
  15. use OCP\Files\Node;
  16. use OCP\Files\Storage\IStorage;
  17. use OCP\IUserManager;
  18. use Psr\Log\LoggerInterface;
  19. class Storage extends Wrapper {
  20. private string $mountPoint;
  21. private bool $trashEnabled = true;
  22. /**
  23. * Storage constructor.
  24. * @param array $parameters
  25. */
  26. public function __construct(
  27. $parameters,
  28. private ?ITrashManager $trashManager = null,
  29. private ?IUserManager $userManager = null,
  30. private ?LoggerInterface $logger = null,
  31. private ?IEventDispatcher $eventDispatcher = null,
  32. private ?IRootFolder $rootFolder = null,
  33. ) {
  34. $this->mountPoint = $parameters['mountPoint'];
  35. parent::__construct($parameters);
  36. }
  37. public function unlink(string $path): bool {
  38. if ($this->trashEnabled) {
  39. try {
  40. return $this->doDelete($path, 'unlink');
  41. } catch (GenericEncryptionException $e) {
  42. // in case of a encryption exception we delete the file right away
  43. $this->logger->info(
  44. "Can't move file " . $path .
  45. ' to the trash bin, therefore it was deleted right away');
  46. return $this->storage->unlink($path);
  47. }
  48. } else {
  49. return $this->storage->unlink($path);
  50. }
  51. }
  52. public function rmdir(string $path): bool {
  53. if ($this->trashEnabled) {
  54. return $this->doDelete($path, 'rmdir');
  55. } else {
  56. return $this->storage->rmdir($path);
  57. }
  58. }
  59. /**
  60. * check if it is a file located in data/user/files only files in the
  61. * 'files' directory should be moved to the trash
  62. */
  63. protected function shouldMoveToTrash(string $path): bool {
  64. $normalized = Filesystem::normalizePath($this->mountPoint . '/' . $path);
  65. $parts = explode('/', $normalized);
  66. if (count($parts) < 4 || strpos($normalized, '/appdata_') === 0) {
  67. return false;
  68. }
  69. // check if there is a app which want to disable the trash bin for this file
  70. $fileId = $this->storage->getCache()->getId($path);
  71. $owner = $this->storage->getOwner($path);
  72. if ($owner === false || $this->storage->instanceOfStorage(\OCA\Files_Sharing\External\Storage::class)) {
  73. $nodes = $this->rootFolder->getById($fileId);
  74. } else {
  75. $nodes = $this->rootFolder->getUserFolder($owner)->getById($fileId);
  76. }
  77. foreach ($nodes as $node) {
  78. $event = $this->createMoveToTrashEvent($node);
  79. $this->eventDispatcher->dispatchTyped($event);
  80. $this->eventDispatcher->dispatch('OCA\Files_Trashbin::moveToTrash', $event);
  81. if ($event->shouldMoveToTrashBin() === false) {
  82. return false;
  83. }
  84. }
  85. if ($parts[2] === 'files' && $this->userManager->userExists($parts[1])) {
  86. return true;
  87. }
  88. return false;
  89. }
  90. /**
  91. * get move to trash event
  92. *
  93. * @param Node $node
  94. * @return MoveToTrashEvent
  95. */
  96. protected function createMoveToTrashEvent(Node $node): MoveToTrashEvent {
  97. return new MoveToTrashEvent($node);
  98. }
  99. /**
  100. * Run the delete operation with the given method
  101. *
  102. * @param string $path path of file or folder to delete
  103. * @param string $method either "unlink" or "rmdir"
  104. *
  105. * @return bool true if the operation succeeded, false otherwise
  106. */
  107. private function doDelete(string $path, string $method): bool {
  108. if (
  109. !\OC::$server->getAppManager()->isEnabledForUser('files_trashbin')
  110. || (pathinfo($path, PATHINFO_EXTENSION) === 'part')
  111. || $this->shouldMoveToTrash($path) === false
  112. ) {
  113. return call_user_func([$this->storage, $method], $path);
  114. }
  115. // check permissions before we continue, this is especially important for
  116. // shared files
  117. if (!$this->isDeletable($path)) {
  118. return false;
  119. }
  120. $isMovedToTrash = $this->trashManager->moveToTrash($this, $path);
  121. if (!$isMovedToTrash) {
  122. return call_user_func([$this->storage, $method], $path);
  123. } else {
  124. return true;
  125. }
  126. }
  127. /**
  128. * Setup the storage wrapper callback
  129. */
  130. public static function setupStorage(): void {
  131. $trashManager = \OC::$server->get(ITrashManager::class);
  132. $userManager = \OC::$server->get(IUserManager::class);
  133. $logger = \OC::$server->get(LoggerInterface::class);
  134. $eventDispatcher = \OC::$server->get(IEventDispatcher::class);
  135. $rootFolder = \OC::$server->get(IRootFolder::class);
  136. Filesystem::addStorageWrapper(
  137. 'oc_trashbin',
  138. function (string $mountPoint, IStorage $storage) use ($trashManager, $userManager, $logger, $eventDispatcher, $rootFolder) {
  139. return new Storage(
  140. ['storage' => $storage, 'mountPoint' => $mountPoint],
  141. $trashManager,
  142. $userManager,
  143. $logger,
  144. $eventDispatcher,
  145. $rootFolder,
  146. );
  147. },
  148. 1);
  149. }
  150. public function getMountPoint() {
  151. return $this->mountPoint;
  152. }
  153. public function moveFromStorage(IStorage $sourceStorage, string $sourceInternalPath, string $targetInternalPath): bool {
  154. $sourceIsTrashbin = $sourceStorage->instanceOfStorage(Storage::class);
  155. try {
  156. // the fallback for moving between storage involves a copy+delete
  157. // we don't want to trigger the trashbin when doing the delete
  158. if ($sourceIsTrashbin) {
  159. /** @var Storage $sourceStorage */
  160. $sourceStorage->disableTrash();
  161. }
  162. $result = parent::moveFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath);
  163. if ($sourceIsTrashbin) {
  164. /** @var Storage $sourceStorage */
  165. $sourceStorage->enableTrash();
  166. }
  167. return $result;
  168. } catch (\Exception $e) {
  169. if ($sourceIsTrashbin) {
  170. /** @var Storage $sourceStorage */
  171. $sourceStorage->enableTrash();
  172. }
  173. throw $e;
  174. }
  175. }
  176. protected function disableTrash(): void {
  177. $this->trashEnabled = false;
  178. }
  179. protected function enableTrash(): void {
  180. $this->trashEnabled = true;
  181. }
  182. }