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