diff options
Diffstat (limited to 'apps/files_versions/lib')
42 files changed, 1625 insertions, 1263 deletions
diff --git a/apps/files_versions/lib/AppInfo/Application.php b/apps/files_versions/lib/AppInfo/Application.php index a07058d04a7..29158276415 100644 --- a/apps/files_versions/lib/AppInfo/Application.php +++ b/apps/files_versions/lib/AppInfo/Application.php @@ -1,29 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions\AppInfo; @@ -33,9 +13,13 @@ use OCA\DAV\Connector\Sabre\Principal; use OCA\Files\Event\LoadAdditionalScriptsEvent; use OCA\Files\Event\LoadSidebar; use OCA\Files_Versions\Capabilities; +use OCA\Files_Versions\Events\VersionRestoredEvent; use OCA\Files_Versions\Listener\FileEventsListener; +use OCA\Files_Versions\Listener\LegacyRollbackListener; use OCA\Files_Versions\Listener\LoadAdditionalListener; use OCA\Files_Versions\Listener\LoadSidebarListener; +use OCA\Files_Versions\Listener\VersionAuthorListener; +use OCA\Files_Versions\Listener\VersionStorageMoveListener; use OCA\Files_Versions\Versions\IVersionManager; use OCA\Files_Versions\Versions\VersionManager; use OCP\Accounts\IAccountManager; @@ -48,11 +32,11 @@ use OCP\Files\Events\Node\BeforeNodeCopiedEvent; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeRenamedEvent; use OCP\Files\Events\Node\BeforeNodeTouchedEvent; +use OCP\Files\Events\Node\BeforeNodeWrittenEvent; use OCP\Files\Events\Node\NodeCopiedEvent; +use OCP\Files\Events\Node\NodeCreatedEvent; use OCP\Files\Events\Node\NodeDeletedEvent; use OCP\Files\Events\Node\NodeRenamedEvent; -use OCP\Files\Events\Node\BeforeNodeWrittenEvent; -use OCP\Files\Events\Node\NodeCreatedEvent; use OCP\Files\Events\Node\NodeTouchedEvent; use OCP\Files\Events\Node\NodeWrittenEvent; use OCP\IConfig; @@ -61,6 +45,7 @@ use OCP\IServerContainer; use OCP\IUserManager; use OCP\IUserSession; use OCP\L10N\IFactory; +use OCP\Server; use OCP\Share\IManager as IShareManager; use Psr\Container\ContainerInterface; use Psr\Log\LoggerInterface; @@ -87,7 +72,7 @@ class Application extends App implements IBootstrap { return new Principal( $server->get(IUserManager::class), $server->get(IGroupManager::class), - \OC::$server->get(IAccountManager::class), + Server::get(IAccountManager::class), $server->get(IShareManager::class), $server->get(IUserSession::class), $server->get(IAppManager::class), @@ -98,9 +83,7 @@ class Application extends App implements IBootstrap { ); }); - $context->registerService(IVersionManager::class, function () { - return new VersionManager(); - }); + $context->registerServiceAlias(IVersionManager::class, VersionManager::class); /** * Register Events @@ -108,6 +91,11 @@ class Application extends App implements IBootstrap { $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class); $context->registerEventListener(LoadSidebar::class, LoadSidebarListener::class); + $context->registerEventListener(BeforeNodeRenamedEvent::class, VersionStorageMoveListener::class); + $context->registerEventListener(NodeRenamedEvent::class, VersionStorageMoveListener::class); + $context->registerEventListener(BeforeNodeCopiedEvent::class, VersionStorageMoveListener::class); + $context->registerEventListener(NodeCopiedEvent::class, VersionStorageMoveListener::class); + $context->registerEventListener(NodeCreatedEvent::class, FileEventsListener::class); $context->registerEventListener(BeforeNodeTouchedEvent::class, FileEventsListener::class); $context->registerEventListener(NodeTouchedEvent::class, FileEventsListener::class); @@ -119,6 +107,11 @@ class Application extends App implements IBootstrap { $context->registerEventListener(NodeCopiedEvent::class, FileEventsListener::class); $context->registerEventListener(BeforeNodeRenamedEvent::class, FileEventsListener::class); $context->registerEventListener(BeforeNodeCopiedEvent::class, FileEventsListener::class); + + // we add the version author listener with lower priority to make sure new versions already are created by FileEventsListener + $context->registerEventListener(NodeWrittenEvent::class, VersionAuthorListener::class, -1); + + $context->registerEventListener(VersionRestoredEvent::class, LegacyRollbackListener::class); } public function boot(IBootContext $context): void { @@ -126,7 +119,7 @@ class Application extends App implements IBootstrap { } public function registerVersionBackends(ContainerInterface $container, IAppManager $appManager, LoggerInterface $logger): void { - foreach ($appManager->getInstalledApps() as $app) { + foreach ($appManager->getEnabledApps() as $app) { $appInfo = $appManager->getAppInfo($app); if (isset($appInfo['versions'])) { $backends = $appInfo['versions']; diff --git a/apps/files_versions/lib/BackgroundJob/ExpireVersions.php b/apps/files_versions/lib/BackgroundJob/ExpireVersions.php index 7e714a059d0..794cbc5b882 100644 --- a/apps/files_versions/lib/BackgroundJob/ExpireVersions.php +++ b/apps/files_versions/lib/BackgroundJob/ExpireVersions.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions\BackgroundJob; +use OC\Files\View; use OCA\Files_Versions\Expiration; use OCA\Files_Versions\Storage; use OCP\AppFramework\Utility\ITimeFactory; @@ -36,18 +19,15 @@ use OCP\IUserManager; class ExpireVersions extends TimedJob { public const ITEMS_PER_SESSION = 1000; - private IConfig $config; - private Expiration $expiration; - private IUserManager $userManager; - - public function __construct(IConfig $config, IUserManager $userManager, Expiration $expiration, ITimeFactory $time) { + public function __construct( + private IConfig $config, + private IUserManager $userManager, + private Expiration $expiration, + ITimeFactory $time, + ) { parent::__construct($time); // Run once per 30 minutes $this->setInterval(60 * 30); - - $this->config = $config; - $this->expiration = $expiration; - $this->userManager = $userManager; } public function run($argument) { @@ -61,7 +41,7 @@ class ExpireVersions extends TimedJob { return; } - $this->userManager->callForSeenUsers(function (IUser $user) { + $this->userManager->callForSeenUsers(function (IUser $user): void { $uid = $user->getUID(); if (!$this->setupFS($uid)) { return; @@ -78,7 +58,7 @@ class ExpireVersions extends TimedJob { \OC_Util::setupFS($user); // Check if this user has a versions directory - $view = new \OC\Files\View('/' . $user); + $view = new View('/' . $user); if (!$view->is_dir('/files_versions')) { return false; } diff --git a/apps/files_versions/lib/Capabilities.php b/apps/files_versions/lib/Capabilities.php index 6524943690a..cb6394f0a36 100644 --- a/apps/files_versions/lib/Capabilities.php +++ b/apps/files_versions/lib/Capabilities.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christopher Schäpers <kondou@ts.unde.re> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Tom Needham <tom@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions; @@ -29,30 +12,23 @@ use OCP\Capabilities\ICapability; use OCP\IConfig; class Capabilities implements ICapability { - private IConfig $config; - private IAppManager $appManager; - public function __construct( - IConfig $config, - IAppManager $appManager + private IConfig $config, + private IAppManager $appManager, ) { - $this->config = $config; - $this->appManager = $appManager; } /** * Return this classes capabilities * - * @return array + * @return array{files: array{versioning: bool, version_labeling: bool, version_deletion: bool}} */ public function getCapabilities() { - $groupFolderOrS3VersioningInstalled = $this->appManager->isInstalled('groupfolders') || $this->appManager->isInstalled('groupfolders'); - return [ 'files' => [ 'versioning' => true, - 'version_labeling' => !$groupFolderOrS3VersioningInstalled && $this->config->getSystemValueBool('enable_version_labeling', true), - 'version_deletion' => !$groupFolderOrS3VersioningInstalled && $this->config->getSystemValueBool('enable_version_deletion', true), + 'version_labeling' => $this->config->getSystemValueBool('enable_version_labeling', true), + 'version_deletion' => $this->config->getSystemValueBool('enable_version_deletion', true), ] ]; } diff --git a/apps/files_versions/lib/Command/CleanUp.php b/apps/files_versions/lib/Command/CleanUp.php index d7bb4caa483..e8c46afef16 100644 --- a/apps/files_versions/lib/Command/CleanUp.php +++ b/apps/files_versions/lib/Command/CleanUp.php @@ -1,29 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Daniel Rudolf <nextcloud.com@daniel-rudolf.de> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions\Command; +use OCA\Files_Versions\Db\VersionsMapper; use OCP\Files\IRootFolder; use OCP\IUserBackend; use OCP\IUserManager; @@ -34,24 +18,15 @@ use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; class CleanUp extends Command { - - /** @var IUserManager */ - protected $userManager; - - /** @var IRootFolder */ - protected $rootFolder; - - /** - * @param IRootFolder $rootFolder - * @param IUserManager $userManager - */ - public function __construct(IRootFolder $rootFolder, IUserManager $userManager) { + public function __construct( + protected IRootFolder $rootFolder, + protected IUserManager $userManager, + protected VersionsMapper $versionMapper, + ) { parent::__construct(); - $this->userManager = $userManager; - $this->rootFolder = $rootFolder; } - protected function configure() { + protected function configure(): void { $this ->setName('versions:cleanup') ->setDescription('Delete versions') @@ -75,8 +50,8 @@ class CleanUp extends Command { $path = $input->getOption('path'); if ($path) { if (!preg_match('#^/([^/]+)/files(/.*)?$#', $path, $pathMatches)) { - $output->writeln("<error>Invalid path given</error>"); - return 1; + $output->writeln('<error>Invalid path given</error>'); + return self::FAILURE; } $users = [ $pathMatches[1] ]; @@ -85,51 +60,53 @@ class CleanUp extends Command { if (!empty($users)) { foreach ($users as $user) { - if ($this->userManager->userExists($user)) { - $output->writeln("Delete versions of <info>$user</info>"); - $this->deleteVersions($user, $path); - } else { + if (!$this->userManager->userExists($user)) { $output->writeln("<error>Unknown user $user</error>"); - return 1; + return self::FAILURE; } + + $output->writeln("Delete versions of <info>$user</info>"); + $this->deleteVersions($user, $path); } - } else { - $output->writeln('Delete all versions'); - foreach ($this->userManager->getBackends() as $backend) { - $name = get_class($backend); + return self::SUCCESS; + } - if ($backend instanceof IUserBackend) { - $name = $backend->getBackendName(); - } + $output->writeln('Delete all versions'); + foreach ($this->userManager->getBackends() as $backend) { + $name = get_class($backend); - $output->writeln("Delete versions for users on backend <info>$name</info>"); - - $limit = 500; - $offset = 0; - do { - $users = $backend->getUsers('', $limit, $offset); - foreach ($users as $user) { - $output->writeln(" <info>$user</info>"); - $this->deleteVersions($user); - } - $offset += $limit; - } while (count($users) >= $limit); + if ($backend instanceof IUserBackend) { + $name = $backend->getBackendName(); } + + $output->writeln("Delete versions for users on backend <info>$name</info>"); + + $limit = 500; + $offset = 0; + do { + $users = $backend->getUsers('', $limit, $offset); + foreach ($users as $user) { + $output->writeln(" <info>$user</info>"); + $this->deleteVersions($user); + } + $offset += $limit; + } while (count($users) >= $limit); } - return 0; + + return self::SUCCESS; } /** * delete versions for the given user - * - * @param string $user - * @param string|null $path */ - protected function deleteVersions(string $user, string $path = null): void { + protected function deleteVersions(string $user, ?string $path = null): void { \OC_Util::tearDownFS(); \OC_Util::setupFS($user); + $userHomeStorageId = $this->rootFolder->getUserFolder($user)->getStorage()->getCache()->getNumericStorageId(); + $this->versionMapper->deleteAllVersionsForUser($userHomeStorageId, $path); + $fullPath = '/' . $user . '/files_versions' . ($path ? '/' . $path : ''); if ($this->rootFolder->nodeExists($fullPath)) { $this->rootFolder->get($fullPath)->delete(); diff --git a/apps/files_versions/lib/Command/Expire.php b/apps/files_versions/lib/Command/Expire.php index 62b2343a5e0..a30e623c347 100644 --- a/apps/files_versions/lib/Command/Expire.php +++ b/apps/files_versions/lib/Command/Expire.php @@ -1,25 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Robin Appelman <robin@icewind.nl> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions\Command; @@ -28,29 +12,21 @@ use OCA\Files_Versions\Storage; use OCP\Command\ICommand; use OCP\Files\StorageNotAvailableException; use OCP\IUserManager; +use OCP\Server; use Psr\Log\LoggerInterface; class Expire implements ICommand { use FileAccess; - /** - * @var string - */ - private $fileName; - - /** - * @var string - */ - private $user; - - public function __construct(string $user, string $fileName) { - $this->user = $user; - $this->fileName = $fileName; + public function __construct( + private string $user, + private string $fileName, + ) { } - public function handle() { + public function handle(): void { /** @var IUserManager $userManager */ - $userManager = \OC::$server->get(IUserManager::class); + $userManager = Server::get(IUserManager::class); if (!$userManager->userExists($this->user)) { // User has been deleted already return; @@ -62,8 +38,7 @@ class Expire implements ICommand { // In case of external storage and session credentials, the expiration // fails because the command does not have those credentials - /** @var LoggerInterface */ - $logger = \OC::$server->get(LoggerInterface::class); + $logger = Server::get(LoggerInterface::class); $logger->warning($e->getMessage(), [ 'exception' => $e, 'uid' => $this->user, diff --git a/apps/files_versions/lib/Command/ExpireVersions.php b/apps/files_versions/lib/Command/ExpireVersions.php index 43068e21451..d3f341a21d2 100644 --- a/apps/files_versions/lib/Command/ExpireVersions.php +++ b/apps/files_versions/lib/Command/ExpireVersions.php @@ -1,30 +1,13 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud GmbH. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud GmbH. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions\Command; +use OC\Files\View; use OCA\Files_Versions\Expiration; use OCA\Files_Versions\Storage; use OCP\IUser; @@ -36,73 +19,58 @@ use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Output\OutputInterface; class ExpireVersions extends Command { - - /** - * @var Expiration - */ - private $expiration; - - /** - * @var IUserManager - */ - private $userManager; - - /** - * @param IUserManager $userManager - * @param Expiration $expiration - */ - public function __construct(IUserManager $userManager, - Expiration $expiration) { + public function __construct( + private IUserManager $userManager, + private Expiration $expiration, + ) { parent::__construct(); - - $this->userManager = $userManager; - $this->expiration = $expiration; } - protected function configure() { + protected function configure(): void { $this ->setName('versions:expire') ->setDescription('Expires the users file versions') ->addArgument( 'user_id', InputArgument::OPTIONAL | InputArgument::IS_ARRAY, - 'expire file versions of the given user(s), if no user is given file versions for all users will be expired.' + 'expire file versions of the given account(s), if no account is given file versions for all accounts will be expired.' ); } protected function execute(InputInterface $input, OutputInterface $output): int { $maxAge = $this->expiration->getMaxAgeAsTimestamp(); if (!$maxAge) { - $output->writeln("Auto expiration is configured - expiration will be handled automatically according to the expiration patterns detailed at the following link https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/file_versioning.html."); - return 1; + $output->writeln('Auto expiration is configured - expiration will be handled automatically according to the expiration patterns detailed at the following link https://docs.nextcloud.com/server/latest/admin_manual/configuration_files/file_versioning.html.'); + return self::FAILURE; } $users = $input->getArgument('user_id'); if (!empty($users)) { foreach ($users as $user) { - if ($this->userManager->userExists($user)) { - $output->writeln("Remove deleted files of <info>$user</info>"); - $userObject = $this->userManager->get($user); - $this->expireVersionsForUser($userObject); - } else { - $output->writeln("<error>Unknown user $user</error>"); - return 1; + if (!$this->userManager->userExists($user)) { + $output->writeln("<error>Unknown account $user</error>"); + return self::FAILURE; } + + $output->writeln("Remove deleted files of <info>$user</info>"); + $userObject = $this->userManager->get($user); + $this->expireVersionsForUser($userObject); } - } else { - $p = new ProgressBar($output); - $p->start(); - $this->userManager->callForSeenUsers(function (IUser $user) use ($p) { - $p->advance(); - $this->expireVersionsForUser($user); - }); - $p->finish(); - $output->writeln(''); + return self::SUCCESS; } - return 0; + + $p = new ProgressBar($output); + $p->start(); + $this->userManager->callForSeenUsers(function (IUser $user) use ($p): void { + $p->advance(); + $this->expireVersionsForUser($user); + }); + $p->finish(); + $output->writeln(''); + return self::SUCCESS; } - public function expireVersionsForUser(IUser $user) { + public function expireVersionsForUser(IUser $user): void { $uid = $user->getUID(); if (!$this->setupFS($uid)) { return; @@ -112,15 +80,13 @@ class ExpireVersions extends Command { /** * Act on behalf on versions item owner - * @param string $user - * @return boolean */ - protected function setupFS($user) { + protected function setupFS(string $user): bool { \OC_Util::tearDownFS(); \OC_Util::setupFS($user); // Check if this user has a version directory - $view = new \OC\Files\View('/' . $user); + $view = new View('/' . $user); if (!$view->is_dir('/files_versions')) { return false; } diff --git a/apps/files_versions/lib/Controller/PreviewController.php b/apps/files_versions/lib/Controller/PreviewController.php index 0e625dc2139..2c3ff8da70d 100644 --- a/apps/files_versions/lib/Controller/PreviewController.php +++ b/apps/files_versions/lib/Controller/PreviewController.php @@ -1,98 +1,89 @@ <?php + /** - * @copyright Copyright (c) 2016, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Morris Jobke <hey@morrisjobke.de> - * @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: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Controller; use OCA\Files_Versions\Versions\IVersionManager; use OCP\AppFramework\Controller; use OCP\AppFramework\Http; +use OCP\AppFramework\Http\Attribute\NoAdminRequired; +use OCP\AppFramework\Http\Attribute\NoCSRFRequired; +use OCP\AppFramework\Http\Attribute\OpenAPI; use OCP\AppFramework\Http\DataResponse; use OCP\AppFramework\Http\FileDisplayResponse; +use OCP\AppFramework\Http\RedirectResponse; use OCP\Files\IRootFolder; use OCP\Files\NotFoundException; use OCP\IPreview; use OCP\IRequest; use OCP\IUserSession; +use OCP\Preview\IMimeIconProvider; +#[OpenAPI(scope: OpenAPI::SCOPE_DEFAULT)] class PreviewController extends Controller { - /** @var IRootFolder */ - private $rootFolder; - - /** @var IUserSession */ - private $userSession; - - /** @var IVersionManager */ - private $versionManager; - - /** @var IPreview */ - private $previewManager; - public function __construct( string $appName, IRequest $request, - IRootFolder $rootFolder, - IUserSession $userSession, - IVersionManager $versionManager, - IPreview $previewManager + private IRootFolder $rootFolder, + private IUserSession $userSession, + private IVersionManager $versionManager, + private IPreview $previewManager, + private IMimeIconProvider $mimeIconProvider, ) { parent::__construct($appName, $request); - - $this->rootFolder = $rootFolder; - $this->userSession = $userSession; - $this->versionManager = $versionManager; - $this->previewManager = $previewManager; } /** - * @NoAdminRequired - * @NoCSRFRequired + * Get the preview for a file version * - * @param string $file - * @param int $x - * @param int $y - * @param string $version - * @return DataResponse|FileDisplayResponse + * @param string $file Path of the file + * @param int $x Width of the preview + * @param int $y Height of the preview + * @param string $version Version of the file to get the preview for + * @param bool $mimeFallback Whether to fallback to the mime icon if no preview is available + * @return FileDisplayResponse<Http::STATUS_OK, array{Content-Type: string}>|DataResponse<Http::STATUS_BAD_REQUEST|Http::STATUS_NOT_FOUND, list<empty>, array{}>|RedirectResponse<Http::STATUS_SEE_OTHER, array{}> + * + * 200: Preview returned + * 303: Redirect to the mime icon url if mimeFallback is true + * 400: Getting preview is not possible + * 404: Preview not found */ + #[NoAdminRequired] + #[NoCSRFRequired] public function getPreview( string $file = '', int $x = 44, int $y = 44, - string $version = '' + string $version = '', + bool $mimeFallback = false, ) { if ($file === '' || $version === '' || $x === 0 || $y === 0) { return new DataResponse([], Http::STATUS_BAD_REQUEST); } + $versionFile = null; try { $user = $this->userSession->getUser(); $userFolder = $this->rootFolder->getUserFolder($user->getUID()); $file = $userFolder->get($file); $versionFile = $this->versionManager->getVersionFile($user, $file, $version); $preview = $this->previewManager->getPreview($versionFile, $x, $y, true, IPreview::MODE_FILL, $versionFile->getMimetype()); - return new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]); + $response = new FileDisplayResponse($preview, Http::STATUS_OK, ['Content-Type' => $preview->getMimeType()]); + $response->cacheFor(3600 * 24, false, true); + return $response; } catch (NotFoundException $e) { + // If we have no preview enabled, we can redirect to the mime icon if any + if ($mimeFallback && $versionFile !== null) { + $url = $this->mimeIconProvider->getMimeIconUrl($versionFile->getMimeType()); + if ($url !== null) { + return new RedirectResponse($url); + } + } + return new DataResponse([], Http::STATUS_NOT_FOUND); } catch (\InvalidArgumentException $e) { return new DataResponse([], Http::STATUS_BAD_REQUEST); diff --git a/apps/files_versions/lib/Db/VersionEntity.php b/apps/files_versions/lib/Db/VersionEntity.php index d5adbcfa104..10f1dc8cbba 100644 --- a/apps/files_versions/lib/Db/VersionEntity.php +++ b/apps/files_versions/lib/Db/VersionEntity.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> - * - * @author 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\Db; @@ -36,8 +19,8 @@ use OCP\DB\Types; * @method void setFileId(int $fileId) * @method int getTimestamp() * @method void setTimestamp(int $timestamp) - * @method int getSize() - * @method void setSize(int $size) + * @method int|float getSize() + * @method void setSize(int|float $size) * @method int getMimetype() * @method void setMimetype(int $mimetype) * @method array|null getMetadata() @@ -70,12 +53,22 @@ class VersionEntity extends Entity implements JsonSerializable { ]; } - public function getLabel(): string { - return $this->metadata['label'] ?? ''; + /** + * @abstract given a key, return the value associated with the key in the metadata column + * if nothing is found, we return an empty string + * @param string $key key associated with the value + */ + public function getMetadataValue(string $key): ?string { + return $this->metadata[$key] ?? null; } - public function setLabel(string $label): void { - $this->metadata['label'] = $label; + /** + * @abstract sets a key value pair in the metadata column + * @param string $key key associated with the value + * @param string $value value associated with the key + */ + public function setMetadataValue(string $key, string $value): void { + $this->metadata[$key] = $value; $this->markFieldUpdated('metadata'); } -}
\ No newline at end of file +} diff --git a/apps/files_versions/lib/Db/VersionsMapper.php b/apps/files_versions/lib/Db/VersionsMapper.php index 86a0be82668..318dd8f0d82 100644 --- a/apps/files_versions/lib/Db/VersionsMapper.php +++ b/apps/files_versions/lib/Db/VersionsMapper.php @@ -3,33 +3,15 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> - * - * @author 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\Db; -use OCA\Files_Versions\Db\VersionEntity; -use OCP\IDBConnection; use OCP\AppFramework\Db\QBMapper; -use OCP\DB\IResult; +use OCP\DB\QueryBuilder\IQueryBuilder; +use OCP\IDBConnection; /** * @extends QBMapper<VersionEntity> @@ -46,8 +28,8 @@ class VersionsMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))); + ->from($this->getTableName()) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))); return $this->findEntities($qb); } @@ -59,10 +41,10 @@ class VersionsMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))) - ->orderBy('timestamp', 'DESC') - ->setMaxResults(1); + ->from($this->getTableName()) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))) + ->orderBy('timestamp', 'DESC') + ->setMaxResults(1); return $this->findEntity($qb); } @@ -71,9 +53,9 @@ class VersionsMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); $qb->select('*') - ->from($this->getTableName()) - ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))) - ->andWhere($qb->expr()->eq('timestamp', $qb->createNamedParameter($timestamp))); + ->from($this->getTableName()) + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))) + ->andWhere($qb->expr()->eq('timestamp', $qb->createNamedParameter($timestamp))); return $this->findEntity($qb); } @@ -82,7 +64,41 @@ class VersionsMapper extends QBMapper { $qb = $this->db->getQueryBuilder(); return $qb->delete($this->getTableName()) - ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))) - ->executeStatement(); + ->where($qb->expr()->eq('file_id', $qb->createNamedParameter($fileId))) + ->executeStatement(); + } + + public function deleteAllVersionsForUser(int $storageId, ?string $path = null): void { + $fileIdsGenerator = $this->getFileIdsGenerator($storageId, $path); + + $versionEntitiesDeleteQuery = $this->db->getQueryBuilder(); + $versionEntitiesDeleteQuery->delete($this->getTableName()) + ->where($versionEntitiesDeleteQuery->expr()->in('file_id', $versionEntitiesDeleteQuery->createParameter('file_ids'))); + + foreach ($fileIdsGenerator as $fileIds) { + $versionEntitiesDeleteQuery->setParameter('file_ids', $fileIds, IQueryBuilder::PARAM_INT_ARRAY); + $versionEntitiesDeleteQuery->executeStatement(); + } + } + + private function getFileIdsGenerator(int $storageId, ?string $path): \Generator { + $offset = 0; + do { + $filesIdsSelect = $this->db->getQueryBuilder(); + $filesIdsSelect->select('fileid') + ->from('filecache') + ->where($filesIdsSelect->expr()->eq('storage', $filesIdsSelect->createNamedParameter($storageId, IQueryBuilder::PARAM_STR))) + ->andWhere($filesIdsSelect->expr()->like('path', $filesIdsSelect->createNamedParameter('files' . ($path ? '/' . $this->db->escapeLikeParameter($path) : '') . '/%', IQueryBuilder::PARAM_STR))) + ->andWhere($filesIdsSelect->expr()->gt('fileid', $filesIdsSelect->createParameter('offset'))) + ->setMaxResults(1000) + ->orderBy('fileid', 'ASC'); + + $filesIdsSelect->setParameter('offset', $offset, IQueryBuilder::PARAM_INT); + $result = $filesIdsSelect->executeQuery(); + $fileIds = $result->fetchAll(\PDO::FETCH_COLUMN); + $offset = end($fileIds); + + yield $fileIds; + } while (!empty($fileIds)); } } diff --git a/apps/files_versions/lib/Events/CreateVersionEvent.php b/apps/files_versions/lib/Events/CreateVersionEvent.php index d1a5bb00c35..92ed26b2dd6 100644 --- a/apps/files_versions/lib/Events/CreateVersionEvent.php +++ b/apps/files_versions/lib/Events/CreateVersionEvent.php @@ -1,26 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2017 Bjoern Schiessle <bjoern@schiessle.org> - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @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: 2017 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Events; @@ -40,17 +22,15 @@ class CreateVersionEvent extends Event { /** @var bool */ private $createVersion; - /** @var Node */ - private $node; - /** * CreateVersionEvent constructor. * * @param Node $node */ - public function __construct(Node $node) { + public function __construct( + private Node $node, + ) { $this->createVersion = true; - $this->node = $node; } /** diff --git a/apps/files_versions/lib/Events/VersionCreatedEvent.php b/apps/files_versions/lib/Events/VersionCreatedEvent.php new file mode 100644 index 00000000000..4dc7a7cb505 --- /dev/null +++ b/apps/files_versions/lib/Events/VersionCreatedEvent.php @@ -0,0 +1,39 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Events; + +use OCA\Files_Versions\Versions\IVersion; +use OCP\EventDispatcher\Event; +use OCP\Files\Node; + +/** + * Event dispatched after a successful creation of a version + */ +class VersionCreatedEvent extends Event { + public function __construct( + private Node $node, + private IVersion $version, + ) { + parent::__construct(); + } + + /** + * Node of the file that has been versioned + */ + public function getNode(): Node { + return $this->node; + } + + /** + * Version of the file that was created + */ + public function getVersion(): IVersion { + return $this->version; + } +} diff --git a/apps/files_versions/lib/Events/VersionRestoredEvent.php b/apps/files_versions/lib/Events/VersionRestoredEvent.php new file mode 100644 index 00000000000..12e91bd258d --- /dev/null +++ b/apps/files_versions/lib/Events/VersionRestoredEvent.php @@ -0,0 +1,33 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Events; + +use OCA\Files_Versions\Versions\IVersion; +use OCP\EventDispatcher\Event; + +/** + * Class VersionRestoredEvent + * + * Event that is called after a successful restore of a previous version + * + * @package OCA\Files_Versions + */ +class VersionRestoredEvent extends Event { + public function __construct( + private IVersion $version, + ) { + } + + /** + * Version that was restored + */ + public function getVersion(): IVersion { + return $this->version; + } +} diff --git a/apps/files_versions/lib/Expiration.php b/apps/files_versions/lib/Expiration.php index 40768e90af7..1e04d93379f 100644 --- a/apps/files_versions/lib/Expiration.php +++ b/apps/files_versions/lib/Expiration.php @@ -1,26 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions; @@ -33,9 +16,6 @@ class Expiration { // how long do we keep files a version if no other value is defined in the config file (unit: days) public const NO_OBLIGATION = -1; - /** @var ITimeFactory */ - private $timeFactory; - /** @var string */ private $retentionObligation; @@ -48,12 +28,11 @@ class Expiration { /** @var bool */ private $canPurgeToSaveSpace; - /** @var LoggerInterface */ - private $logger; - - public function __construct(IConfig $config, ITimeFactory $timeFactory, LoggerInterface $logger) { - $this->timeFactory = $timeFactory; - $this->logger = $logger; + public function __construct( + IConfig $config, + private ITimeFactory $timeFactory, + private LoggerInterface $logger, + ) { $this->retentionObligation = $config->getSystemValue('versions_retention_obligation', 'auto'); if ($this->retentionObligation !== 'disabled') { @@ -121,6 +100,20 @@ class Expiration { } /** + * Get minimal retention obligation as a timestamp + * + * @return int|false + */ + public function getMinAgeAsTimestamp() { + $minAge = false; + if ($this->isEnabled() && $this->minAge !== self::NO_OBLIGATION) { + $time = $this->timeFactory->getTime(); + $minAge = $time - ($this->minAge * 86400); + } + return $minAge; + } + + /** * Get maximal retention obligation as a timestamp * * @return int|false @@ -157,16 +150,16 @@ class Expiration { if (!ctype_digit($minValue) && $minValue !== 'auto') { $isValid = false; $this->logger->warning( - $minValue . ' is not a valid value for minimal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.', - ['app' => 'files_versions'] + $minValue . ' is not a valid value for minimal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.', + ['app' => 'files_versions'] ); } if (!ctype_digit($maxValue) && $maxValue !== 'auto') { $isValid = false; $this->logger->warning( - $maxValue . ' is not a valid value for maximal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.', - ['app' => 'files_versions'] + $maxValue . ' is not a valid value for maximal versions retention obligation. Check versions_retention_obligation in your config.php. Falling back to auto.', + ['app' => 'files_versions'] ); } diff --git a/apps/files_versions/lib/Listener/FileEventsListener.php b/apps/files_versions/lib/Listener/FileEventsListener.php index 76590733e26..969ca4ded45 100644 --- a/apps/files_versions/lib/Listener/FileEventsListener.php +++ b/apps/files_versions/lib/Listener/FileEventsListener.php @@ -1,32 +1,9 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Bart Visscher <bartv@thisnet.nl> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Sam Tuke <mail@samtuke.com> - * @author Louis Chmn <louis@chmn.me> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions\Listener; @@ -35,11 +12,13 @@ use OC\DB\Exceptions\DbalException; use OC\Files\Filesystem; use OC\Files\Mount\MoveableMount; use OC\Files\Node\NonExistingFile; +use OC\Files\Node\NonExistingFolder; use OC\Files\View; -use OCA\Files_Versions\Db\VersionEntity; -use OCA\Files_Versions\Db\VersionsMapper; use OCA\Files_Versions\Storage; +use OCA\Files_Versions\Versions\INeedSyncVersionBackend; +use OCA\Files_Versions\Versions\IVersionManager; use OCP\AppFramework\Db\DoesNotExistException; +use OCP\DB\Exception; use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Files\Events\Node\BeforeNodeCopiedEvent; @@ -53,14 +32,17 @@ use OCP\Files\Events\Node\NodeDeletedEvent; use OCP\Files\Events\Node\NodeRenamedEvent; use OCP\Files\Events\Node\NodeTouchedEvent; use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\Files\File; use OCP\Files\Folder; use OCP\Files\IMimeTypeLoader; use OCP\Files\IRootFolder; use OCP\Files\Node; +use OCP\Files\NotFoundException; +use OCP\IUserSession; +use Psr\Log\LoggerInterface; +/** @template-implements IEventListener<BeforeNodeCopiedEvent|BeforeNodeDeletedEvent|BeforeNodeRenamedEvent|BeforeNodeTouchedEvent|BeforeNodeWrittenEvent|NodeCopiedEvent|NodeCreatedEvent|NodeDeletedEvent|NodeRenamedEvent|NodeTouchedEvent|NodeWrittenEvent> */ class FileEventsListener implements IEventListener { - private IRootFolder $rootFolder; - private VersionsMapper $versionsMapper; /** * @var array<int, array> */ @@ -73,16 +55,14 @@ class FileEventsListener implements IEventListener { * @var array<string, Node> */ private array $versionsDeleted = []; - private IMimeTypeLoader $mimeTypeLoader; public function __construct( - IRootFolder $rootFolder, - VersionsMapper $versionsMapper, - IMimeTypeLoader $mimeTypeLoader + private IRootFolder $rootFolder, + private IVersionManager $versionManager, + private IMimeTypeLoader $mimeTypeLoader, + private IUserSession $userSession, + private LoggerInterface $logger, ) { - $this->rootFolder = $rootFolder; - $this->versionsMapper = $versionsMapper; - $this->mimeTypeLoader = $mimeTypeLoader; } public function handle(Event $event): void { @@ -146,6 +126,22 @@ class FileEventsListener implements IEventListener { } public function touch_hook(Node $node): void { + // Do not handle folders. + if ($node instanceof Folder) { + return; + } + + if ($node instanceof NonExistingFile) { + $this->logger->error( + 'Failed to create or update version for {path}, node does not exist', + [ + 'path' => $node->getPath(), + ] + ); + + return; + } + $previousNode = $this->nodesTouched[$node->getId()] ?? null; if ($previousNode === null) { @@ -155,11 +151,12 @@ class FileEventsListener implements IEventListener { unset($this->nodesTouched[$node->getId()]); try { - // We update the timestamp of the version entity associated with the previousNode. - $versionEntity = $this->versionsMapper->findVersionForFileId($previousNode->getId(), $previousNode->getMTime()); - // Create a version in the DB for the current content. - $versionEntity->setTimestamp($node->getMTime()); - $this->versionsMapper->update($versionEntity); + if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) { + $revision = $this->versionManager->getRevision($previousNode); + + // We update the timestamp of the version entity associated with the previousNode. + $this->versionManager->updateVersionEntity($node, $revision, ['timestamp' => $node->getMTime()]); + } } catch (DbalException $ex) { // Ignore UniqueConstraintViolationException, as we are probably in the middle of a rollback // Where the previous node would temporary have the mtime of the old version, so the rollback touches it to fix it. @@ -174,17 +171,24 @@ class FileEventsListener implements IEventListener { public function created(Node $node): void { // Do not handle folders. - if ($node instanceof Folder) { + if (!($node instanceof File)) { return; } - $versionEntity = new VersionEntity(); - $versionEntity->setFileId($node->getId()); - $versionEntity->setTimestamp($node->getMTime()); - $versionEntity->setSize($node->getSize()); - $versionEntity->setMimetype($this->mimeTypeLoader->getId($node->getMimetype())); - $versionEntity->setMetadata([]); - $this->versionsMapper->insert($versionEntity); + if ($node instanceof NonExistingFile) { + $this->logger->error( + 'Failed to create version for {path}, node does not exist', + [ + 'path' => $node->getPath(), + ] + ); + + return; + } + + if ($this->versionManager instanceof INeedSyncVersionBackend) { + $this->versionManager->createVersionEntity($node); + } } /** @@ -220,30 +224,72 @@ class FileEventsListener implements IEventListener { return; } + if ($node instanceof NonExistingFile) { + $this->logger->error( + 'Failed to create or update version for {path}, node does not exist', + [ + 'path' => $node->getPath(), + ] + ); + + return; + } + $writeHookInfo = $this->writeHookInfo[$node->getId()] ?? null; if ($writeHookInfo === null) { return; } - if ($writeHookInfo['versionCreated'] && $node->getMTime() !== $writeHookInfo['previousNode']->getMTime()) { + if ( + $writeHookInfo['versionCreated'] + && $node->getMTime() !== $writeHookInfo['previousNode']->getMTime() + ) { // If a new version was created, insert a version in the DB for the current content. - // Unless both versions have the same mtime. - $versionEntity = new VersionEntity(); - $versionEntity->setFileId($node->getId()); - $versionEntity->setTimestamp($node->getMTime()); - $versionEntity->setSize($node->getSize()); - $versionEntity->setMimetype($this->mimeTypeLoader->getId($node->getMimetype())); - $versionEntity->setMetadata([]); - $this->versionsMapper->insert($versionEntity); + // If both versions have the same mtime, it means the latest version file simply got overrode, + // so no need to create a new version. + $this->created($node); } else { - // If no new version was stored in the FS, no new version should be added in the DB. - // So we simply update the associated version. - $currentVersionEntity = $this->versionsMapper->findVersionForFileId($node->getId(), $writeHookInfo['previousNode']->getMtime()); - $currentVersionEntity->setTimestamp($node->getMTime()); - $currentVersionEntity->setSize($node->getSize()); - $currentVersionEntity->setMimetype($this->mimeTypeLoader->getId($node->getMimetype())); - $this->versionsMapper->update($currentVersionEntity); + try { + // If no new version was stored in the FS, no new version should be added in the DB. + // So we simply update the associated version. + if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) { + $revision = $this->versionManager->getRevision($writeHookInfo['previousNode']); + + $this->versionManager->updateVersionEntity( + $node, + $revision, + [ + 'timestamp' => $node->getMTime(), + 'size' => $node->getSize(), + 'mimetype' => $this->mimeTypeLoader->getId($node->getMimetype()), + ], + ); + } + } catch (DoesNotExistException $e) { + // This happens if the versions app was not enabled while the file was created or updated the last time. + // meaning there is no such revision and we need to create this file. + if ($writeHookInfo['versionCreated']) { + $this->created($node); + } else { + // Normally this should not happen so we re-throw the exception to not hide any potential issues. + throw $e; + } + } catch (Exception $e) { + $this->logger->error('Failed to update existing version for ' . $node->getPath(), [ + 'exception' => $e, + 'versionCreated' => $writeHookInfo['versionCreated'], + 'previousNode' => [ + 'size' => $writeHookInfo['previousNode']->getSize(), + 'mtime' => $writeHookInfo['previousNode']->getMTime(), + ], + 'node' => [ + 'size' => $node->getSize(), + 'mtime' => $node->getMTime(), + ] + ]); + throw $e; + } } unset($this->writeHookInfo[$node->getId()]); @@ -252,7 +298,7 @@ class FileEventsListener implements IEventListener { /** * Erase versions of deleted file * - * This function is connected to the delete signal of OC_Filesystem + * This function is connected to the NodeDeletedEvent event * cleanup the versions directory if the actual file gets deleted */ public function remove_hook(Node $node): void { @@ -265,7 +311,11 @@ class FileEventsListener implements IEventListener { $relativePath = $this->getPathForNode($node); unset($this->versionsDeleted[$path]); Storage::delete($relativePath); - $this->versionsMapper->deleteAllVersionsForFileId($node->getId()); + // If no new version was stored in the FS, no new version should be added in the DB. + // So we simply update the associated version. + if ($node instanceof File && $this->versionManager instanceof INeedSyncVersionBackend) { + $this->versionManager->deleteVersionsEntity($node); + } } /** @@ -280,10 +330,17 @@ class FileEventsListener implements IEventListener { /** * rename/move versions of renamed/moved files * - * This function is connected to the rename signal of OC_Filesystem and adjust the name and location + * This function is connected to the NodeRenamedEvent event and adjust the name and location * of the stored versions along the actual file */ public function rename_hook(Node $source, Node $target): void { + $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage()); + $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage()); + // If different backends, do nothing. + if ($sourceBackend !== $targetBackend) { + return; + } + $oldPath = $this->getPathForNode($source); $newPath = $this->getPathForNode($target); Storage::renameOrCopy($oldPath, $newPath, 'rename'); @@ -292,10 +349,17 @@ class FileEventsListener implements IEventListener { /** * copy versions of copied files * - * This function is connected to the copy signal of OC_Filesystem and copies the + * This function is connected to the NodeCopiedEvent event and copies the * the stored versions to the new location */ public function copy_hook(Node $source, Node $target): void { + $sourceBackend = $this->versionManager->getBackendForStorage($source->getParent()->getStorage()); + $targetBackend = $this->versionManager->getBackendForStorage($target->getStorage()); + // If different backends, do nothing. + if ($sourceBackend !== $targetBackend) { + return; + } + $oldPath = $this->getPathForNode($source); $newPath = $this->getPathForNode($target); Storage::renameOrCopy($oldPath, $newPath, 'copy'); @@ -309,11 +373,26 @@ class FileEventsListener implements IEventListener { * */ public function pre_renameOrCopy_hook(Node $source, Node $target): void { - // if we rename a movable mount point, then the versions don't have - // to be renamed + $sourceBackend = $this->versionManager->getBackendForStorage($source->getStorage()); + $targetBackend = $this->versionManager->getBackendForStorage($target->getParent()->getStorage()); + // If different backends, do nothing. + if ($sourceBackend !== $targetBackend) { + return; + } + + // if we rename a movable mount point, then the versions don't have to be renamed $oldPath = $this->getPathForNode($source); $newPath = $this->getPathForNode($target); - $absOldPath = Filesystem::normalizePath('/' . \OC_User::getUser() . '/files' . $oldPath); + if ($oldPath === null || $newPath === null) { + return; + } + + $user = $this->userSession->getUser()?->getUID(); + if ($user === null) { + return; + } + + $absOldPath = Filesystem::normalizePath('/' . $user . '/files' . $oldPath); $manager = Filesystem::getMountManager(); $mount = $manager->find($absOldPath); $internalPath = $mount->getInternalPath($absOldPath); @@ -321,7 +400,7 @@ class FileEventsListener implements IEventListener { return; } - $view = new View(\OC_User::getUser() . '/files'); + $view = new View($user . '/files'); if ($view->file_exists($newPath)) { Storage::store($newPath); } else { @@ -331,17 +410,63 @@ class FileEventsListener implements IEventListener { /** * Retrieve the path relative to the current user root folder. - * If no user is connected, use the node's owner. + * If no user is connected, try to use the node's owner. */ private function getPathForNode(Node $node): ?string { - try { - return $this->rootFolder - ->getUserFolder(\OC_User::getUser()) + $user = $this->userSession->getUser()?->getUID(); + if ($user) { + $path = $this->rootFolder + ->getUserFolder($user) ->getRelativePath($node->getPath()); - } catch (\Throwable $ex) { - return $this->rootFolder - ->getUserFolder($node->getOwner()->getUid()) + + if ($path !== null) { + return $path; + } + } + + try { + $owner = $node->getOwner()?->getUid(); + } catch (NotFoundException) { + $owner = null; + } + + // If no owner, extract it from the path. + // e.g. /user/files/foobar.txt + if (!$owner) { + $parts = explode('/', $node->getPath(), 4); + if (count($parts) === 4) { + $owner = $parts[1]; + } + } + + if ($owner) { + $path = $this->rootFolder + ->getUserFolder($owner) ->getRelativePath($node->getPath()); + + if ($path !== null) { + return $path; + } + } + + if (!($node instanceof NonExistingFile) && !($node instanceof NonExistingFolder)) { + $this->logger->debug('Failed to compute path for node', [ + 'node' => [ + 'path' => $node->getPath(), + 'owner' => $owner, + 'fileid' => $node->getId(), + 'size' => $node->getSize(), + 'mtime' => $node->getMTime(), + ] + ]); + } else { + $this->logger->debug('Failed to compute path for node', [ + 'node' => [ + 'path' => $node->getPath(), + 'owner' => $owner, + ] + ]); } + return null; } } diff --git a/apps/files_versions/lib/Listener/LegacyRollbackListener.php b/apps/files_versions/lib/Listener/LegacyRollbackListener.php new file mode 100644 index 00000000000..072c1511caa --- /dev/null +++ b/apps/files_versions/lib/Listener/LegacyRollbackListener.php @@ -0,0 +1,35 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Listener; + +use OCA\Files_Versions\Events\VersionRestoredEvent; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; + +/** + * This listener is designed to be compatible with third-party code + * that can still use a hook. This listener will be removed in + * the next version and the rollback hook will stop working. + * + * @deprecated 32.0.0 + * @template-implements IEventListener<VersionRestoredEvent> + */ +class LegacyRollbackListener implements IEventListener { + public function handle(Event $event): void { + if (!($event instanceof VersionRestoredEvent)) { + return; + } + $version = $event->getVersion(); + \OC_Hook::emit('\OCP\Versions', 'rollback', [ + 'path' => $version->getVersionPath(), + 'revision' => $version->getRevisionId(), + 'node' => $version->getSourceFile(), + ]); + } +} diff --git a/apps/files_versions/lib/Listener/LoadAdditionalListener.php b/apps/files_versions/lib/Listener/LoadAdditionalListener.php index aeea8756515..cb955629c0f 100644 --- a/apps/files_versions/lib/Listener/LoadAdditionalListener.php +++ b/apps/files_versions/lib/Listener/LoadAdditionalListener.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @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: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Listener; @@ -33,6 +14,7 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Util; +/** @template-implements IEventListener<LoadAdditionalScriptsEvent> */ class LoadAdditionalListener implements IEventListener { public function handle(Event $event): void { if (!($event instanceof LoadAdditionalScriptsEvent)) { diff --git a/apps/files_versions/lib/Listener/LoadSidebarListener.php b/apps/files_versions/lib/Listener/LoadSidebarListener.php index 27261fbc3d2..b8d13fa4810 100644 --- a/apps/files_versions/lib/Listener/LoadSidebarListener.php +++ b/apps/files_versions/lib/Listener/LoadSidebarListener.php @@ -3,27 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author John Molakvoæ <skjnldsv@protonmail.com> - * @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: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Listener; @@ -33,6 +14,7 @@ use OCP\EventDispatcher\Event; use OCP\EventDispatcher\IEventListener; use OCP\Util; +/** @template-implements IEventListener<LoadSidebar> */ class LoadSidebarListener implements IEventListener { public function handle(Event $event): void { if (!($event instanceof LoadSidebar)) { diff --git a/apps/files_versions/lib/Listener/VersionAuthorListener.php b/apps/files_versions/lib/Listener/VersionAuthorListener.php new file mode 100644 index 00000000000..9b93b1f888b --- /dev/null +++ b/apps/files_versions/lib/Listener/VersionAuthorListener.php @@ -0,0 +1,56 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files_Versions\Listener; + +use OC\Files\Node\Folder; +use OCA\Files_Versions\Sabre\Plugin; +use OCA\Files_Versions\Versions\IMetadataVersionBackend; +use OCA\Files_Versions\Versions\IVersionManager; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\NodeWrittenEvent; +use OCP\Files\Node; +use OCP\IUserSession; + +/** @template-implements IEventListener<NodeWrittenEvent> */ +class VersionAuthorListener implements IEventListener { + public function __construct( + private IVersionManager $versionManager, + private IUserSession $userSession, + ) { + } + + /** + * @abstract handles events from a nodes version being changed + * @param Event $event the event that triggered this listener to activate + */ + public function handle(Event $event): void { + if ($event instanceof NodeWrittenEvent) { + $this->post_write_hook($event->getNode()); + } + } + + /** + * @abstract handles the NodeWrittenEvent, and sets the metadata for the associated node + * @param Node $node the node that is currently being written + */ + public function post_write_hook(Node $node): void { + $user = $this->userSession->getUser(); + // Do not handle folders or users that we cannot get metadata from + if ($node instanceof Folder || is_null($user)) { + return; + } + // check if our version manager supports setting the metadata + if ($this->versionManager instanceof IMetadataVersionBackend) { + $revision = $this->versionManager->getRevision($node); + $author = $user->getUID(); + $this->versionManager->setMetadataValue($node, $revision, Plugin::AUTHOR, $author); + } + } +} diff --git a/apps/files_versions/lib/Listener/VersionStorageMoveListener.php b/apps/files_versions/lib/Listener/VersionStorageMoveListener.php new file mode 100644 index 00000000000..d0a0bcf4a92 --- /dev/null +++ b/apps/files_versions/lib/Listener/VersionStorageMoveListener.php @@ -0,0 +1,140 @@ +<?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\Listener; + +use Exception; +use OC\Files\Node\NonExistingFile; +use OC\Files\Node\NonExistingFolder; +use OCA\Files_Versions\Versions\IVersionBackend; +use OCA\Files_Versions\Versions\IVersionManager; +use OCA\Files_Versions\Versions\IVersionsImporterBackend; +use OCP\EventDispatcher\Event; +use OCP\EventDispatcher\IEventListener; +use OCP\Files\Events\Node\AbstractNodesEvent; +use OCP\Files\Events\Node\BeforeNodeRenamedEvent; +use OCP\Files\Events\Node\NodeCopiedEvent; +use OCP\Files\Events\Node\NodeRenamedEvent; +use OCP\Files\File; +use OCP\Files\Folder; +use OCP\Files\Node; +use OCP\Files\Storage\IStorage; +use OCP\IUser; +use OCP\IUserSession; + +/** @template-implements IEventListener<Event> */ +class VersionStorageMoveListener implements IEventListener { + /** @var File[] */ + private array $movedNodes = []; + + public function __construct( + private IVersionManager $versionManager, + private IUserSession $userSession, + ) { + } + + /** + * @abstract Moves version across storages if necessary. + * @throws Exception No user in session + */ + public function handle(Event $event): void { + if (!($event instanceof AbstractNodesEvent)) { + return; + } + + $source = $event->getSource(); + $target = $event->getTarget(); + + $sourceStorage = $this->getNodeStorage($source); + $targetStorage = $this->getNodeStorage($target); + + $sourceBackend = $this->versionManager->getBackendForStorage($sourceStorage); + $targetBackend = $this->versionManager->getBackendForStorage($targetStorage); + + // If same backend, nothing to do. + if ($sourceBackend === $targetBackend) { + return; + } + + $user = $this->userSession->getUser() ?? $source->getOwner(); + + if ($user === null) { + throw new Exception('Cannot move versions across storages without a user.'); + } + + if ($event instanceof BeforeNodeRenamedEvent) { + $this->recursivelyPrepareMove($source); + } elseif ($event instanceof NodeRenamedEvent || $event instanceof NodeCopiedEvent) { + $this->recursivelyHandleMoveOrCopy($event, $user, $source, $target, $sourceBackend, $targetBackend); + } + } + + /** + * Store all sub files in this->movedNodes so their info can be used after the operation. + */ + private function recursivelyPrepareMove(Node $source): void { + if ($source instanceof File) { + $this->movedNodes[$source->getId()] = $source; + } elseif ($source instanceof Folder) { + foreach ($source->getDirectoryListing() as $child) { + $this->recursivelyPrepareMove($child); + } + } + } + + /** + * Call handleMoveOrCopy on each sub files + * @param NodeRenamedEvent|NodeCopiedEvent $event + */ + private function recursivelyHandleMoveOrCopy(Event $event, IUser $user, ?Node $source, Node $target, IVersionBackend $sourceBackend, IVersionBackend $targetBackend): void { + if ($target instanceof File) { + if ($event instanceof NodeRenamedEvent) { + $source = $this->movedNodes[$target->getId()]; + } + + /** @var File $source */ + $this->handleMoveOrCopy($event, $user, $source, $target, $sourceBackend, $targetBackend); + } elseif ($target instanceof Folder) { + /** @var Folder $source */ + foreach ($target->getDirectoryListing() as $targetChild) { + if ($event instanceof NodeCopiedEvent) { + $sourceChild = $source->get($targetChild->getName()); + } else { + $sourceChild = null; + } + + $this->recursivelyHandleMoveOrCopy($event, $user, $sourceChild, $targetChild, $sourceBackend, $targetBackend); + } + } + } + + /** + * Called only during NodeRenamedEvent or NodeCopiedEvent + * Will send the source node versions to the new backend, and then delete them from the old backend. + * @param NodeRenamedEvent|NodeCopiedEvent $event + */ + private function handleMoveOrCopy(Event $event, IUser $user, File $source, File $target, IVersionBackend $sourceBackend, IVersionBackend $targetBackend): void { + if ($targetBackend instanceof IVersionsImporterBackend) { + $versions = $sourceBackend->getVersionsForFile($user, $source); + $targetBackend->importVersionsForFile($user, $source, $target, $versions); + } + + if ($event instanceof NodeRenamedEvent && $sourceBackend instanceof IVersionsImporterBackend) { + $sourceBackend->clearVersionsForFile($user, $source, $target); + } + } + + private function getNodeStorage(Node $node): IStorage { + if ($node instanceof NonExistingFile || $node instanceof NonExistingFolder) { + return $node->getParent()->getStorage(); + } else { + return $node->getStorage(); + } + } +} diff --git a/apps/files_versions/lib/Migration/Version1020Date20221114144058.php b/apps/files_versions/lib/Migration/Version1020Date20221114144058.php index f0d58284c9f..77c8c2201f3 100644 --- a/apps/files_versions/lib/Migration/Version1020Date20221114144058.php +++ b/apps/files_versions/lib/Migration/Version1020Date20221114144058.php @@ -3,25 +3,8 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2022 Louis Chmn <louis@chmn.me> - * - * @author 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\Migration; @@ -46,11 +29,11 @@ class Version1020Date20221114144058 extends SimpleMigrationStep { /** @var ISchemaWrapper $schema */ $schema = $schemaClosure(); - if ($schema->hasTable("files_versions")) { + if ($schema->hasTable('files_versions')) { return null; } - $table = $schema->createTable("files_versions"); + $table = $schema->createTable('files_versions'); $table->addColumn('id', Types::BIGINT, [ 'autoincrement' => true, 'notnull' => true, diff --git a/apps/files_versions/lib/Sabre/Plugin.php b/apps/files_versions/lib/Sabre/Plugin.php index 4fd17194ba6..984c4a36e5b 100644 --- a/apps/files_versions/lib/Sabre/Plugin.php +++ b/apps/files_versions/lib/Sabre/Plugin.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @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: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files_Versions\Sabre; use OC\AppFramework\Http\Request; +use OCA\DAV\Connector\Sabre\FilesPlugin; +use OCP\IPreview; use OCP\IRequest; use Sabre\DAV\Exception\NotFound; use Sabre\DAV\INode; @@ -39,12 +23,18 @@ use Sabre\HTTP\ResponseInterface; class Plugin extends ServerPlugin { private Server $server; - private IRequest $request; + + public const LABEL = 'label'; + + public const AUTHOR = 'author'; public const VERSION_LABEL = '{http://nextcloud.org/ns}version-label'; + public const VERSION_AUTHOR = '{http://nextcloud.org/ns}version-author'; // dav property for author + public function __construct( - IRequest $request + private IRequest $request, + private IPreview $previewManager, ) { $this->request = $request; } @@ -90,7 +80,12 @@ class Plugin extends ServerPlugin { public function propFind(PropFind $propFind, INode $node): void { if ($node instanceof VersionFile) { - $propFind->handle(self::VERSION_LABEL, fn() => $node->getLabel()); + $propFind->handle(self::VERSION_LABEL, fn () => $node->getMetadataValue(self::LABEL)); + $propFind->handle(self::VERSION_AUTHOR, fn () => $node->getMetadataValue(self::AUTHOR)); + $propFind->handle( + FilesPlugin::HAS_PREVIEW_PROPERTYNAME, + fn (): string => $this->previewManager->isMimeSupported($node->getContentType()) ? 'true' : 'false', + ); } } @@ -98,7 +93,7 @@ class Plugin extends ServerPlugin { $node = $this->server->tree->getNodeForPath($path); if ($node instanceof VersionFile) { - $propPatch->handle(self::VERSION_LABEL, fn ($label) => $node->setLabel($label)); + $propPatch->handle(self::VERSION_LABEL, fn (string $label) => $node->setMetadataValue(self::LABEL, $label)); } } } diff --git a/apps/files_versions/lib/Sabre/RestoreFolder.php b/apps/files_versions/lib/Sabre/RestoreFolder.php index 31d87cc7112..7904b098a4f 100644 --- a/apps/files_versions/lib/Sabre/RestoreFolder.php +++ b/apps/files_versions/lib/Sabre/RestoreFolder.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.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\Sabre; diff --git a/apps/files_versions/lib/Sabre/RootCollection.php b/apps/files_versions/lib/Sabre/RootCollection.php index 835df5930bc..1e7129f23da 100644 --- a/apps/files_versions/lib/Sabre/RootCollection.php +++ b/apps/files_versions/lib/Sabre/RootCollection.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.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\Sabre; @@ -34,33 +17,16 @@ use Sabre\DAVACL\PrincipalBackend; class RootCollection extends AbstractPrincipalCollection { - /** @var IRootFolder */ - private $rootFolder; - - /** @var IUserManager */ - private $userManager; - - /** @var IVersionManager */ - private $versionManager; - - /** @var IUserSession */ - private $userSession; - public function __construct( PrincipalBackend\BackendInterface $principalBackend, - IRootFolder $rootFolder, + private IRootFolder $rootFolder, IConfig $config, - IUserManager $userManager, - IVersionManager $versionManager, - IUserSession $userSession + private IUserManager $userManager, + private IVersionManager $versionManager, + private IUserSession $userSession, ) { parent::__construct($principalBackend, 'principals/users'); - $this->rootFolder = $rootFolder; - $this->userManager = $userManager; - $this->versionManager = $versionManager; - $this->userSession = $userSession; - $this->disableListing = !$config->getSystemValue('debug', false); } diff --git a/apps/files_versions/lib/Sabre/VersionCollection.php b/apps/files_versions/lib/Sabre/VersionCollection.php index 946ac9baad7..375d5cf99f2 100644 --- a/apps/files_versions/lib/Sabre/VersionCollection.php +++ b/apps/files_versions/lib/Sabre/VersionCollection.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.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\Sabre; @@ -36,19 +18,11 @@ use Sabre\DAV\ICollection; class VersionCollection implements ICollection { - /** @var File */ - private $file; - - /** @var IUser */ - private $user; - - /** @var IVersionManager */ - private $versionManager; - - public function __construct(File $file, IUser $user, IVersionManager $versionManager) { - $this->file = $file; - $this->user = $user; - $this->versionManager = $versionManager; + public function __construct( + private File $file, + private IUser $user, + private IVersionManager $versionManager, + ) { } public function createFile($name, $data = null) { diff --git a/apps/files_versions/lib/Sabre/VersionFile.php b/apps/files_versions/lib/Sabre/VersionFile.php index 20ae25a7623..faa03473648 100644 --- a/apps/files_versions/lib/Sabre/VersionFile.php +++ b/apps/files_versions/lib/Sabre/VersionFile.php @@ -3,30 +3,14 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.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\Sabre; use OCA\Files_Versions\Versions\IDeletableVersionBackend; +use OCA\Files_Versions\Versions\IMetadataVersion; +use OCA\Files_Versions\Versions\IMetadataVersionBackend; use OCA\Files_Versions\Versions\INameableVersion; use OCA\Files_Versions\Versions\INameableVersionBackend; use OCA\Files_Versions\Versions\IVersion; @@ -37,15 +21,10 @@ use Sabre\DAV\Exception\NotFound; use Sabre\DAV\IFile; class VersionFile implements IFile { - /** @var IVersion */ - private $version; - - /** @var IVersionManager */ - private $versionManager; - - public function __construct(IVersion $version, IVersionManager $versionManager) { - $this->version = $version; - $this->versionManager = $versionManager; + public function __construct( + private IVersion $version, + private IVersionManager $versionManager, + ) { } public function put($data) { @@ -68,7 +47,11 @@ class VersionFile implements IFile { return (string)$this->version->getRevisionId(); } - public function getSize(): int { + /** + * @psalm-suppress ImplementedReturnTypeMismatch \Sabre\DAV\IFile::getSize signature does not support 32bit + * @return int|float + */ + public function getSize(): int|float { return $this->version->getSize(); } @@ -88,23 +71,29 @@ class VersionFile implements IFile { throw new Forbidden(); } - public function getLabel(): ?string { - if ($this->version instanceof INameableVersion && $this->version->getSourceFile()->getSize() > 0) { - return $this->version->getLabel(); - } else { - return null; - } - } + public function setMetadataValue(string $key, string $value): bool { + $backend = $this->version->getBackend(); - public function setLabel($label): bool { - if ($this->versionManager instanceof INameableVersionBackend) { - $this->versionManager->setVersionLabel($this->version, $label); + if ($backend instanceof IMetadataVersionBackend) { + $backend->setMetadataValue($this->version->getSourceFile(), $this->version->getTimestamp(), $key, $value); + return true; + } elseif ($key === 'label' && $backend instanceof INameableVersionBackend) { + $backend->setVersionLabel($this->version, $value); return true; } else { return false; } } + public function getMetadataValue(string $key): ?string { + if ($this->version instanceof IMetadataVersion) { + return $this->version->getMetadataValue($key); + } elseif ($key === 'label' && $this->version instanceof INameableVersion) { + return $this->version->getLabel(); + } + return null; + } + public function getLastModified(): int { return $this->version->getTimestamp(); } diff --git a/apps/files_versions/lib/Sabre/VersionHome.php b/apps/files_versions/lib/Sabre/VersionHome.php index 13505d9a96c..07ac491f2a1 100644 --- a/apps/files_versions/lib/Sabre/VersionHome.php +++ b/apps/files_versions/lib/Sabre/VersionHome.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.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\Sabre; @@ -32,23 +15,12 @@ use Sabre\DAV\ICollection; class VersionHome implements ICollection { - /** @var array */ - private $principalInfo; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IUserManager */ - private $userManager; - - /** @var IVersionManager */ - private $versionManager; - - public function __construct(array $principalInfo, IRootFolder $rootFolder, IUserManager $userManager, IVersionManager $versionManager) { - $this->principalInfo = $principalInfo; - $this->rootFolder = $rootFolder; - $this->userManager = $userManager; - $this->versionManager = $versionManager; + public function __construct( + private array $principalInfo, + private IRootFolder $rootFolder, + private IUserManager $userManager, + private IVersionManager $versionManager, + ) { } private function getUser() { diff --git a/apps/files_versions/lib/Sabre/VersionRoot.php b/apps/files_versions/lib/Sabre/VersionRoot.php index 69ac12ed8e9..7f7014fbee3 100644 --- a/apps/files_versions/lib/Sabre/VersionRoot.php +++ b/apps/files_versions/lib/Sabre/VersionRoot.php @@ -3,26 +3,8 @@ declare(strict_types=1); /** - * @copyright 2018, Roeland Jago Douma <roeland@famdouma.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\Sabre; @@ -36,19 +18,11 @@ use Sabre\DAV\ICollection; class VersionRoot implements ICollection { - /** @var IUser */ - private $user; - - /** @var IRootFolder */ - private $rootFolder; - - /** @var IVersionManager */ - private $versionManager; - - public function __construct(IUser $user, IRootFolder $rootFolder, IVersionManager $versionManager) { - $this->user = $user; - $this->rootFolder = $rootFolder; - $this->versionManager = $versionManager; + public function __construct( + private IUser $user, + private IRootFolder $rootFolder, + private IVersionManager $versionManager, + ) { } public function delete() { @@ -75,14 +49,12 @@ class VersionRoot implements ICollection { $userFolder = $this->rootFolder->getUserFolder($this->user->getUID()); $fileId = (int)$name; - $nodes = $userFolder->getById($fileId); + $node = $userFolder->getFirstNodeById($fileId); - if ($nodes === []) { + if (!$node) { throw new NotFound(); } - $node = array_pop($nodes); - if (!$node instanceof File) { throw new NotFound(); } diff --git a/apps/files_versions/lib/Storage.php b/apps/files_versions/lib/Storage.php index 2fd208cd364..6d53a19a518 100644 --- a/apps/files_versions/lib/Storage.php +++ b/apps/files_versions/lib/Storage.php @@ -1,72 +1,49 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Arthur Schiwon <blizzz@arthur-schiwon.de> - * @author Bart Visscher <bartv@thisnet.nl> - * @author Bjoern Schiessle <bjoern@schiessle.org> - * @author Björn Schießle <bjoern@schiessle.org> - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Felix Moeller <mail@felixmoeller.de> - * @author Felix Nieuwenhuizen <felix@tdlrali.com> - * @author Joas Schilling <coding@schilljs.com> - * @author Jörn Friedrich Dreyer <jfd@butonic.de> - * @author Julius Härtl <jus@bitgrid.net> - * @author Liam JACK <liamjack@users.noreply.github.com> - * @author Lukas Reschke <lukas@statuscode.ch> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Robin Appelman <robin@icewind.nl> - * @author Robin McCorkell <robin@mccorkell.me.uk> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Thomas Müller <thomas.mueller@tmit.eu> - * @author Victor Dubiniuk <dubiniuk@owncloud.com> - * @author Vincent Petry <vincent@nextcloud.com> - * - * @license AGPL-3.0 - * - * This code is free software: you can redistribute it and/or modify - * it under the terms of the GNU Affero General Public License, version 3, - * as published by the Free Software Foundation. - * - * This program is distributed in the hope that it will be useful, - * but WITHOUT ANY WARRANTY; without even the implied warranty of - * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the - * GNU Affero General Public License for more details. - * - * You should have received a copy of the GNU Affero General Public License, version 3, - * along with this program. If not, see <http://www.gnu.org/licenses/> - * + * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files_Versions; +use OC\Files\Filesystem; +use OC\Files\ObjectStore\ObjectStoreStorage; use OC\Files\Search\SearchBinaryOperator; use OC\Files\Search\SearchComparison; use OC\Files\Search\SearchQuery; -use OC_User; -use OC\Files\Filesystem; use OC\Files\View; +use OC\User\NoUserException; +use OC_User; use OCA\Files_Sharing\SharedMount; use OCA\Files_Versions\AppInfo\Application; use OCA\Files_Versions\Command\Expire; use OCA\Files_Versions\Db\VersionsMapper; use OCA\Files_Versions\Events\CreateVersionEvent; use OCA\Files_Versions\Versions\IVersionManager; +use OCP\AppFramework\Db\DoesNotExistException; +use OCP\Command\IBus; +use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files; use OCP\Files\FileInfo; use OCP\Files\Folder; +use OCP\Files\IMimeTypeDetector; use OCP\Files\IRootFolder; use OCP\Files\Node; -use OCP\Command\IBus; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\IMimeTypeDetector; use OCP\Files\NotFoundException; +use OCP\Files\NotPermittedException; use OCP\Files\Search\ISearchBinaryOperator; use OCP\Files\Search\ISearchComparison; +use OCP\Files\Storage\IWriteStreamStorage; +use OCP\Files\StorageInvalidException; use OCP\Files\StorageNotAvailableException; use OCP\IURLGenerator; use OCP\IUser; use OCP\IUserManager; use OCP\Lock\ILockingProvider; +use OCP\Server; +use OCP\Util; use Psr\Log\LoggerInterface; class Storage { @@ -98,7 +75,7 @@ class Storage { 6 => ['intervalEndsAfter' => -1, 'step' => 604800], ]; - /** @var \OCA\Files_Versions\AppInfo\Application */ + /** @var Application */ private static $application; /** @@ -107,11 +84,11 @@ class Storage { * * @param string $filename * @return array - * @throws \OC\User\NoUserException + * @throws NoUserException */ public static function getUidAndFilename($filename) { $uid = Filesystem::getOwner($filename); - $userManager = \OC::$server->get(IUserManager::class); + $userManager = Server::get(IUserManager::class); // if the user with the UID doesn't exists, e.g. because the UID points // to a remote user with a federated cloud ID we use the current logged-in // user. We need a valid local user to create the versions @@ -121,7 +98,7 @@ class Storage { Filesystem::initMountPoints($uid); if ($uid !== OC_User::getUser()) { $info = Filesystem::getFileInfo($filename); - $ownerView = new View('/'.$uid.'/files'); + $ownerView = new View('/' . $uid . '/files'); try { $filename = $ownerView->getPath($info['fileid']); // make sure that the file name doesn't end with a trailing slash @@ -194,10 +171,10 @@ class Storage { $uid = Filesystem::getView()->getOwner(''); /** @var IRootFolder $rootFolder */ - $rootFolder = \OC::$server->get(IRootFolder::class); + $rootFolder = Server::get(IRootFolder::class); $userFolder = $rootFolder->getUserFolder($uid); - $eventDispatcher = \OC::$server->get(IEventDispatcher::class); + $eventDispatcher = Server::get(IEventDispatcher::class); try { $file = $userFolder->get($filename); } catch (NotFoundException $e) { @@ -207,15 +184,15 @@ class Storage { $mount = $file->getMountPoint(); if ($mount instanceof SharedMount) { $ownerFolder = $rootFolder->getUserFolder($mount->getShare()->getShareOwner()); - $ownerNodes = $ownerFolder->getById($file->getId()); - if (count($ownerNodes)) { - $file = current($ownerNodes); + $ownerNode = $ownerFolder->getFirstNodeById($file->getId()); + if ($ownerNode) { + $file = $ownerNode; $uid = $mount->getShare()->getShareOwner(); } } /** @var IUserManager $userManager */ - $userManager = \OC::$server->get(IUserManager::class); + $userManager = Server::get(IUserManager::class); $user = $userManager->get($uid); if (!$user) { @@ -234,7 +211,7 @@ class Storage { } /** @var IVersionManager $versionManager */ - $versionManager = \OC::$server->get(IVersionManager::class); + $versionManager = Server::get(IVersionManager::class); $versionManager->createVersion($user, $file); } @@ -306,9 +283,9 @@ class Storage { * Rename or copy versions of a file of the given paths * * @param string $sourcePath source path of the file to move, relative to - * the currently logged in user's "files" folder + * the currently logged in user's "files" folder * @param string $targetPath target path of the file to move, relative to - * the currently logged in user's "files" folder + * the currently logged in user's "files" folder * @param string $operation can be 'copy' or 'rename' */ public static function renameOrCopy($sourcePath, $targetPath, $operation) { @@ -333,7 +310,7 @@ class Storage { // does the directory exists for versions too ? if ($rootView->is_dir('/' . $sourceOwner . '/files_versions/' . $sourcePath)) { // create missing dirs if necessary - self::createMissingDirectories($targetPath, new View('/'. $targetOwner)); + self::createMissingDirectories($targetPath, new View('/' . $targetOwner)); // move the directory containing the versions $rootView->$operation( @@ -343,13 +320,13 @@ class Storage { } } elseif ($versions = Storage::getVersions($sourceOwner, '/' . $sourcePath)) { // create missing dirs if necessary - self::createMissingDirectories($targetPath, new View('/'. $targetOwner)); + self::createMissingDirectories($targetPath, new View('/' . $targetOwner)); foreach ($versions as $v) { // move each version one by one to the target directory $rootView->$operation( - '/' . $sourceOwner . '/files_versions/' . $sourcePath.'.v' . $v['version'], - '/' . $targetOwner . '/files_versions/' . $targetPath.'.v'.$v['version'] + '/' . $sourceOwner . '/files_versions/' . $sourcePath . '.v' . $v['version'], + '/' . $targetOwner . '/files_versions/' . $targetPath . '.v' . $v['version'] ); } } @@ -372,11 +349,11 @@ class Storage { $filename = '/' . ltrim($file, '/'); // Fetch the userfolder to trigger view hooks - $root = \OC::$server->get(IRootFolder::class); + $root = Server::get(IRootFolder::class); $userFolder = $root->getUserFolder($user->getUID()); - $users_view = new View('/'.$user->getUID()); - $files_view = new View('/'. $user->getUID().'/files'); + $users_view = new View('/' . $user->getUID()); + $files_view = new View('/' . $user->getUID() . '/files'); $versionCreated = false; @@ -388,9 +365,9 @@ class Storage { } //first create a new version - $version = 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename); + $version = 'files_versions' . $filename . '.v' . $users_view->filemtime('files' . $filename); if (!$users_view->file_exists($version)) { - $users_view->copy('files'.$filename, 'files_versions'.$filename.'.v'.$users_view->filemtime('files'.$filename)); + $users_view->copy('files' . $filename, 'files_versions' . $filename . '.v' . $users_view->filemtime('files' . $filename)); $versionCreated = true; } @@ -405,7 +382,8 @@ class Storage { $fileInfo->getId(), [ 'encrypted' => $oldVersion, 'encryptedVersion' => $oldVersion, - 'size' => $oldFileInfo->getSize() + 'size' => $oldFileInfo->getData()['size'], + 'unencrypted_size' => $oldFileInfo->getData()['unencrypted_size'], ] ); @@ -414,14 +392,6 @@ class Storage { $files_view->touch($file, $revision); Storage::scheduleExpire($user->getUID(), $file); - $node = $userFolder->get($file); - - // TODO: move away from those legacy hooks! - \OC_Hook::emit('\OCP\Versions', 'rollback', [ - 'path' => $filename, - 'revision' => $revision, - 'node' => $node, - ]); return true; } elseif ($versionCreated) { self::deleteVersion($users_view, $version); @@ -448,24 +418,45 @@ class Storage { $view->lockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); $view->lockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); - // TODO add a proper way of overwriting a file while maintaining file ids - if ($storage1->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage') || $storage2->instanceOfStorage('\OC\Files\ObjectStore\ObjectStoreStorage')) { - $source = $storage1->fopen($internalPath1, 'r'); - $target = $storage2->fopen($internalPath2, 'w'); - [, $result] = \OC_Helper::streamCopy($source, $target); - fclose($source); - fclose($target); + try { + // TODO add a proper way of overwriting a file while maintaining file ids + if ($storage1->instanceOfStorage(ObjectStoreStorage::class) + || $storage2->instanceOfStorage(ObjectStoreStorage::class) + ) { + $source = $storage1->fopen($internalPath1, 'r'); + $result = $source !== false; + if ($result) { + if ($storage2->instanceOfStorage(IWriteStreamStorage::class)) { + /** @var IWriteStreamStorage $storage2 */ + $storage2->writeStream($internalPath2, $source); + } else { + $target = $storage2->fopen($internalPath2, 'w'); + $result = $target !== false; + if ($result) { + [, $result] = Files::streamCopy($source, $target, true); + } + // explicit check as S3 library closes streams already + if (is_resource($target)) { + fclose($target); + } + } + } + // explicit check as S3 library closes streams already + if (is_resource($source)) { + fclose($source); + } - if ($result !== false) { - $storage1->unlink($internalPath1); + if ($result !== false) { + $storage1->unlink($internalPath1); + } + } else { + $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); } - } else { - $result = $storage2->moveFromStorage($storage1, $internalPath1, $internalPath2); + } finally { + $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); + $view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); } - $view->unlockFile($path1, ILockingProvider::LOCK_EXCLUSIVE); - $view->unlockFile($path2, ILockingProvider::LOCK_EXCLUSIVE); - return ($result !== false); } @@ -507,7 +498,7 @@ class Storage { $pathparts = pathinfo($entryName); $timestamp = substr($pathparts['extension'] ?? '', 1); if (!is_numeric($timestamp)) { - \OC::$server->get(LoggerInterface::class)->error( + Server::get(LoggerInterface::class)->error( 'Version file {path} has incorrect name format', [ 'path' => $entryName, @@ -524,14 +515,14 @@ class Storage { $versions[$key]['preview'] = ''; } else { /** @var IURLGenerator $urlGenerator */ - $urlGenerator = \OC::$server->get(IURLGenerator::class); + $urlGenerator = Server::get(IURLGenerator::class); $versions[$key]['preview'] = $urlGenerator->linkToRoute('files_version.Preview.getPreview', ['file' => $userFullPath, 'version' => $timestamp]); } $versions[$key]['path'] = Filesystem::normalizePath($pathinfo['dirname'] . '/' . $filename); $versions[$key]['name'] = $versionedFile; $versions[$key]['size'] = $view->filesize($dir . '/' . $entryName); - $versions[$key]['mimetype'] = \OC::$server->get(IMimeTypeDetector::class)->detectPath($versionedFile); + $versions[$key]['mimetype'] = Server::get(IMimeTypeDetector::class)->detectPath($versionedFile); } } } @@ -551,7 +542,7 @@ class Storage { */ public static function expireOlderThanMaxForUser($uid) { /** @var IRootFolder $root */ - $root = \OC::$server->get(IRootFolder::class); + $root = Server::get(IRootFolder::class); try { /** @var Folder $versionsRoot */ $versionsRoot = $root->get('/' . $uid . '/files_versions'); @@ -575,7 +566,7 @@ class Storage { )); /** @var VersionsMapper $versionsMapper */ - $versionsMapper = \OC::$server->get(VersionsMapper::class); + $versionsMapper = Server::get(VersionsMapper::class); $userFolder = $root->getUserFolder($uid); $versionEntities = []; @@ -591,12 +582,28 @@ class Storage { // Check that the version does not have a label. $path = $versionsRoot->getRelativePath($info->getPath()); - $node = $userFolder->get(substr($path, 0, -strlen('.v'.$version))); - $versionEntity = $versionsMapper->findVersionForFileId($node->getId(), $version); - $versionEntities[$info->getId()] = $versionEntity; + if ($path === null) { + throw new DoesNotExistException('Could not find relative path of (' . $info->getPath() . ')'); + } + + try { + $node = $userFolder->get(substr($path, 0, -strlen('.v' . $version))); + $versionEntity = $versionsMapper->findVersionForFileId($node->getId(), $version); + $versionEntities[$info->getId()] = $versionEntity; - if ($versionEntity->getLabel() !== '') { + if ($versionEntity->getMetadataValue('label') !== null && $versionEntity->getMetadataValue('label') !== '') { + return false; + } + } catch (NotFoundException $e) { + // Original node not found, delete the version + return true; + } catch (StorageNotAvailableException|StorageInvalidException $e) { + // Storage can't be used, but it might only be temporary so we can't always delete the version + // since we can't determine if the version is named we take the safe route and don't expire return false; + } catch (DoesNotExistException $ex) { + // Version on FS can have no equivalent in the DB if they were created before the version naming feature. + // So we ignore DoesNotExistException. } // Check that the version's timestamp is lower than $threshold @@ -606,9 +613,18 @@ class Storage { foreach ($versions as $version) { $internalPath = $version->getInternalPath(); \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]); - $versionsMapper->delete($versionEntities[$version->getId()]); - $version->delete(); - \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]); + + $versionEntity = isset($versionEntities[$version->getId()]) ? $versionEntities[$version->getId()] : null; + if (!is_null($versionEntity)) { + $versionsMapper->delete($versionEntity); + } + + try { + $version->delete(); + \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $internalPath, 'trigger' => self::DELETE_TRIGGER_RETENTION_CONSTRAINT]); + } catch (NotPermittedException $e) { + Server::get(LoggerInterface::class)->error("Missing permissions to delete version: {$internalPath}", ['app' => 'files_versions', 'exception' => $e]); + } } } @@ -622,19 +638,19 @@ class Storage { $diff = time() - $timestamp; if ($diff < 60) { // first minute - return $diff . " seconds ago"; + return $diff . ' seconds ago'; } elseif ($diff < 3600) { //first hour - return round($diff / 60) . " minutes ago"; + return round($diff / 60) . ' minutes ago'; } elseif ($diff < 86400) { // first day - return round($diff / 3600) . " hours ago"; + return round($diff / 3600) . ' hours ago'; } elseif ($diff < 604800) { //first week - return round($diff / 86400) . " days ago"; + return round($diff / 86400) . ' days ago'; } elseif ($diff < 2419200) { //first month - return round($diff / 604800) . " weeks ago"; + return round($diff / 604800) . ' weeks ago'; } elseif ($diff < 29030400) { // first year - return round($diff / 2419200) . " months ago"; + return round($diff / 2419200) . ' months ago'; } else { - return round($diff / 29030400) . " years ago"; + return round($diff / 29030400) . ' years ago'; } } @@ -677,7 +693,7 @@ class Storage { ]; foreach ($versions as $key => $value) { - $size = $view->filesize(self::VERSIONS_ROOT.'/'.$value['path'].'.v'.$value['timestamp']); + $size = $view->filesize(self::VERSIONS_ROOT . '/' . $value['path'] . '.v' . $value['timestamp']); $filename = $value['path']; $result['all'][$key]['version'] = $value['timestamp']; @@ -703,14 +719,28 @@ class Storage { $expiration = self::getExpiration(); if ($expiration->shouldAutoExpire()) { - [$toDelete, $size] = self::getAutoExpireList($time, $versions); + // Exclude versions that are newer than the minimum age from the auto expiration logic. + $minAge = $expiration->getMinAgeAsTimestamp(); + if ($minAge !== false) { + $versionsToAutoExpire = array_filter($versions, fn ($version) => $version['version'] < $minAge); + } else { + $versionsToAutoExpire = $versions; + } + + [$toDelete, $size] = self::getAutoExpireList($time, $versionsToAutoExpire); } else { $size = 0; $toDelete = []; // versions we want to delete } foreach ($versions as $key => $version) { - if ($expiration->isExpired($version['version'], $quotaExceeded) && !isset($toDelete[$key])) { + if (!is_numeric($version['version'])) { + Server::get(LoggerInterface::class)->error( + 'Found a non-numeric timestamp version: ' . json_encode($version), + ['app' => 'files_versions']); + continue; + } + if ($expiration->isExpired((int)($version['version']), $quotaExceeded) && !isset($toDelete[$key])) { $size += $version['size']; $toDelete[$key] = $version['path'] . '.v' . $version['version']; } @@ -756,7 +786,7 @@ class Storage { //distance between two version too small, mark to delete $toDelete[$key] = $version['path'] . '.v' . $version['version']; $size += $version['size']; - \OC::$server->get(LoggerInterface::class)->info('Mark to expire '. $version['path'] .' next version should be ' . $nextVersion . " or smaller. (prevTimestamp: " . $prevTimestamp . "; step: " . $step, ['app' => 'files_versions']); + Server::get(LoggerInterface::class)->info('Mark to expire ' . $version['path'] . ' next version should be ' . $nextVersion . ' or smaller. (prevTimestamp: ' . $prevTimestamp . '; step: ' . $step, ['app' => 'files_versions']); } else { $nextVersion = $version['version'] - $step; $prevTimestamp = $version['version']; @@ -791,7 +821,7 @@ class Storage { if ($expiration->isEnabled()) { $command = new Expire($uid, $fileName); /** @var IBus $bus */ - $bus = \OC::$server->get(IBus::class); + $bus = Server::get(IBus::class); $bus->push($command); } } @@ -810,14 +840,14 @@ class Storage { $expiration = self::getExpiration(); /** @var LoggerInterface $logger */ - $logger = \OC::$server->get(LoggerInterface::class); + $logger = Server::get(LoggerInterface::class); if ($expiration->isEnabled()) { // get available disk space for user - $user = \OC::$server->get(IUserManager::class)->get($uid); + $user = Server::get(IUserManager::class)->get($uid); if (is_null($user)) { $logger->error('Backends provided no user object for ' . $uid, ['app' => 'files_versions']); - throw new \OC\User\NoUserException('Backends provided no user object for ' . $uid); + throw new NoUserException('Backends provided no user object for ' . $uid); } \OC_Util::setupFS($uid); @@ -836,7 +866,7 @@ class Storage { // file maybe renamed or deleted return false; } - $versionsFileview = new View('/'.$uid.'/files_versions'); + $versionsFileview = new View('/' . $uid . '/files_versions'); $softQuota = true; $quota = $user->getQuota(); @@ -844,7 +874,7 @@ class Storage { $quota = Filesystem::free_space('/'); $softQuota = false; } else { - $quota = \OCP\Util::computerFileSize($quota); + $quota = Util::computerFileSize($quota); } // make sure that we have the current size of the version history @@ -854,7 +884,7 @@ class Storage { // subtract size of files and current versions size from quota if ($quota >= 0) { if ($softQuota) { - $root = \OC::$server->get(IRootFolder::class); + $root = Server::get(IRootFolder::class); $userFolder = $root->getUserFolder($uid); if (is_null($userFolder)) { $availableSpace = 0; @@ -896,6 +926,21 @@ class Storage { } foreach ($toDelete as $key => $path) { + // Make sure to cleanup version table relations as expire does not pass deleteVersion + try { + /** @var VersionsMapper $versionsMapper */ + $versionsMapper = Server::get(VersionsMapper::class); + $file = Server::get(IRootFolder::class)->getUserFolder($uid)->get($filename); + $pathparts = pathinfo($path); + $timestamp = (int)substr($pathparts['extension'] ?? '', 1); + $versionEntity = $versionsMapper->findVersionForFileId($file->getId(), $timestamp); + if ($versionEntity->getMetadataValue('label') !== null && $versionEntity->getMetadataValue('label') !== '') { + continue; + } + $versionsMapper->delete($versionEntity); + } catch (DoesNotExistException $e) { + } + \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]); self::deleteVersion($versionsFileview, $path); \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $path, 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]); @@ -913,10 +958,10 @@ class Storage { reset($allVersions); while ($availableSpace < 0 && $i < $numOfVersions) { $version = current($allVersions); - \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]); + \OC_Hook::emit('\OCP\Versions', 'preDelete', ['path' => $version['path'] . '.v' . $version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]); self::deleteVersion($versionsFileview, $version['path'] . '.v' . $version['version']); - \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $version['path'].'.v'.$version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]); - $logger->info('running out of space! Delete oldest version: ' . $version['path'].'.v'.$version['version'], ['app' => 'files_versions']); + \OC_Hook::emit('\OCP\Versions', 'delete', ['path' => $version['path'] . '.v' . $version['version'], 'trigger' => self::DELETE_TRIGGER_QUOTA_EXCEEDED]); + $logger->info('running out of space! Delete oldest version: ' . $version['path'] . '.v' . $version['version'], ['app' => 'files_versions']); $versionsSize -= $version['size']; $availableSpace += $version['size']; next($allVersions); @@ -934,13 +979,13 @@ class Storage { * that match the given path to a file. * * @param string $filename $path to a file, relative to the user's - * "files" folder + * "files" folder * @param View $view view on data/user/ */ public static function createMissingDirectories($filename, $view) { $dirname = Filesystem::normalizePath(dirname($filename)); $dirParts = explode('/', $dirname); - $dir = "/files_versions"; + $dir = '/files_versions'; foreach ($dirParts as $part) { $dir = $dir . '/' . $part; if (!$view->file_exists($dir)) { @@ -955,7 +1000,7 @@ class Storage { */ protected static function getExpiration() { if (self::$application === null) { - self::$application = \OC::$server->get(Application::class); + self::$application = Server::get(Application::class); } return self::$application->getContainer()->get(Expiration::class); } 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; + } + } } |