aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/lib/Service
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/lib/Service')
-rw-r--r--apps/files/lib/Service/ChunkedUploadConfig.php30
-rw-r--r--apps/files/lib/Service/DirectEditingService.php67
-rw-r--r--apps/files/lib/Service/LivePhotosService.php36
-rw-r--r--apps/files/lib/Service/OwnershipTransferService.php624
-rw-r--r--apps/files/lib/Service/SettingsService.php63
-rw-r--r--apps/files/lib/Service/TagService.php119
-rw-r--r--apps/files/lib/Service/UserConfig.php182
-rw-r--r--apps/files/lib/Service/ViewConfig.php168
8 files changed, 1189 insertions, 100 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
new file mode 100644
index 00000000000..3d756ee56fa
--- /dev/null
+++ b/apps/files/lib/Service/DirectEditingService.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Service;
+
+use OCP\DirectEditing\ACreateEmpty;
+use OCP\DirectEditing\ACreateFromTemplate;
+use OCP\DirectEditing\IEditor;
+use OCP\DirectEditing\IManager;
+use OCP\DirectEditing\RegisterDirectEditorEvent;
+use OCP\EventDispatcher\IEventDispatcher;
+
+class DirectEditingService {
+
+ public function __construct(
+ private IEventDispatcher $eventDispatcher,
+ private IManager $directEditingManager,
+ ) {
+ }
+
+ public function getDirectEditingETag(): string {
+ return \md5(\json_encode($this->getDirectEditingCapabilitites()));
+ }
+
+ public function getDirectEditingCapabilitites(): array {
+ $this->eventDispatcher->dispatchTyped(new RegisterDirectEditorEvent($this->directEditingManager));
+
+ $capabilities = [
+ 'editors' => [],
+ 'creators' => []
+ ];
+
+ if (!$this->directEditingManager->isEnabled()) {
+ return $capabilities;
+ }
+
+ /**
+ * @var string $id
+ * @var IEditor $editor
+ */
+ foreach ($this->directEditingManager->getEditors() as $id => $editor) {
+ $capabilities['editors'][$id] = [
+ 'id' => $editor->getId(),
+ 'name' => $editor->getName(),
+ 'mimetypes' => $editor->getMimetypes(),
+ 'optionalMimetypes' => $editor->getMimetypesOptional(),
+ 'secure' => $editor->isSecure(),
+ ];
+ /** @var ACreateEmpty|ACreateFromTemplate $creator */
+ foreach ($editor->getCreators() as $creator) {
+ $id = $creator->getId();
+ $capabilities['creators'][$id] = [
+ 'id' => $id,
+ 'editor' => $editor->getId(),
+ 'name' => $creator->getName(),
+ 'extension' => $creator->getExtension(),
+ 'templates' => $creator instanceof ACreateFromTemplate,
+ 'mimetype' => $creator->getMimetype()
+ ];
+ }
+ }
+ return $capabilities;
+ }
+}
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
new file mode 100644
index 00000000000..afef5d2093d
--- /dev/null
+++ b/apps/files/lib/Service/OwnershipTransferService.php
@@ -0,0 +1,624 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * 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\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;
+use Symfony\Component\Console\Output\NullOutput;
+use Symfony\Component\Console\Output\OutputInterface;
+use function array_merge;
+use function basename;
+use function count;
+use function date;
+use function is_dir;
+use function rtrim;
+
+class OwnershipTransferService {
+
+ public function __construct(
+ private IEncryptionManager $encryptionManager,
+ private IShareManager $shareManager,
+ private IMountManager $mountManager,
+ private IUserMountCache $userMountCache,
+ private IUserManager $userManager,
+ private IFactory $l10nFactory,
+ private IRootFolder $rootFolder,
+ ) {
+ }
+
+ /**
+ * @param IUser $sourceUser
+ * @param IUser $destinationUser
+ * @param string $path
+ *
+ * @param OutputInterface|null $output
+ * @param bool $move
+ * @throws TransferOwnershipException
+ * @throws NoUserException
+ */
+ public function transfer(
+ IUser $sourceUser,
+ IUser $destinationUser,
+ string $path,
+ ?OutputInterface $output = null,
+ bool $move = false,
+ bool $firstLogin = false,
+ bool $includeExternalStorage = false,
+ ): void {
+ $output = $output ?? new NullOutput();
+ $sourceUid = $sourceUser->getUID();
+ $destinationUid = $destinationUser->getUID();
+ $sourcePath = rtrim($sourceUid . '/files/' . $path, '/');
+
+ // 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);
+ }
+
+ // 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());
+ $this->rootFolder->getUserFolder($sourceUser->getUID());
+ $this->rootFolder->getUserFolder($destinationUser->getUID());
+ Filesystem::initMountPoints($sourceUid);
+ Filesystem::initMountPoints($destinationUid);
+
+ $view = new View();
+
+ if ($move) {
+ $finalTarget = "$destinationUid/files/";
+ } else {
+ $l = $this->l10nFactory->get('files', $this->l10nFactory->getUserLanguage($destinationUser));
+ $date = date('Y-m-d H-i-s');
+
+ $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/" . $this->sanitizeFolderName($l->t('Transferred from %1$s on %2$s', [$sourceUid, $date]));
+ }
+ }
+
+ if (!($view->is_dir($sourcePath) || $view->is_file($sourcePath))) {
+ throw new TransferOwnershipException("Unknown path provided: $path", 1);
+ }
+
+ if ($move && !$view->is_dir($finalTarget)) {
+ // Initialize storage
+ \OC_Util::setupFS($destinationUser->getUID());
+ }
+
+ if ($move && !$firstLogin && count($view->getDirectoryContent($finalTarget)) > 0) {
+ throw new TransferOwnershipException('Destination path does not exists or is not empty', 1);
+ }
+
+
+ // analyse source folder
+ $this->analyse(
+ $sourceUid,
+ $destinationUid,
+ $sourcePath,
+ $view,
+ $output
+ );
+
+ // collect all the shares
+ $shares = $this->collectUsersShares(
+ $sourceUid,
+ $output,
+ $view,
+ $sourcePath
+ );
+
+ $sourceSize = $view->getFileInfo($sourcePath)->getSize();
+
+ // transfer the files
+ $this->transferFiles(
+ $sourceUid,
+ $sourcePath,
+ $finalTarget,
+ $view,
+ $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
+ );
+ 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)) {
+ return;
+ }
+ if ($fileInfo->getType() === FileInfo::TYPE_FOLDER) {
+ $this->walkFiles($view, $fileInfo->getPath(), $callBack);
+ }
+ }
+ }
+
+ /**
+ * @param OutputInterface $output
+ *
+ * @throws TransferOwnershipException
+ */
+ protected function analyse(
+ string $sourceUid,
+ string $destinationUid,
+ string $sourcePath,
+ View $view,
+ OutputInterface $output,
+ bool $includeExternalStorage = false,
+ ): void {
+ $output->writeln('Validating quota');
+ $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) {
+ 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 = [];
+ 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>');
+ foreach ($encryptedFiles as $encryptedFile) {
+ /** @var FileInfo $encryptedFile */
+ $output->writeln(' ' . $encryptedFile->getPath());
+ }
+ throw new TransferOwnershipException('Some files are encrypted - please decrypt them first.', 1);
+ }
+ }
+
+ /**
+ * @return array<array{share: IShare, suffix: string}>
+ */
+ private function collectUsersShares(
+ string $sourceUid,
+ OutputInterface $output,
+ View $view,
+ string $path,
+ ): array {
+ $output->writeln("Collecting all share information for files and folders of $sourceUid ...");
+
+ $shares = [];
+ $progress = new ProgressBar($output);
+
+ $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, onlyValid: false);
+ $progress->advance(count($sharePage));
+ if (empty($sharePage)) {
+ break;
+ }
+ if ($path !== "$sourceUid/files") {
+ $sharePage = array_filter($sharePage, function (IShare $share) use ($view, $normalizedPath) {
+ try {
+ $sourceNode = $share->getNode();
+ $relativePath = $view->getRelativePath($sourceNode->getPath());
+
+ return str_starts_with($relativePath . '/', $normalizedPath . '/');
+ } catch (Exception $e) {
+ return false;
+ }
+ });
+ }
+ $shares = array_merge($shares, $sharePage);
+ $offset += 50;
+ }
+ }
+
+ $progress->finish();
+ $output->writeln('');
+
+ 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,
+ OutputInterface $output,
+ ?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) {
+ $sharePage = $this->shareManager->getSharedWith($sourceUid, IShare::TYPE_USER, null, 50, $offset);
+ $progress->advance(count($sharePage));
+ if (empty($sharePage)) {
+ break;
+ }
+
+ 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;
+ }
+
+
+ $progress->finish();
+ $output->writeln('');
+ return $shares;
+ }
+
+ /**
+ * @throws TransferOwnershipException
+ */
+ protected function transferFiles(
+ string $sourceUid,
+ string $sourcePath,
+ string $finalTarget,
+ View $view,
+ OutputInterface $output,
+ bool $includeExternalStorage,
+ ): void {
+ $output->writeln("Transferring files to $finalTarget ...");
+
+ // This change will help user to transfer the folder specified using --path option.
+ // Else only the content inside folder is transferred which is not correct.
+ if ($sourcePath !== "$sourceUid/files") {
+ $view->mkdir($finalTarget);
+ $finalTarget = $finalTarget . '/' . basename($sourcePath);
+ }
+ $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 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,
+ ):void {
+ $output->writeln('Restoring shares ...');
+ $progress = new ProgressBar($output, count($shares));
+
+ foreach ($shares as ['share' => $share, 'suffix' => $suffix]) {
+ try {
+ $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) {
+ $this->mountManager->removeMount($shareMountPoint->getMountPoint());
+ }
+ $this->shareManager->deleteShare($share);
+ } else {
+ if ($share->getShareOwner() === $sourceUid) {
+ $share->setShareOwner($destinationUid);
+ }
+ if ($share->getSharedBy() === $sourceUid) {
+ $share->setSharedBy($destinationUid);
+ }
+
+ 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);
+ continue;
+ } else {
+ // 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();
+
+ 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 (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>');
+ }
+ $progress->advance();
+ }
+ $progress->finish();
+ $output->writeln('');
+ }
+
+ private function transferIncomingShares(string $sourceUid,
+ string $destinationUid,
+ array $sourceShares,
+ array $destinationShares,
+ OutputInterface $output,
+ string $path,
+ string $finalTarget,
+ bool $move): void {
+ $output->writeln('Restoring incoming shares ...');
+ $progress = new ProgressBar($output, count($sourceShares));
+ $prefix = "$destinationUid/files";
+ $finalShareTarget = '';
+ if (str_starts_with($finalTarget, $prefix)) {
+ $finalShareTarget = substr($finalTarget, strlen($prefix));
+ }
+ foreach ($sourceShares as $share) {
+ try {
+ // Only restore if share is in given path.
+ $pathToCheck = '/';
+ if (trim($path, '/') !== '') {
+ $pathToCheck = '/' . trim($path) . '/';
+ }
+ if (!str_starts_with($share->getTarget(), $pathToCheck)) {
+ continue;
+ }
+ $shareTarget = $share->getTarget();
+ $shareTarget = $finalShareTarget . $shareTarget;
+ if ($share->getShareType() === IShare::TYPE_USER
+ && $share->getSharedBy() === $destinationUid) {
+ $this->shareManager->deleteShare($share);
+ } elseif (isset($destinationShares[$share->getNodeId()])) {
+ $destinationShare = $destinationShares[$share->getNodeId()];
+ // Keep the share which has the most permissions and discard the other one.
+ if ($destinationShare->getPermissions() < $share->getPermissions()) {
+ $this->shareManager->deleteShare($destinationShare);
+ $share->setSharedWith($destinationUid);
+ // 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);
+ // The share is already transferred.
+ $progress->advance();
+ if ($move) {
+ continue;
+ }
+ $share->setTarget($shareTarget);
+ $this->shareManager->moveShare($share, $destinationUid);
+ continue;
+ }
+ $this->shareManager->deleteShare($share);
+ } elseif ($share->getShareOwner() === $destinationUid) {
+ $this->shareManager->deleteShare($share);
+ } else {
+ $share->setSharedWith($destinationUid);
+ $share->setNodeId($share->getNode()->getId());
+ $this->shareManager->updateShare($share);
+ // 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();
+ // The share is already transferred.
+ $progress->advance();
+ if ($move) {
+ continue;
+ }
+ $share->setTarget($shareTarget);
+ $this->shareManager->moveShare($share, $destinationUid);
+ continue;
+ }
+ } 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>');
+ }
+ $progress->advance();
+ }
+ $progress->finish();
+ $output->writeln('');
+ }
+}
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 7437f0c31ad..63c54d01fd0 100644
--- a/apps/files/lib/Service/TagService.php
+++ b/apps/files/lib/Service/TagService.php
@@ -1,74 +1,29 @@
<?php
+
/**
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- *
- * @author Joas Schilling <coding@schilljs.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Vincent Petry <pvince81@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: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files\Service;
-use OC\Tags;
-use OCA\Files\Activity\FavoriteProvider;
use OCP\Activity\IManager;
use OCP\Files\Folder;
+use OCP\Files\NotFoundException;
use OCP\ITags;
-use OCP\IUser;
use OCP\IUserSession;
-use Symfony\Component\EventDispatcher\EventDispatcherInterface;
-use Symfony\Component\EventDispatcher\GenericEvent;
/**
* Service class to manage tags on files.
*/
class TagService {
- /** @var IUserSession */
- private $userSession;
- /** @var IManager */
- private $activityManager;
- /** @var ITags */
- private $tagger;
- /** @var Folder */
- private $homeFolder;
- /** @var EventDispatcherInterface */
- private $dispatcher;
-
- /**
- * @param IUserSession $userSession
- * @param IManager $activityManager
- * @param ITags $tagger
- * @param Folder $homeFolder
- * @param EventDispatcherInterface $dispatcher
- */
public function __construct(
- IUserSession $userSession,
- IManager $activityManager,
- ITags $tagger,
- Folder $homeFolder,
- EventDispatcherInterface $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;
}
/**
@@ -77,14 +32,21 @@ 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) {
+ throw new \RuntimeException('No tagger set');
+ }
+ if ($this->homeFolder === null) {
+ throw new \RuntimeException('No homeFolder set');
+ }
+
$fileId = $this->homeFolder->get($path)->getId();
- $currentTags = $this->tagger->getTagsForObjects(array($fileId));
+ $currentTags = $this->tagger->getTagsForObjects([$fileId]);
if (!empty($currentTags)) {
$currentTags = current($currentTags);
@@ -92,16 +54,10 @@ class TagService {
$newTags = array_diff($tags, $currentTags);
foreach ($newTags as $tag) {
- if ($tag === Tags::TAG_FAVORITE) {
- $this->addActivity(true, $fileId, $path);
- }
$this->tagger->tagAs($fileId, $tag);
}
$deletedTags = array_diff($currentTags, $tags);
foreach ($deletedTags as $tag) {
- if ($tag === Tags::TAG_FAVORITE) {
- $this->addActivity(false, $fileId, $path);
- }
$this->tagger->unTag($fileId, $tag);
}
@@ -109,41 +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;
- }
-
- $eventName = $addToFavorite ? 'addFavorite' : 'removeFavorite';
- $this->dispatcher->dispatch(self::class . '::' . $eventName, new GenericEvent(null, [
- 'userId' => $user->getUID(),
- 'fileId' => $fileId,
- 'path' => $path,
- ]));
-
- $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
new file mode 100644
index 00000000000..dcf30b7796d
--- /dev/null
+++ b/apps/files/lib/Service/UserConfig.php
@@ -0,0 +1,182 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Service;
+
+use OCA\Files\AppInfo\Application;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class UserConfig {
+ public const ALLOWED_CONFIGS = [
+ [
+ // Whether to crop the files previews or not in the files list
+ 'key' => 'crop_image_previews',
+ 'default' => true,
+ '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 sort folders before files in the list or not
+ 'key' => 'sort_folders_first',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ ];
+ protected ?IUser $user = null;
+
+ public function __construct(
+ protected IConfig $config,
+ IUserSession $userSession,
+ ) {
+ $this->user = $userSession->getUser();
+ }
+
+ /**
+ * Get the list of all allowed user config keys
+ * @return string[]
+ */
+ public function getAllowedConfigKeys(): array {
+ return array_map(function ($config) {
+ return $config['key'];
+ }, self::ALLOWED_CONFIGS);
+ }
+
+ /**
+ * Get the list of allowed config values for a given key
+ *
+ * @param string $key a valid config key
+ * @return array
+ */
+ private function getAllowedConfigValues(string $key): array {
+ foreach (self::ALLOWED_CONFIGS as $config) {
+ if ($config['key'] === $key) {
+ return $config['allowed'];
+ }
+ }
+ return [];
+ }
+
+ /**
+ * Get the default config value for a given key
+ *
+ * @param string $key a valid config key
+ * @return string|bool
+ */
+ private function getDefaultConfigValue(string $key) {
+ foreach (self::ALLOWED_CONFIGS as $config) {
+ if ($config['key'] === $key) {
+ return $config['default'];
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Set a user config
+ *
+ * @param string $key
+ * @param string|bool $value
+ * @throws \Exception
+ * @throws \InvalidArgumentException
+ */
+ public function setConfig(string $key, $value): void {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ 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');
+ }
+
+ if (is_bool($value)) {
+ $value = $value ? '1' : '0';
+ }
+
+ $this->config->setUserValue($this->user->getUID(), Application::APP_ID, $key, $value);
+ }
+
+ /**
+ * Get the current user configs array
+ *
+ * @return array
+ */
+ public function getConfigs(): array {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ $userId = $this->user->getUID();
+ $userConfigs = array_map(function (string $key) use ($userId) {
+ $value = $this->config->getUserValue($userId, Application::APP_ID, $key, $this->getDefaultConfigValue($key));
+ // If the default is expected to be a boolean, we need to cast the value
+ if (is_bool($this->getDefaultConfigValue($key)) && is_string($value)) {
+ return $value === '1';
+ }
+ return $value;
+ }, $this->getAllowedConfigKeys());
+
+ return array_combine($this->getAllowedConfigKeys(), $userConfigs);
+ }
+}
diff --git a/apps/files/lib/Service/ViewConfig.php b/apps/files/lib/Service/ViewConfig.php
new file mode 100644
index 00000000000..cf8bebd5372
--- /dev/null
+++ b/apps/files/lib/Service/ViewConfig.php
@@ -0,0 +1,168 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Service;
+
+use OCA\Files\AppInfo\Application;
+use OCP\IConfig;
+use OCP\IUser;
+use OCP\IUserSession;
+
+class ViewConfig {
+ public const CONFIG_KEY = 'files_views_configs';
+ public const ALLOWED_CONFIGS = [
+ [
+ // The default sorting key for the files list view
+ 'key' => 'sorting_mode',
+ // null by default as views can provide default sorting key
+ // and will fallback to it if user hasn't change it
+ 'default' => null,
+ ],
+ [
+ // The default sorting direction for the files list view
+ 'key' => 'sorting_direction',
+ 'default' => 'asc',
+ 'allowed' => ['asc', 'desc'],
+ ],
+ [
+ // If the navigation entry for this view is expanded or not
+ 'key' => 'expanded',
+ 'default' => true,
+ 'allowed' => [true, false],
+ ],
+ ];
+ protected ?IUser $user = null;
+
+ public function __construct(
+ protected IConfig $config,
+ IUserSession $userSession,
+ ) {
+ $this->user = $userSession->getUser();
+ }
+
+ /**
+ * Get the list of all allowed user config keys
+ * @return string[]
+ */
+ public function getAllowedConfigKeys(): array {
+ return array_map(function ($config) {
+ return $config['key'];
+ }, self::ALLOWED_CONFIGS);
+ }
+
+ /**
+ * Get the list of allowed config values for a given key
+ *
+ * @param string $key a valid config key
+ * @return array
+ */
+ private function getAllowedConfigValues(string $key): array {
+ foreach (self::ALLOWED_CONFIGS as $config) {
+ if ($config['key'] === $key) {
+ return $config['allowed'] ?? [];
+ }
+ }
+ return [];
+ }
+
+ /**
+ * Get the default config value for a given key
+ *
+ * @param string $key a valid config key
+ * @return string|bool|null
+ */
+ private function getDefaultConfigValue(string $key) {
+ foreach (self::ALLOWED_CONFIGS as $config) {
+ if ($config['key'] === $key) {
+ return $config['default'];
+ }
+ }
+ return '';
+ }
+
+ /**
+ * Set a user config
+ *
+ * @param string $view
+ * @param string $key
+ * @param string|bool $value
+ * @throws \Exception
+ * @throws \InvalidArgumentException
+ */
+ public function setConfig(string $view, string $key, $value): void {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ if (!$view) {
+ throw new \Exception('Unknown view');
+ }
+
+ 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');
+ }
+
+ // Cast boolean values
+ if (is_bool($this->getDefaultConfigValue($key))) {
+ $value = $value === '1';
+ }
+
+ $config = $this->getConfigs();
+ $config[$view][$key] = $value;
+
+ $this->config->setUserValue($this->user->getUID(), Application::APP_ID, self::CONFIG_KEY, json_encode($config));
+ }
+
+ /**
+ * Get the current user configs array for a given view
+ *
+ * @return array
+ */
+ public function getConfig(string $view): array {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ $userId = $this->user->getUID();
+ $configs = json_decode($this->config->getUserValue($userId, Application::APP_ID, self::CONFIG_KEY, '[]'), true);
+
+ if (!isset($configs[$view])) {
+ $configs[$view] = [];
+ }
+
+ // Extend undefined values with defaults
+ return array_reduce(self::ALLOWED_CONFIGS, function ($carry, $config) use ($view, $configs) {
+ $key = $config['key'];
+ $carry[$key] = $configs[$view][$key] ?? $this->getDefaultConfigValue($key);
+ return $carry;
+ }, []);
+ }
+
+ /**
+ * Get the current user configs array
+ *
+ * @return array
+ */
+ public function getConfigs(): array {
+ if ($this->user === null) {
+ throw new \Exception('No user logged in');
+ }
+
+ $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;
+ }, []);
+ }
+}