diff options
Diffstat (limited to 'apps/files_versions/lib/Versions')
14 files changed, 577 insertions, 325 deletions
diff --git a/apps/files_versions/lib/Versions/BackendNotFoundException.php b/apps/files_versions/lib/Versions/BackendNotFoundException.php index 151957c116a..f1fbecb852a 100644 --- a/apps/files_versions/lib/Versions/BackendNotFoundException.php +++ b/apps/files_versions/lib/Versions/BackendNotFoundException.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; diff --git a/apps/files_versions/lib/Versions/IDeletableVersionBackend.php b/apps/files_versions/lib/Versions/IDeletableVersionBackend.php index abb43d09d90..fefc038864f 100644 --- a/apps/files_versions/lib/Versions/IDeletableVersionBackend.php +++ b/apps/files_versions/lib/Versions/IDeletableVersionBackend.php @@ -3,23 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; diff --git a/apps/files_versions/lib/Versions/IMetadataVersion.php b/apps/files_versions/lib/Versions/IMetadataVersion.php new file mode 100644 index 00000000000..bc4cd77138b --- /dev/null +++ b/apps/files_versions/lib/Versions/IMetadataVersion.php @@ -0,0 +1,31 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Versions; + +/** + * This interface allows for just direct accessing of the metadata column JSON + * @since 29.0.0 + */ +interface IMetadataVersion { + /** + * retrieves the all the metadata + * + * @return string[] + * @since 29.0.0 + */ + public function getMetadata(): array; + + /** + * retrieves the metadata value from our $key param + * + * @param string $key the key for the json value of the metadata column + * @since 29.0.0 + */ + public function getMetadataValue(string $key): ?string; +} diff --git a/apps/files_versions/lib/Versions/IMetadataVersionBackend.php b/apps/files_versions/lib/Versions/IMetadataVersionBackend.php new file mode 100644 index 00000000000..79db85e460b --- /dev/null +++ b/apps/files_versions/lib/Versions/IMetadataVersionBackend.php @@ -0,0 +1,29 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Versions; + +use OCP\Files\Node; + +/** + * This interface edits the metadata column of a node. + * Each column of the metadata has a key => value mapping. + * @since 29.0.0 + */ +interface IMetadataVersionBackend { + /** + * Sets a key value pair in the metadata column corresponding to the node's version. + * + * @param Node $node the node that triggered the Metadata event listener, aka, the file version + * @param int $revision the key for the json value of the metadata column + * @param string $key the key for the json value of the metadata column + * @param string $value the value that corresponds to the key in the metadata column + * @since 29.0.0 + */ + public function setMetadataValue(Node $node, int $revision, string $key, string $value): void; +} diff --git a/apps/files_versions/lib/Versions/INameableVersion.php b/apps/files_versions/lib/Versions/INameableVersion.php index b6ddb951e25..a470239f128 100644 --- a/apps/files_versions/lib/Versions/INameableVersion.php +++ b/apps/files_versions/lib/Versions/INameableVersion.php @@ -3,33 +3,19 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; /** + * @deprecated 29.0.0 * @since 26.0.0 */ interface INameableVersion { /** * Get the user created label - * + * @deprecated 29.0.0 * @return string * @since 26.0.0 */ diff --git a/apps/files_versions/lib/Versions/INameableVersionBackend.php b/apps/files_versions/lib/Versions/INameableVersionBackend.php index 4a8c094cf18..d2ab7ed8135 100644 --- a/apps/files_versions/lib/Versions/INameableVersionBackend.php +++ b/apps/files_versions/lib/Versions/INameableVersionBackend.php @@ -3,33 +3,19 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; /** + * @deprecated 29.0.0 * @since 26.0.0 */ interface INameableVersionBackend { /** * Set the label for a version. - * + * @deprecated 29.0.0 * @since 26.0.0 */ public function setVersionLabel(IVersion $version, string $label): void; diff --git a/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php b/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php new file mode 100644 index 00000000000..e52e2f8e8bc --- /dev/null +++ b/apps/files_versions/lib/Versions/INeedSyncVersionBackend.php @@ -0,0 +1,25 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Versions; + +use OCA\Files_Versions\Db\VersionEntity; +use OCP\Files\File; + +/** + * @since 28.0.0 + */ +interface INeedSyncVersionBackend { + /** + * 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/IVersion.php b/apps/files_versions/lib/Versions/IVersion.php index 8ab3357b1e2..e5fd53d0157 100644 --- a/apps/files_versions/lib/Versions/IVersion.php +++ b/apps/files_versions/lib/Versions/IVersion.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; @@ -65,10 +48,10 @@ interface IVersion { /** * Get the size of this version * - * @return int + * @return int|float * @since 15.0.0 */ - public function getSize(): int; + public function getSize(): int|float; /** * Get the name of the source file at the time of making this version diff --git a/apps/files_versions/lib/Versions/IVersionBackend.php b/apps/files_versions/lib/Versions/IVersionBackend.php index c06e395e4c1..18f8c17f0ac 100644 --- a/apps/files_versions/lib/Versions/IVersionBackend.php +++ b/apps/files_versions/lib/Versions/IVersionBackend.php @@ -3,29 +3,12 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; +use OC\Files\Node\Node; use OCP\Files\File; use OCP\Files\FileInfo; use OCP\Files\NotFoundException; @@ -78,7 +61,7 @@ interface IVersionBackend { * Open the file for reading * * @param IVersion $version - * @return resource + * @return resource|false * @throws NotFoundException * @since 15.0.0 */ @@ -96,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/IVersionManager.php b/apps/files_versions/lib/Versions/IVersionManager.php index afc3046fa48..ecd424d0cc1 100644 --- a/apps/files_versions/lib/Versions/IVersionManager.php +++ b/apps/files_versions/lib/Versions/IVersionManager.php @@ -3,28 +3,13 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; +use OCP\Files\Storage\IStorage; + /** * @since 15.0.0 */ @@ -37,4 +22,10 @@ interface IVersionManager extends IVersionBackend { * @since 15.0.0 */ public function registerBackend(string $storageType, IVersionBackend $backend); + + /** + * @throws BackendNotFoundException + * @since 29.0.0 + */ + public function getBackendForStorage(IStorage $storage): IVersionBackend; } diff --git a/apps/files_versions/lib/Versions/IVersionsImporterBackend.php b/apps/files_versions/lib/Versions/IVersionsImporterBackend.php new file mode 100644 index 00000000000..db9349328e9 --- /dev/null +++ b/apps/files_versions/lib/Versions/IVersionsImporterBackend.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Versions; + +use OCP\Files\Node; +use OCP\IUser; + +/** + * @since 29.0.0 + */ +interface IVersionsImporterBackend { + /** + * Import the given versions for the target file. + * + * @param Node $source - The source might not exist anymore. + * @param IVersion[] $versions + * @since 29.0.0 + */ + public function importVersionsForFile(IUser $user, Node $source, Node $target, array $versions): void; + + /** + * Clear all versions for a file + * + * @since 29.0.0 + */ + public function clearVersionsForFile(IUser $user, Node $source, Node $target): void; +} diff --git a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php index cbfbc001e0c..48d69d31629 100644 --- a/apps/files_versions/lib/Versions/LegacyVersionsBackend.php +++ b/apps/files_versions/lib/Versions/LegacyVersionsBackend.php @@ -3,34 +3,19 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ + namespace OCA\Files_Versions\Versions; +use Exception; use OC\Files\View; -use OCA\Files_Sharing\SharedStorage; +use OCA\DAV\Connector\Sabre\Exception\Forbidden; 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; @@ -38,26 +23,22 @@ 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; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; -class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, IDeletableVersionBackend { - private IRootFolder $rootFolder; - private IUserManager $userManager; - private VersionsMapper $versionsMapper; - private IMimeTypeLoader $mimeTypeLoader; - +class LegacyVersionsBackend implements IVersionBackend, IDeletableVersionBackend, INeedSyncVersionBackend, IMetadataVersionBackend, IVersionsImporterBackend { public function __construct( - IRootFolder $rootFolder, - IUserManager $userManager, - VersionsMapper $versionsMapper, - IMimeTypeLoader $mimeTypeLoader + private IRootFolder $rootFolder, + private IUserManager $userManager, + private VersionsMapper $versionsMapper, + private IMimeTypeLoader $mimeTypeLoader, + private IUserSession $userSession, + private LoggerInterface $logger, ) { - $this->rootFolder = $rootFolder; - $this->userManager = $userManager; - $this->versionsMapper = $versionsMapper; - $this->mimeTypeLoader = $mimeTypeLoader; } public function useBackendForStorage(IStorage $storage): bool { @@ -66,66 +47,104 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, 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); - } - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - $nodes = $userFolder->getById($file->getId()); - $file2 = array_pop($nodes); + $fileId = $file->getId(); + if ($fileId === null) { + throw new NotFoundException("File not found ($fileId)"); + } + + if ($user === null) { + throw new NotFoundException("User $owner not found for $fileId"); + } - $versions = $this->getVersionsForFileFromDB($file2, $user); + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); - if (count($versions) > 0) { - return $versions; + $file = $userFolder->getFirstNodeById($fileId); + + if (!$file) { + throw new NotFoundException('version file not found for share owner'); + } + } else { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); } - // Insert the entry in the DB for the current version. - $versionEntity = new VersionEntity(); - $versionEntity->setFileId($file2->getId()); - $versionEntity->setTimestamp($file2->getMTime()); - $versionEntity->setSize($file2->getSize()); - $versionEntity->setMimetype($this->mimeTypeLoader->getId($file2->getMimetype())); - $versionEntity->setMetadata([]); - $this->versionsMapper->insert($versionEntity); + $fileId = $file->getId(); + if ($fileId === null) { + throw new NotFoundException("File not found ($fileId)"); + } // Insert entries in the DB for existing versions. - $versionsOnFS = Storage::getVersions($user->getUID(), $userFolder->getRelativePath($file2->getPath())); - foreach ($versionsOnFS as $version) { - $versionEntity = new VersionEntity(); - $versionEntity->setFileId($file2->getId()); - $versionEntity->setTimestamp((int)$version['version']); - $versionEntity->setSize((int)$version['size']); - $versionEntity->setMimetype($this->mimeTypeLoader->getId($version['mimetype'])); - $versionEntity->setMetadata([]); - $this->versionsMapper->insert($versionEntity); + $relativePath = $userFolder->getRelativePath($file->getPath()); + if ($relativePath === null) { + throw new NotFoundException("Relative path not found for file $fileId (" . $file->getPath() . ')'); } - return $this->getVersionsForFileFromDB($file2, $user); - } + $currentVersion = [ + 'version' => (string)$file->getMtime(), + 'size' => $file->getSize(), + 'mimetype' => $file->getMimetype(), + ]; - /** - * @return IVersion[] - */ - private function getVersionsForFileFromDB(Node $file, IUser $user): array { - $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $versionsInDB = $this->versionsMapper->findAllVersionsForFileId($file->getId()); + /** @var array<int, array> */ + $versionsInFS = array_values(Storage::getVersions($user->getUID(), $relativePath)); + + /** @var array<int, array{db: ?VersionEntity, fs: ?mixed}> */ + $groupedVersions = []; + $davVersions = []; + + foreach ($versionsInDB as $version) { + $revisionId = $version->getTimestamp(); + $groupedVersions[$revisionId] = $groupedVersions[$revisionId] ?? []; + $groupedVersions[$revisionId]['db'] = $version; + } - return array_map( - fn (VersionEntity $versionEntity) => new Version( - $versionEntity->getTimestamp(), - $versionEntity->getTimestamp(), + foreach ([$currentVersion, ...$versionsInFS] as $version) { + $revisionId = $version['version']; + $groupedVersions[$revisionId] = $groupedVersions[$revisionId] ?? []; + $groupedVersions[$revisionId]['fs'] = $version; + } + + /** @var array<string, array{db: ?VersionEntity, fs: ?mixed}> $groupedVersions */ + foreach ($groupedVersions as $versions) { + if (empty($versions['db']) && !empty($versions['fs'])) { + $versions['db'] = new VersionEntity(); + $versions['db']->setFileId($fileId); + $versions['db']->setTimestamp((int)$versions['fs']['version']); + $versions['db']->setSize((int)$versions['fs']['size']); + $versions['db']->setMimetype($this->mimeTypeLoader->getId($versions['fs']['mimetype'])); + $versions['db']->setMetadata([]); + $this->versionsMapper->insert($versions['db']); + } elseif (!empty($versions['db']) && empty($versions['fs'])) { + $this->versionsMapper->delete($versions['db']); + continue; + } + + $version = new Version( + $versions['db']->getTimestamp(), + $versions['db']->getTimestamp(), $file->getName(), - $versionEntity->getSize(), - $this->mimeTypeLoader->getMimetypeById($versionEntity->getMimetype()), + $versions['db']->getSize(), + $this->mimeTypeLoader->getMimetypeById($versions['db']->getMimetype()), $userFolder->getRelativePath($file->getPath()), $file, $this, $user, - $versionEntity->getLabel(), - ), - $this->versionsMapper->findAllVersionsForFileId($file->getId()) - ); + $versions['db']->getMetadata() ?? [], + ); + + array_push($davVersions, $version); + } + + return $davVersions; } public function createVersion(IUser $user, FileInfo $file) { @@ -144,6 +163,10 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, } public function rollback(IVersion $version) { + 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.'); + } + return Storage::rollback($version->getVersionPath(), $version->getRevisionId(), $version->getUser()); } @@ -168,25 +191,36 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, public function getVersionFile(IUser $user, FileInfo $sourceFile, $revision): File { $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $owner = $sourceFile->getOwner(); + $storage = $sourceFile->getStorage(); + + // 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 ISharedStorage $storage */ + $userFolder = $this->rootFolder->getUserFolder($owner->getUID()); + $user = $owner; + $ownerPathInStorage = $sourceFile->getInternalPath(); + $sourceFile = $storage->getShare()->getNode(); + if ($sourceFile instanceof Folder) { + $sourceFile = $sourceFile->get($ownerPathInStorage); + } + } + $versionFolder = $this->getVersionFolder($user); /** @var File $file */ $file = $versionFolder->get($userFolder->getRelativePath($sourceFile->getPath()) . '.v' . $revision); return $file; } - public function setVersionLabel(IVersion $version, string $label): void { - $versionEntity = $this->versionsMapper->findVersionForFileId( - $version->getSourceFile()->getId(), - $version->getTimestamp(), - ); - if (trim($label) === '') { - $label = null; - } - $versionEntity->setLabel($label ?? ''); - $this->versionsMapper->update($versionEntity); + public function getRevision(Node $node): int { + return $node->getMTime(); } public function deleteVersion(IVersion $version): void { + 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.'); + } + Storage::deleteRevision($version->getVersionPath(), $version->getRevisionId()); $versionEntity = $this->versionsMapper->findVersionForFileId( $version->getSourceFile()->getId(), @@ -194,4 +228,168 @@ class LegacyVersionsBackend implements IVersionBackend, INameableVersionBackend, ); $this->versionsMapper->delete($versionEntity); } + + 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([]); + + $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 { + $versionEntity = $this->versionsMapper->findVersionForFileId($sourceFile->getId(), $revision); + + if (isset($properties['timestamp'])) { + $versionEntity->setTimestamp($properties['timestamp']); + } + + if (isset($properties['size'])) { + $versionEntity->setSize($properties['size']); + } + + if (isset($properties['mimetype'])) { + $versionEntity->setMimetype($properties['mimetype']); + } + + $this->versionsMapper->update($versionEntity); + } + + public function deleteVersionsEntity(File $file): void { + $this->versionsMapper->deleteAllVersionsForFileId($file->getId()); + } + + private function currentUserHasPermissions(FileInfo $sourceFile, int $permissions): bool { + $currentUserId = $this->userSession->getUser()?->getUID(); + + if ($currentUserId === null) { + throw new NotFoundException('No user logged in'); + } + + if ($sourceFile->getOwner()?->getUID() === $currentUserId) { + return ($sourceFile->getPermissions() & $permissions) === $permissions; + } + + $nodes = $this->rootFolder->getUserFolder($currentUserId)->getById($sourceFile->getId()); + + if (count($nodes) === 0) { + throw new NotFoundException('Version file not accessible by current user'); + } + + foreach ($nodes as $node) { + if (($node->getPermissions() & $permissions) === $permissions) { + return true; + } + } + + return false; + } + + public function setMetadataValue(Node $node, int $revision, string $key, string $value): void { + 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.'); + } + + $versionEntity = $this->versionsMapper->findVersionForFileId($node->getId(), $revision); + + $versionEntity->setMetadataValue($key, $value); + $this->versionsMapper->update($versionEntity); + } + + + /** + * @inheritdoc + */ + public function importVersionsForFile(IUser $user, Node $source, Node $target, array $versions): void { + $userFolder = $this->rootFolder->getUserFolder($user->getUID()); + $relativePath = $userFolder->getRelativePath($target->getPath()); + + if ($relativePath === null) { + throw new \Exception('Target does not have a relative path' . $target->getPath()); + } + + $userView = new View('/' . $user->getUID()); + // create all parent folders + Storage::createMissingDirectories($relativePath, $userView); + Storage::scheduleExpire($user->getUID(), $relativePath); + + foreach ($versions as $version) { + // 1. Import the file in its new location. + // Nothing to do for the current version. + if ($version->getTimestamp() !== $source->getMTime()) { + $backend = $version->getBackend(); + $versionFile = $backend->getVersionFile($user, $source, $version->getRevisionId()); + $newVersionPath = 'files_versions/' . $relativePath . '.v' . $version->getTimestamp(); + + $versionContent = $versionFile->fopen('r'); + if ($versionContent === false) { + $this->logger->warning('Fail to open version file.', ['source' => $source, 'version' => $version, 'versionFile' => $versionFile]); + continue; + } + + $userView->file_put_contents($newVersionPath, $versionContent); + // ensure the file is scanned + $userView->getFileInfo($newVersionPath); + } + + // 2. Create the entity in the database + $versionEntity = new VersionEntity(); + $versionEntity->setFileId($target->getId()); + $versionEntity->setTimestamp($version->getTimestamp()); + $versionEntity->setSize($version->getSize()); + $versionEntity->setMimetype($this->mimeTypeLoader->getId($version->getMimetype())); + if ($version instanceof IMetadataVersion) { + $versionEntity->setMetadata($version->getMetadata()); + } + $this->versionsMapper->insert($versionEntity); + } + } + + /** + * @inheritdoc + */ + public function clearVersionsForFile(IUser $user, Node $source, Node $target): void { + $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()); + } + + $versionFolder = $this->rootFolder->get($userId . '/files_versions'); + if (!$versionFolder instanceof Folder) { + throw new Exception('User versions folder does not exist'); + } + + $versions = Storage::getVersions($userId, $relativePath); + foreach ($versions as $version) { + $versionFolder->get($version['path'] . '.v' . (int)$version['version'])->delete(); + } + + $this->versionsMapper->deleteAllVersionsForFileId($target->getId()); + } } diff --git a/apps/files_versions/lib/Versions/Version.php b/apps/files_versions/lib/Versions/Version.php index e87c2a593d7..e202a69b7d7 100644 --- a/apps/files_versions/lib/Versions/Version.php +++ b/apps/files_versions/lib/Versions/Version.php @@ -3,83 +3,27 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Versions; use OCP\Files\FileInfo; use OCP\IUser; -class Version implements IVersion, INameableVersion { - /** @var int */ - private $timestamp; - - /** @var int|string */ - private $revisionId; - - /** @var string */ - private $name; - - private string $label; - - /** @var int */ - private $size; - - /** @var string */ - private $mimetype; - - /** @var string */ - private $path; - - /** @var FileInfo */ - private $sourceFileInfo; - - /** @var IVersionBackend */ - private $backend; - - /** @var IUser */ - private $user; - +class Version implements IVersion, IMetadataVersion { public function __construct( - int $timestamp, - $revisionId, - string $name, - int $size, - string $mimetype, - string $path, - FileInfo $sourceFileInfo, - IVersionBackend $backend, - IUser $user, - string $label = '' + private int $timestamp, + private int|string $revisionId, + private string $name, + private int|float $size, + private string $mimetype, + private string $path, + private FileInfo $sourceFileInfo, + private IVersionBackend $backend, + private IUser $user, + private array $metadata = [], ) { - $this->timestamp = $timestamp; - $this->revisionId = $revisionId; - $this->name = $name; - $this->label = $label; - $this->size = $size; - $this->mimetype = $mimetype; - $this->path = $path; - $this->sourceFileInfo = $sourceFileInfo; - $this->backend = $backend; - $this->user = $user; } public function getBackend(): IVersionBackend { @@ -98,7 +42,7 @@ class Version implements IVersion, INameableVersion { return $this->timestamp; } - public function getSize(): int { + public function getSize(): int|float { return $this->size; } @@ -106,10 +50,6 @@ class Version implements IVersion, INameableVersion { return $this->name; } - public function getLabel(): string { - return $this->label; - } - public function getMimeType(): string { return $this->mimetype; } @@ -121,4 +61,12 @@ class Version implements IVersion, INameableVersion { public function getUser(): IUser { return $this->user; } + + public function getMetadata(): array { + return $this->metadata; + } + + public function getMetadataValue(string $key): ?string { + return $this->metadata[$key] ?? null; + } } diff --git a/apps/files_versions/lib/Versions/VersionManager.php b/apps/files_versions/lib/Versions/VersionManager.php index bfae0937df8..9acea8c6513 100644 --- a/apps/files_versions/lib/Versions/VersionManager.php +++ b/apps/files_versions/lib/Versions/VersionManager.php @@ -3,37 +3,37 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2018 Robin Appelman <robin@icewind.nl> - * - * @author Robin Appelman <robin@icewind.nl> - * - * @license GNU AGPL version 3 or any later version - * - * This program is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License as - * published by the Free Software Foundation, either version 3 of the - * License, or (at your option) any later version. - * - * 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 - * along with this program. If not, see <http://www.gnu.org/licenses/>. - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ 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; +use OCP\Files\Lock\ILock; +use OCP\Files\Lock\ILockManager; +use OCP\Files\Lock\LockContext; +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 { -class VersionManager implements IVersionManager, INameableVersionBackend, IDeletableVersionBackend { /** @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] = []; @@ -62,8 +62,8 @@ class VersionManager implements IVersionManager, INameableVersionBackend, IDelet 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 */ @@ -94,7 +94,12 @@ class VersionManager implements IVersionManager, INameableVersionBackend, IDelet public function rollback(IVersion $version) { $backend = $version->getBackend(); - return $backend->rollback($version); + $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) { + $this->dispatcher->dispatchTyped(new VersionRestoredEvent($version)); + } + return $result; } public function read(IVersion $version) { @@ -107,21 +112,99 @@ class VersionManager implements IVersionManager, INameableVersionBackend, IDelet return $backend->getVersionFile($user, $sourceFile, $revision); } - public function useBackendForStorage(IStorage $storage): bool { - return false; + public function getRevision(Node $node): int { + $backend = $this->getBackendForStorage($node->getStorage()); + return $backend->getRevision($node); } - public function setVersionLabel(IVersion $version, string $label): void { - $backend = $this->getBackendForStorage($version->getSourceFile()->getStorage()); - if ($backend instanceof INameableVersionBackend) { - $backend->setVersionLabel($version, $label); - } + public function useBackendForStorage(IStorage $storage): bool { + return false; } public function deleteVersion(IVersion $version): void { - $backend = $this->getBackendForStorage($version->getSourceFile()->getStorage()); + $backend = $version->getBackend(); if ($backend instanceof IDeletableVersionBackend) { $backend->deleteVersion($version); } } + + public function createVersionEntity(File $file): void { + $backend = $this->getBackendForStorage($file->getStorage()); + if ($backend instanceof INeedSyncVersionBackend) { + $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; + } + } + } + } + } + + public function updateVersionEntity(File $sourceFile, int $revision, array $properties): void { + $backend = $this->getBackendForStorage($sourceFile->getStorage()); + if ($backend instanceof INeedSyncVersionBackend) { + $backend->updateVersionEntity($sourceFile, $revision, $properties); + } + } + + public function deleteVersionsEntity(File $file): void { + $backend = $this->getBackendForStorage($file->getStorage()); + if ($backend instanceof INeedSyncVersionBackend) { + $backend->deleteVersionsEntity($file); + } + } + + public function setMetadataValue(Node $node, int $revision, string $key, string $value): void { + $backend = $this->getBackendForStorage($node->getStorage()); + if ($backend instanceof IMetadataVersionBackend) { + $backend->setMetadataValue($node, $revision, $key, $value); + } + } + + /** + * Catch ManuallyLockedException and retry in app context if possible. + * + * Allow users to go back to old versions via the versions tab in the sidebar + * even when the file is opened in the viewer next to it. + * + * Context: If a file is currently opened for editing + * the files_lock app will throw ManuallyLockedExceptions. + * This prevented the user from rolling an opened file back to a previous version. + * + * Text and Richdocuments can handle changes of open files. + * So we execute the rollback under their lock context + * to let them handle the conflict. + * + * @param callable $callback function to run with app locks handled + * @return bool|null + * @throws ManuallyLockedException + * + */ + private static function handleAppLocks(callable $callback): ?bool { + try { + return $callback(); + } catch (ManuallyLockedException $e) { + $owner = (string)$e->getOwner(); + $appsThatHandleUpdates = ['text', 'richdocuments']; + if (!in_array($owner, $appsThatHandleUpdates)) { + throw $e; + } + // The LockWrapper in the files_lock app only compares the lock type and owner + // 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 = Server::get(IRootFolder::class); + $lockContext = new LockContext($root, ILock::TYPE_APP, $owner); + $lockManager = Server::get(ILockManager::class); + $result = null; + $lockManager->runInScope($lockContext, function () use ($callback, &$result): void { + $result = $callback(); + }); + return $result; + } + } } |