Vous ne pouvez pas sélectionner plus de 25 sujets Les noms de sujets doivent commencer par une lettre ou un nombre, peuvent contenir des tirets ('-') et peuvent comporter jusqu'à 35 caractères.

FileEventsListener.php 12KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376
  1. <?php
  2. /**
  3. * @copyright Copyright (c) 2016, ownCloud, Inc.
  4. *
  5. * @author Bart Visscher <bartv@thisnet.nl>
  6. * @author Björn Schießle <bjoern@schiessle.org>
  7. * @author Christoph Wurst <christoph@winzerhof-wurst.at>
  8. * @author John Molakvoæ <skjnldsv@protonmail.com>
  9. * @author Jörn Friedrich Dreyer <jfd@butonic.de>
  10. * @author Morris Jobke <hey@morrisjobke.de>
  11. * @author Robin Appelman <robin@icewind.nl>
  12. * @author Robin McCorkell <robin@mccorkell.me.uk>
  13. * @author Sam Tuke <mail@samtuke.com>
  14. * @author Louis Chmn <louis@chmn.me>
  15. *
  16. * @license AGPL-3.0
  17. *
  18. * This code is free software: you can redistribute it and/or modify
  19. * it under the terms of the GNU Affero General Public License, version 3,
  20. * as published by the Free Software Foundation.
  21. *
  22. * This program is distributed in the hope that it will be useful,
  23. * but WITHOUT ANY WARRANTY; without even the implied warranty of
  24. * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
  25. * GNU Affero General Public License for more details.
  26. *
  27. * You should have received a copy of the GNU Affero General Public License, version 3,
  28. * along with this program. If not, see <http://www.gnu.org/licenses/>
  29. *
  30. */
  31. namespace OCA\Files_Versions\Listener;
  32. use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
  33. use OC\DB\Exceptions\DbalException;
  34. use OC\Files\Filesystem;
  35. use OC\Files\Mount\MoveableMount;
  36. use OC\Files\Node\NonExistingFile;
  37. use OC\Files\View;
  38. use OCA\Files_Versions\Storage;
  39. use OCA\Files_Versions\Versions\INeedSyncVersionBackend;
  40. use OCA\Files_Versions\Versions\IVersionManager;
  41. use OCP\AppFramework\Db\DoesNotExistException;
  42. use OCP\DB\Exception;
  43. use OCP\EventDispatcher\Event;
  44. use OCP\EventDispatcher\IEventListener;
  45. use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
  46. use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
  47. use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
  48. use OCP\Files\Events\Node\BeforeNodeTouchedEvent;
  49. use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
  50. use OCP\Files\Events\Node\NodeCopiedEvent;
  51. use OCP\Files\Events\Node\NodeCreatedEvent;
  52. use OCP\Files\Events\Node\NodeDeletedEvent;
  53. use OCP\Files\Events\Node\NodeRenamedEvent;
  54. use OCP\Files\Events\Node\NodeTouchedEvent;
  55. use OCP\Files\Events\Node\NodeWrittenEvent;
  56. use OCP\Files\File;
  57. use OCP\Files\Folder;
  58. use OCP\Files\IMimeTypeLoader;
  59. use OCP\Files\IRootFolder;
  60. use OCP\Files\Node;
  61. use OCP\IUserSession;
  62. use Psr\Log\LoggerInterface;
  63. /** @template-implements IEventListener<BeforeNodeCopiedEvent|BeforeNodeDeletedEvent|BeforeNodeRenamedEvent|BeforeNodeTouchedEvent|BeforeNodeWrittenEvent|NodeCopiedEvent|NodeCreatedEvent|NodeDeletedEvent|NodeRenamedEvent|NodeTouchedEvent|NodeWrittenEvent> */
  64. class FileEventsListener implements IEventListener {
  65. /**
  66. * @var array<int, array>
  67. */
  68. private array $writeHookInfo = [];
  69. /**
  70. * @var array<int, Node>
  71. */
  72. private array $nodesTouched = [];
  73. /**
  74. * @var array<string, Node>
  75. */
  76. private array $versionsDeleted = [];
  77. public function __construct(
  78. private IRootFolder $rootFolder,
  79. private IVersionManager $versionManager,
  80. private IMimeTypeLoader $mimeTypeLoader,
  81. private IUserSession $userSession,
  82. private LoggerInterface $logger,
  83. ) {
  84. }
  85. public function handle(Event $event): void {
  86. if ($event instanceof NodeCreatedEvent) {
  87. $this->created($event->getNode());
  88. }
  89. if ($event instanceof BeforeNodeTouchedEvent) {
  90. $this->pre_touch_hook($event->getNode());
  91. }
  92. if ($event instanceof NodeTouchedEvent) {
  93. $this->touch_hook($event->getNode());
  94. }
  95. if ($event instanceof BeforeNodeWrittenEvent) {
  96. $this->write_hook($event->getNode());
  97. }
  98. if ($event instanceof NodeWrittenEvent) {
  99. $this->post_write_hook($event->getNode());
  100. }
  101. if ($event instanceof BeforeNodeDeletedEvent) {
  102. $this->pre_remove_hook($event->getNode());
  103. }
  104. if ($event instanceof NodeDeletedEvent) {
  105. $this->remove_hook($event->getNode());
  106. }
  107. if ($event instanceof NodeRenamedEvent) {
  108. $this->rename_hook($event->getSource(), $event->getTarget());
  109. }
  110. if ($event instanceof NodeCopiedEvent) {
  111. $this->copy_hook($event->getSource(), $event->getTarget());
  112. }
  113. if ($event instanceof BeforeNodeRenamedEvent) {
  114. $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
  115. }
  116. if ($event instanceof BeforeNodeCopiedEvent) {
  117. $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
  118. }
  119. }
  120. public function pre_touch_hook(Node $node): void {
  121. // Do not handle folders.
  122. if ($node instanceof Folder) {
  123. return;
  124. }
  125. // $node is a non-existing on file creation.
  126. if ($node instanceof NonExistingFile) {
  127. return;
  128. }
  129. $this->nodesTouched[$node->getId()] = $node;
  130. }
  131. public function touch_hook(Node $node): void {
  132. $previousNode = $this->nodesTouched[$node->getId()] ?? null;
  133. if ($previousNode === null) {
  134. return;
  135. }
  136. unset($this->nodesTouched[$node->getId()]);
  137. try {
  138. if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
  139. // We update the timestamp of the version entity associated with the previousNode.
  140. $this->versionManager->updateVersionEntity($node, $previousNode->getMTime(), ['timestamp' => $node->getMTime()]);
  141. }
  142. } catch (DbalException $ex) {
  143. // Ignore UniqueConstraintViolationException, as we are probably in the middle of a rollback
  144. // Where the previous node would temporary have the mtime of the old version, so the rollback touches it to fix it.
  145. if (!($ex->getPrevious() instanceof UniqueConstraintViolationException)) {
  146. throw $ex;
  147. }
  148. } catch (DoesNotExistException $ex) {
  149. // Ignore DoesNotExistException, as we are probably in the middle of a rollback
  150. // Where the previous node would temporary have a wrong mtime, so the rollback touches it to fix it.
  151. }
  152. }
  153. public function created(Node $node): void {
  154. // Do not handle folders.
  155. if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
  156. $this->versionManager->createVersionEntity($node);
  157. }
  158. }
  159. /**
  160. * listen to write event.
  161. */
  162. public function write_hook(Node $node): void {
  163. // Do not handle folders.
  164. if ($node instanceof Folder) {
  165. return;
  166. }
  167. // $node is a non-existing on file creation.
  168. if ($node instanceof NonExistingFile) {
  169. return;
  170. }
  171. $path = $this->getPathForNode($node);
  172. $result = Storage::store($path);
  173. // Store the result of the version creation so it can be used in post_write_hook.
  174. $this->writeHookInfo[$node->getId()] = [
  175. 'previousNode' => $node,
  176. 'versionCreated' => $result !== false
  177. ];
  178. }
  179. /**
  180. * listen to post_write event.
  181. */
  182. public function post_write_hook(Node $node): void {
  183. // Do not handle folders.
  184. if ($node instanceof Folder) {
  185. return;
  186. }
  187. $writeHookInfo = $this->writeHookInfo[$node->getId()] ?? null;
  188. if ($writeHookInfo === null) {
  189. return;
  190. }
  191. if (
  192. ($writeHookInfo['versionCreated'] || $writeHookInfo['previousNode']->getSize() === 0) &&
  193. $node->getMTime() !== $writeHookInfo['previousNode']->getMTime()
  194. ) {
  195. // If a new version was created, insert a version in the DB for the current content.
  196. // Unless both versions have the same mtime.
  197. $this->created($node);
  198. } else {
  199. try {
  200. // If no new version was stored in the FS, no new version should be added in the DB.
  201. // So we simply update the associated version.
  202. if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
  203. $this->versionManager->updateVersionEntity(
  204. $node,
  205. $writeHookInfo['previousNode']->getMtime(),
  206. [
  207. 'timestamp' => $node->getMTime(),
  208. 'size' => $node->getSize(),
  209. 'mimetype' => $this->mimeTypeLoader->getId($node->getMimetype()),
  210. ],
  211. );
  212. }
  213. } catch (Exception $e) {
  214. $this->logger->error('Failed to update existing version for ' . $node->getPath(), [
  215. 'exception' => $e,
  216. 'versionCreated' => $writeHookInfo['versionCreated'],
  217. 'previousNode' => [
  218. 'size' => $writeHookInfo['previousNode']->getSize(),
  219. 'mtime' => $writeHookInfo['previousNode']->getMTime(),
  220. ],
  221. 'node' => [
  222. 'size' => $node->getSize(),
  223. 'mtime' => $node->getMTime(),
  224. ]
  225. ]);
  226. throw $e;
  227. }
  228. }
  229. unset($this->writeHookInfo[$node->getId()]);
  230. }
  231. /**
  232. * Erase versions of deleted file
  233. *
  234. * This function is connected to the delete signal of OC_Filesystem
  235. * cleanup the versions directory if the actual file gets deleted
  236. */
  237. public function remove_hook(Node $node): void {
  238. // Need to normalize the path as there is an issue with path concatenation in View.php::getAbsolutePath.
  239. $path = Filesystem::normalizePath($node->getPath());
  240. if (!array_key_exists($path, $this->versionsDeleted)) {
  241. return;
  242. }
  243. $node = $this->versionsDeleted[$path];
  244. $relativePath = $this->getPathForNode($node);
  245. unset($this->versionsDeleted[$path]);
  246. Storage::delete($relativePath);
  247. // If no new version was stored in the FS, no new version should be added in the DB.
  248. // So we simply update the associated version.
  249. if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
  250. $this->versionManager->deleteVersionsEntity($node);
  251. }
  252. }
  253. /**
  254. * mark file as "deleted" so that we can clean up the versions if the file is gone
  255. */
  256. public function pre_remove_hook(Node $node): void {
  257. $path = $this->getPathForNode($node);
  258. Storage::markDeletedFile($path);
  259. $this->versionsDeleted[$node->getPath()] = $node;
  260. }
  261. /**
  262. * rename/move versions of renamed/moved files
  263. *
  264. * This function is connected to the rename signal of OC_Filesystem and adjust the name and location
  265. * of the stored versions along the actual file
  266. */
  267. public function rename_hook(Node $source, Node $target): void {
  268. $oldPath = $this->getPathForNode($source);
  269. $newPath = $this->getPathForNode($target);
  270. Storage::renameOrCopy($oldPath, $newPath, 'rename');
  271. }
  272. /**
  273. * copy versions of copied files
  274. *
  275. * This function is connected to the copy signal of OC_Filesystem and copies the
  276. * the stored versions to the new location
  277. */
  278. public function copy_hook(Node $source, Node $target): void {
  279. $oldPath = $this->getPathForNode($source);
  280. $newPath = $this->getPathForNode($target);
  281. Storage::renameOrCopy($oldPath, $newPath, 'copy');
  282. }
  283. /**
  284. * Remember owner and the owner path of the source file.
  285. * If the file already exists, then it was a upload of a existing file
  286. * over the web interface and we call Storage::store() directly
  287. *
  288. *
  289. */
  290. public function pre_renameOrCopy_hook(Node $source, Node $target): void {
  291. // if we rename a movable mount point, then the versions don't have
  292. // to be renamed
  293. $oldPath = $this->getPathForNode($source);
  294. $newPath = $this->getPathForNode($target);
  295. $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files' . $oldPath);
  296. $manager = Filesystem::getMountManager();
  297. $mount = $manager->find($absOldPath);
  298. $internalPath = $mount->getInternalPath($absOldPath);
  299. if ($internalPath === '' and $mount instanceof MoveableMount) {
  300. return;
  301. }
  302. $view = new View(\OC_User::getUser() . '/files');
  303. if ($view->file_exists($newPath)) {
  304. Storage::store($newPath);
  305. } else {
  306. Storage::setSourcePathAndUser($oldPath);
  307. }
  308. }
  309. /**
  310. * Retrieve the path relative to the current user root folder.
  311. * If no user is connected, try to use the node's owner.
  312. */
  313. private function getPathForNode(Node $node): ?string {
  314. $user = $this->userSession->getUser()?->getUID();
  315. if ($user) {
  316. $path = $this->rootFolder
  317. ->getUserFolder($user)
  318. ->getRelativePath($node->getPath());
  319. if ($path !== null) {
  320. return $path;
  321. }
  322. }
  323. $owner = $node->getOwner()?->getUid();
  324. if ($owner) {
  325. $path = $this->rootFolder
  326. ->getUserFolder($owner)
  327. ->getRelativePath($node->getPath());
  328. if ($path !== null) {
  329. return $path;
  330. }
  331. }
  332. return null;
  333. }
  334. }