diff --git a/apps/admin_audit/lib/Actions/Files.php b/apps/admin_audit/lib/Actions/Files.php index 3993f1a76ee11..8e833812f552a 100644 --- a/apps/admin_audit/lib/Actions/Files.php +++ b/apps/admin_audit/lib/Actions/Files.php @@ -10,7 +10,6 @@ use OC\Files\Node\NonExistingFile; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeReadEvent; -use OCP\Files\Events\Node\BeforeNodeRenamedEvent; use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Events\Node\NodeCreatedEvent; use OCP\Files\Events\Node\NodeRenamedEvent; @@ -26,9 +25,6 @@ * @package OCA\AdminAudit\Actions */ class Files extends Action { - - private array $renamedNodes = []; - /** * Logs file read actions */ @@ -52,31 +48,16 @@ public function read(BeforeNodeReadEvent $event): void { ); } - /** - * Logs rename actions of files - */ - public function beforeRename(BeforeNodeRenamedEvent $event): void { - try { - $source = $event->getSource(); - $this->renamedNodes[$source->getId()] = $source; - } catch (InvalidPathException|NotFoundException $e) { - Server::get(LoggerInterface::class)->error( - 'Exception thrown in file rename: ' . $e->getMessage(), ['app' => 'admin_audit', 'exception' => $e] - ); - return; - } - } - /** * Logs rename actions of files */ public function afterRename(NodeRenamedEvent $event): void { try { $target = $event->getTarget(); - $originalSource = $this->renamedNodes[$target->getId()]; + $source = $event->getSource(); $params = [ 'newid' => $target->getId(), - 'oldpath' => mb_substr($originalSource->getInternalPath(), 5), + 'oldpath' => mb_substr($source->getInternalPath(), 5), 'newpath' => mb_substr($target->getInternalPath(), 5), ]; } catch (InvalidPathException|NotFoundException $e) { diff --git a/apps/admin_audit/lib/AppInfo/Application.php b/apps/admin_audit/lib/AppInfo/Application.php index 201a8fe255a93..08c13c66d595b 100644 --- a/apps/admin_audit/lib/AppInfo/Application.php +++ b/apps/admin_audit/lib/AppInfo/Application.php @@ -41,7 +41,6 @@ use OCP\EventDispatcher\IEventDispatcher; use OCP\Files\Events\Node\BeforeNodeDeletedEvent; use OCP\Files\Events\Node\BeforeNodeReadEvent; -use OCP\Files\Events\Node\BeforeNodeRenamedEvent; use OCP\Files\Events\Node\NodeCopiedEvent; use OCP\Files\Events\Node\NodeCreatedEvent; use OCP\Files\Events\Node\NodeRenamedEvent; @@ -168,13 +167,6 @@ private function tagHooks(IAuditLogger $logger, private function fileHooks(IAuditLogger $logger, IEventDispatcher $eventDispatcher): void { $fileActions = new Files($logger); - $eventDispatcher->addListener( - BeforeNodeRenamedEvent::class, - function (BeforeNodeRenamedEvent $event) use ($fileActions): void { - $fileActions->beforeRename($event); - } - ); - $eventDispatcher->addListener( NodeRenamedEvent::class, function (NodeRenamedEvent $event) use ($fileActions): void { diff --git a/apps/files_sharing/tests/EncryptedSizePropagationTest.php b/apps/files_sharing/tests/EncryptedSizePropagationTest.php index af2cf379358e3..53da510f9f14e 100644 --- a/apps/files_sharing/tests/EncryptedSizePropagationTest.php +++ b/apps/files_sharing/tests/EncryptedSizePropagationTest.php @@ -16,12 +16,17 @@ class EncryptedSizePropagationTest extends SizePropagationTest { use EncryptionTrait; protected function setupUser($name, $password = '') { + $this->config->setAppValue('encryption', 'useMasterKey', '0'); $this->createUser($name, $password); $tmpFolder = \OC::$server->getTempManager()->getTemporaryFolder(); $this->registerMount($name, '\OC\Files\Storage\Local', '/' . $name, ['datadir' => $tmpFolder]); - $this->config->setAppValue('encryption', 'useMasterKey', '0'); $this->setupForUser($name, $password); $this->loginWithEncryption($name); return new View('/' . $name . '/files'); } + + protected function loginHelper($user, $create = false, $password = false) { + $this->setupForUser($user, $password); + parent::loginHelper($user, $create, $password); + } } diff --git a/apps/files_sharing/tests/TestCase.php b/apps/files_sharing/tests/TestCase.php index d12a6bf0ec2c3..b05c597b6bea6 100644 --- a/apps/files_sharing/tests/TestCase.php +++ b/apps/files_sharing/tests/TestCase.php @@ -102,7 +102,7 @@ protected function setUp(): void { \OC::$server->get(DisplayNameCache::class)->clear(); //login as user1 - self::loginHelper(self::TEST_FILES_SHARING_API_USER1); + $this->loginHelper(self::TEST_FILES_SHARING_API_USER1); $this->data = 'foobar'; $this->view = new View('/' . self::TEST_FILES_SHARING_API_USER1 . '/files'); @@ -167,7 +167,7 @@ public static function tearDownAfterClass(): void { * @param bool $create * @param bool $password */ - protected static function loginHelper($user, $create = false, $password = false) { + protected function loginHelper($user, $create = false, $password = false) { if ($password === false) { $password = $user; } diff --git a/apps/files_trashbin/tests/TrashbinTest.php b/apps/files_trashbin/tests/TrashbinTest.php index ea9abedd14489..b30e9d458ad4b 100644 --- a/apps/files_trashbin/tests/TrashbinTest.php +++ b/apps/files_trashbin/tests/TrashbinTest.php @@ -19,7 +19,7 @@ use OCP\AppFramework\Utility\ITimeFactory; use OCP\Constants; use OCP\Files\FileInfo; -use OCP\IConfig; +use OCP\Server; use OCP\Share\IShare; /** @@ -110,8 +110,11 @@ protected function setUp(): void { \OC::$server->getAppManager()->enableApp('files_trashbin'); $config = \OC::$server->getConfig(); - $mockConfig = $this->createMock(IConfig::class); - $mockConfig + $mockConfig = $this->getMockBuilder(AllConfig::class) + ->onlyMethods(['getSystemValue']) + ->setConstructorArgs([Server::get(\OC\SystemConfig::class)]) + ->getMock(); + $mockConfig->expects($this->any()) ->method('getSystemValue') ->willReturnCallback(static function ($key, $default) use ($config) { if ($key === 'filesystem_check_changes') { @@ -120,16 +123,6 @@ protected function setUp(): void { return $config->getSystemValue($key, $default); } }); - $mockConfig - ->method('getUserValue') - ->willReturnCallback(static function ($userId, $appName, $key, $default = '') use ($config) { - return $config->getUserValue($userId, $appName, $key, $default); - }); - $mockConfig - ->method('getAppValue') - ->willReturnCallback(static function ($appName, $key, $default = '') use ($config) { - return $config->getAppValue($appName, $key, $default); - }); $this->overwriteService(AllConfig::class, $mockConfig); $this->trashRoot1 = '/' . self::TEST_TRASHBIN_USER1 . '/files_trashbin'; diff --git a/apps/files_versions/lib/Listener/FileEventsListener.php b/apps/files_versions/lib/Listener/FileEventsListener.php index c078f4bc34755..cd4db886b7898 100644 --- a/apps/files_versions/lib/Listener/FileEventsListener.php +++ b/apps/files_versions/lib/Listener/FileEventsListener.php @@ -241,7 +241,7 @@ public function post_write_hook(Node $node): void { /** * Erase versions of deleted file * - * This function is connected to the delete signal of OC_Filesystem + * This function is connected to the NodeDeletedEvent event * cleanup the versions directory if the actual file gets deleted */ public function remove_hook(Node $node): void { @@ -273,7 +273,7 @@ public function pre_remove_hook(Node $node): void { /** * rename/move versions of renamed/moved files * - * This function is connected to the rename signal of OC_Filesystem and adjust the name and location + * This function is connected to the NodeRenamedEvent event and adjust the name and location * of the stored versions along the actual file */ public function rename_hook(Node $source, Node $target): void { @@ -292,7 +292,7 @@ public function rename_hook(Node $source, Node $target): void { /** * copy versions of copied files * - * This function is connected to the copy signal of OC_Filesystem and copies the + * This function is connected to the NodeCopiedEvent event and copies the * the stored versions to the new location */ public function copy_hook(Node $source, Node $target): void { diff --git a/apps/files_versions/tests/VersioningTest.php b/apps/files_versions/tests/VersioningTest.php index 20a0a783ecc87..abad0293f1a8f 100644 --- a/apps/files_versions/tests/VersioningTest.php +++ b/apps/files_versions/tests/VersioningTest.php @@ -19,7 +19,6 @@ use OCA\Files_Versions\Versions\IVersionManager; use OCP\Constants; use OCP\Files\IMimeTypeLoader; -use OCP\IConfig; use OCP\IUser; use OCP\Server; use OCP\Share\IShare; @@ -79,7 +78,10 @@ protected function setUp(): void { parent::setUp(); $config = \OC::$server->getConfig(); - $mockConfig = $this->createMock(IConfig::class); + $mockConfig = $this->getMockBuilder(AllConfig::class) + ->onlyMethods(['getSystemValue']) + ->setConstructorArgs([Server::get(\OC\SystemConfig::class)]) + ->getMock(); $mockConfig->expects($this->any()) ->method('getSystemValue') ->willReturnCallback(function ($key, $default) use ($config) { @@ -423,8 +425,24 @@ public function testMoveFileIntoSharedFolderAsRecipient(): void { $this->rootView->file_put_contents($v2, 'version2'); // move file into the shared folder as recipient - Filesystem::rename('/test.txt', '/folder1/test.txt'); + $success = Filesystem::rename('/test.txt', '/folder1/test.txt'); + + // TODO: Voir is $v2 existe pour voir si c’est la copie ou la suppression qui foire + // Mettre du debug dans Wrapper/Encrytion::copyBetweenStorage c’est le suspect n-1 + $versionsFolder1 = '/' . self::TEST_VERSIONS_USER . '/files_versions'; + $v1Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t1; + $v2Renamed = $versionsFolder1 . '/folder1/test.txt.v' . $t2; + var_dump([ + 'success' => $success, + 'source exists' => Filesystem::file_exists('/test.txt'), + 'target exists' => Filesystem::file_exists('/folder1/test.txt'), + 'v1 exists' => $this->rootView->file_exists($v1), + 'v2 exists' => $this->rootView->file_exists($v2), + 'v1Renamed exists' => $this->rootView->file_exists($v1Renamed), + 'v2Renamed exists' => $this->rootView->file_exists($v2Renamed), + ]); + $this->assertTrue($success); // renvoi true :-O $this->assertFalse($this->rootView->file_exists($v1)); $this->assertFalse($this->rootView->file_exists($v2)); diff --git a/build/psalm-baseline.xml b/build/psalm-baseline.xml index 2824f99f79d1c..1b646654ebd6a 100644 --- a/build/psalm-baseline.xml +++ b/build/psalm-baseline.xml @@ -2031,9 +2031,6 @@ - - copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename)]]> - diff --git a/lib/base.php b/lib/base.php index 25f978df836c3..879ba92104d39 100644 --- a/lib/base.php +++ b/lib/base.php @@ -6,9 +6,9 @@ * SPDX-FileCopyrightText: 2013-2016 ownCloud, Inc. * SPDX-License-Identifier: AGPL-3.0-only */ -use OC\Encryption\HookManager; use OC\Share20\Hooks; use OCP\EventDispatcher\IEventDispatcher; +use OCP\Files\Events\BeforeFileSystemSetupEvent; use OCP\Group\Events\UserRemovedEvent; use OCP\ILogger; use OCP\IRequest; @@ -16,7 +16,6 @@ use OCP\IUserSession; use OCP\Security\Bruteforce\IThrottler; use OCP\Server; -use OCP\Share; use OCP\User\Events\UserChangedEvent; use Psr\Log\LoggerInterface; use Symfony\Component\Routing\Exception\MethodNotAllowedException; @@ -868,15 +867,16 @@ public static function registerCleanupHooks(\OC\SystemConfig $systemConfig): voi } private static function registerEncryptionWrapperAndHooks(): void { + /** @var \OC\Encryption\Manager */ $manager = Server::get(\OCP\Encryption\IManager::class); - \OCP\Util::connectHook('OC_Filesystem', 'preSetup', $manager, 'setupStorage'); + Server::get(IEventDispatcher::class)->addListener( + BeforeFileSystemSetupEvent::class, + $manager->setupStorage(...), + ); $enabled = $manager->isEnabled(); if ($enabled) { - \OCP\Util::connectHook(Share::class, 'post_shared', HookManager::class, 'postShared'); - \OCP\Util::connectHook(Share::class, 'post_unshare', HookManager::class, 'postUnshared'); - \OCP\Util::connectHook('OC_Filesystem', 'post_rename', HookManager::class, 'postRename'); - \OCP\Util::connectHook('\OCA\Files_Trashbin\Trashbin', 'post_restore', HookManager::class, 'postRestore'); + \OC\Encryption\EncryptionEventListener::register(Server::get(IEventDispatcher::class)); } } diff --git a/lib/composer/composer/autoload_classmap.php b/lib/composer/composer/autoload_classmap.php index cbadc2deb15b4..00ff9165b3b73 100644 --- a/lib/composer/composer/autoload_classmap.php +++ b/lib/composer/composer/autoload_classmap.php @@ -1515,6 +1515,7 @@ 'OC\\DirectEditing\\Token' => $baseDir . '/lib/private/DirectEditing/Token.php', 'OC\\EmojiHelper' => $baseDir . '/lib/private/EmojiHelper.php', 'OC\\Encryption\\DecryptAll' => $baseDir . '/lib/private/Encryption/DecryptAll.php', + 'OC\\Encryption\\EncryptionEventListener' => $baseDir . '/lib/private/Encryption/EncryptionEventListener.php', 'OC\\Encryption\\EncryptionWrapper' => $baseDir . '/lib/private/Encryption/EncryptionWrapper.php', 'OC\\Encryption\\Exceptions\\DecryptionFailedException' => $baseDir . '/lib/private/Encryption/Exceptions/DecryptionFailedException.php', 'OC\\Encryption\\Exceptions\\EmptyEncryptionDataException' => $baseDir . '/lib/private/Encryption/Exceptions/EmptyEncryptionDataException.php', @@ -1525,7 +1526,6 @@ 'OC\\Encryption\\Exceptions\\ModuleDoesNotExistsException' => $baseDir . '/lib/private/Encryption/Exceptions/ModuleDoesNotExistsException.php', 'OC\\Encryption\\Exceptions\\UnknownCipherException' => $baseDir . '/lib/private/Encryption/Exceptions/UnknownCipherException.php', 'OC\\Encryption\\File' => $baseDir . '/lib/private/Encryption/File.php', - 'OC\\Encryption\\HookManager' => $baseDir . '/lib/private/Encryption/HookManager.php', 'OC\\Encryption\\Keys\\Storage' => $baseDir . '/lib/private/Encryption/Keys/Storage.php', 'OC\\Encryption\\Manager' => $baseDir . '/lib/private/Encryption/Manager.php', 'OC\\Encryption\\Update' => $baseDir . '/lib/private/Encryption/Update.php', diff --git a/lib/composer/composer/autoload_static.php b/lib/composer/composer/autoload_static.php index 10086cabce066..e07c252f75ed1 100644 --- a/lib/composer/composer/autoload_static.php +++ b/lib/composer/composer/autoload_static.php @@ -1564,6 +1564,7 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\DirectEditing\\Token' => __DIR__ . '/../../..' . '/lib/private/DirectEditing/Token.php', 'OC\\EmojiHelper' => __DIR__ . '/../../..' . '/lib/private/EmojiHelper.php', 'OC\\Encryption\\DecryptAll' => __DIR__ . '/../../..' . '/lib/private/Encryption/DecryptAll.php', + 'OC\\Encryption\\EncryptionEventListener' => __DIR__ . '/../../..' . '/lib/private/Encryption/EncryptionEventListener.php', 'OC\\Encryption\\EncryptionWrapper' => __DIR__ . '/../../..' . '/lib/private/Encryption/EncryptionWrapper.php', 'OC\\Encryption\\Exceptions\\DecryptionFailedException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/DecryptionFailedException.php', 'OC\\Encryption\\Exceptions\\EmptyEncryptionDataException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/EmptyEncryptionDataException.php', @@ -1574,7 +1575,6 @@ class ComposerStaticInit749170dad3f5e7f9ca158f5a9f04f6a2 'OC\\Encryption\\Exceptions\\ModuleDoesNotExistsException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/ModuleDoesNotExistsException.php', 'OC\\Encryption\\Exceptions\\UnknownCipherException' => __DIR__ . '/../../..' . '/lib/private/Encryption/Exceptions/UnknownCipherException.php', 'OC\\Encryption\\File' => __DIR__ . '/../../..' . '/lib/private/Encryption/File.php', - 'OC\\Encryption\\HookManager' => __DIR__ . '/../../..' . '/lib/private/Encryption/HookManager.php', 'OC\\Encryption\\Keys\\Storage' => __DIR__ . '/../../..' . '/lib/private/Encryption/Keys/Storage.php', 'OC\\Encryption\\Manager' => __DIR__ . '/../../..' . '/lib/private/Encryption/Manager.php', 'OC\\Encryption\\Update' => __DIR__ . '/../../..' . '/lib/private/Encryption/Update.php', diff --git a/lib/private/Encryption/EncryptionEventListener.php b/lib/private/Encryption/EncryptionEventListener.php new file mode 100644 index 0000000000000..1801a34be5664 --- /dev/null +++ b/lib/private/Encryption/EncryptionEventListener.php @@ -0,0 +1,95 @@ + */ +class EncryptionEventListener implements IEventListener { + private ?Update $updater = null; + + public function __construct( + private IUserSession $userSession, + private SetupManager $setupManager, + private Manager $encryptionManager, + ) { + } + + public static function register(IEventDispatcher $dispatcher): void { + $dispatcher->addServiceListener(NodeRenamedEvent::class, static::class); + $dispatcher->addServiceListener(ShareCreatedEvent::class, static::class); + $dispatcher->addServiceListener(ShareDeletedEvent::class, static::class); + $dispatcher->addServiceListener(NodeRestoredEvent::class, static::class); + } + + public function handle(Event $event): void { + if (!$this->encryptionManager->isEnabled()) { + return; + } + if ($event instanceof NodeRenamedEvent) { + $this->getUpdate()->postRename($event->getSource(), $event->getTarget()); + } elseif ($event instanceof ShareCreatedEvent) { + $this->getUpdate()->postShared($event->getShare()->getNode()); + } elseif ($event instanceof ShareDeletedEvent) { + // In case the unsharing happens in a background job, we don't have + // a session and we load instead the user from the UserManager + $owner = $event->getShare()->getNode()->getOwner(); + $this->getUpdate($owner)->postUnshared($event->getShare()->getNode()); + } elseif ($event instanceof NodeRestoredEvent) { + $this->getUpdate()->postRestore($event->getTarget()); + } + } + + private function getUpdate(?IUser $owner = null): Update { + if (is_null($this->updater)) { + $user = $this->userSession->getUser(); + if (!$user && ($owner !== null)) { + $user = $owner; + } + if (!$user) { + throw new \Exception('Inconsistent data, File unshared, but owner not found. Should not happen'); + } + + $uid = $user->getUID(); + + if (!$this->setupManager->isSetupComplete($user)) { + $this->setupManager->setupForUser($user); + } + + $this->updater = new Update( + new Util( + new View(), + \OC::$server->getUserManager(), + \OC::$server->getGroupManager(), + \OC::$server->getConfig()), + Filesystem::getMountManager(), + \OC::$server->getEncryptionManager(), + \OC::$server->get(IFile::class), + \OC::$server->get(LoggerInterface::class), + $uid + ); + } + + return $this->updater; + } +} diff --git a/lib/private/Encryption/EncryptionWrapper.php b/lib/private/Encryption/EncryptionWrapper.php index 7f355b603d620..b9db9616538a6 100644 --- a/lib/private/Encryption/EncryptionWrapper.php +++ b/lib/private/Encryption/EncryptionWrapper.php @@ -75,15 +75,6 @@ public function wrapStorage(string $mountPoint, IStorage $storage, IMountPoint $ \OC::$server->getGroupManager(), \OC::$server->getConfig() ); - $update = new Update( - new View(), - $util, - Filesystem::getMountManager(), - $this->manager, - $fileHelper, - $this->logger, - $uid - ); return new Encryption( $parameters, $this->manager, @@ -92,7 +83,6 @@ public function wrapStorage(string $mountPoint, IStorage $storage, IMountPoint $ $fileHelper, $uid, $keyStorage, - $update, $mountManager, $this->arrayCache ); diff --git a/lib/private/Encryption/HookManager.php b/lib/private/Encryption/HookManager.php deleted file mode 100644 index 39e7edabb9561..0000000000000 --- a/lib/private/Encryption/HookManager.php +++ /dev/null @@ -1,75 +0,0 @@ -postShared($params); - } - public static function postUnshared($params): void { - // In case the unsharing happens in a background job, we don't have - // a session and we load instead the user from the UserManager - $path = Filesystem::getPath($params['fileSource']); - $owner = Filesystem::getOwner($path); - self::getUpdate($owner)->postUnshared($params); - } - - public static function postRename($params): void { - self::getUpdate()->postRename($params); - } - - public static function postRestore($params): void { - self::getUpdate()->postRestore($params); - } - - private static function getUpdate(?string $owner = null): Update { - if (is_null(self::$updater)) { - $user = \OC::$server->getUserSession()->getUser(); - if (!$user && $owner) { - $user = \OC::$server->getUserManager()->get($owner); - } - if (!$user) { - throw new \Exception('Inconsistent data, File unshared, but owner not found. Should not happen'); - } - - $uid = ''; - if ($user) { - $uid = $user->getUID(); - } - - $setupManager = \OC::$server->get(SetupManager::class); - if (!$setupManager->isSetupComplete($user)) { - $setupManager->setupForUser($user); - } - - self::$updater = new Update( - new View(), - new Util( - new View(), - \OC::$server->getUserManager(), - \OC::$server->getGroupManager(), - \OC::$server->getConfig()), - Filesystem::getMountManager(), - \OC::$server->getEncryptionManager(), - \OC::$server->get(IFile::class), - \OC::$server->get(LoggerInterface::class), - $uid - ); - } - - return self::$updater; - } -} diff --git a/lib/private/Encryption/Update.php b/lib/private/Encryption/Update.php index 0b27d63c19a2a..293a1ce653ca4 100644 --- a/lib/private/Encryption/Update.php +++ b/lib/private/Encryption/Update.php @@ -1,146 +1,85 @@ view = $view; - $this->util = $util; - $this->mountManager = $mountManager; - $this->encryptionManager = $encryptionManager; - $this->file = $file; - $this->logger = $logger; - $this->uid = $uid; } /** * hook after file was shared - * - * @param array $params */ - public function postShared($params) { - if ($this->encryptionManager->isEnabled()) { - if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') { - $path = Filesystem::getPath($params['fileSource']); - [$owner, $ownerPath] = $this->getOwnerPath($path); - $absPath = '/' . $owner . '/files/' . $ownerPath; - $this->update($absPath); - } - } + public function postShared(OCPFile|Folder $node): void { + $this->update($node); } /** * hook after file was unshared - * - * @param array $params */ - public function postUnshared($params) { - if ($this->encryptionManager->isEnabled()) { - if ($params['itemType'] === 'file' || $params['itemType'] === 'folder') { - $path = Filesystem::getPath($params['fileSource']); - [$owner, $ownerPath] = $this->getOwnerPath($path); - $absPath = '/' . $owner . '/files/' . $ownerPath; - $this->update($absPath); - } - } + public function postUnshared(OCPFile|Folder $node): void { + $this->update($node); } /** * inform encryption module that a file was restored from the trash bin, * e.g. to update the encryption keys - * - * @param array $params */ - public function postRestore($params) { - if ($this->encryptionManager->isEnabled()) { - $path = Filesystem::normalizePath('/' . $this->uid . '/files/' . $params['filePath']); - $this->update($path); - } + public function postRestore(OCPFile|Folder $node): void { + $this->update($node); } /** * inform encryption module that a file was renamed, * e.g. to update the encryption keys - * - * @param array $params */ - public function postRename($params) { - $source = $params['oldpath']; - $target = $params['newpath']; - if ( - $this->encryptionManager->isEnabled() && - dirname($source) !== dirname($target) - ) { - [$owner, $ownerPath] = $this->getOwnerPath($target); - $absPath = '/' . $owner . '/files/' . $ownerPath; - $this->update($absPath); + public function postRename(OCPFile|Folder $source, OCPFile|Folder $target): void { + if (dirname($source->getPath()) !== dirname($target->getPath())) { + $this->update($target); } } /** - * get owner and path relative to data//files + * get owner and path relative to data/ * - * @param string $path path to file for current user - * @return array ['owner' => $owner, 'path' => $path] * @throws \InvalidArgumentException */ - protected function getOwnerPath($path) { - $info = Filesystem::getFileInfo($path); - $owner = Filesystem::getOwner($path); + protected function getOwnerPath(OCPFile|Folder $node): string { + $owner = $node->getOwner()?->getUID(); + if ($owner === null) { + throw new InvalidArgumentException('No owner found for ' . $node->getId()); + } $view = new View('/' . $owner . '/files'); - $path = $view->getPath($info->getId()); - if ($path === null) { - throw new InvalidArgumentException('No file found for ' . $info->getId()); + try { + $path = $view->getPath($node->getId()); + } catch (NotFoundException $e) { + throw new InvalidArgumentException('No file found for ' . $node->getId(), previous:$e); } - - return [$owner, $path]; + return '/' . $owner . '/files/' . $path; } /** @@ -149,7 +88,7 @@ protected function getOwnerPath($path) { * @param string $path relative to data/ * @throws Exceptions\ModuleDoesNotExistsException */ - public function update($path) { + public function update(OCPFile|Folder $node): void { $encryptionModule = $this->encryptionManager->getEncryptionModule(); // if the encryption module doesn't encrypt the files on a per-user basis @@ -158,15 +97,14 @@ public function update($path) { return; } + $path = $this->getOwnerPath($node); // if a folder was shared, get a list of all (sub-)folders - if ($this->view->is_dir($path)) { + if ($node instanceof Folder) { $allFiles = $this->util->getAllFiles($path); } else { $allFiles = [$path]; } - - foreach ($allFiles as $file) { $usersSharing = $this->file->getAccessList($file); try { diff --git a/lib/private/Encryption/Util.php b/lib/private/Encryption/Util.php index 1fb08b15696ec..0566ab9a760cb 100644 --- a/lib/private/Encryption/Util.php +++ b/lib/private/Encryption/Util.php @@ -304,7 +304,7 @@ public function isExcluded($path) { // detect user specific folders if ($this->userManager->userExists($root[1]) - && in_array($root[2], $this->excludedPaths)) { + && in_array($root[2] ?? '', $this->excludedPaths)) { return true; } } diff --git a/lib/private/Files/ObjectStore/ObjectStoreStorage.php b/lib/private/Files/ObjectStore/ObjectStoreStorage.php index e05de7cd91d04..8069215370fa7 100644 --- a/lib/private/Files/ObjectStore/ObjectStoreStorage.php +++ b/lib/private/Files/ObjectStore/ObjectStoreStorage.php @@ -176,13 +176,17 @@ private function rmObjects(ICacheEntry $entry): bool { } public function unlink(string $path): bool { + echo "[DEBUG] unlink($path) " . __FILE__ . ':' . __LINE__ . "\n"; $path = $this->normalizePath($path); + echo "[DEBUG] unlink($path) " . __FILE__ . ':' . __LINE__ . "\n"; $entry = $this->getCache()->get($path); if ($entry instanceof ICacheEntry) { if ($entry->getMimeType() === ICacheEntry::DIRECTORY_MIMETYPE) { + echo "[DEBUG] unlink directory $path " . __FILE__ . ':' . __LINE__ . "\n"; return $this->rmObjects($entry); } else { + echo "[DEBUG] unlink file $path " . __FILE__ . ':' . __LINE__ . "\n"; return $this->rmObject($entry); } } diff --git a/lib/private/Files/Storage/Wrapper/Encryption.php b/lib/private/Files/Storage/Wrapper/Encryption.php index ba23f3c43ecd3..5a84ae3b27635 100644 --- a/lib/private/Files/Storage/Wrapper/Encryption.php +++ b/lib/private/Files/Storage/Wrapper/Encryption.php @@ -8,7 +8,6 @@ namespace OC\Files\Storage\Wrapper; use OC\Encryption\Exceptions\ModuleDoesNotExistsException; -use OC\Encryption\Update; use OC\Encryption\Util; use OC\Files\Cache\CacheEntry; use OC\Files\Filesystem; @@ -49,7 +48,6 @@ public function __construct( private IFile $fileHelper, private ?string $uid, private IStorage $keyStorage, - private Update $update, private Manager $mountManager, private ArrayCache $arrayCache, ) { @@ -64,7 +62,8 @@ public function filesize(string $path): int|float|false { $info = $this->getCache()->get($path); if ($info === false) { - return false; + /* Pass call to wrapped storage, it may be a special file like a part file */ + return $this->storage->filesize($path); } if (isset($this->unencryptedSize[$fullPath])) { $size = $this->unencryptedSize[$fullPath]; @@ -168,8 +167,11 @@ public function file_put_contents(string $path, mixed $data): int|float|false { } public function unlink(string $path): bool { + echo "[DEBUG] unlink($path) " . __FILE__ . ':' . __LINE__ . "\n"; $fullPath = $this->getFullPath($path); + echo "[DEBUG] unlink($fullPath) " . __FILE__ . ':' . __LINE__ . "\n"; if ($this->util->isExcluded($fullPath)) { + echo "[DEBUG] excluded $fullPath " . __FILE__ . ':' . __LINE__ . "\n"; return $this->storage->unlink($path); } @@ -318,7 +320,7 @@ public function fopen(string $path, string $mode) { if (!empty($encryptionModuleId)) { $encryptionModule = $this->encryptionManager->getEncryptionModule($encryptionModuleId); $shouldEncrypt = true; - } elseif (empty($encryptionModuleId) && $info['encrypted'] === true) { + } elseif ($info !== false && $info['encrypted'] === true) { // we come from a old installation. No header and/or no module defined // but the file is encrypted. In this case we need to use the // OC_DEFAULT_MODULE to read the file @@ -521,15 +523,31 @@ public function moveFromStorage( // - remove $this->copyBetweenStorage if (!$sourceStorage->isDeletable($sourceInternalPath)) { + echo "[DEBUG] $sourceInternalPath $targetInternalPath not deletable " . __FILE__ . ':' . __LINE__ . "\n"; return false; } $result = $this->copyBetweenStorage($sourceStorage, $sourceInternalPath, $targetInternalPath, $preserveMtime, true); + echo "[DEBUG] $sourceInternalPath $targetInternalPath copy:$result " . __FILE__ . ':' . __LINE__ . "\n"; if ($result) { - if ($sourceStorage->is_dir($sourceInternalPath)) { - $result = $sourceStorage->rmdir($sourceInternalPath); - } else { - $result = $sourceStorage->unlink($sourceInternalPath); + echo "[DEBUG] $sourceInternalPath $targetInternalPath copy success " . __FILE__ . ':' . __LINE__ . "\n"; + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(true); + } + try { + if ($sourceStorage->is_dir($sourceInternalPath)) { + $result = $sourceStorage->rmdir($sourceInternalPath); + echo "[DEBUG] $sourceInternalPath rmdir:$result " . __FILE__ . ':' . __LINE__ . "\n"; + } else { + $result = $sourceStorage->unlink($sourceInternalPath); + echo "[DEBUG] $sourceInternalPath unlink:$result " . __FILE__ . ':' . __LINE__ . "\n"; + } + } finally { + if ($sourceStorage->instanceOfStorage(ObjectStoreStorage::class)) { + /** @var ObjectStoreStorage $sourceStorage */ + $sourceStorage->setPreserveCacheOnDelete(false); + } } } return $result; @@ -609,10 +627,12 @@ private function copyBetweenStorage( bool $preserveMtime, bool $isRename, ): bool { + echo "[DEBUG] $sourceInternalPath $targetInternalPath $isRename " . __FILE__ . ':' . __LINE__ . "\n"; // for versions we have nothing to do, because versions should always use the // key from the original file. Just create a 1:1 copy and done if ($this->isVersion($targetInternalPath) || $this->isVersion($sourceInternalPath)) { + echo "[DEBUG] $sourceInternalPath $targetInternalPath $isRename " . __FILE__ . ':' . __LINE__ . "\n"; // remember that we try to create a version so that we can detect it during // fopen($sourceInternalPath) and by-pass the encryption in order to // create a 1:1 copy of the file @@ -630,6 +650,7 @@ private function copyBetweenStorage( } $this->updateEncryptedVersion($sourceStorage, $sourceInternalPath, $targetInternalPath, $isRename, true); } + echo "[DEBUG] $result " . __FILE__ . ':' . __LINE__ . "\n"; return $result; } @@ -654,7 +675,7 @@ private function copyBetweenStorage( if (is_resource($dh)) { while ($result && ($file = readdir($dh)) !== false) { if (!Filesystem::isIgnoredDir($file)) { - $result &= $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, false, $isRename); + $result = $this->copyFromStorage($sourceStorage, $sourceInternalPath . '/' . $file, $targetInternalPath . '/' . $file, $preserveMtime, $isRename); } } } diff --git a/lib/private/Files/View.php b/lib/private/Files/View.php index 5e09ce69fb22c..5a5c0ea14dfa9 100644 --- a/lib/private/Files/View.php +++ b/lib/private/Files/View.php @@ -273,7 +273,9 @@ protected function removeUpdate(Storage $storage, string $internalPath): void { } protected function renameUpdate(Storage $sourceStorage, Storage $targetStorage, string $sourceInternalPath, string $targetInternalPath): void { + echo "[DEBUG] renameUpdate($sourceInternalPath, $targetInternalPath) " . __FILE__ . ':' . __LINE__ . "\n"; if ($this->updaterEnabled) { + echo "[DEBUG] renameUpdate($sourceInternalPath, $targetInternalPath)! " . __FILE__ . ':' . __LINE__ . "\n"; $targetStorage->getUpdater()->renameFromStorage($sourceStorage, $sourceInternalPath, $targetInternalPath); } } diff --git a/tests/lib/Encryption/UpdateTest.php b/tests/lib/Encryption/UpdateTest.php index 2627e18601d26..9940549981f32 100644 --- a/tests/lib/Encryption/UpdateTest.php +++ b/tests/lib/Encryption/UpdateTest.php @@ -1,4 +1,7 @@ view = $this->createMock(View::class); $this->util = $this->createMock(Util::class); - $this->mountManager = $this->createMock(Manager::class); $this->encryptionManager = $this->createMock(\OC\Encryption\Manager::class); $this->fileHelper = $this->createMock(File::class); $this->encryptionModule = $this->createMock(IEncryptionModule::class); $this->logger = $this->createMock(LoggerInterface::class); $this->uid = 'testUser1'; + } - $this->update = new Update( - $this->view, - $this->util, - $this->mountManager, - $this->encryptionManager, - $this->fileHelper, - $this->logger, - $this->uid); + private function getUserMock(string $uid): IUser&MockObject { + $user = $this->createMock(IUser::class); + $user->expects(self::any()) + ->method('getUID') + ->willReturn($uid); + return $user; + } + + private function getFileMock(string $path, string $owner): OCPFile&MockObject { + $node = $this->createMock(OCPFile::class); + $node->expects(self::atLeastOnce()) + ->method('getPath') + ->willReturn($path); + $node->expects(self::any()) + ->method('getOwner') + ->willReturn($this->getUserMock($owner)); + + return $node; + } + + private function getFolderMock(string $path, string $owner): Folder&MockObject { + $node = $this->createMock(Folder::class); + $node->expects(self::atLeastOnce()) + ->method('getPath') + ->willReturn($path); + $node->expects(self::any()) + ->method('getOwner') + ->willReturn($this->getUserMock($owner)); + + return $node; } /** @@ -76,18 +85,21 @@ protected function setUp(): void { * @param integer $numberOfFiles */ public function testUpdate($path, $isDir, $allFiles, $numberOfFiles): void { + $updateMock = $this->getUpdateMock(['getOwnerPath']); + $updateMock->expects($this->once())->method('getOwnerPath') + ->willReturnCallback(fn (OCPFile|Folder $node) => '/user/' . $node->getPath()); + $this->encryptionManager->expects($this->once()) ->method('getEncryptionModule') ->willReturn($this->encryptionModule); - $this->view->expects($this->once()) - ->method('is_dir') - ->willReturn($isDir); - if ($isDir) { $this->util->expects($this->once()) ->method('getAllFiles') ->willReturn($allFiles); + $node = $this->getFolderMock($path, 'user'); + } else { + $node = $this->getFileMock($path, 'user'); } $this->fileHelper->expects($this->exactly($numberOfFiles)) @@ -98,7 +110,7 @@ public function testUpdate($path, $isDir, $allFiles, $numberOfFiles): void { ->method('update') ->willReturn(true); - $this->update->update($path); + $updateMock->update($node); } /** @@ -118,32 +130,26 @@ public function dataTestUpdate() { * * @param string $source * @param string $target - * @param boolean $encryptionEnabled */ - public function testPostRename($source, $target, $encryptionEnabled): void { - $updateMock = $this->getUpdateMock(['update', 'getOwnerPath']); + public function testPostRename($source, $target): void { + $updateMock = $this->getUpdateMock(['update','getOwnerPath']); - $this->encryptionManager->expects($this->once()) - ->method('isEnabled') - ->willReturn($encryptionEnabled); + $sourceNode = $this->getFileMock($source, 'user'); + $targetNode = $this->getFileMock($target, 'user'); - if (dirname($source) === dirname($target) || $encryptionEnabled === false) { + if (dirname($source) === dirname($target)) { $updateMock->expects($this->never())->method('getOwnerPath'); $updateMock->expects($this->never())->method('update'); } else { - $updateMock->expects($this->once()) - ->method('getOwnerPath') - ->willReturnCallback(function ($path) use ($target) { - $this->assertSame( - $target, - $path, - 'update needs to be executed for the target destination'); - return ['owner', $path]; - }); - $updateMock->expects($this->once())->method('update'); + $updateMock->expects($this->once())->method('update') + ->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame( + $target, + $node->getPath(), + 'update needs to be executed for the target destination' + )); } - $updateMock->postRename(['oldpath' => $source, 'newpath' => $target]); + $updateMock->postRename($sourceNode, $targetNode); } /** @@ -153,62 +159,35 @@ public function testPostRename($source, $target, $encryptionEnabled): void { */ public function dataTestPostRename() { return [ - ['/test.txt', '/testNew.txt', true], - ['/test.txt', '/testNew.txt', false], - ['/folder/test.txt', '/testNew.txt', true], - ['/folder/test.txt', '/testNew.txt', false], - ['/folder/test.txt', '/testNew.txt', true], - ['/test.txt', '/folder/testNew.txt', false], + ['/test.txt', '/testNew.txt'], + ['/folder/test.txt', '/testNew.txt'], + ['/test.txt', '/folder/testNew.txt'], ]; } - - /** - * @dataProvider dataTestPostRestore - * - * @param boolean $encryptionEnabled - */ - public function testPostRestore($encryptionEnabled): void { + public function testPostRestore(): void { $updateMock = $this->getUpdateMock(['update']); - $this->encryptionManager->expects($this->once()) - ->method('isEnabled') - ->willReturn($encryptionEnabled); + $updateMock->expects($this->once())->method('update') + ->willReturnCallback(fn (OCPFile|Folder $node) => $this->assertSame( + '/folder/test.txt', + $node->getPath(), + 'update needs to be executed for the target destination' + )); - if ($encryptionEnabled) { - $updateMock->expects($this->once())->method('update'); - } else { - $updateMock->expects($this->never())->method('update'); - } - - $updateMock->postRestore(['filePath' => '/folder/test.txt']); - } - - /** - * test data for testPostRestore() - * - * @return array - */ - public function dataTestPostRestore() { - return [ - [true], - [false], - ]; + $updateMock->postRestore($this->getFileMock('/folder/test.txt', 'user')); } /** * create mock of the update method * * @param array $methods methods which should be set - * @return \OC\Encryption\Update | \PHPUnit\Framework\MockObject\MockObject */ - protected function getUpdateMock($methods) { - return $this->getMockBuilder('\OC\Encryption\Update') + protected function getUpdateMock(array $methods): Update&MockObject { + return $this->getMockBuilder(Update::class) ->setConstructorArgs( [ - $this->view, $this->util, - $this->mountManager, $this->encryptionManager, $this->fileHelper, $this->logger, diff --git a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php index bb3df36dec2bf..e8ad557dae550 100644 --- a/tests/lib/Files/Storage/Wrapper/EncryptionTest.php +++ b/tests/lib/Files/Storage/Wrapper/EncryptionTest.php @@ -11,7 +11,6 @@ use OC; use OC\Encryption\Exceptions\ModuleDoesNotExistsException; use OC\Encryption\File; -use OC\Encryption\Update; use OC\Encryption\Util; use OC\Files\Cache\Cache; use OC\Files\Cache\CacheEntry; @@ -46,7 +45,6 @@ class EncryptionTest extends Storage { private Util&MockObject $util; private \OC\Encryption\Manager&MockObject $encryptionManager; private IEncryptionModule&MockObject $encryptionModule; - private Update&MockObject $update; private Cache&MockObject $cache; private LoggerInterface&MockObject $logger; private File&MockObject $file; @@ -111,9 +109,6 @@ protected function setUp(): void { $this->keyStore = $this->getMockBuilder('\OC\Encryption\Keys\Storage') ->disableOriginalConstructor()->getMock(); - $this->update = $this->getMockBuilder('\OC\Encryption\Update') - ->disableOriginalConstructor()->getMock(); - $this->mount = $this->getMockBuilder('\OC\Files\Mount\MountPoint') ->disableOriginalConstructor() ->setMethods(['getOption']) @@ -155,7 +150,6 @@ protected function setUp(): void { $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache ] @@ -237,7 +231,6 @@ function ($path) use ($encrypted) { $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache, ] @@ -316,7 +309,6 @@ public function testFilesize(): void { $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache, ] @@ -361,7 +353,6 @@ public function testVerifyUnencryptedSize($encryptedSize, $unencryptedSize, $fai $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache, ] @@ -491,7 +482,6 @@ public function testRmdir($path, $rmdirResult, $isExcluded, $encryptionEnabled): $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache, ); @@ -598,7 +588,6 @@ public function testGetHeader($path, $strippedPathExists, $strippedPath): void { $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache, ] @@ -692,7 +681,6 @@ public function testGetHeaderAddLegacyModule($header, $isEncrypted, $strippedPat $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache, ] @@ -867,7 +855,6 @@ public function testCopyBetweenStorageVersions($sourceInternalPath, $targetInter $this->file, null, $this->keyStore, - $this->update, $this->mountManager, $this->arrayCache ] @@ -968,7 +955,6 @@ public function testShouldEncrypt( $util = $this->createMock(Util::class); $fileHelper = $this->createMock(IFile::class); $keyStorage = $this->createMock(IStorage::class); - $update = $this->createMock(Update::class); $mountManager = $this->createMock(\OC\Files\Mount\Manager::class); $mount = $this->createMock(IMountPoint::class); $arrayCache = $this->createMock(ArrayCache::class); @@ -986,7 +972,6 @@ public function testShouldEncrypt( $fileHelper, null, $keyStorage, - $update, $mountManager, $arrayCache ] diff --git a/tests/lib/Files/ViewTest.php b/tests/lib/Files/ViewTest.php index a6a7722f363bb..350858f630a78 100644 --- a/tests/lib/Files/ViewTest.php +++ b/tests/lib/Files/ViewTest.php @@ -25,6 +25,7 @@ use OCP\Files\GenericFileException; use OCP\Files\Mount\IMountManager; use OCP\Files\Storage\IStorage; +use OCP\Files\Storage\IStorageFactory; use OCP\IDBConnection; use OCP\Lock\ILockingProvider; use OCP\Lock\LockedException; @@ -506,10 +507,10 @@ public function testMoveBetweenStorageCrossNonLocal(): void { } public function moveBetweenStorages($storage1, $storage2) { - Filesystem::mount($storage1, [], '/'); - Filesystem::mount($storage2, [], '/substorage'); + Filesystem::mount($storage1, [], '/' . $this->user . '/'); + Filesystem::mount($storage2, [], '/' . $this->user . '/substorage'); - $rootView = new View(''); + $rootView = new View('/' . $this->user); $rootView->rename('foo.txt', 'substorage/folder/foo.txt'); $this->assertFalse($rootView->file_exists('foo.txt')); $this->assertTrue($rootView->file_exists('substorage/folder/foo.txt')); @@ -929,14 +930,16 @@ public function testPartFileInfo(): void { $storage = new Temporary([]); $scanner = $storage->getScanner(); Filesystem::mount($storage, [], '/test/'); - $storage->file_put_contents('test.part', 'foobar'); + $sizeWritten = $storage->file_put_contents('test.part', 'foobar'); $scanner->scan(''); $view = new View('/test'); $info = $view->getFileInfo('test.part'); $this->assertInstanceOf('\OCP\Files\FileInfo', $info); $this->assertNull($info->getId()); + $this->assertEquals(6, $sizeWritten); $this->assertEquals(6, $info->getSize()); + $this->assertEquals('foobar', $view->file_get_contents('test.part')); } public function absolutePathProvider() { @@ -1935,6 +1938,8 @@ public function testLockBasicOperation( /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */ Server::get(ITrashManager::class)->pauseTrash(); + /* Same thing with encryption wrapper */ + Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); Filesystem::mount($storage, [], $this->user . '/'); @@ -2085,6 +2090,8 @@ public function testLockBasicOperationUnlocksAfterException( /* Pause trash to avoid the trashbin intercepting rmdir and unlink calls */ Server::get(ITrashManager::class)->pauseTrash(); + /* Same thing with encryption wrapper */ + Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); Filesystem::mount($storage, [], $this->user . '/'); @@ -2226,6 +2233,9 @@ public function testLockFileRename($operation, $expectedLockTypeSourceDuring): v $sourcePath = 'original.txt'; $targetPath = 'target.txt'; + /* Disable encryption wrapper to avoid it intercepting mocked call */ + Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); + Filesystem::mount($storage, [], $this->user . '/'); $storage->mkdir('files'); $view->file_put_contents($sourcePath, 'meh'); @@ -2279,6 +2289,9 @@ public function testLockFileCopyException(): void { $sourcePath = 'original.txt'; $targetPath = 'target.txt'; + /* Disable encryption wrapper to avoid it intercepting mocked call */ + Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); + Filesystem::mount($storage, [], $this->user . '/'); $storage->mkdir('files'); $view->file_put_contents($sourcePath, 'meh'); @@ -2415,6 +2428,9 @@ public function testLockFileRenameCrossStorage($viewOperation, $storageOperation $sourcePath = 'original.txt'; $targetPath = 'substorage/target.txt'; + /* Disable encryption wrapper to avoid it intercepting mocked call */ + Server::get(IStorageFactory::class)->removeStorageWrapper('oc_encryption'); + Filesystem::mount($storage, [], $this->user . '/'); Filesystem::mount($storage2, [], $this->user . '/files/substorage'); $storage->mkdir('files');