FileEventsListener.php 12 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387
  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'] &&
  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. // If both versions have the same mtime, it means the latest version file simply got overrode,
  197. // so no need to create a new version.
  198. $this->created($node);
  199. } else {
  200. try {
  201. // If no new version was stored in the FS, no new version should be added in the DB.
  202. // So we simply update the associated version.
  203. if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
  204. $this->versionManager->updateVersionEntity(
  205. $node,
  206. $writeHookInfo['previousNode']->getMtime(),
  207. [
  208. 'timestamp' => $node->getMTime(),
  209. 'size' => $node->getSize(),
  210. 'mimetype' => $this->mimeTypeLoader->getId($node->getMimetype()),
  211. ],
  212. );
  213. }
  214. } catch (Exception $e) {
  215. $this->logger->error('Failed to update existing version for ' . $node->getPath(), [
  216. 'exception' => $e,
  217. 'versionCreated' => $writeHookInfo['versionCreated'],
  218. 'previousNode' => [
  219. 'size' => $writeHookInfo['previousNode']->getSize(),
  220. 'mtime' => $writeHookInfo['previousNode']->getMTime(),
  221. ],
  222. 'node' => [
  223. 'size' => $node->getSize(),
  224. 'mtime' => $node->getMTime(),
  225. ]
  226. ]);
  227. throw $e;
  228. }
  229. }
  230. unset($this->writeHookInfo[$node->getId()]);
  231. }
  232. /**
  233. * Erase versions of deleted file
  234. *
  235. * This function is connected to the delete signal of OC_Filesystem
  236. * cleanup the versions directory if the actual file gets deleted
  237. */
  238. public function remove_hook(Node $node): void {
  239. // Need to normalize the path as there is an issue with path concatenation in View.php::getAbsolutePath.
  240. $path = Filesystem::normalizePath($node->getPath());
  241. if (!array_key_exists($path, $this->versionsDeleted)) {
  242. return;
  243. }
  244. $node = $this->versionsDeleted[$path];
  245. $relativePath = $this->getPathForNode($node);
  246. unset($this->versionsDeleted[$path]);
  247. Storage::delete($relativePath);
  248. // If no new version was stored in the FS, no new version should be added in the DB.
  249. // So we simply update the associated version.
  250. if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
  251. $this->versionManager->deleteVersionsEntity($node);
  252. }
  253. }
  254. /**
  255. * mark file as "deleted" so that we can clean up the versions if the file is gone
  256. */
  257. public function pre_remove_hook(Node $node): void {
  258. $path = $this->getPathForNode($node);
  259. Storage::markDeletedFile($path);
  260. $this->versionsDeleted[$node->getPath()] = $node;
  261. }
  262. /**
  263. * rename/move versions of renamed/moved files
  264. *
  265. * This function is connected to the rename signal of OC_Filesystem and adjust the name and location
  266. * of the stored versions along the actual file
  267. */
  268. public function rename_hook(Node $source, Node $target): void {
  269. $oldPath = $this->getPathForNode($source);
  270. $newPath = $this->getPathForNode($target);
  271. Storage::renameOrCopy($oldPath, $newPath, 'rename');
  272. }
  273. /**
  274. * copy versions of copied files
  275. *
  276. * This function is connected to the copy signal of OC_Filesystem and copies the
  277. * the stored versions to the new location
  278. */
  279. public function copy_hook(Node $source, Node $target): void {
  280. $oldPath = $this->getPathForNode($source);
  281. $newPath = $this->getPathForNode($target);
  282. Storage::renameOrCopy($oldPath, $newPath, 'copy');
  283. }
  284. /**
  285. * Remember owner and the owner path of the source file.
  286. * If the file already exists, then it was a upload of a existing file
  287. * over the web interface and we call Storage::store() directly
  288. *
  289. *
  290. */
  291. public function pre_renameOrCopy_hook(Node $source, Node $target): void {
  292. // if we rename a movable mount point, then the versions don't have
  293. // to be renamed
  294. $oldPath = $this->getPathForNode($source);
  295. $newPath = $this->getPathForNode($target);
  296. $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files' . $oldPath);
  297. $manager = Filesystem::getMountManager();
  298. $mount = $manager->find($absOldPath);
  299. $internalPath = $mount->getInternalPath($absOldPath);
  300. if ($internalPath === '' and $mount instanceof MoveableMount) {
  301. return;
  302. }
  303. $view = new View(\OC_User::getUser() . '/files');
  304. if ($view->file_exists($newPath)) {
  305. Storage::store($newPath);
  306. } else {
  307. Storage::setSourcePathAndUser($oldPath);
  308. }
  309. }
  310. /**
  311. * Retrieve the path relative to the current user root folder.
  312. * If no user is connected, try to use the node's owner.
  313. */
  314. private function getPathForNode(Node $node): ?string {
  315. $user = $this->userSession->getUser()?->getUID();
  316. if ($user) {
  317. $path = $this->rootFolder
  318. ->getUserFolder($user)
  319. ->getRelativePath($node->getPath());
  320. if ($path !== null) {
  321. return $path;
  322. }
  323. }
  324. $owner = $node->getOwner()?->getUid();
  325. // If no owner, extract it from the path.
  326. // e.g. /user/files/foobar.txt
  327. if (!$owner) {
  328. $parts = explode('/', $node->getPath(), 4);
  329. if (count($parts) === 4) {
  330. $owner = $parts[1];
  331. }
  332. }
  333. if ($owner) {
  334. $path = $this->rootFolder
  335. ->getUserFolder($owner)
  336. ->getRelativePath($node->getPath());
  337. if ($path !== null) {
  338. return $path;
  339. }
  340. }
  341. return null;
  342. }
  343. }