diff options
Diffstat (limited to 'apps/files/lib/Service')
-rw-r--r-- | apps/files/lib/Service/ChunkedUploadConfig.php | 30 | ||||
-rw-r--r-- | apps/files/lib/Service/DirectEditingService.php | 35 | ||||
-rw-r--r-- | apps/files/lib/Service/LivePhotosService.php | 36 | ||||
-rw-r--r-- | apps/files/lib/Service/OwnershipTransferService.php | 429 | ||||
-rw-r--r-- | apps/files/lib/Service/SettingsService.php | 63 | ||||
-rw-r--r-- | apps/files/lib/Service/TagService.php | 102 | ||||
-rw-r--r-- | apps/files/lib/Service/UserConfig.php | 80 | ||||
-rw-r--r-- | apps/files/lib/Service/ViewConfig.php | 36 |
8 files changed, 478 insertions, 333 deletions
diff --git a/apps/files/lib/Service/ChunkedUploadConfig.php b/apps/files/lib/Service/ChunkedUploadConfig.php new file mode 100644 index 00000000000..29661750f8b --- /dev/null +++ b/apps/files/lib/Service/ChunkedUploadConfig.php @@ -0,0 +1,30 @@ +<?php + +declare(strict_types=1); + +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files\Service; + +use OCP\IConfig; +use OCP\Server; + +class ChunkedUploadConfig { + private const KEY_MAX_SIZE = 'files.chunked_upload.max_size'; + private const KEY_MAX_PARALLEL_COUNT = 'files.chunked_upload.max_parallel_count'; + + public static function getMaxChunkSize(): int { + return Server::get(IConfig::class)->getSystemValueInt(self::KEY_MAX_SIZE, 100 * 1024 * 1024); + } + + public static function setMaxChunkSize(int $maxChunkSize): void { + Server::get(IConfig::class)->setSystemValue(self::KEY_MAX_SIZE, $maxChunkSize); + } + + public static function getMaxParallelCount(): int { + return Server::get(IConfig::class)->getSystemValueInt(self::KEY_MAX_PARALLEL_COUNT, 5); + } +} diff --git a/apps/files/lib/Service/DirectEditingService.php b/apps/files/lib/Service/DirectEditingService.php index 1ee3a53d85a..3d756ee56fa 100644 --- a/apps/files/lib/Service/DirectEditingService.php +++ b/apps/files/lib/Service/DirectEditingService.php @@ -1,25 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2019 Julius Härtl <jus@bitgrid.net> - * - * @author Julius Härtl <jus@bitgrid.net> - * @author Tobias Kaminsky <tobias@kaminsky.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: 2019 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files\Service; @@ -32,14 +15,10 @@ use OCP\EventDispatcher\IEventDispatcher; class DirectEditingService { - /** @var IManager */ - private $directEditingManager; - /** @var IEventDispatcher */ - private $eventDispatcher; - - public function __construct(IEventDispatcher $eventDispatcher, IManager $directEditingManager) { - $this->directEditingManager = $directEditingManager; - $this->eventDispatcher = $eventDispatcher; + public function __construct( + private IEventDispatcher $eventDispatcher, + private IManager $directEditingManager, + ) { } public function getDirectEditingETag(): string { diff --git a/apps/files/lib/Service/LivePhotosService.php b/apps/files/lib/Service/LivePhotosService.php new file mode 100644 index 00000000000..3ac6601d5dc --- /dev/null +++ b/apps/files/lib/Service/LivePhotosService.php @@ -0,0 +1,36 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ + +namespace OCA\Files\Service; + +use OCP\FilesMetadata\Exceptions\FilesMetadataNotFoundException; +use OCP\FilesMetadata\IFilesMetadataManager; + +class LivePhotosService { + public function __construct( + private IFilesMetadataManager $filesMetadataManager, + ) { + } + + /** + * Get the associated live photo for a given file id + */ + public function getLivePhotoPeerId(int $fileId): ?int { + try { + $metadata = $this->filesMetadataManager->getMetadata($fileId); + } catch (FilesMetadataNotFoundException $ex) { + return null; + } + + if (!$metadata->hasKey('files-live-photo')) { + return null; + } + + return (int)$metadata->getString('files-live-photo'); + } +} diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php index 3499d809f2c..afef5d2093d 100644 --- a/apps/files/lib/Service/OwnershipTransferService.php +++ b/apps/files/lib/Service/OwnershipTransferService.php @@ -3,47 +3,33 @@ declare(strict_types=1); /** - * @copyright 2019 Christoph Wurst <christoph@winzerhof-wurst.at> - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Joas Schilling <coding@schilljs.com> - * @author Julius Härtl <jus@bitgrid.net> - * @author Morris Jobke <hey@morrisjobke.de> - * @author Roeland Jago Douma <roeland@famdouma.nl> - * @author Sascha Wiswedel <sascha.wiswedel@nextcloud.com> - * @author Tobia De Koninck <LEDfan@users.noreply.github.com> - * - * @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\Service; use Closure; +use Exception; use OC\Files\Filesystem; use OC\Files\View; +use OC\User\NoUserException; +use OCA\Encryption\Util; use OCA\Files\Exception\TransferOwnershipException; +use OCA\Files_External\Config\ConfigAdapter; use OCP\Encryption\IManager as IEncryptionManager; +use OCP\Files\Config\IHomeMountProvider; use OCP\Files\Config\IUserMountCache; +use OCP\Files\File; use OCP\Files\FileInfo; -use OCP\Files\IHomeStorage; use OCP\Files\InvalidPathException; +use OCP\Files\IRootFolder; use OCP\Files\Mount\IMountManager; +use OCP\Files\NotFoundException; use OCP\IUser; use OCP\IUserManager; +use OCP\L10N\IFactory; +use OCP\Server; use OCP\Share\IManager as IShareManager; use OCP\Share\IShare; use Symfony\Component\Console\Helper\ProgressBar; @@ -58,31 +44,15 @@ use function rtrim; class OwnershipTransferService { - /** @var IEncryptionManager */ - private $encryptionManager; - - /** @var IShareManager */ - private $shareManager; - - /** @var IMountManager */ - private $mountManager; - - /** @var IUserMountCache */ - private $userMountCache; - - /** @var IUserManager */ - private $userManager; - - public function __construct(IEncryptionManager $manager, - IShareManager $shareManager, - IMountManager $mountManager, - IUserMountCache $userMountCache, - IUserManager $userManager) { - $this->encryptionManager = $manager; - $this->shareManager = $shareManager; - $this->mountManager = $mountManager; - $this->userMountCache = $userMountCache; - $this->userManager = $userManager; + public function __construct( + private IEncryptionManager $encryptionManager, + private IShareManager $shareManager, + private IMountManager $mountManager, + private IUserMountCache $userMountCache, + private IUserManager $userManager, + private IFactory $l10nFactory, + private IRootFolder $rootFolder, + ) { } /** @@ -93,15 +63,17 @@ class OwnershipTransferService { * @param OutputInterface|null $output * @param bool $move * @throws TransferOwnershipException - * @throws \OC\User\NoUserException + * @throws NoUserException */ - public function transfer(IUser $sourceUser, + public function transfer( + IUser $sourceUser, IUser $destinationUser, string $path, ?OutputInterface $output = null, bool $move = false, bool $firstLogin = false, - bool $transferIncomingShares = false): void { + bool $includeExternalStorage = false, + ): void { $output = $output ?? new NullOutput(); $sourceUid = $sourceUser->getUID(); $destinationUid = $destinationUser->getUID(); @@ -110,15 +82,17 @@ class OwnershipTransferService { // If encryption is on we have to ensure the user has logged in before and that all encryption modules are ready if (($this->encryptionManager->isEnabled() && $destinationUser->getLastLogin() === 0) || !$this->encryptionManager->isReadyForUser($destinationUid)) { - throw new TransferOwnershipException("The target user is not ready to accept files. The user has at least to have logged in once.", 2); + throw new TransferOwnershipException('The target user is not ready to accept files. The user has at least to have logged in once.', 2); } // setup filesystem // Requesting the user folder will set it up if the user hasn't logged in before // We need a setupFS for the full filesystem setup before as otherwise we will just return // a lazy root folder which does not create the destination users folder + \OC_Util::setupFS($sourceUser->getUID()); \OC_Util::setupFS($destinationUser->getUID()); - \OC::$server->getUserFolder($destinationUser->getUID()); + $this->rootFolder->getUserFolder($sourceUser->getUID()); + $this->rootFolder->getUserFolder($destinationUser->getUID()); Filesystem::initMountPoints($sourceUid); Filesystem::initMountPoints($destinationUid); @@ -127,19 +101,15 @@ class OwnershipTransferService { if ($move) { $finalTarget = "$destinationUid/files/"; } else { + $l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser)); $date = date('Y-m-d H-i-s'); - // Remove some characters which are prone to cause errors - $cleanUserName = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $sourceUser->getDisplayName()); - // Replace multiple dashes with one dash - $cleanUserName = preg_replace('/-{2,}/s', '-', $cleanUserName); - $cleanUserName = $cleanUserName ?: $sourceUid; - - $finalTarget = "$destinationUid/files/transferred from $cleanUserName on $date"; + $cleanUserName = $this->sanitizeFolderName($sourceUser->getDisplayName()) ?: $sourceUid; + $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$cleanUserName, $date])); try { $view->verifyPath(dirname($finalTarget), basename($finalTarget)); } catch (InvalidPathException $e) { - $finalTarget = "$destinationUid/files/transferred from $sourceUid on $date"; + $finalTarget = "$destinationUid/files/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date])); } } @@ -153,7 +123,7 @@ class OwnershipTransferService { } if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) { - throw new TransferOwnershipException("Destination path does not exists or is not empty", 1); + throw new TransferOwnershipException('Destination path does not exists or is not empty', 1); } @@ -174,49 +144,62 @@ class OwnershipTransferService { $sourcePath ); + $sourceSize = $view->getFileInfo($sourcePath)->getSize(); + // transfer the files $this->transferFiles( $sourceUid, $sourcePath, $finalTarget, $view, - $output + $output, + $includeExternalStorage, ); + $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize(); + // transfer the incoming shares + $sourceShares = $this->collectIncomingShares( + $sourceUid, + $output, + $sourcePath, + ); + $destinationShares = $this->collectIncomingShares( + $destinationUid, + $output, + null, + ); + $this->transferIncomingShares( + $sourceUid, + $destinationUid, + $sourceShares, + $destinationShares, + $output, + $path, + $finalTarget, + $move + ); + + $destinationPath = $finalTarget . '/' . $path; // restore the shares $this->restoreShares( $sourceUid, $destinationUid, + $destinationPath, $shares, $output ); - - // transfer the incoming shares - if ($transferIncomingShares === true) { - $sourceShares = $this->collectIncomingShares( - $sourceUid, - $output, - $view - ); - $destinationShares = $this->collectIncomingShares( - $destinationUid, - $output, - $view, - true - ); - $this->transferIncomingShares( - $sourceUid, - $destinationUid, - $sourceShares, - $destinationShares, - $output, - $path, - $finalTarget, - $move - ); + if ($sizeDifference !== 0) { + $output->writeln("Transferred folder have a size difference of: $sizeDifference Bytes which means the transfer may be incomplete. Please check the logs if there was any issue during the transfer operation."); } } + private function sanitizeFolderName(string $name): string { + // Remove some characters which are prone to cause errors + $name = str_replace(['\\', '/', ':', '.', '?', '#', '\'', '"'], '-', $name); + // Replace multiple dashes with one dash + return preg_replace('/-{2,}/s', '-', $name); + } + private function walkFiles(View $view, $path, Closure $callBack) { foreach ($view->getDirectoryContent($path) as $fileInfo) { if (!$callBack($fileInfo)) { @@ -231,85 +214,130 @@ class OwnershipTransferService { /** * @param OutputInterface $output * - * @throws \Exception + * @throws TransferOwnershipException */ - protected function analyse(string $sourceUid, + protected function analyse( + string $sourceUid, string $destinationUid, string $sourcePath, View $view, - OutputInterface $output): void { + OutputInterface $output, + bool $includeExternalStorage = false, + ): void { $output->writeln('Validating quota'); - $size = $view->getFileInfo($sourcePath, false)->getSize(false); + $sourceFileInfo = $view->getFileInfo($sourcePath, false); + if ($sourceFileInfo === false) { + throw new TransferOwnershipException("Unknown path provided: $sourcePath", 1); + } + $size = $sourceFileInfo->getSize(false); $freeSpace = $view->free_space($destinationUid . '/files/'); if ($size > $freeSpace && $freeSpace !== FileInfo::SPACE_UNKNOWN) { - $output->writeln('<error>Target user does not have enough free space available.</error>'); - throw new \Exception('Execution terminated.'); + throw new TransferOwnershipException('Target user does not have enough free space available.', 1); } $output->writeln("Analysing files of $sourceUid ..."); $progress = new ProgressBar($output); $progress->start(); + if ($this->encryptionManager->isEnabled()) { + $masterKeyEnabled = Server::get(Util::class)->isMasterKeyEnabled(); + } else { + $masterKeyEnabled = false; + } $encryptedFiles = []; - $this->walkFiles($view, $sourcePath, - function (FileInfo $fileInfo) use ($progress) { - if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { - // only analyze into folders from main storage, - if (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) { - return false; - } - return true; - } - $progress->advance(); - if ($fileInfo->isEncrypted()) { - $encryptedFiles[] = $fileInfo; - } - return true; - }); + if ($sourceFileInfo->getType() === FileInfo::TYPE_FOLDER) { + if ($sourceFileInfo->isEncrypted()) { + /* Encrypted folder means e2ee encrypted */ + $encryptedFiles[] = $sourceFileInfo; + } else { + $this->walkFiles($view, $sourcePath, + function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles, $includeExternalStorage) { + if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) { + $mount = $fileInfo->getMountPoint(); + // only analyze into folders from main storage, + if ( + $mount->getMountProvider() instanceof IHomeMountProvider + || ($includeExternalStorage && $mount->getMountProvider() instanceof ConfigAdapter) + ) { + if ($fileInfo->isEncrypted()) { + /* Encrypted folder means e2ee encrypted, we cannot transfer it */ + $encryptedFiles[] = $fileInfo; + } + return true; + } else { + return false; + } + } + $progress->advance(); + if ($fileInfo->isEncrypted() && !$masterKeyEnabled) { + /* Encrypted file means SSE, we can only transfer it if master key is enabled */ + $encryptedFiles[] = $fileInfo; + } + return true; + }); + } + } elseif ($sourceFileInfo->isEncrypted() && !$masterKeyEnabled) { + /* Encrypted file means SSE, we can only transfer it if master key is enabled */ + $encryptedFiles[] = $sourceFileInfo; + } $progress->finish(); $output->writeln(''); // no file is allowed to be encrypted if (!empty($encryptedFiles)) { - $output->writeln("<error>Some files are encrypted - please decrypt them first.</error>"); + $output->writeln('<error>Some files are encrypted - please decrypt them first.</error>'); foreach ($encryptedFiles as $encryptedFile) { /** @var FileInfo $encryptedFile */ - $output->writeln(" " . $encryptedFile->getPath()); + $output->writeln(' ' . $encryptedFile->getPath()); } - throw new \Exception('Execution terminated.'); + throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1); } } - private function collectUsersShares(string $sourceUid, + /** + * @return array<array{share: IShare, suffix: string}> + */ + private function collectUsersShares( + string $sourceUid, OutputInterface $output, View $view, - string $path): array { + string $path, + ): array { $output->writeln("Collecting all share information for files and folders of $sourceUid ..."); $shares = []; $progress = new ProgressBar($output); - foreach ([IShare::TYPE_GROUP, IShare::TYPE_USER, IShare::TYPE_LINK, IShare::TYPE_REMOTE, IShare::TYPE_ROOM, IShare::TYPE_EMAIL, IShare::TYPE_CIRCLE, IShare::TYPE_DECK, IShare::TYPE_SCIENCEMESH] as $shareType) { + $normalizedPath = Filesystem::normalizePath($path); + + $supportedShareTypes = [ + IShare::TYPE_GROUP, + IShare::TYPE_USER, + IShare::TYPE_LINK, + IShare::TYPE_REMOTE, + IShare::TYPE_ROOM, + IShare::TYPE_EMAIL, + IShare::TYPE_CIRCLE, + IShare::TYPE_DECK, + IShare::TYPE_SCIENCEMESH, + ]; + + foreach ($supportedShareTypes as $shareType) { $offset = 0; while (true) { - $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset); + $sharePage = $this->shareManager->getSharesBy($sourceUid, $shareType, null, true, 50, $offset, onlyValid: false); $progress->advance(count($sharePage)); if (empty($sharePage)) { break; } if ($path !== "$sourceUid/files") { - $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $path) { + $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) { try { - $relativePath = $view->getPath($share->getNodeId()); - $singleFileTranfer = $view->is_file($path); - if ($singleFileTranfer) { - return Filesystem::normalizePath($relativePath) === Filesystem::normalizePath($path); - } + $sourceNode = $share->getNode(); + $relativePath = $view->getRelativePath($sourceNode->getPath()); - return mb_strpos( - Filesystem::normalizePath($relativePath . '/', false), - Filesystem::normalizePath($path . '/', false)) === 0; - } catch (\Exception $e) { + return str_starts_with($relativePath . '/', $normalizedPath . '/'); + } catch (Exception $e) { return false; } }); @@ -321,17 +349,32 @@ class OwnershipTransferService { $progress->finish(); $output->writeln(''); - return $shares; + + return array_values(array_filter(array_map(function (IShare $share) use ($view, $normalizedPath, $output, $sourceUid) { + try { + $nodePath = $view->getRelativePath($share->getNode()->getPath()); + } catch (NotFoundException $e) { + $output->writeln("<error>Failed to find path for shared file {$share->getNodeId()} for user $sourceUid, skipping</error>"); + return null; + } + + return [ + 'share' => $share, + 'suffix' => substr(Filesystem::normalizePath($nodePath), strlen($normalizedPath)), + ]; + }, $shares))); } - private function collectIncomingShares(string $sourceUid, + private function collectIncomingShares( + string $sourceUid, OutputInterface $output, - View $view, - bool $addKeys = false): array { + ?string $path, + ): array { $output->writeln("Collecting all incoming share information for files and folders of $sourceUid ..."); $shares = []; $progress = new ProgressBar($output); + $normalizedPath = Filesystem::normalizePath($path); $offset = 0; while (true) { @@ -340,14 +383,19 @@ class OwnershipTransferService { if (empty($sharePage)) { break; } - if ($addKeys) { - foreach ($sharePage as $singleShare) { - $shares[$singleShare->getNodeId()] = $singleShare; - } - } else { - foreach ($sharePage as $singleShare) { - $shares[] = $singleShare; - } + + if ($path !== null && $path !== "$sourceUid/files") { + $sharePage = array_filter($sharePage, static function (IShare $share) use ($sourceUid, $normalizedPath) { + try { + return str_starts_with(Filesystem::normalizePath($sourceUid . '/files' . $share->getTarget() . '/', false), $normalizedPath . '/'); + } catch (Exception) { + return false; + } + }); + } + + foreach ($sharePage as $share) { + $shares[$share->getNodeId()] = $share; } $offset += 50; @@ -362,11 +410,14 @@ class OwnershipTransferService { /** * @throws TransferOwnershipException */ - protected function transferFiles(string $sourceUid, + protected function transferFiles( + string $sourceUid, string $sourcePath, string $finalTarget, View $view, - OutputInterface $output): void { + OutputInterface $output, + bool $includeExternalStorage, + ): void { $output->writeln("Transferring files to $finalTarget ..."); // This change will help user to transfer the folder specified using --path option. @@ -375,26 +426,69 @@ class OwnershipTransferService { $view->mkdir($finalTarget); $finalTarget = $finalTarget . '/' . basename($sourcePath); } - if ($view->rename($sourcePath, $finalTarget) === false) { - throw new TransferOwnershipException("Could not transfer files.", 1); + $sourceInfo = $view->getFileInfo($sourcePath); + + /// handle the external storages mounted at the root, or the admin specifying an external storage with --path + if ($sourceInfo->getInternalPath() === '' && $includeExternalStorage) { + $this->moveMountContents($view, $sourcePath, $finalTarget); + } else { + if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === false) { + throw new TransferOwnershipException('Could not transfer files.', 1); + } + } + + if ($includeExternalStorage) { + $nestedMounts = $this->mountManager->findIn($sourcePath); + foreach ($nestedMounts as $mount) { + if ($mount->getMountProvider() === ConfigAdapter::class) { + $relativePath = substr(trim($mount->getMountPoint(), '/'), strlen($sourcePath)); + $this->moveMountContents($view, $mount->getMountPoint(), $finalTarget . $relativePath); + } + } } + if (!is_dir("$sourceUid/files")) { // because the files folder is moved away we need to recreate it $view->mkdir("$sourceUid/files"); } } - private function restoreShares(string $sourceUid, + private function moveMountContents(View $rootView, string $source, string $target) { + if ($rootView->copy($source, $target)) { + // just doing `rmdir` on the mountpoint would cause it to try and unmount the storage + // we need to empty the contents instead + $content = $rootView->getDirectoryContent($source); + foreach ($content as $item) { + if ($item->getType() === FileInfo::TYPE_FOLDER) { + $rootView->rmdir($item->getPath()); + } else { + $rootView->unlink($item->getPath()); + } + } + } else { + throw new TransferOwnershipException("Could not transfer $source to $target"); + } + } + + /** + * @param string $targetLocation New location of the transfered node + * @param array<array{share: IShare, suffix: string}> $shares previously collected share information + */ + private function restoreShares( + string $sourceUid, string $destinationUid, + string $targetLocation, array $shares, - OutputInterface $output) { - $output->writeln("Restoring shares ..."); + OutputInterface $output, + ):void { + $output->writeln('Restoring shares ...'); $progress = new ProgressBar($output, count($shares)); - foreach ($shares as $share) { + foreach ($shares as ['share' => $share, 'suffix' => $suffix]) { try { - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedWith() === $destinationUid) { + $output->writeln('Transfering share ' . $share->getId() . ' of type ' . $share->getShareType(), OutputInterface::VERBOSITY_VERBOSE); + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedWith() === $destinationUid) { // Unmount the shares before deleting, so we don't try to get the storage later on. $shareMountPoint = $this->mountManager->find('/' . $destinationUid . '/files' . $share->getTarget()); if ($shareMountPoint) { @@ -409,8 +503,8 @@ class OwnershipTransferService { $share->setSharedBy($destinationUid); } - if ($share->getShareType() === IShare::TYPE_USER && - !$this->userManager->userExists($share->getSharedWith())) { + if ($share->getShareType() === IShare::TYPE_USER + && !$this->userManager->userExists($share->getSharedWith())) { // stray share with deleted user $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted user "' . $share->getSharedWith() . '", deleting</error>'); $this->shareManager->deleteShare($share); @@ -419,12 +513,25 @@ class OwnershipTransferService { // trigger refetching of the node so that the new owner and mountpoint are taken into account // otherwise the checks on the share update will fail due to the original node not being available in the new user scope $this->userMountCache->clear(); - $share->setNodeId($share->getNode()->getId()); - $this->shareManager->updateShare($share); + try { + // Try to get the "old" id. + // Normally the ID is preserved, + // but for transferes between different storages the ID might change + $newNodeId = $share->getNode()->getId(); + } catch (NotFoundException) { + // ID has changed due to transfer between different storages + // Try to get the new ID from the target path and suffix of the share + $node = $this->rootFolder->get(Filesystem::normalizePath($targetLocation . '/' . $suffix)); + $newNodeId = $node->getId(); + $output->writeln('Had to change node id to ' . $newNodeId, OutputInterface::VERBOSITY_VERY_VERBOSE); + } + $share->setNodeId($newNodeId); + + $this->shareManager->updateShare($share, onlyValid: false); } } - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>'); } catch (\Throwable $e) { $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getMessage() . ' : ' . $e->getTraceAsString() . '</error>'); @@ -443,11 +550,11 @@ class OwnershipTransferService { string $path, string $finalTarget, bool $move): void { - $output->writeln("Restoring incoming shares ..."); + $output->writeln('Restoring incoming shares ...'); $progress = new ProgressBar($output, count($sourceShares)); $prefix = "$destinationUid/files"; $finalShareTarget = ''; - if (substr($finalTarget, 0, strlen($prefix)) === $prefix) { + if (str_starts_with($finalTarget, $prefix)) { $finalShareTarget = substr($finalTarget, strlen($prefix)); } foreach ($sourceShares as $share) { @@ -457,13 +564,13 @@ class OwnershipTransferService { if (trim($path, '/') !== '') { $pathToCheck = '/' . trim($path) . '/'; } - if (substr($share->getTarget(), 0, strlen($pathToCheck)) !== $pathToCheck) { + if (!str_starts_with($share->getTarget(), $pathToCheck)) { continue; } $shareTarget = $share->getTarget(); $shareTarget = $finalShareTarget . $shareTarget; - if ($share->getShareType() === IShare::TYPE_USER && - $share->getSharedBy() === $destinationUid) { + if ($share->getShareType() === IShare::TYPE_USER + && $share->getSharedBy() === $destinationUid) { $this->shareManager->deleteShare($share); } elseif (isset($destinationShares[$share->getNodeId()])) { $destinationShare = $destinationShares[$share->getNodeId()]; @@ -504,7 +611,7 @@ class OwnershipTransferService { $this->shareManager->moveShare($share, $destinationUid); continue; } - } catch (\OCP\Files\NotFoundException $e) { + } catch (NotFoundException $e) { $output->writeln('<error>Share with id ' . $share->getId() . ' points at deleted file, skipping</error>'); } catch (\Throwable $e) { $output->writeln('<error>Could not restore share with id ' . $share->getId() . ':' . $e->getTraceAsString() . '</error>'); diff --git a/apps/files/lib/Service/SettingsService.php b/apps/files/lib/Service/SettingsService.php new file mode 100644 index 00000000000..d07e907a5f6 --- /dev/null +++ b/apps/files/lib/Service/SettingsService.php @@ -0,0 +1,63 @@ +<?php + +declare(strict_types=1); +/** + * SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later + */ +namespace OCA\Files\Service; + +use OC\Files\FilenameValidator; +use OCP\IConfig; +use Psr\Log\LoggerInterface; + +class SettingsService { + + protected const WINDOWS_EXTENSION = [ + ' ', + '.', + ]; + + protected const WINDOWS_BASENAMES = [ + 'con', 'prn', 'aux', 'nul', 'com0', 'com1', 'com2', 'com3', 'com4', 'com5', + 'com6', 'com7', 'com8', 'com9', 'com¹', 'com²', 'com³', 'lpt0', 'lpt1', 'lpt2', + 'lpt3', 'lpt4', 'lpt5', 'lpt6', 'lpt7', 'lpt8', 'lpt9', 'lpt¹', 'lpt²', 'lpt³', + ]; + + protected const WINDOWS_CHARACTERS = [ + '<', '>', ':', + '"', '|', '?', + '*', + ]; + + public function __construct( + private IConfig $config, + private FilenameValidator $filenameValidator, + private LoggerInterface $logger, + ) { + } + + public function hasFilesWindowsSupport(): bool { + return empty(array_diff(self::WINDOWS_BASENAMES, $this->filenameValidator->getForbiddenBasenames())) + && empty(array_diff(self::WINDOWS_CHARACTERS, $this->filenameValidator->getForbiddenCharacters())) + && empty(array_diff(self::WINDOWS_EXTENSION, $this->filenameValidator->getForbiddenExtensions())); + } + + public function setFilesWindowsSupport(bool $enabled = true): void { + if ($enabled) { + $basenames = array_unique(array_merge(self::WINDOWS_BASENAMES, $this->filenameValidator->getForbiddenBasenames())); + $characters = array_unique(array_merge(self::WINDOWS_CHARACTERS, $this->filenameValidator->getForbiddenCharacters())); + $extensions = array_unique(array_merge(self::WINDOWS_EXTENSION, $this->filenameValidator->getForbiddenExtensions())); + } else { + $basenames = array_unique(array_values(array_diff($this->filenameValidator->getForbiddenBasenames(), self::WINDOWS_BASENAMES))); + $characters = array_unique(array_values(array_diff($this->filenameValidator->getForbiddenCharacters(), self::WINDOWS_CHARACTERS))); + $extensions = array_unique(array_values(array_diff($this->filenameValidator->getForbiddenExtensions(), self::WINDOWS_EXTENSION))); + } + $values = [ + 'forbidden_filename_basenames' => empty($basenames) ? null : $basenames, + 'forbidden_filename_characters' => empty($characters) ? null : $characters, + 'forbidden_filename_extensions' => empty($extensions) ? null : $extensions, + ]; + $this->config->setSystemValues($values); + } +} diff --git a/apps/files/lib/Service/TagService.php b/apps/files/lib/Service/TagService.php index e29848bf21d..63c54d01fd0 100644 --- a/apps/files/lib/Service/TagService.php +++ b/apps/files/lib/Service/TagService.php @@ -1,37 +1,16 @@ <?php + /** - * @copyright Copyright (c) 2016, ownCloud, Inc. - * - * @author Christoph Wurst <christoph@winzerhof-wurst.at> - * @author Daniel Kesselberg <mail@danielkesselberg.de> - * @author Joas Schilling <coding@schilljs.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-2024 Nextcloud GmbH and Nextcloud contributors + * SPDX-FileCopyrightText: 2016 ownCloud, Inc. + * SPDX-License-Identifier: AGPL-3.0-only */ namespace OCA\Files\Service; -use OCA\Files\Activity\FavoriteProvider; use OCP\Activity\IManager; -use OCP\EventDispatcher\IEventDispatcher; -use OCP\Files\Events\NodeAddedToFavorite; -use OCP\Files\Events\NodeRemovedFromFavorite; use OCP\Files\Folder; +use OCP\Files\NotFoundException; use OCP\ITags; -use OCP\IUser; use OCP\IUserSession; /** @@ -39,29 +18,12 @@ use OCP\IUserSession; */ class TagService { - /** @var IUserSession */ - private $userSession; - /** @var IManager */ - private $activityManager; - /** @var ITags|null */ - private $tagger; - /** @var Folder|null */ - private $homeFolder; - /** @var IEventDispatcher */ - private $dispatcher; - public function __construct( - IUserSession $userSession, - IManager $activityManager, - ?ITags $tagger, - ?Folder $homeFolder, - IEventDispatcher $dispatcher, + private IUserSession $userSession, + private IManager $activityManager, + private ?ITags $tagger, + private ?Folder $homeFolder, ) { - $this->userSession = $userSession; - $this->activityManager = $activityManager; - $this->tagger = $tagger; - $this->homeFolder = $homeFolder; - $this->dispatcher = $dispatcher; } /** @@ -70,9 +32,9 @@ class TagService { * replace the actual tag selection. * * @param string $path path - * @param array $tags array of tags + * @param array $tags array of tags * @return array list of tags - * @throws \OCP\Files\NotFoundException if the file does not exist + * @throws NotFoundException if the file does not exist */ public function updateFileTags($path, $tags) { if ($this->tagger === null) { @@ -92,16 +54,10 @@ class TagService { $newTags = array_diff($tags, $currentTags); foreach ($newTags as $tag) { - if ($tag === ITags::TAG_FAVORITE) { - $this->addActivity(true, $fileId, $path); - } $this->tagger->tagAs($fileId, $tag); } $deletedTags = array_diff($currentTags, $tags); foreach ($deletedTags as $tag) { - if ($tag === ITags::TAG_FAVORITE) { - $this->addActivity(false, $fileId, $path); - } $this->tagger->unTag($fileId, $tag); } @@ -109,40 +65,4 @@ class TagService { // list is up to date, in case of concurrent changes ? return $tags; } - - /** - * @param bool $addToFavorite - * @param int $fileId - * @param string $path - */ - protected function addActivity($addToFavorite, $fileId, $path) { - $user = $this->userSession->getUser(); - if (!$user instanceof IUser) { - return; - } - - if ($addToFavorite) { - $event = new NodeAddedToFavorite($user, $fileId, $path); - } else { - $event = new NodeRemovedFromFavorite($user, $fileId, $path); - } - $this->dispatcher->dispatchTyped($event); - - $event = $this->activityManager->generateEvent(); - try { - $event->setApp('files') - ->setObject('files', $fileId, $path) - ->setType('favorite') - ->setAuthor($user->getUID()) - ->setAffectedUser($user->getUID()) - ->setTimestamp(time()) - ->setSubject( - $addToFavorite ? FavoriteProvider::SUBJECT_ADDED : FavoriteProvider::SUBJECT_REMOVED, - ['id' => $fileId, 'path' => $path] - ); - $this->activityManager->publish($event); - } catch (\InvalidArgumentException $e) { - } catch (\BadMethodCallException $e) { - } - } } diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php index 00569dc6aeb..dcf30b7796d 100644 --- a/apps/files/lib/Service/UserConfig.php +++ b/apps/files/lib/Service/UserConfig.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @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\Service; @@ -36,30 +20,72 @@ class UserConfig { 'allowed' => [true, false], ], [ + // The view to start the files app in + 'key' => 'default_view', + 'default' => 'files', + 'allowed' => ['files', 'personal'], + ], + [ + // Whether to show the folder tree + 'key' => 'folder_tree', + 'default' => true, + 'allowed' => [true, false], + ], + [ + // Whether to show the files list in grid view or not + 'key' => 'grid_view', + 'default' => false, + 'allowed' => [true, false], + ], + [ + // Whether to show the "confirm file deletion" warning + 'key' => 'show_dialog_deletion', + 'default' => false, + 'allowed' => [true, false], + ], + [ + // Whether to show the "confirm file extension change" warning + 'key' => 'show_dialog_file_extension', + 'default' => true, + 'allowed' => [true, false], + ], + [ + // Whether to show the files extensions in the files list or not + 'key' => 'show_files_extensions', + 'default' => true, + 'allowed' => [true, false], + ], + [ // Whether to show the hidden files or not in the files list 'key' => 'show_hidden', 'default' => false, 'allowed' => [true, false], ], [ + // Whether to show the mime column or not + 'key' => 'show_mime_column', + 'default' => false, + 'allowed' => [true, false], + ], + [ // Whether to sort favorites first in the list or not 'key' => 'sort_favorites_first', 'default' => true, 'allowed' => [true, false], ], [ - // Whether to show the files list in grid view or not - 'key' => 'grid_view', - 'default' => false, + // Whether to sort folders before files in the list or not + 'key' => 'sort_folders_first', + 'default' => true, 'allowed' => [true, false], ], ]; - - protected IConfig $config; protected ?IUser $user = null; - public function __construct(IConfig $config, IUserSession $userSession) { - $this->config = $config; + public function __construct( + protected IConfig $config, + IUserSession $userSession, + ) { $this->user = $userSession->getUser(); } @@ -119,7 +145,7 @@ class UserConfig { if (!in_array($key, $this->getAllowedConfigKeys())) { throw new \InvalidArgumentException('Unknown config key'); } - + if (!in_array($value, $this->getAllowedConfigValues($key))) { throw new \InvalidArgumentException('Invalid config value'); } diff --git a/apps/files/lib/Service/ViewConfig.php b/apps/files/lib/Service/ViewConfig.php index fefc3f6a7de..cf8bebd5372 100644 --- a/apps/files/lib/Service/ViewConfig.php +++ b/apps/files/lib/Service/ViewConfig.php @@ -1,24 +1,8 @@ <?php + /** - * @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com> - * - * @author John Molakvoæ <skjnldsv@protonmail.com> - * - * @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: 2023 Nextcloud GmbH and Nextcloud contributors + * SPDX-License-Identifier: AGPL-3.0-or-later */ namespace OCA\Files\Service; @@ -50,12 +34,12 @@ class ViewConfig { 'allowed' => [true, false], ], ]; - - protected IConfig $config; protected ?IUser $user = null; - public function __construct(IConfig $config, IUserSession $userSession) { - $this->config = $config; + public function __construct( + protected IConfig $config, + IUserSession $userSession, + ) { $this->user = $userSession->getUser(); } @@ -120,7 +104,7 @@ class ViewConfig { if (!in_array($key, $this->getAllowedConfigKeys())) { throw new \InvalidArgumentException('Unknown config key'); } - + if (!in_array($value, $this->getAllowedConfigValues($key)) && !empty($this->getAllowedConfigValues($key))) { throw new \InvalidArgumentException('Invalid config value'); @@ -149,7 +133,7 @@ class ViewConfig { $userId = $this->user->getUID(); $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true); - + if (!isset($configs[$view])) { $configs[$view] = []; } @@ -175,7 +159,7 @@ class ViewConfig { $userId = $this->user->getUID(); $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true); $views = array_keys($configs); - + return array_reduce($views, function ($carry, $view) use ($configs) { $carry[$view] = $this->getConfig($view); return $carry; |