aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_versions/lib/Versions
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_versions/lib/Versions')
-rw-r--r--apps/files_versions/lib/Versions/BackendNotFoundException.php22
-rw-r--r--apps/files_versions/lib/Versions/IDeletableVersionBackend.php19
-rw-r--r--apps/files_versions/lib/Versions/IMetadataVersion.php31
-rw-r--r--apps/files_versions/lib/Versions/IMetadataVersionBackend.php29
-rw-r--r--apps/files_versions/lib/Versions/INameableVersion.php22
-rw-r--r--apps/files_versions/lib/Versions/INameableVersionBackend.php22
-rw-r--r--apps/files_versions/lib/Versions/INeedSyncVersionBackend.php25
-rw-r--r--apps/files_versions/lib/Versions/IVersion.php25
-rw-r--r--apps/files_versions/lib/Versions/IVersionBackend.php32
-rw-r--r--apps/files_versions/lib/Versions/IVersionManager.php29
-rw-r--r--apps/files_versions/lib/Versions/IVersionsImporterBackend.php33
-rw-r--r--apps/files_versions/lib/Versions/LegacyVersionsBackend.php372
-rw-r--r--apps/files_versions/lib/Versions/Version.php96
-rw-r--r--apps/files_versions/lib/Versions/VersionManager.php145
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;
+ }
+ }
}