diff options
Diffstat (limited to 'apps/files/lib')
27 files changed, 300 insertions, 266 deletions
diff --git a/apps/files/lib/Activity/FavoriteProvider.php b/apps/files/lib/Activity/FavoriteProvider.php index 7a660287192..e56b13b902a 100644 --- a/apps/files/lib/Activity/FavoriteProvider.php +++ b/apps/files/lib/Activity/FavoriteProvider.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Filter/Favorites.php b/apps/files/lib/Activity/Filter/Favorites.php index 6683f0bc44e..0159dd20b82 100644 --- a/apps/files/lib/Activity/Filter/Favorites.php +++ b/apps/files/lib/Activity/Filter/Favorites.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Filter/FileChanges.php b/apps/files/lib/Activity/Filter/FileChanges.php index 215510147d6..0ca8f6792e0 100644 --- a/apps/files/lib/Activity/Filter/FileChanges.php +++ b/apps/files/lib/Activity/Filter/FileChanges.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Provider.php b/apps/files/lib/Activity/Provider.php index faa2bbd0b3b..3ef79ac107f 100644 --- a/apps/files/lib/Activity/Provider.php +++ b/apps/files/lib/Activity/Provider.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Settings/FavoriteAction.php b/apps/files/lib/Activity/Settings/FavoriteAction.php index b572a9546e0..73b200341ec 100644 --- a/apps/files/lib/Activity/Settings/FavoriteAction.php +++ b/apps/files/lib/Activity/Settings/FavoriteAction.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Settings/FileChanged.php b/apps/files/lib/Activity/Settings/FileChanged.php index 5af87456550..c33ed5e1eba 100644 --- a/apps/files/lib/Activity/Settings/FileChanged.php +++ b/apps/files/lib/Activity/Settings/FileChanged.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Activity/Settings/FileFavoriteChanged.php b/apps/files/lib/Activity/Settings/FileFavoriteChanged.php index e4ac572f3f3..5000902ed3f 100644 --- a/apps/files/lib/Activity/Settings/FileFavoriteChanged.php +++ b/apps/files/lib/Activity/Settings/FileFavoriteChanged.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php index 4b3948d43b5..32c072ef0f4 100644 --- a/apps/files/lib/AppInfo/Application.php +++ b/apps/files/lib/AppInfo/Application.php @@ -132,7 +132,6 @@ class Application extends App implements IBootstrap { public function boot(IBootContext $context): void { $context->injectFn(Closure::fromCallable([$this, 'registerCollaboration'])); $context->injectFn([Listener::class, 'register']); - $this->registerTemplates(); $this->registerHooks(); } @@ -140,13 +139,6 @@ class Application extends App implements IBootstrap { $providerManager->registerResourceProvider(ResourceProvider::class); } - private function registerTemplates(): void { - $templateManager = \OC_Helper::getFileTemplateManager(); - $templateManager->registerTemplate('application/vnd.oasis.opendocument.presentation', 'core/templates/filetemplates/template.odp'); - $templateManager->registerTemplate('application/vnd.oasis.opendocument.text', 'core/templates/filetemplates/template.odt'); - $templateManager->registerTemplate('application/vnd.oasis.opendocument.spreadsheet', 'core/templates/filetemplates/template.ods'); - } - private function registerHooks(): void { Util::connectHook('\OCP\Config', 'js', '\OCA\Files\App', 'extendJsConfig'); } diff --git a/apps/files/lib/BackgroundJob/ScanFiles.php b/apps/files/lib/BackgroundJob/ScanFiles.php index 3a39382d0b4..f3f9093d648 100644 --- a/apps/files/lib/BackgroundJob/ScanFiles.php +++ b/apps/files/lib/BackgroundJob/ScanFiles.php @@ -69,7 +69,7 @@ class ScanFiles extends TimedJob { $query->select('m.user_id') ->from('filecache', 'f') ->leftJoin('f', 'mounts', 'm', $query->expr()->eq('m.storage_id', 'f.storage')) - ->where($query->expr()->lt('f.size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->where($query->expr()->eq('f.size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->gt('f.parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->setMaxResults(10) ->groupBy('f.storage') @@ -90,7 +90,7 @@ class ScanFiles extends TimedJob { $query->select('m.user_id') ->from('filecache', 'f') ->leftJoin('f', 'mounts', 'm', $query->expr()->eq('m.storage_id', 'f.storage')) - ->where($query->expr()->lt('f.size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->where($query->expr()->eq('f.size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->gt('f.parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->in('f.storage', $query->createNamedParameter($storages, IQueryBuilder::PARAM_INT_ARRAY))) ->setMaxResults(1) @@ -101,7 +101,7 @@ class ScanFiles extends TimedJob { $query->select('m.user_id') ->from('filecache', 'f') ->innerJoin('f', 'mounts', 'm', $query->expr()->eq('m.storage_id', 'f.storage')) - ->where($query->expr()->lt('f.size', $query->createNamedParameter(0, IQueryBuilder::PARAM_INT))) + ->where($query->expr()->eq('f.size', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->andWhere($query->expr()->gt('f.parent', $query->createNamedParameter(-1, IQueryBuilder::PARAM_INT))) ->setMaxResults(1) ->runAcrossAllShards(); diff --git a/apps/files/lib/Command/Copy.php b/apps/files/lib/Command/Copy.php index e51a1689907..ad0dfa90de1 100644 --- a/apps/files/lib/Command/Copy.php +++ b/apps/files/lib/Command/Copy.php @@ -19,10 +19,9 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class Copy extends Command { - private FileUtils $fileUtils; - - public function __construct(FileUtils $fileUtils) { - $this->fileUtils = $fileUtils; + public function __construct( + private FileUtils $fileUtils, + ) { parent::__construct(); } diff --git a/apps/files/lib/Command/DeleteOrphanedFiles.php b/apps/files/lib/Command/DeleteOrphanedFiles.php index 0f3375d2e93..37cb3159f4a 100644 --- a/apps/files/lib/Command/DeleteOrphanedFiles.php +++ b/apps/files/lib/Command/DeleteOrphanedFiles.php @@ -54,7 +54,7 @@ class DeleteOrphanedFiles extends Command { $deletedMounts = $this->cleanupOrphanedMounts(); $output->writeln("$deletedMounts orphaned mount entries deleted"); - + return self::SUCCESS; } @@ -112,7 +112,7 @@ class DeleteOrphanedFiles extends Command { return $deletedEntries; } - + /** * @param array<int, int[]> $fileIdsByStorage * @return int diff --git a/apps/files/lib/Command/Move.php b/apps/files/lib/Command/Move.php index cd9e56f8e29..29dd8860b2a 100644 --- a/apps/files/lib/Command/Move.php +++ b/apps/files/lib/Command/Move.php @@ -20,10 +20,9 @@ use Symfony\Component\Console\Output\OutputInterface; use Symfony\Component\Console\Question\ConfirmationQuestion; class Move extends Command { - private FileUtils $fileUtils; - - public function __construct(FileUtils $fileUtils) { - $this->fileUtils = $fileUtils; + public function __construct( + private FileUtils $fileUtils, + ) { parent::__construct(); } diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php index ea01afd20d6..88d41d1cb5e 100644 --- a/apps/files/lib/Command/SanitizeFilenames.php +++ b/apps/files/lib/Command/SanitizeFilenames.php @@ -27,7 +27,7 @@ use Symfony\Component\Console\Output\OutputInterface; class SanitizeFilenames extends Base { private OutputInterface $output; - private string $charReplacement; + private ?string $charReplacement; private bool $dryRun; public function __construct( @@ -43,10 +43,6 @@ class SanitizeFilenames extends Base { protected function configure(): void { parent::configure(); - $forbiddenCharacter = $this->filenameValidator->getForbiddenCharacters(); - $charReplacement = array_diff([' ', '_', '-'], $forbiddenCharacter); - $charReplacement = reset($charReplacement) ?: ''; - $this ->setName('files:sanitize-filenames') ->setDescription('Renames files to match naming constraints') @@ -65,16 +61,25 @@ class SanitizeFilenames extends Base { 'c', mode: InputOption::VALUE_REQUIRED, description: 'Replacement for invalid character (by default space, underscore or dash is used)', - default: $charReplacement, ); - + } protected function execute(InputInterface $input, OutputInterface $output): int { $this->charReplacement = $input->getOption('char-replacement'); - if ($this->charReplacement === '' || mb_strlen($this->charReplacement) > 1) { - $output->writeln('<error>No character replacement given</error>'); - return 1; + // check if replacement is needed + $c = $this->filenameValidator->getForbiddenCharacters(); + if (count($c) > 0) { + try { + $this->filenameValidator->sanitizeFilename($c[0], $this->charReplacement); + } catch (\InvalidArgumentException) { + if ($this->charReplacement === null) { + $output->writeln('<error>Character replacement required</error>'); + } else { + $output->writeln('<error>Invalid character replacement given</error>'); + } + return 1; + } } $this->dryRun = $input->getOption('dry-run'); @@ -115,8 +120,8 @@ class SanitizeFilenames extends Base { try { $oldName = $node->getName(); - if (!$this->filenameValidator->isFilenameValid($oldName)) { - $newName = $this->sanitizeName($oldName); + $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement); + if ($oldName !== $newName) { $newName = $folder->getNonExistingName($newName); $path = rtrim(dirname($node->getPath()), '/'); @@ -132,8 +137,9 @@ class SanitizeFilenames extends Base { $this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (file is locked)</>'); } catch (NotPermittedException) { $this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (no permissions)</>'); - } catch (Exception) { + } catch (Exception $error) { $this->output->writeln('<error>failed: ' . $node->getPath() . '</>'); + $this->output->writeln('<error>' . $error->getMessage() . '</>', OutputInterface::OUTPUT_NORMAL | OutputInterface::VERBOSITY_VERBOSE); } if ($node instanceof Folder) { @@ -142,27 +148,4 @@ class SanitizeFilenames extends Base { } } - private function sanitizeName(string $name): string { - $l10n = $this->l10nFactory->get('files'); - - foreach ($this->filenameValidator->getForbiddenExtensions() as $extension) { - if (str_ends_with($name, $extension)) { - $name = substr($name, 0, strlen($name) - strlen($extension)); - } - } - - $basename = substr($name, 0, strpos($name, '.', 1) ?: null); - if (in_array($basename, $this->filenameValidator->getForbiddenBasenames())) { - $name = str_replace($basename, $l10n->t('%1$s (renamed)', [$basename]), $name); - } - - if ($name === '') { - $name = $l10n->t('renamed file'); - } - - $forbiddenCharacter = $this->filenameValidator->getForbiddenCharacters(); - $name = str_replace($forbiddenCharacter, $this->charReplacement, $name); - - return $name; - } } diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php index 7f45a9ea518..b9057139b0e 100644 --- a/apps/files/lib/Command/Scan.php +++ b/apps/files/lib/Command/Scan.php @@ -11,6 +11,7 @@ use OC\Core\Command\Base; use OC\Core\Command\InterruptedException; use OC\DB\Connection; use OC\DB\ConnectionAdapter; +use OC\Files\Storage\Wrapper\Jail; use OC\Files\Utils\Scanner; use OC\FilesMetadata\FilesMetadataManager; use OC\ForbiddenException; @@ -99,7 +100,15 @@ class Scan extends Base { ); } - protected function scanFiles(string $user, string $path, ?string $scanMetadata, OutputInterface $output, bool $backgroundScan = false, bool $recursive = true, bool $homeOnly = false): void { + protected function scanFiles( + string $user, + string $path, + ?string $scanMetadata, + OutputInterface $output, + callable $mountFilter, + bool $backgroundScan = false, + bool $recursive = true, + ): void { $connection = $this->reconnectToDatabase($output); $scanner = new Scanner( $user, @@ -153,7 +162,7 @@ class Scan extends Base { if ($backgroundScan) { $scanner->backgroundScan($path); } else { - $scanner->scan($path, $recursive, $homeOnly ? [$this, 'filterHomeMount'] : null); + $scanner->scan($path, $recursive, $mountFilter); } } catch (ForbiddenException $e) { $output->writeln("<error>Home storage for user $user not writable or 'files' subdirectory missing</error>"); @@ -179,7 +188,7 @@ class Scan extends Base { } } - public function filterHomeMount(IMountPoint $mountPoint): bool { + public function isHomeMount(IMountPoint $mountPoint): bool { // any mountpoint inside '/$user/files/' return substr_count($mountPoint->getMountPoint(), '/') <= 3; } @@ -211,6 +220,29 @@ class Scan extends Base { $metadata = $input->getOption('generate-metadata') ?? ''; } + $homeOnly = $input->getOption('home-only'); + $scannedStorages = []; + $mountFilter = function (IMountPoint $mount) use ($homeOnly, &$scannedStorages) { + if ($homeOnly && !$this->isHomeMount($mount)) { + return false; + } + + // when scanning multiple users, the scanner might encounter the same storage multiple times (e.g. external storages, or group folders) + // we can filter out any storage we've already scanned to avoid double work + $storage = $mount->getStorage(); + $storageKey = $storage->getId(); + while ($storage->instanceOfStorage(Jail::class)) { + $storageKey .= '/' . $storage->getUnjailedPath(''); + $storage = $storage->getUnjailedStorage(); + } + if (array_key_exists($storageKey, $scannedStorages)) { + return false; + } + + $scannedStorages[$storageKey] = true; + return true; + }; + $user_count = 0; foreach ($users as $user) { if (is_object($user)) { @@ -220,7 +252,15 @@ class Scan extends Base { ++$user_count; if ($this->userManager->userExists($user)) { $output->writeln("Starting scan for user $user_count out of $users_total ($user)"); - $this->scanFiles($user, $path, $metadata, $output, $input->getOption('unscanned'), !$input->getOption('shallow'), $input->getOption('home-only')); + $this->scanFiles( + $user, + $path, + $metadata, + $output, + $mountFilter, + $input->getOption('unscanned'), + !$input->getOption('shallow'), + ); $output->writeln('', OutputInterface::VERBOSITY_VERBOSE); } else { $output->writeln("<error>Unknown user $user_count $user</error>"); @@ -246,8 +286,8 @@ class Scan extends Base { $this->execTime = -microtime(true); // Convert PHP errors to exceptions set_error_handler( - fn (int $severity, string $message, string $file, int $line): bool => - $this->exceptionErrorHandler($output, $severity, $message, $file, $line), + fn (int $severity, string $message, string $file, int $line): bool + => $this->exceptionErrorHandler($output, $severity, $message, $file, $line), E_ALL ); } diff --git a/apps/files/lib/Command/ScanAppData.php b/apps/files/lib/Command/ScanAppData.php index 81c80cab373..0e08c6a8cfe 100644 --- a/apps/files/lib/Command/ScanAppData.php +++ b/apps/files/lib/Command/ScanAppData.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Command/TransferOwnership.php b/apps/files/lib/Command/TransferOwnership.php index edc73e62c38..f7663e26f28 100644 --- a/apps/files/lib/Command/TransferOwnership.php +++ b/apps/files/lib/Command/TransferOwnership.php @@ -11,20 +11,26 @@ namespace OCA\Files\Command; use OCA\Files\Exception\TransferOwnershipException; use OCA\Files\Service\OwnershipTransferService; +use OCA\Files_External\Config\ConfigAdapter; +use OCP\Files\Mount\IMountManager; +use OCP\Files\Mount\IMountPoint; use OCP\IConfig; use OCP\IUser; use OCP\IUserManager; use Symfony\Component\Console\Command\Command; +use Symfony\Component\Console\Helper\QuestionHelper; use Symfony\Component\Console\Input\InputArgument; use Symfony\Component\Console\Input\InputInterface; use Symfony\Component\Console\Input\InputOption; use Symfony\Component\Console\Output\OutputInterface; +use Symfony\Component\Console\Question\ConfirmationQuestion; class TransferOwnership extends Command { public function __construct( private IUserManager $userManager, private OwnershipTransferService $transferService, private IConfig $config, + private IMountManager $mountManager, ) { parent::__construct(); } @@ -58,8 +64,18 @@ class TransferOwnership extends Command { 'transfer-incoming-shares', null, InputOption::VALUE_OPTIONAL, - 'transfer incoming user file shares to destination user. Usage: --transfer-incoming-shares=1 (value required)', + 'Incoming shares are always transferred now, so this option does not affect the ownership transfer anymore', '2' + )->addOption( + 'include-external-storage', + null, + InputOption::VALUE_NONE, + 'include files on external storages, this will _not_ setup an external storage for the target user, but instead moves all the files from the external storages into the target users home directory', + )->addOption( + 'force-include-external-storage', + null, + InputOption::VALUE_NONE, + 'don\'t ask for confirmation for transferring external storages', ); } @@ -87,36 +103,40 @@ class TransferOwnership extends Command { return self::FAILURE; } - try { - $includeIncomingArgument = $input->getOption('transfer-incoming-shares'); - - switch ($includeIncomingArgument) { - case '0': - $includeIncoming = false; - break; - case '1': - $includeIncoming = true; - break; - case '2': - $includeIncoming = $this->config->getSystemValue('transferIncomingShares', false); - if (gettype($includeIncoming) !== 'boolean') { - $output->writeln("<error> config.php: 'transfer-incoming-shares': wrong usage. Transfer aborted.</error>"); + $path = ltrim($input->getOption('path'), '/'); + $includeExternalStorage = $input->getOption('include-external-storage'); + if ($includeExternalStorage) { + $mounts = $this->mountManager->findIn('/' . rtrim($sourceUserObject->getUID() . '/files/' . $path, '/')); + /** @var IMountPoint[] $mounts */ + $mounts = array_filter($mounts, fn ($mount) => $mount->getMountProvider() === ConfigAdapter::class); + if (count($mounts) > 0) { + $output->writeln(count($mounts) . ' external storages will be transferred:'); + foreach ($mounts as $mount) { + $output->writeln(' - <info>' . $mount->getMountPoint() . '</info>'); + } + $output->writeln(''); + $output->writeln('<comment>Any other users with access to these external storages will lose access to the files.</comment>'); + $output->writeln(''); + if (!$input->getOption('force-include-external-storage')) { + /** @var QuestionHelper $helper */ + $helper = $this->getHelper('question'); + $question = new ConfirmationQuestion('Are you sure you want to transfer external storages? (y/N) ', false); + if (!$helper->ask($input, $output, $question)) { return self::FAILURE; } - break; - default: - $output->writeln('<error>Option --transfer-incoming-shares: wrong usage. Transfer aborted.</error>'); - return self::FAILURE; + } } + } + try { $this->transferService->transfer( $sourceUserObject, $destinationUserObject, - ltrim($input->getOption('path'), '/'), + $path, $output, $input->getOption('move') === true, false, - $includeIncoming + $includeExternalStorage, ); } catch (TransferOwnershipException $e) { $output->writeln('<error>' . $e->getMessage() . '</error>'); diff --git a/apps/files/lib/Controller/ApiController.php b/apps/files/lib/Controller/ApiController.php index 9c683b7f41f..8bb024fb698 100644 --- a/apps/files/lib/Controller/ApiController.php +++ b/apps/files/lib/Controller/ApiController.php @@ -105,11 +105,12 @@ class ApiController extends Controller { } // Validate the user is allowed to download the file (preview is some kind of download) + /** @var ISharedStorage $storage */ $storage = $file->getStorage(); if ($storage->instanceOfStorage(ISharedStorage::class)) { - /** @var ISharedStorage $storage */ - $attributes = $storage->getShare()->getAttributes(); - if ($attributes !== null && $attributes->getAttribute('permissions', 'download') === false) { + /** @var IShare $share */ + $share = $storage->getShare(); + if (!$share->canSeeContent()) { throw new NotFoundException(); } } diff --git a/apps/files/lib/Controller/DirectEditingController.php b/apps/files/lib/Controller/DirectEditingController.php index 2c910006df5..c8addc33e98 100644 --- a/apps/files/lib/Controller/DirectEditingController.php +++ b/apps/files/lib/Controller/DirectEditingController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Controller/DirectEditingViewController.php b/apps/files/lib/Controller/DirectEditingViewController.php index 1d78e2af0e0..b13e68f7766 100644 --- a/apps/files/lib/Controller/DirectEditingViewController.php +++ b/apps/files/lib/Controller/DirectEditingViewController.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Controller/TemplateController.php b/apps/files/lib/Controller/TemplateController.php index 3321fb5f119..ee4c86941c7 100644 --- a/apps/files/lib/Controller/TemplateController.php +++ b/apps/files/lib/Controller/TemplateController.php @@ -53,6 +53,24 @@ class TemplateController extends OCSController { } /** + * List the fields for the template specified by the given file ID + * + * @param int $fileId File ID of the template + * @return DataResponse<Http::STATUS_OK, array<string, FilesTemplateField>, array{}> + * + * 200: Fields returned + */ + #[NoAdminRequired] + public function listTemplateFields(int $fileId): DataResponse { + $fields = $this->templateManager->listTemplateFields($fileId); + + return new DataResponse( + array_merge([], ...$fields), + Http::STATUS_OK + ); + } + + /** * Create a template * * @param string $filePath Path of the file diff --git a/apps/files/lib/Helper.php b/apps/files/lib/Helper.php index 82d1cc94934..b1439ac7fa5 100644 --- a/apps/files/lib/Helper.php +++ b/apps/files/lib/Helper.php @@ -9,10 +9,6 @@ namespace OCA\Files; use OC\Files\Filesystem; use OCP\Files\FileInfo; -use OCP\Files\IMimeTypeDetector; -use OCP\Files\NotFoundException; -use OCP\ITagManager; -use OCP\Server; use OCP\Util; /** @@ -20,56 +16,6 @@ use OCP\Util; */ class Helper { /** - * @param string $dir - * @return array - * @throws NotFoundException - */ - public static function buildFileStorageStatistics($dir) { - // information about storage capacities - $storageInfo = \OC_Helper::getStorageInfo($dir); - $l = Util::getL10N('files'); - $maxUploadFileSize = Util::maxUploadFilesize($dir, $storageInfo['free']); - $maxHumanFileSize = Util::humanFileSize($maxUploadFileSize); - $maxHumanFileSize = $l->t('Upload (max. %s)', [$maxHumanFileSize]); - - return [ - 'uploadMaxFilesize' => $maxUploadFileSize, - 'maxHumanFilesize' => $maxHumanFileSize, - 'freeSpace' => $storageInfo['free'], - 'quota' => $storageInfo['quota'], - 'total' => $storageInfo['total'], - 'used' => $storageInfo['used'], - 'usedSpacePercent' => $storageInfo['relative'], - 'owner' => $storageInfo['owner'], - 'ownerDisplayName' => $storageInfo['ownerDisplayName'], - 'mountType' => $storageInfo['mountType'], - 'mountPoint' => $storageInfo['mountPoint'], - ]; - } - - /** - * Determine icon for a given file - * - * @param FileInfo $file file info - * @return string icon URL - */ - public static function determineIcon($file) { - if ($file['type'] === 'dir') { - $icon = Server::get(IMimeTypeDetector::class)->mimeTypeIcon('dir'); - // TODO: move this part to the client side, using mountType - if ($file->isShared()) { - $icon = Server::get(IMimeTypeDetector::class)->mimeTypeIcon('dir-shared'); - } elseif ($file->isMounted()) { - $icon = Server::get(IMimeTypeDetector::class)->mimeTypeIcon('dir-external'); - } - } else { - $icon = Server::get(IMimeTypeDetector::class)->mimeTypeIcon($file->getMimetype()); - } - - return substr($icon, 0, -3) . 'svg'; - } - - /** * Comparator function to sort files alphabetically and have * the directories appear first * @@ -162,20 +108,6 @@ class Helper { } /** - * Format file info for JSON - * @param FileInfo[] $fileInfos file infos - * @return array - */ - public static function formatFileInfos($fileInfos) { - $files = []; - foreach ($fileInfos as $i) { - $files[] = self::formatFileInfo($i); - } - - return $files; - } - - /** * Retrieves the contents of the given directory and * returns it as a sorted array of FileInfo. * @@ -192,43 +124,6 @@ class Helper { } /** - * Populate the result set with file tags - * - * @psalm-template T of array{tags?: list<string>, file_source: int, ...array<string, mixed>} - * @param list<T> $fileList - * @return list<T> file list populated with tags - */ - public static function populateTags(array $fileList, ITagManager $tagManager) { - $tagger = $tagManager->load('files'); - $tags = $tagger->getTagsForObjects(array_map(static fn (array $fileData) => $fileData['file_source'], $fileList)); - - if (!is_array($tags)) { - throw new \UnexpectedValueException('$tags must be an array'); - } - - // Set empty tag array - foreach ($fileList as &$fileData) { - $fileData['tags'] = []; - } - unset($fileData); - - if (!empty($tags)) { - foreach ($tags as $fileId => $fileTags) { - foreach ($fileList as &$fileData) { - if ($fileId !== $fileData['file_source']) { - continue; - } - - $fileData['tags'] = $fileTags; - } - unset($fileData); - } - } - - return $fileList; - } - - /** * Sort the given file info array * * @param FileInfo[] $files files to sort diff --git a/apps/files/lib/Listener/SyncLivePhotosListener.php b/apps/files/lib/Listener/SyncLivePhotosListener.php index 6334e5d16a6..b6773e8c452 100644 --- a/apps/files/lib/Listener/SyncLivePhotosListener.php +++ b/apps/files/lib/Listener/SyncLivePhotosListener.php @@ -37,6 +37,8 @@ class SyncLivePhotosListener implements IEventListener { private array $pendingRenames = []; /** @var Array<int, bool> */ private array $pendingDeletion = []; + /** @var Array<int> */ + private array $pendingCopies = []; public function __construct( private ?Folder $userFolder, @@ -153,7 +155,6 @@ class SyncLivePhotosListener implements IEventListener { $targetName = $targetFile->getName(); $peerTargetName = substr($targetName, 0, -strlen($sourceExtension)) . $peerFileExtension; - if ($targetParent->nodeExists($peerTargetName)) { // If the copy was a folder copy, then the peer file already exists. $targetPeerFile = $targetParent->get($peerTargetName); @@ -225,6 +226,11 @@ class SyncLivePhotosListener implements IEventListener { $this->handleCopyRecursive($event, $sourceChild, $targetChild); } } elseif ($sourceNode instanceof File && $targetNode instanceof File) { + // in case the copy was initiated from this listener, we stop right now + if (in_array($sourceNode->getId(), $this->pendingCopies)) { + return; + } + $peerFileId = $this->livePhotosService->getLivePhotoPeerId($sourceNode->getId()); if ($peerFileId === null) { return; @@ -234,11 +240,13 @@ class SyncLivePhotosListener implements IEventListener { return; } + $this->pendingCopies[] = $peerFileId; if ($event instanceof BeforeNodeCopiedEvent) { $this->runMoveOrCopyChecks($sourceNode, $targetNode, $peerFile); } elseif ($event instanceof NodeCopiedEvent) { $this->handleCopy($sourceNode, $targetNode, $peerFile); } + $this->pendingCopies = array_diff($this->pendingCopies, [$peerFileId]); } else { throw new Exception('Source and target type are not matching'); } diff --git a/apps/files/lib/Service/DirectEditingService.php b/apps/files/lib/Service/DirectEditingService.php index 243ddebdc67..3d756ee56fa 100644 --- a/apps/files/lib/Service/DirectEditingService.php +++ b/apps/files/lib/Service/DirectEditingService.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later diff --git a/apps/files/lib/Service/OwnershipTransferService.php b/apps/files/lib/Service/OwnershipTransferService.php index 7f6681a9b89..84c99f32109 100644 --- a/apps/files/lib/Service/OwnershipTransferService.php +++ b/apps/files/lib/Service/OwnershipTransferService.php @@ -10,15 +10,18 @@ declare(strict_types=1); 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; @@ -69,7 +72,7 @@ class OwnershipTransferService { ?OutputInterface $output = null, bool $move = false, bool $firstLogin = false, - bool $transferIncomingShares = false, + bool $includeExternalStorage = false, ): void { $output = $output ?? new NullOutput(); $sourceUid = $sourceUser->getUID(); @@ -149,34 +152,32 @@ class OwnershipTransferService { $sourcePath, $finalTarget, $view, - $output + $output, + $includeExternalStorage, ); $sizeDifference = $sourceSize - $view->getFileInfo($finalTarget)->getSize(); // 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 - ); - } + $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 @@ -215,11 +216,14 @@ class OwnershipTransferService { * * @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'); $sourceFileInfo = $view->getFileInfo($sourcePath, false); if ($sourceFileInfo === false) { @@ -247,17 +251,22 @@ class OwnershipTransferService { $encryptedFiles[] = $sourceFileInfo; } else { $this->walkFiles($view, $sourcePath, - function (FileInfo $fileInfo) use ($progress, $masterKeyEnabled, &$encryptedFiles) { + 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 (!$fileInfo->getStorage()->instanceOfStorage(IHomeStorage::class)) { + 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; } - if ($fileInfo->isEncrypted()) { - /* Encrypted folder means e2ee encrypted, we cannot transfer it */ - $encryptedFiles[] = $fileInfo; - } - return true; } $progress->advance(); if ($fileInfo->isEncrypted() && !$masterKeyEnabled) { @@ -333,7 +342,7 @@ class OwnershipTransferService { return mb_strpos( Filesystem::normalizePath($relativePath . '/', false), $normalizedPath . '/') === 0; - } catch (\Exception $e) { + } catch (Exception $e) { return false; } }); @@ -361,14 +370,16 @@ class OwnershipTransferService { }, $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) { @@ -377,14 +388,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; @@ -399,11 +415,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. @@ -412,15 +431,50 @@ class OwnershipTransferService { $view->mkdir($finalTarget); $finalTarget = $finalTarget . '/' . basename($sourcePath); } - if ($view->rename($sourcePath, $finalTarget, ['checkSubMounts' => false]) === 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 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 @@ -438,8 +492,8 @@ class OwnershipTransferService { 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) { + 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) { @@ -454,8 +508,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); @@ -520,8 +574,8 @@ class OwnershipTransferService { } $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()]; diff --git a/apps/files/lib/Service/UserConfig.php b/apps/files/lib/Service/UserConfig.php index b01caf9f960..415ca65b579 100644 --- a/apps/files/lib/Service/UserConfig.php +++ b/apps/files/lib/Service/UserConfig.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -19,6 +20,24 @@ 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 extension change" warning 'key' => 'show_dialog_file_extension', 'default' => true, @@ -31,6 +50,12 @@ class UserConfig { '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, @@ -42,18 +67,6 @@ class UserConfig { '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 folder tree - 'key' => 'folder_tree', - 'default' => true, - 'allowed' => [true, false], - ], ]; protected ?IUser $user = null; diff --git a/apps/files/lib/Service/ViewConfig.php b/apps/files/lib/Service/ViewConfig.php index 348ce6596e5..cf8bebd5372 100644 --- a/apps/files/lib/Service/ViewConfig.php +++ b/apps/files/lib/Service/ViewConfig.php @@ -1,4 +1,5 @@ <?php + /** * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors * SPDX-License-Identifier: AGPL-3.0-or-later @@ -103,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'); @@ -132,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] = []; } @@ -158,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; diff --git a/apps/files/lib/Settings/DeclarativeAdminSettings.php b/apps/files/lib/Settings/DeclarativeAdminSettings.php index 2f363f05958..bbf97cc4d32 100644 --- a/apps/files/lib/Settings/DeclarativeAdminSettings.php +++ b/apps/files/lib/Settings/DeclarativeAdminSettings.php @@ -49,7 +49,7 @@ class DeclarativeAdminSettings implements IDeclarativeSettingsFormWithHandlers { 'doc_url' => $this->urlGenerator->linkToDocs('admin-windows-compatible-filenames'), 'description' => ( $this->l->t('Allow to restrict filenames to ensure files can be synced with all clients. By default all filenames valid on POSIX (e.g. Linux or macOS) are allowed.') - . "\n" . $this->l->t('After enabling the windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.') + . "\n" . $this->l->t('After enabling the Windows compatible filenames, existing files cannot be modified anymore but can be renamed to valid new names by their owner.') . "\n" . $this->l->t('It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command.') ), |