123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303304305306307308309310311312313314315316317318319320321322323324325326327328329330331332333334335336337338339340341342343344345346347348349350351352353354355356357358359360361362363364365366367368369370371372373374375376377378379380381382383384385386387388389390391392393394395396397398399400401402403404405406407408 |
- <?php
- /**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <bjoern@schiessle.org>
- * @author Christoph Wurst <christoph@winzerhof-wurst.at>
- * @author John Molakvoæ <skjnldsv@protonmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <robin@icewind.nl>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Sam Tuke <mail@samtuke.com>
- * @author Louis Chmn <louis@chmn.me>
- *
- * @license AGPL-3.0
- *
- * This code is free software: you can redistribute it and/or modify
- * it under the terms of the GNU Affero General Public License, version 3,
- * as published by the Free Software Foundation.
- *
- * This program is distributed in the hope that it will be useful,
- * but WITHOUT ANY WARRANTY; without even the implied warranty of
- * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
- * GNU Affero General Public License for more details.
- *
- * You should have received a copy of the GNU Affero General Public License, version 3,
- * along with this program. If not, see <http://www.gnu.org/licenses/>
- *
- */
- namespace OCA\Files_Versions\Listener;
- use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
- use OC\DB\Exceptions\DbalException;
- use OC\Files\Filesystem;
- use OC\Files\Mount\MoveableMount;
- use OC\Files\Node\NonExistingFile;
- use OC\Files\View;
- use OCA\Files_Versions\Storage;
- use OCA\Files_Versions\Versions\INeedSyncVersionBackend;
- use OCA\Files_Versions\Versions\IVersionManager;
- use OCP\AppFramework\Db\DoesNotExistException;
- use OCP\DB\Exception;
- use OCP\EventDispatcher\Event;
- use OCP\EventDispatcher\IEventListener;
- use OCP\Files\Events\Node\BeforeNodeCopiedEvent;
- use OCP\Files\Events\Node\BeforeNodeDeletedEvent;
- use OCP\Files\Events\Node\BeforeNodeRenamedEvent;
- use OCP\Files\Events\Node\BeforeNodeTouchedEvent;
- use OCP\Files\Events\Node\BeforeNodeWrittenEvent;
- use OCP\Files\Events\Node\NodeCopiedEvent;
- use OCP\Files\Events\Node\NodeCreatedEvent;
- use OCP\Files\Events\Node\NodeDeletedEvent;
- use OCP\Files\Events\Node\NodeRenamedEvent;
- use OCP\Files\Events\Node\NodeTouchedEvent;
- use OCP\Files\Events\Node\NodeWrittenEvent;
- use OCP\Files\File;
- use OCP\Files\Folder;
- use OCP\Files\IMimeTypeLoader;
- use OCP\Files\IRootFolder;
- use OCP\Files\Node;
- use OCP\IUserSession;
- use Psr\Log\LoggerInterface;
- /** @template-implements IEventListener<BeforeNodeCopiedEvent|BeforeNodeDeletedEvent|BeforeNodeRenamedEvent|BeforeNodeTouchedEvent|BeforeNodeWrittenEvent|NodeCopiedEvent|NodeCreatedEvent|NodeDeletedEvent|NodeRenamedEvent|NodeTouchedEvent|NodeWrittenEvent> */
- class FileEventsListener implements IEventListener {
- /**
- * @var array<int, array>
- */
- private array $writeHookInfo = [];
- /**
- * @var array<int, Node>
- */
- private array $nodesTouched = [];
- /**
- * @var array<string, Node>
- */
- private array $versionsDeleted = [];
- public function __construct(
- private IRootFolder $rootFolder,
- private IVersionManager $versionManager,
- private IMimeTypeLoader $mimeTypeLoader,
- private IUserSession $userSession,
- private LoggerInterface $logger,
- ) {
- }
- public function handle(Event $event): void {
- if ($event instanceof NodeCreatedEvent) {
- $this->created($event->getNode());
- }
- if ($event instanceof BeforeNodeTouchedEvent) {
- $this->pre_touch_hook($event->getNode());
- }
- if ($event instanceof NodeTouchedEvent) {
- $this->touch_hook($event->getNode());
- }
- if ($event instanceof BeforeNodeWrittenEvent) {
- $this->write_hook($event->getNode());
- }
- if ($event instanceof NodeWrittenEvent) {
- $this->post_write_hook($event->getNode());
- }
- if ($event instanceof BeforeNodeDeletedEvent) {
- $this->pre_remove_hook($event->getNode());
- }
- if ($event instanceof NodeDeletedEvent) {
- $this->remove_hook($event->getNode());
- }
- if ($event instanceof NodeRenamedEvent) {
- $this->rename_hook($event->getSource(), $event->getTarget());
- }
- if ($event instanceof NodeCopiedEvent) {
- $this->copy_hook($event->getSource(), $event->getTarget());
- }
- if ($event instanceof BeforeNodeRenamedEvent) {
- $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
- }
- if ($event instanceof BeforeNodeCopiedEvent) {
- $this->pre_renameOrCopy_hook($event->getSource(), $event->getTarget());
- }
- }
- public function pre_touch_hook(Node $node): void {
- // Do not handle folders.
- if ($node instanceof Folder) {
- return;
- }
- // $node is a non-existing on file creation.
- if ($node instanceof NonExistingFile) {
- return;
- }
- $this->nodesTouched[$node->getId()] = $node;
- }
- public function touch_hook(Node $node): void {
- $previousNode = $this->nodesTouched[$node->getId()] ?? null;
- if ($previousNode === null) {
- return;
- }
- unset($this->nodesTouched[$node->getId()]);
- try {
- if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
- // We update the timestamp of the version entity associated with the previousNode.
- $this->versionManager->updateVersionEntity($node, $previousNode->getMTime(), ['timestamp' => $node->getMTime()]);
- }
- } catch (DbalException $ex) {
- // Ignore UniqueConstraintViolationException, as we are probably in the middle of a rollback
- // Where the previous node would temporary have the mtime of the old version, so the rollback touches it to fix it.
- if (!($ex->getPrevious() instanceof UniqueConstraintViolationException)) {
- throw $ex;
- }
- } catch (DoesNotExistException $ex) {
- // Ignore DoesNotExistException, as we are probably in the middle of a rollback
- // Where the previous node would temporary have a wrong mtime, so the rollback touches it to fix it.
- }
- }
- public function created(Node $node): void {
- // Do not handle folders.
- if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
- $this->versionManager->createVersionEntity($node);
- }
- }
- /**
- * listen to write event.
- */
- public function write_hook(Node $node): void {
- // Do not handle folders.
- if ($node instanceof Folder) {
- return;
- }
- // $node is a non-existing on file creation.
- if ($node instanceof NonExistingFile) {
- return;
- }
- $path = $this->getPathForNode($node);
- $result = Storage::store($path);
- // Store the result of the version creation so it can be used in post_write_hook.
- $this->writeHookInfo[$node->getId()] = [
- 'previousNode' => $node,
- 'versionCreated' => $result !== false
- ];
- }
- /**
- * listen to post_write event.
- */
- public function post_write_hook(Node $node): void {
- // Do not handle folders.
- if ($node instanceof Folder) {
- return;
- }
- $writeHookInfo = $this->writeHookInfo[$node->getId()] ?? null;
- if ($writeHookInfo === null) {
- return;
- }
- if (
- $writeHookInfo['versionCreated'] &&
- $node->getMTime() !== $writeHookInfo['previousNode']->getMTime()
- ) {
- // If a new version was created, insert a version in the DB for the current content.
- // If both versions have the same mtime, it means the latest version file simply got overrode,
- // so no need to create a new version.
- $this->created($node);
- } else {
- try {
- // If no new version was stored in the FS, no new version should be added in the DB.
- // So we simply update the associated version.
- if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
- $this->versionManager->updateVersionEntity(
- $node,
- $writeHookInfo['previousNode']->getMtime(),
- [
- 'timestamp' => $node->getMTime(),
- 'size' => $node->getSize(),
- 'mimetype' => $this->mimeTypeLoader->getId($node->getMimetype()),
- ],
- );
- }
- } catch (Exception $e) {
- $this->logger->error('Failed to update existing version for ' . $node->getPath(), [
- 'exception' => $e,
- 'versionCreated' => $writeHookInfo['versionCreated'],
- 'previousNode' => [
- 'size' => $writeHookInfo['previousNode']->getSize(),
- 'mtime' => $writeHookInfo['previousNode']->getMTime(),
- ],
- 'node' => [
- 'size' => $node->getSize(),
- 'mtime' => $node->getMTime(),
- ]
- ]);
- throw $e;
- }
- }
- unset($this->writeHookInfo[$node->getId()]);
- }
- /**
- * Erase versions of deleted file
- *
- * This function is connected to the delete signal of OC_Filesystem
- * cleanup the versions directory if the actual file gets deleted
- */
- public function remove_hook(Node $node): void {
- // Need to normalize the path as there is an issue with path concatenation in View.php::getAbsolutePath.
- $path = Filesystem::normalizePath($node->getPath());
- if (!array_key_exists($path, $this->versionsDeleted)) {
- return;
- }
- $node = $this->versionsDeleted[$path];
- $relativePath = $this->getPathForNode($node);
- unset($this->versionsDeleted[$path]);
- Storage::delete($relativePath);
- // If no new version was stored in the FS, no new version should be added in the DB.
- // So we simply update the associated version.
- if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) {
- $this->versionManager->deleteVersionsEntity($node);
- }
- }
- /**
- * mark file as "deleted" so that we can clean up the versions if the file is gone
- */
- public function pre_remove_hook(Node $node): void {
- $path = $this->getPathForNode($node);
- Storage::markDeletedFile($path);
- $this->versionsDeleted[$node->getPath()] = $node;
- }
- /**
- * rename/move versions of renamed/moved files
- *
- * This function is connected to the rename signal of OC_Filesystem and adjust the name and location
- * of the stored versions along the actual file
- */
- public function rename_hook(Node $source, Node $target): void {
- $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage());
- $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage());
- // If different backends, do nothing.
- if ($sourceBackend !== $targetBackend) {
- return;
- }
- $oldPath = $this->getPathForNode($source);
- $newPath = $this->getPathForNode($target);
- Storage::renameOrCopy($oldPath, $newPath, 'rename');
- }
- /**
- * copy versions of copied files
- *
- * This function is connected to the copy signal of OC_Filesystem and copies the
- * the stored versions to the new location
- */
- public function copy_hook(Node $source, Node $target): void {
- $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage());
- $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage());
- // If different backends, do nothing.
- if ($sourceBackend !== $targetBackend) {
- return;
- }
- $oldPath = $this->getPathForNode($source);
- $newPath = $this->getPathForNode($target);
- Storage::renameOrCopy($oldPath, $newPath, 'copy');
- }
- /**
- * Remember owner and the owner path of the source file.
- * If the file already exists, then it was a upload of a existing file
- * over the web interface and we call Storage::store() directly
- *
- *
- */
- public function pre_renameOrCopy_hook(Node $source, Node $target): void {
- $sourceBackend = $this->versionManager->getBackendForStorage($source->getStorage());
- $targetBackend = $this->versionManager->getBackendForStorage($target->getParent()->getStorage());
- // If different backends, do nothing.
- if ($sourceBackend !== $targetBackend) {
- return;
- }
- // if we rename a movable mount point, then the versions don't have
- // to be renamed
- $oldPath = $this->getPathForNode($source);
- $newPath = $this->getPathForNode($target);
- $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files' . $oldPath);
- $manager = Filesystem::getMountManager();
- $mount = $manager->find($absOldPath);
- $internalPath = $mount->getInternalPath($absOldPath);
- if ($internalPath === '' and $mount instanceof MoveableMount) {
- return;
- }
- $view = new View(\OC_User::getUser() . '/files');
- if ($view->file_exists($newPath)) {
- Storage::store($newPath);
- } else {
- Storage::setSourcePathAndUser($oldPath);
- }
- }
- /**
- * Retrieve the path relative to the current user root folder.
- * If no user is connected, try to use the node's owner.
- */
- private function getPathForNode(Node $node): ?string {
- $user = $this->userSession->getUser()?->getUID();
- if ($user) {
- $path = $this->rootFolder
- ->getUserFolder($user)
- ->getRelativePath($node->getPath());
- if ($path !== null) {
- return $path;
- }
- }
- $owner = $node->getOwner()?->getUid();
- // If no owner, extract it from the path.
- // e.g. /user/files/foobar.txt
- if (!$owner) {
- $parts = explode('/', $node->getPath(), 4);
- if (count($parts) === 4) {
- $owner = $parts[1];
- }
- }
- if ($owner) {
- $path = $this->rootFolder
- ->getUserFolder($owner)
- ->getRelativePath($node->getPath());
- if ($path !== null) {
- return $path;
- }
- }
- return null;
- }
- }
|