aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files/lib')
-rw-r--r--apps/files/lib/Activity/FavoriteProvider.php3
-rw-r--r--apps/files/lib/Activity/Filter/Favorites.php1
-rw-r--r--apps/files/lib/Activity/Filter/FileChanges.php1
-rw-r--r--apps/files/lib/Activity/Provider.php14
-rw-r--r--apps/files/lib/Activity/Settings/FavoriteAction.php1
-rw-r--r--apps/files/lib/Activity/Settings/FileChanged.php1
-rw-r--r--apps/files/lib/Activity/Settings/FileFavoriteChanged.php1
-rw-r--r--apps/files/lib/AdvancedCapabilities.php38
-rw-r--r--apps/files/lib/AppInfo/Application.php12
-rw-r--r--apps/files/lib/BackgroundJob/CleanupFileLocks.php4
-rw-r--r--apps/files/lib/BackgroundJob/ScanFiles.php6
-rw-r--r--apps/files/lib/Command/Copy.php7
-rw-r--r--apps/files/lib/Command/DeleteOrphanedFiles.php4
-rw-r--r--apps/files/lib/Command/Move.php7
-rw-r--r--apps/files/lib/Command/Object/Info.php80
-rw-r--r--apps/files/lib/Command/Object/ListObject.php50
-rw-r--r--apps/files/lib/Command/Object/ObjectUtil.php21
-rw-r--r--apps/files/lib/Command/Object/Orphans.php79
-rw-r--r--apps/files/lib/Command/SanitizeFilenames.php151
-rw-r--r--apps/files/lib/Command/Scan.php66
-rw-r--r--apps/files/lib/Command/ScanAppData.php8
-rw-r--r--apps/files/lib/Command/TransferOwnership.php62
-rw-r--r--apps/files/lib/Command/WindowsCompatibleFilenames.php52
-rw-r--r--apps/files/lib/Controller/ApiController.php7
-rw-r--r--apps/files/lib/Controller/DirectEditingController.php1
-rw-r--r--apps/files/lib/Controller/DirectEditingViewController.php1
-rw-r--r--apps/files/lib/Controller/TemplateController.php18
-rw-r--r--apps/files/lib/Controller/ViewController.php54
-rw-r--r--apps/files/lib/Dashboard/FavoriteWidget.php19
-rw-r--r--apps/files/lib/Helper.php103
-rw-r--r--apps/files/lib/Listener/SyncLivePhotosListener.php10
-rw-r--r--apps/files/lib/Service/DirectEditingService.php1
-rw-r--r--apps/files/lib/Service/OwnershipTransferService.php174
-rw-r--r--apps/files/lib/Service/UserConfig.php51
-rw-r--r--apps/files/lib/Service/ViewConfig.php7
-rw-r--r--apps/files/lib/Settings/DeclarativeAdminSettings.php9
36 files changed, 859 insertions, 265 deletions
diff --git a/apps/files/lib/Activity/FavoriteProvider.php b/apps/files/lib/Activity/FavoriteProvider.php
index 84c509f493f..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
@@ -138,7 +139,7 @@ class FavoriteProvider implements IProvider {
}
$parameter = [
'type' => 'file',
- 'id' => $subjectParams['id'],
+ 'id' => (string)$subjectParams['id'],
'name' => basename($subjectParams['path']),
'path' => trim($subjectParams['path'], '/'),
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $subjectParams['id']]),
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 6788a5cbd0d..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
@@ -25,8 +26,6 @@ use OCP\L10N\IFactory;
class Provider implements IProvider {
/** @var IL10N */
protected $l;
- /** @var IL10N */
- protected $activityLang;
/** @var string[] cached displayNames - key is the cloud id and value the displayname */
protected $displayNames = [];
@@ -59,7 +58,6 @@ class Provider implements IProvider {
}
$this->l = $this->languageFactory->get('files', $language);
- $this->activityLang = $this->languageFactory->get('activity', $language);
if ($this->activityManager->isFormattingFilteredObject()) {
try {
@@ -114,7 +112,7 @@ class Provider implements IProvider {
if (!isset($parsedParameters['user'])) {
// External user via public link share
- $subject = str_replace('{user}', $this->activityLang->t('"remote account"'), $subject);
+ $subject = str_replace('{user}', $this->l->t('"remote account"'), $subject);
}
$this->setSubjects($event, $subject, $parsedParameters);
@@ -232,7 +230,7 @@ class Provider implements IProvider {
if (!isset($parsedParameters['user'])) {
// External user via public link share
- $subject = str_replace('{user}', $this->activityLang->t('"remote account"'), $subject);
+ $subject = str_replace('{user}', $this->l->t('"remote account"'), $subject);
}
$this->setSubjects($event, $subject, $parsedParameters);
@@ -322,7 +320,7 @@ class Provider implements IProvider {
protected function getFile($parameter, ?IEvent $event = null): array {
if (is_array($parameter)) {
$path = reset($parameter);
- $id = (string)key($parameter);
+ $id = (int)key($parameter);
} elseif ($event !== null) {
// Legacy from before ownCloud 8.2
$path = $parameter;
@@ -344,7 +342,7 @@ class Provider implements IProvider {
return [
'type' => 'file',
- 'id' => $encryptionContainer->getId(),
+ 'id' => (string)$encryptionContainer->getId(),
'name' => $encryptionContainer->getName(),
'path' => $path,
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $encryptionContainer->getId()]),
@@ -357,7 +355,7 @@ class Provider implements IProvider {
return [
'type' => 'file',
- 'id' => $id,
+ 'id' => (string)$id,
'name' => basename($path),
'path' => trim($path, '/'),
'link' => $this->url->linkToRouteAbsolute('files.viewcontroller.showFile', ['fileid' => $id]),
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/AdvancedCapabilities.php b/apps/files/lib/AdvancedCapabilities.php
new file mode 100644
index 00000000000..22f990f0cf8
--- /dev/null
+++ b/apps/files/lib/AdvancedCapabilities.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files;
+
+use OCA\Files\Service\SettingsService;
+use OCP\Capabilities\ICapability;
+use OCP\Capabilities\IInitialStateExcludedCapability;
+
+/**
+ * Capabilities not needed for every request.
+ * This capabilities might be hard to compute or no used by the webui.
+ */
+class AdvancedCapabilities implements ICapability, IInitialStateExcludedCapability {
+
+ public function __construct(
+ protected SettingsService $service,
+ ) {
+ }
+
+ /**
+ * Return this classes capabilities
+ *
+ * @return array{files: array{'windows_compatible_filenames': bool}}
+ */
+ public function getCapabilities(): array {
+ return [
+ 'files' => [
+ 'windows_compatible_filenames' => $this->service->hasFilesWindowsSupport(),
+ ],
+ ];
+ }
+}
diff --git a/apps/files/lib/AppInfo/Application.php b/apps/files/lib/AppInfo/Application.php
index 8d00912f7bb..2761b44ecf9 100644
--- a/apps/files/lib/AppInfo/Application.php
+++ b/apps/files/lib/AppInfo/Application.php
@@ -9,6 +9,7 @@ declare(strict_types=1);
namespace OCA\Files\AppInfo;
use Closure;
+use OCA\Files\AdvancedCapabilities;
use OCA\Files\Capabilities;
use OCA\Files\Collaboration\Resources\Listener;
use OCA\Files\Collaboration\Resources\ResourceProvider;
@@ -107,6 +108,7 @@ class Application extends App implements IBootstrap {
* Register capabilities
*/
$context->registerCapability(Capabilities::class);
+ $context->registerCapability(AdvancedCapabilities::class);
$context->registerCapability(DirectEditingCapabilities::class);
$context->registerDeclarativeSettings(DeclarativeAdminSettings::class);
@@ -115,7 +117,7 @@ class Application extends App implements IBootstrap {
$context->registerEventListener(RenderReferenceEvent::class, RenderReferenceEventListener::class);
$context->registerEventListener(BeforeNodeRenamedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(BeforeNodeDeletedEvent::class, SyncLivePhotosListener::class);
- $context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class);
+ $context->registerEventListener(CacheEntryRemovedEvent::class, SyncLivePhotosListener::class, 1); // Ensure this happen before the metadata are deleted.
$context->registerEventListener(BeforeNodeCopiedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(NodeCopiedEvent::class, SyncLivePhotosListener::class);
$context->registerEventListener(LoadSearchPlugins::class, LoadSearchPluginsListener::class);
@@ -130,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();
}
@@ -138,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/CleanupFileLocks.php b/apps/files/lib/BackgroundJob/CleanupFileLocks.php
index a332c1434fc..91bb145884b 100644
--- a/apps/files/lib/BackgroundJob/CleanupFileLocks.php
+++ b/apps/files/lib/BackgroundJob/CleanupFileLocks.php
@@ -10,6 +10,8 @@ namespace OCA\Files\BackgroundJob;
use OC\Lock\DBLockingProvider;
use OCP\AppFramework\Utility\ITimeFactory;
use OCP\BackgroundJob\TimedJob;
+use OCP\Lock\ILockingProvider;
+use OCP\Server;
/**
* Clean up all file locks that are expired for the DB file locking provider
@@ -30,7 +32,7 @@ class CleanupFileLocks extends TimedJob {
* @throws \Exception
*/
public function run($argument) {
- $lockingProvider = \OC::$server->getLockingProvider();
+ $lockingProvider = Server::get(ILockingProvider::class);
if ($lockingProvider instanceof DBLockingProvider) {
$lockingProvider->cleanExpiredLocks();
}
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/Object/Info.php b/apps/files/lib/Command/Object/Info.php
new file mode 100644
index 00000000000..6748de37cfe
--- /dev/null
+++ b/apps/files/lib/Command/Object/Info.php
@@ -0,0 +1,80 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OC\Core\Command\Base;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\ObjectStore\IObjectStoreMetaData;
+use OCP\Util;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Info extends Base {
+ public function __construct(
+ private ObjectUtil $objectUtils,
+ private IMimeTypeDetector $mimeTypeDetector,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:info')
+ ->setDescription('Get the metadata of an object')
+ ->addArgument('object', InputArgument::REQUIRED, 'Object to get')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to get the object from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $object = $input->getArgument('object');
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore instanceof IObjectStoreMetaData) {
+ $output->writeln('<error>Configured object store does currently not support retrieve metadata</error>');
+ return self::FAILURE;
+ }
+
+ if (!$objectStore->objectExists($object)) {
+ $output->writeln("<error>Object $object does not exist</error>");
+ return self::FAILURE;
+ }
+
+ try {
+ $meta = $objectStore->getObjectMetaData($object);
+ } catch (\Exception $e) {
+ $msg = $e->getMessage();
+ $output->writeln("<error>Failed to read $object from object store: $msg</error>");
+ return self::FAILURE;
+ }
+
+ if ($input->getOption('output') === 'plain' && isset($meta['size'])) {
+ $meta['size'] = Util::humanFileSize($meta['size']);
+ }
+ if (isset($meta['mtime'])) {
+ $meta['mtime'] = $meta['mtime']->format(\DateTimeImmutable::ATOM);
+ }
+ if (!isset($meta['mimetype'])) {
+ $handle = $objectStore->readObject($object);
+ $head = fread($handle, 8192);
+ fclose($handle);
+ $meta['mimetype'] = $this->mimeTypeDetector->detectString($head);
+ }
+
+ $this->writeArrayInOutputFormat($input, $output, $meta);
+
+ return self::SUCCESS;
+ }
+
+}
diff --git a/apps/files/lib/Command/Object/ListObject.php b/apps/files/lib/Command/Object/ListObject.php
new file mode 100644
index 00000000000..5d30232e09f
--- /dev/null
+++ b/apps/files/lib/Command/Object/ListObject.php
@@ -0,0 +1,50 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OC\Core\Command\Base;
+use OCP\Files\ObjectStore\IObjectStoreMetaData;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class ListObject extends Base {
+ private const CHUNK_SIZE = 100;
+
+ public function __construct(
+ private readonly ObjectUtil $objectUtils,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:list')
+ ->setDescription('List all objects in the object store')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore instanceof IObjectStoreMetaData) {
+ $output->writeln('<error>Configured object store does currently not support listing objects</error>');
+ return self::FAILURE;
+ }
+ $objects = $objectStore->listObjects();
+ $objects = $this->objectUtils->formatObjects($objects, $input->getOption('output') === self::OUTPUT_FORMAT_PLAIN);
+ $this->writeStreamingTableInOutputFormat($input, $output, $objects, self::CHUNK_SIZE);
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files/lib/Command/Object/ObjectUtil.php b/apps/files/lib/Command/Object/ObjectUtil.php
index c4ab59608fb..5f053c2c42f 100644
--- a/apps/files/lib/Command/Object/ObjectUtil.php
+++ b/apps/files/lib/Command/Object/ObjectUtil.php
@@ -12,6 +12,7 @@ use OCP\DB\QueryBuilder\IQueryBuilder;
use OCP\Files\ObjectStore\IObjectStore;
use OCP\IConfig;
use OCP\IDBConnection;
+use OCP\Util;
use Symfony\Component\Console\Output\OutputInterface;
class ObjectUtil {
@@ -91,4 +92,24 @@ class ObjectUtil {
return $fileId;
}
+
+ public function formatObjects(\Iterator $objects, bool $humanOutput): \Iterator {
+ foreach ($objects as $object) {
+ yield $this->formatObject($object, $humanOutput);
+ }
+ }
+
+ public function formatObject(array $object, bool $humanOutput): array {
+ $row = array_merge([
+ 'urn' => $object['urn'],
+ ], ($object['metadata'] ?? []));
+
+ if ($humanOutput && isset($row['size'])) {
+ $row['size'] = Util::humanFileSize($row['size']);
+ }
+ if (isset($row['mtime'])) {
+ $row['mtime'] = $row['mtime']->format(\DateTimeImmutable::ATOM);
+ }
+ return $row;
+ }
}
diff --git a/apps/files/lib/Command/Object/Orphans.php b/apps/files/lib/Command/Object/Orphans.php
new file mode 100644
index 00000000000..f7132540fc8
--- /dev/null
+++ b/apps/files/lib/Command/Object/Orphans.php
@@ -0,0 +1,79 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files\Command\Object;
+
+use OC\Core\Command\Base;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\ObjectStore\IObjectStoreMetaData;
+use OCP\IDBConnection;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Orphans extends Base {
+ private const CHUNK_SIZE = 100;
+
+ private ?IQueryBuilder $query = null;
+
+ public function __construct(
+ private readonly ObjectUtil $objectUtils,
+ private readonly IDBConnection $connection,
+ ) {
+ parent::__construct();
+ }
+
+ private function getQuery(): IQueryBuilder {
+ if (!$this->query) {
+ $this->query = $this->connection->getQueryBuilder();
+ $this->query->select('fileid')
+ ->from('filecache')
+ ->where($this->query->expr()->eq('fileid', $this->query->createParameter('file_id')));
+ }
+ return $this->query;
+ }
+
+ protected function configure(): void {
+ parent::configure();
+ $this
+ ->setName('files:object:orphans')
+ ->setDescription('List all objects in the object store that don\'t have a matching entry in the database')
+ ->addOption('bucket', 'b', InputOption::VALUE_REQUIRED, "Bucket to list the objects from, only required in cases where it can't be determined from the config");
+ }
+
+ public function execute(InputInterface $input, OutputInterface $output): int {
+ $objectStore = $this->objectUtils->getObjectStore($input->getOption('bucket'), $output);
+ if (!$objectStore) {
+ return self::FAILURE;
+ }
+
+ if (!$objectStore instanceof IObjectStoreMetaData) {
+ $output->writeln('<error>Configured object store does currently not support listing objects</error>');
+ return self::FAILURE;
+ }
+ $prefixLength = strlen('urn:oid:');
+
+ $objects = $objectStore->listObjects('urn:oid:');
+ $orphans = new \CallbackFilterIterator($objects, function (array $object) use ($prefixLength) {
+ $fileId = (int)substr($object['urn'], $prefixLength);
+ return !$this->fileIdInDb($fileId);
+ });
+
+ $orphans = $this->objectUtils->formatObjects($orphans, $input->getOption('output') === self::OUTPUT_FORMAT_PLAIN);
+ $this->writeStreamingTableInOutputFormat($input, $output, $orphans, self::CHUNK_SIZE);
+
+ return self::SUCCESS;
+ }
+
+ private function fileIdInDb(int $fileId): bool {
+ $query = $this->getQuery();
+ $query->setParameter('file_id', $fileId, IQueryBuilder::PARAM_INT);
+ $result = $query->executeQuery();
+ return $result->fetchOne() !== false;
+ }
+}
diff --git a/apps/files/lib/Command/SanitizeFilenames.php b/apps/files/lib/Command/SanitizeFilenames.php
new file mode 100644
index 00000000000..88d41d1cb5e
--- /dev/null
+++ b/apps/files/lib/Command/SanitizeFilenames.php
@@ -0,0 +1,151 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Command;
+
+use Exception;
+use OC\Core\Command\Base;
+use OC\Files\FilenameValidator;
+use OCP\Files\Folder;
+use OCP\Files\IRootFolder;
+use OCP\Files\NotPermittedException;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\L10N\IFactory;
+use OCP\Lock\LockedException;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Input\InputOption;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class SanitizeFilenames extends Base {
+
+ private OutputInterface $output;
+ private ?string $charReplacement;
+ private bool $dryRun;
+
+ public function __construct(
+ private IUserManager $userManager,
+ private IRootFolder $rootFolder,
+ private IUserSession $session,
+ private IFactory $l10nFactory,
+ private FilenameValidator $filenameValidator,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName('files:sanitize-filenames')
+ ->setDescription('Renames files to match naming constraints')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL | InputArgument::IS_ARRAY,
+ 'will only rename files the given user(s) have access to'
+ )
+ ->addOption(
+ 'dry-run',
+ mode: InputOption::VALUE_NONE,
+ description: 'Do not actually rename any files but just check filenames.',
+ )
+ ->addOption(
+ 'char-replacement',
+ 'c',
+ mode: InputOption::VALUE_REQUIRED,
+ description: 'Replacement for invalid character (by default space, underscore or dash is used)',
+ );
+
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $this->charReplacement = $input->getOption('char-replacement');
+ // 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');
+ if ($this->dryRun) {
+ $output->writeln('<info>Dry run is enabled, no actual renaming will be applied.</>');
+ }
+
+ $this->output = $output;
+ $users = $input->getArgument('user_id');
+ if (!empty($users)) {
+ foreach ($users as $userId) {
+ $user = $this->userManager->get($userId);
+ if ($user === null) {
+ $output->writeln("<error>User '$userId' does not exist - skipping</>");
+ continue;
+ }
+ $this->sanitizeUserFiles($user);
+ }
+ } else {
+ $this->userManager->callForSeenUsers($this->sanitizeUserFiles(...));
+ }
+ return self::SUCCESS;
+ }
+
+ private function sanitizeUserFiles(IUser $user): void {
+ // Set an active user so that event listeners can correctly work (e.g. files versions)
+ $this->session->setVolatileActiveUser($user);
+
+ $this->output->writeln('<info>Analyzing files of ' . $user->getUID() . '</>');
+
+ $folder = $this->rootFolder->getUserFolder($user->getUID());
+ $this->sanitizeFiles($folder);
+ }
+
+ private function sanitizeFiles(Folder $folder): void {
+ foreach ($folder->getDirectoryListing() as $node) {
+ $this->output->writeln('scanning: ' . $node->getPath(), OutputInterface::VERBOSITY_VERBOSE);
+
+ try {
+ $oldName = $node->getName();
+ $newName = $this->filenameValidator->sanitizeFilename($oldName, $this->charReplacement);
+ if ($oldName !== $newName) {
+ $newName = $folder->getNonExistingName($newName);
+ $path = rtrim(dirname($node->getPath()), '/');
+
+ if (!$this->dryRun) {
+ $node->move("$path/$newName");
+ } elseif (!$folder->isCreatable()) {
+ // simulate error for dry run
+ throw new NotPermittedException();
+ }
+ $this->output->writeln('renamed: "' . $oldName . '" to "' . $newName . '"');
+ }
+ } catch (LockedException) {
+ $this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (file is locked)</>');
+ } catch (NotPermittedException) {
+ $this->output->writeln('<comment>skipping: ' . $node->getPath() . ' (no permissions)</>');
+ } 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) {
+ $this->sanitizeFiles($node);
+ }
+ }
+ }
+
+}
diff --git a/apps/files/lib/Command/Scan.php b/apps/files/lib/Command/Scan.php
index cf1cb04b9af..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;
@@ -24,6 +25,8 @@ use OCP\Files\NotFoundException;
use OCP\Files\StorageNotAvailableException;
use OCP\FilesMetadata\IFilesMetadataManager;
use OCP\IUserManager;
+use OCP\Lock\LockedException;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
@@ -97,13 +100,21 @@ 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,
new ConnectionAdapter($connection),
- \OC::$server->get(IEventDispatcher::class),
- \OC::$server->get(LoggerInterface::class)
+ Server::get(IEventDispatcher::class),
+ Server::get(LoggerInterface::class)
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
@@ -151,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>");
@@ -164,6 +175,12 @@ class Scan extends Base {
} catch (NotFoundException $e) {
$output->writeln('<error>Path not found: ' . $e->getMessage() . '</error>');
++$this->errorsCounter;
+ } catch (LockedException $e) {
+ if (str_starts_with($e->getPath(), 'scanner::')) {
+ $output->writeln('<error>Another process is already scanning \'' . substr($e->getPath(), strlen('scanner::')) . '\'</error>');
+ } else {
+ throw $e;
+ }
} catch (\Exception $e) {
$output->writeln('<error>Exception during scan: ' . $e->getMessage() . '</error>');
$output->writeln('<error>' . $e->getTraceAsString() . '</error>');
@@ -171,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;
}
@@ -203,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)) {
@@ -212,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>");
@@ -238,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
);
}
@@ -310,7 +358,7 @@ class Scan extends Base {
protected function reconnectToDatabase(OutputInterface $output): Connection {
/** @var Connection $connection */
- $connection = \OC::$server->get(Connection::class);
+ $connection = Server::get(Connection::class);
try {
$connection->close();
} catch (\Exception $ex) {
diff --git a/apps/files/lib/Command/ScanAppData.php b/apps/files/lib/Command/ScanAppData.php
index 02f0977cba9..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
@@ -18,6 +19,7 @@ use OCP\Files\Node;
use OCP\Files\NotFoundException;
use OCP\Files\StorageNotAvailableException;
use OCP\IConfig;
+use OCP\Server;
use Psr\Log\LoggerInterface;
use Symfony\Component\Console\Helper\Table;
use Symfony\Component\Console\Input\InputArgument;
@@ -70,8 +72,8 @@ class ScanAppData extends Base {
$scanner = new Scanner(
null,
new ConnectionAdapter($connection),
- \OC::$server->query(IEventDispatcher::class),
- \OC::$server->get(LoggerInterface::class)
+ Server::get(IEventDispatcher::class),
+ Server::get(LoggerInterface::class)
);
# check on each file/folder if there was a user interrupt (ctrl-c) and throw an exception
@@ -213,7 +215,7 @@ class ScanAppData extends Base {
protected function reconnectToDatabase(OutputInterface $output): Connection {
/** @var Connection $connection */
- $connection = \OC::$server->get(Connection::class);
+ $connection = Server::get(Connection::class);
try {
$connection->close();
} catch (\Exception $ex) {
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/Command/WindowsCompatibleFilenames.php b/apps/files/lib/Command/WindowsCompatibleFilenames.php
new file mode 100644
index 00000000000..84a1b277824
--- /dev/null
+++ b/apps/files/lib/Command/WindowsCompatibleFilenames.php
@@ -0,0 +1,52 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files\Service\SettingsService;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class WindowsCompatibleFilenames extends Base {
+
+ public function __construct(
+ private SettingsService $service,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ parent::configure();
+
+ $this
+ ->setName('files:windows-compatible-filenames')
+ ->setDescription('Enforce naming constraints for windows compatible filenames')
+ ->addOption('enable', description: 'Enable windows naming constraints')
+ ->addOption('disable', description: 'Disable windows naming constraints');
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ if ($input->getOption('enable')) {
+ if ($this->service->hasFilesWindowsSupport()) {
+ $output->writeln('<error>Windows compatible filenames already enforced.</error>', OutputInterface::VERBOSITY_VERBOSE);
+ }
+ $this->service->setFilesWindowsSupport(true);
+ $output->writeln('Windows compatible filenames enforced.');
+ } elseif ($input->getOption('disable')) {
+ if (!$this->service->hasFilesWindowsSupport()) {
+ $output->writeln('<error>Windows compatible filenames already disabled.</error>', OutputInterface::VERBOSITY_VERBOSE);
+ }
+ $this->service->setFilesWindowsSupport(false);
+ $output->writeln('Windows compatible filename constraints removed.');
+ } else {
+ $output->writeln('Windows compatible filenames are ' . ($this->service->hasFilesWindowsSupport() ? 'enforced' : 'disabled'));
+ }
+ return self::SUCCESS;
+ }
+}
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/Controller/ViewController.php b/apps/files/lib/Controller/ViewController.php
index a0b5c1c282c..ecf21cef313 100644
--- a/apps/files/lib/Controller/ViewController.php
+++ b/apps/files/lib/Controller/ViewController.php
@@ -26,6 +26,7 @@ use OCP\AppFramework\Http\RedirectResponse;
use OCP\AppFramework\Http\Response;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\AppFramework\Services\IInitialState;
+use OCP\Authentication\TwoFactorAuth\IRegistry;
use OCP\Collaboration\Resources\LoadAdditionalScriptsEvent as ResourcesLoadAdditionalScriptsEvent;
use OCP\EventDispatcher\IEventDispatcher;
use OCP\Files\Folder;
@@ -60,6 +61,7 @@ class ViewController extends Controller {
private UserConfig $userConfig,
private ViewConfig $viewConfig,
private FilenameValidator $filenameValidator,
+ private IRegistry $twoFactorRegistry,
) {
parent::__construct($appName, $request);
}
@@ -82,16 +84,18 @@ class ViewController extends Controller {
*/
#[NoAdminRequired]
#[NoCSRFRequired]
- public function showFile(?string $fileid = null): Response {
+ public function showFile(?string $fileid = null, ?string $opendetails = null, ?string $openfile = null): Response {
if (!$fileid) {
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index'));
}
// This is the entry point from the `/f/{fileid}` URL which is hardcoded in the server.
try {
- return $this->redirectToFile((int)$fileid);
+ return $this->redirectToFile((int)$fileid, $opendetails, $openfile);
} catch (NotFoundException $e) {
- return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.index', ['fileNotFound' => true]));
+ // Keep the fileid even if not found, it will be used
+ // to detect the file could not be found and warn the user
+ return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', ['fileid' => $fileid, 'view' => 'files']));
}
}
@@ -100,38 +104,35 @@ class ViewController extends Controller {
* @param string $dir
* @param string $view
* @param string $fileid
- * @param bool $fileNotFound
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
- public function indexView($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
- return $this->index($dir, $view, $fileid, $fileNotFound);
+ public function indexView($dir = '', $view = '', $fileid = null) {
+ return $this->index($dir, $view, $fileid);
}
/**
* @param string $dir
* @param string $view
* @param string $fileid
- * @param bool $fileNotFound
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
- public function indexViewFileid($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
- return $this->index($dir, $view, $fileid, $fileNotFound);
+ public function indexViewFileid($dir = '', $view = '', $fileid = null) {
+ return $this->index($dir, $view, $fileid);
}
/**
* @param string $dir
* @param string $view
* @param string $fileid
- * @param bool $fileNotFound
* @return TemplateResponse|RedirectResponse
*/
#[NoAdminRequired]
#[NoCSRFRequired]
- public function index($dir = '', $view = '', $fileid = null, $fileNotFound = false) {
+ public function index($dir = '', $view = '', $fileid = null) {
if ($fileid !== null && $view !== 'trashbin') {
try {
return $this->redirectToFileIfInTrashbin((int)$fileid);
@@ -143,7 +144,8 @@ class ViewController extends Controller {
Util::addInitScript('files', 'init');
Util::addScript('files', 'main');
- $userId = $this->userSession->getUser()->getUID();
+ $user = $this->userSession->getUser();
+ $userId = $user->getUID();
// If the file doesn't exists in the folder and
// exists in only one occurrence, redirect to that file
@@ -159,8 +161,6 @@ class ViewController extends Controller {
if (count($nodes) === 1 && $relativePath !== $dir && $nodePath !== $dir) {
return $this->redirectToFile((int)$fileid);
}
- } else { // fileid does not exist anywhere
- $fileNotFound = true;
}
}
@@ -198,6 +198,15 @@ class ViewController extends Controller {
$this->initialState->provideInitialState('templates_path', $this->templateManager->hasTemplateDirectory() ? $this->templateManager->getTemplatePath() : false);
$this->initialState->provideInitialState('templates', $this->templateManager->listCreators());
+ $isTwoFactorEnabled = false;
+ foreach ($this->twoFactorRegistry->getProviderStates($user) as $providerId => $providerState) {
+ if ($providerId !== 'backup_codes' && $providerState === true) {
+ $isTwoFactorEnabled = true;
+ }
+ }
+
+ $this->initialState->provideInitialState('isTwoFactorEnabled', $isTwoFactorEnabled);
+
$response = new TemplateResponse(
Application::APP_ID,
'index',
@@ -249,10 +258,12 @@ class ViewController extends Controller {
* Redirects to the file list and highlight the given file id
*
* @param int $fileId file id to show
+ * @param string|null $openDetails open details parameter
+ * @param string|null $openFile open file parameter
* @return RedirectResponse redirect response or not found response
* @throws NotFoundException
*/
- private function redirectToFile(int $fileId) {
+ private function redirectToFile(int $fileId, ?string $openDetails = null, ?string $openFile = null): RedirectResponse {
$uid = $this->userSession->getUser()->getUID();
$baseFolder = $this->rootFolder->getUserFolder($uid);
$node = $baseFolder->getFirstNodeById($fileId);
@@ -274,6 +285,19 @@ class ViewController extends Controller {
// open the file by default (opening the viewer)
$params['openfile'] = 'true';
}
+
+ // Forward open parameters if any.
+ // - openfile is true by default
+ // - opendetails is undefined by default
+ // - both will be evaluated as truthy
+ if ($openDetails !== null) {
+ $params['opendetails'] = $openDetails !== 'false' ? 'true' : 'false';
+ }
+
+ if ($openFile !== null) {
+ $params['openfile'] = $openFile !== 'false' ? 'true' : 'false';
+ }
+
return new RedirectResponse($this->urlGenerator->linkToRoute('files.view.indexViewFileid', $params));
}
diff --git a/apps/files/lib/Dashboard/FavoriteWidget.php b/apps/files/lib/Dashboard/FavoriteWidget.php
index a36dd65c81b..b68b8a56b2e 100644
--- a/apps/files/lib/Dashboard/FavoriteWidget.php
+++ b/apps/files/lib/Dashboard/FavoriteWidget.php
@@ -18,6 +18,7 @@ use OCP\Dashboard\Model\WidgetButton;
use OCP\Dashboard\Model\WidgetItem;
use OCP\Dashboard\Model\WidgetItems;
use OCP\Dashboard\Model\WidgetOptions;
+use OCP\Files\File;
use OCP\Files\IMimeTypeDetector;
use OCP\Files\IRootFolder;
use OCP\IL10N;
@@ -88,13 +89,17 @@ class FavoriteWidget implements IIconWidget, IAPIWidgetV2, IButtonWidget, IOptio
$url = $this->urlGenerator->linkToRouteAbsolute(
'files.view.showFile', ['fileid' => $node->getId()]
);
- $icon = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', [
- 'x' => 256,
- 'y' => 256,
- 'fileId' => $node->getId(),
- 'c' => $node->getEtag(),
- 'mimeFallback' => true,
- ]);
+ if ($node instanceof File) {
+ $icon = $this->urlGenerator->linkToRouteAbsolute('core.Preview.getPreviewByFileId', [
+ 'x' => 256,
+ 'y' => 256,
+ 'fileId' => $node->getId(),
+ 'c' => $node->getEtag(),
+ 'mimeFallback' => true,
+ ]);
+ } else {
+ $icon = $this->urlGenerator->getAbsoluteURL($this->urlGenerator->imagePath('core', 'filetypes/folder.svg'));
+ }
$favoriteNodes[] = new WidgetItem(
$node->getName(),
'',
diff --git a/apps/files/lib/Helper.php b/apps/files/lib/Helper.php
index b66dc9a2056..b1439ac7fa5 100644
--- a/apps/files/lib/Helper.php
+++ b/apps/files/lib/Helper.php
@@ -9,8 +9,6 @@ namespace OCA\Files;
use OC\Files\Filesystem;
use OCP\Files\FileInfo;
-use OCP\Files\NotFoundException;
-use OCP\ITagManager;
use OCP\Util;
/**
@@ -18,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 = \OC::$server->getMimeTypeDetector()->mimeTypeIcon('dir');
- // TODO: move this part to the client side, using mountType
- if ($file->isShared()) {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon('dir-shared');
- } elseif ($file->isMounted()) {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon('dir-external');
- }
- } else {
- $icon = \OC::$server->getMimeTypeDetector()->mimeTypeIcon($file->getMimetype());
- }
-
- return substr($icon, 0, -3) . 'svg';
- }
-
- /**
* Comparator function to sort files alphabetically and have
* the directories appear first
*
@@ -160,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.
*
@@ -190,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 23eb4f9df61..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();
@@ -141,39 +144,40 @@ 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
- 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
@@ -184,6 +188,9 @@ class OwnershipTransferService {
$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 {
@@ -209,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) {
@@ -241,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) {
@@ -327,7 +342,7 @@ class OwnershipTransferService {
return mb_strpos(
Filesystem::normalizePath($relativePath . '/', false),
$normalizedPath . '/') === 0;
- } catch (\Exception $e) {
+ } catch (Exception $e) {
return false;
}
});
@@ -355,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) {
@@ -371,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;
@@ -393,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.
@@ -406,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
@@ -432,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) {
@@ -448,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);
@@ -514,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 0abcfb2a6ad..dcf30b7796d 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,32 +20,62 @@ class UserConfig {
'allowed' => [true, false],
],
[
- // Whether to show the hidden files or not in the files list
- 'key' => 'show_hidden',
+ // 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 sort favorites first in the list or not
- 'key' => 'sort_favorites_first',
+ // 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 sort folders before files in the list or not
- 'key' => 'sort_folders_first',
+ // 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 files list in grid view or not
- 'key' => 'grid_view',
+ // Whether to show the hidden files or not in the files list
+ 'key' => 'show_hidden',
'default' => false,
'allowed' => [true, false],
],
[
- // Whether to show the folder tree
- 'key' => 'folder_tree',
+ // 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],
],
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 e509ad2233b..bbf97cc4d32 100644
--- a/apps/files/lib/Settings/DeclarativeAdminSettings.php
+++ b/apps/files/lib/Settings/DeclarativeAdminSettings.php
@@ -9,6 +9,7 @@ namespace OCA\Files\Settings;
use OCA\Files\Service\SettingsService;
use OCP\IL10N;
+use OCP\IURLGenerator;
use OCP\IUser;
use OCP\Settings\DeclarativeSettingsTypes;
use OCP\Settings\IDeclarativeSettingsFormWithHandlers;
@@ -18,6 +19,7 @@ class DeclarativeAdminSettings implements IDeclarativeSettingsFormWithHandlers {
public function __construct(
private IL10N $l,
private SettingsService $service,
+ private IURLGenerator $urlGenerator,
) {
}
@@ -44,7 +46,12 @@ class DeclarativeAdminSettings implements IDeclarativeSettingsFormWithHandlers {
'section_id' => 'server',
'storage_type' => DeclarativeSettingsTypes::STORAGE_TYPE_EXTERNAL,
'title' => $this->l->t('Files compatibility'),
- '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.'),
+ '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('It is also possible to migrate files automatically after enabling this setting, please refer to the documentation about the occ command.')
+ ),
'fields' => [
[