diff options
Diffstat (limited to 'apps/files_versions/lib/Versions')
5 files changed, 99 insertions, 31 deletions
diff --git a/apps/files_versions/lib/Versions/BackendNotFoundException.php b/apps/files_versions/lib/Versions/BackendNotFoundException.php index af903e9abeb..f1fbecb852a 100644 --- a/apps/files_versions/lib/Versions/BackendNotFoundException.php +++ b/apps/files_versions/lib/Versions/BackendNotFoundException.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php b/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php index 4faeaa48513..e52e2f8e8bc 100644 --- a/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php +++ b/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php @@ -8,13 +8,18 @@ declare(strict_types=1); */ namespace OCA\Files_Versions\Versions; +use OCA\Files_Versions\Db\VersionEntity; use OCP\Files\File; /** * @since 28.0.0 */ interface INeedSyncVersionBackend { - public function createVersionEntity(File $file): void; + /** + * TODO: Convert return type to strong type once all implementations are fixed. + * @return null|VersionEntity + */ + public function createVersionEntity(File $file); public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void; public function deleteVersionsEntity(File $file): void; } diff --git a/apps/files_versions/lib/Versions/IVersionBackend.php b/apps/files_versions/lib/Versions/IVersionBackend.php index 28e3f4dffcf..18f8c17f0ac 100644 --- a/apps/files_versions/lib/Versions/IVersionBackend.php +++ b/apps/files_versions/lib/Versions/IVersionBackend.php @@ -8,6 +8,7 @@ declare(strict_types=1); */ namespace OCA\Files_Versions\Versions; +use OC\Files\Node\Node; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\Files\NotFoundException; @@ -78,4 +79,11 @@ interface IVersionBackend { * @since 15.0.0 */ public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File; + + /** + * Get the revision for a node + * + * @since 32.0.0 + */ + public function getRevision(Node $node): int; } diff --git a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php index 1483d03bcc6..48d69d31629 100644 --- a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php +++ b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php @@ -12,11 +12,10 @@ namespace OCA\Files_Versions\Versions; use Exception; use OC\Files\View; use OCA\DAV\Connector\Sabre\Exception\Forbidden; -use OCA\Files_Sharing\ISharedStorage; -use OCA\Files_Sharing\SharedStorage; use OCA\Files_Versions\Db\VersionEntity; use OCA\Files_Versions\Db\VersionsMapper; use OCA\Files_Versions\Storage; +use OCP\Constants; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\Files\Folder; @@ -24,6 +23,7 @@ use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; use OCP\Files\Node; use OCP\Files\NotFoundException; +use OCP\Files\Storage\ISharedStorage; use OCP\Files\Storage\IStorage; use OCP\IUser; use OCP\IUserManager; @@ -48,8 +48,12 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend public function getVersionsForFile(IUser $user, FileInfo $file): array { $storage = $file->getStorage(); - if ($storage->instanceOfStorage(SharedStorage::class)) { + if ($storage->instanceOfStorage(ISharedStorage::class)) { $owner = $storage->getOwner(''); + if ($owner === false) { + throw new NotFoundException('No owner for ' . $file->getPath()); + } + $user = $this->userManager->get($owner); $fileId = $file->getId(); @@ -66,7 +70,7 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend $file = $userFolder->getFirstNodeById($fileId); if (!$file) { - throw new NotFoundException("version file not found for share owner"); + throw new NotFoundException('version file not found for share owner'); } } else { $userFolder = $this->rootFolder->getUserFolder($user->getUID()); @@ -159,7 +163,7 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend } public function rollback(IVersion $version) { - if (!$this->currentUserHasPermissions($version->getSourceFile(), \OCP\Constants::PERMISSION_UPDATE)) { + if (!$this->currentUserHasPermissions($version->getSourceFile(), Constants::PERMISSION_UPDATE)) { throw new Forbidden('You cannot restore this version because you do not have update permissions on the source file.'); } @@ -192,7 +196,7 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend // Shared files have their versions in the owners root folder so we need to obtain them from there if ($storage->instanceOfStorage(ISharedStorage::class) && $owner) { - /** @var SharedStorage $storage */ + /** @var ISharedStorage $storage */ $userFolder = $this->rootFolder->getUserFolder($owner->getUID()); $user = $owner; $ownerPathInStorage = $sourceFile->getInternalPath(); @@ -208,8 +212,12 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend return $file; } + public function getRevision(Node $node): int { + return $node->getMTime(); + } + public function deleteVersion(IVersion $version): void { - if (!$this->currentUserHasPermissions($version->getSourceFile(), \OCP\Constants::PERMISSION_DELETE)) { + if (!$this->currentUserHasPermissions($version->getSourceFile(), Constants::PERMISSION_DELETE)) { throw new Forbidden('You cannot delete this version because you do not have delete permissions on the source file.'); } @@ -221,14 +229,35 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend $this->versionsMapper->delete($versionEntity); } - public function createVersionEntity(File $file): void { + public function createVersionEntity(File $file): ?VersionEntity { $versionEntity = new VersionEntity(); $versionEntity->setFileId($file->getId()); $versionEntity->setTimestamp($file->getMTime()); $versionEntity->setSize($file->getSize()); $versionEntity->setMimetype($this->mimeTypeLoader->getId($file->getMimetype())); $versionEntity->setMetadata([]); - $this->versionsMapper->insert($versionEntity); + + $tries = 1; + while ($tries < 5) { + try { + $this->versionsMapper->insert($versionEntity); + return $versionEntity; + } catch (\OCP\DB\Exception $e) { + if (!in_array($e->getReason(), [ + \OCP\DB\Exception::REASON_CONSTRAINT_VIOLATION, + \OCP\DB\Exception::REASON_UNIQUE_CONSTRAINT_VIOLATION, + ]) + ) { + throw $e; + } + /* Conflict with another version, increase mtime and try again */ + $versionEntity->setTimestamp($versionEntity->getTimestamp() + 1); + $tries++; + $this->logger->warning('Constraint violation while inserting version, retrying with increased timestamp', ['exception' => $e]); + } + } + + return null; } public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void { @@ -257,7 +286,7 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend $currentUserId = $this->userSession->getUser()?->getUID(); if ($currentUserId === null) { - throw new NotFoundException("No user logged in"); + throw new NotFoundException('No user logged in'); } if ($sourceFile->getOwner()?->getUID() === $currentUserId) { @@ -267,7 +296,7 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend $nodes = $this->rootFolder->getUserFolder($currentUserId)->getById($sourceFile->getId()); if (count($nodes) === 0) { - throw new NotFoundException("Version file not accessible by current user"); + throw new NotFoundException('Version file not accessible by current user'); } foreach ($nodes as $node) { @@ -280,7 +309,7 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend } public function setMetadataValue(Node $node, int $revision, string $key, string $value): void { - if (!$this->currentUserHasPermissions($node, \OCP\Constants::PERMISSION_UPDATE)) { + if (!$this->currentUserHasPermissions($node, Constants::PERMISSION_UPDATE)) { throw new Forbidden('You cannot update the version\'s metadata because you do not have update permissions on the source file.'); } @@ -343,16 +372,20 @@ class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend * @inheritdoc */ public function clearVersionsForFile(IUser $user, Node $source, Node $target): void { - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $userId = $user->getUID(); + $userFolder = $this->rootFolder->getUserFolder($userId); $relativePath = $userFolder->getRelativePath($source->getPath()); if ($relativePath === null) { - throw new Exception("Relative path not found for node with path: " . $source->getPath()); + throw new Exception('Relative path not found for node with path: ' . $source->getPath()); + } + + $versionFolder = $this->rootFolder->get($userId . '/files_versions'); + if (!$versionFolder instanceof Folder) { + throw new Exception('User versions folder does not exist'); } - $versions = Storage::getVersions($user->getUID(), $relativePath); - /** @var Folder versionFolder */ - $versionFolder = $this->rootFolder->get('admin/files_versions'); + $versions = Storage::getVersions($userId, $relativePath); foreach ($versions as $version) { $versionFolder->get($version['path'] . '.v' . (int)$version['version'])->delete(); } diff --git a/apps/files_versions/lib/Versions/VersionManager.php b/apps/files_versions/lib/Versions/VersionManager.php index c666192f3c0..9acea8c6513 100644 --- a/apps/files_versions/lib/Versions/VersionManager.php +++ b/apps/files_versions/lib/Versions/VersionManager.php @@ -8,6 +8,10 @@ declare(strict_types=1); */ namespace OCA\Files_Versions\Versions; +use OCA\Files_Versions\Db\VersionEntity; +use OCA\Files_Versions\Events\VersionCreatedEvent; +use OCA\Files_Versions\Events\VersionRestoredEvent; +use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\Files\IRootFolder; @@ -18,11 +22,18 @@ use OCP\Files\Node; use OCP\Files\Storage\IStorage; use OCP\IUser; use OCP\Lock\ManuallyLockedException; +use OCP\Server; class VersionManager implements IVersionManager, IDeletableVersionBackend, INeedSyncVersionBackend, IMetadataVersionBackend { + /** @var (IVersionBackend[])[] */ private $backends = []; + public function __construct( + private IEventDispatcher $dispatcher, + ) { + } + public function registerBackend(string $storageType, IVersionBackend $backend) { if (!isset($this->backends[$storageType])) { $this->backends[$storageType] = []; @@ -51,8 +62,8 @@ class VersionManager implements IVersionManager, IDeletableVersionBackend, INeed foreach ($backends as $type => $backendsForType) { if ( - $storage->instanceOfStorage($type) && - ($foundType === '' || is_subclass_of($type, $foundType)) + $storage->instanceOfStorage($type) + && ($foundType === '' || is_subclass_of($type, $foundType)) ) { foreach ($backendsForType as $backend) { /** @var IVersionBackend $backend */ @@ -86,11 +97,7 @@ class VersionManager implements IVersionManager, IDeletableVersionBackend, INeed $result = self::handleAppLocks(fn (): ?bool => $backend->rollback($version)); // rollback doesn't have a return type yet and some implementations don't return anything if ($result === null || $result === true) { - \OC_Hook::emit('\OCP\Versions', 'rollback', [ - 'path' => $version->getVersionPath(), - 'revision' => $version->getRevisionId(), - 'node' => $version->getSourceFile(), - ]); + $this->dispatcher->dispatchTyped(new VersionRestoredEvent($version)); } return $result; } @@ -105,6 +112,11 @@ class VersionManager implements IVersionManager, IDeletableVersionBackend, INeed return $backend->getVersionFile($user, $sourceFile, $revision); } + public function getRevision(Node $node): int { + $backend = $this->getBackendForStorage($node->getStorage()); + return $backend->getRevision($node); + } + public function useBackendForStorage(IStorage $storage): bool { return false; } @@ -119,7 +131,16 @@ class VersionManager implements IVersionManager, IDeletableVersionBackend, INeed public function createVersionEntity(File $file): void { $backend = $this->getBackendForStorage($file->getStorage()); if ($backend instanceof INeedSyncVersionBackend) { - $backend->createVersionEntity($file); + $versionEntity = $backend->createVersionEntity($file); + + if ($versionEntity instanceof VersionEntity) { + foreach ($backend->getVersionsForFile($file->getOwner(), $file) as $version) { + if ($version->getRevisionId() === $versionEntity->getTimestamp()) { + $this->dispatcher->dispatchTyped(new VersionCreatedEvent($file, $version)); + break; + } + } + } } } @@ -167,8 +188,8 @@ class VersionManager implements IVersionManager, IDeletableVersionBackend, INeed try { return $callback(); } catch (ManuallyLockedException $e) { - $owner = (string) $e->getOwner(); - $appsThatHandleUpdates = ["text", "richdocuments"]; + $owner = (string)$e->getOwner(); + $appsThatHandleUpdates = ['text', 'richdocuments']; if (!in_array($owner, $appsThatHandleUpdates)) { throw $e; } @@ -176,11 +197,11 @@ class VersionManager implements IVersionManager, IDeletableVersionBackend, INeed // when checking the lock against the current scope. // So we do not need to get the actual node here // and use the root node instead. - $root = \OC::$server->get(IRootFolder::class); + $root = Server::get(IRootFolder::class); $lockContext = new LockContext($root, ILock::TYPE_APP, $owner); - $lockManager = \OC::$server->get(ILockManager::class); + $lockManager = Server::get(ILockManager::class); $result = null; - $lockManager->runInScope($lockContext, function () use ($callback, &$result) { + $lockManager->runInScope($lockContext, function () use ($callback, &$result): void { $result = $callback(); }); return $result; |