aboutsummaryrefslogtreecommitdiffstats
path: root/apps/files_external/lib
diff options
context:
space:
mode:
Diffstat (limited to 'apps/files_external/lib')
-rw-r--r--apps/files_external/lib/AppInfo/Application.php156
-rw-r--r--apps/files_external/lib/BackgroundJob/CredentialsCleanup.php47
-rw-r--r--apps/files_external/lib/Command/Applicable.php124
-rw-r--r--apps/files_external/lib/Command/Backends.php104
-rw-r--r--apps/files_external/lib/Command/Config.php96
-rw-r--r--apps/files_external/lib/Command/Create.php184
-rw-r--r--apps/files_external/lib/Command/Delete.php82
-rw-r--r--apps/files_external/lib/Command/Dependencies.php57
-rw-r--r--apps/files_external/lib/Command/Export.php44
-rw-r--r--apps/files_external/lib/Command/Import.php179
-rw-r--r--apps/files_external/lib/Command/ListCommand.php239
-rw-r--r--apps/files_external/lib/Command/Notify.php253
-rw-r--r--apps/files_external/lib/Command/Option.php57
-rw-r--r--apps/files_external/lib/Command/Scan.php166
-rw-r--r--apps/files_external/lib/Command/StorageAuthBase.php114
-rw-r--r--apps/files_external/lib/Command/Verify.php121
-rw-r--r--apps/files_external/lib/Config/ConfigAdapter.php171
-rw-r--r--apps/files_external/lib/Config/ExternalMountPoint.php34
-rw-r--r--apps/files_external/lib/Config/IConfigHandler.php22
-rw-r--r--apps/files_external/lib/Config/SimpleSubstitutionTrait.php69
-rw-r--r--apps/files_external/lib/Config/SystemMountPoint.php15
-rw-r--r--apps/files_external/lib/Config/UserContext.php61
-rw-r--r--apps/files_external/lib/Config/UserPlaceholderHandler.php25
-rw-r--r--apps/files_external/lib/ConfigLexicon.php41
-rw-r--r--apps/files_external/lib/Controller/AjaxController.php107
-rw-r--r--apps/files_external/lib/Controller/ApiController.php101
-rw-r--r--apps/files_external/lib/Controller/GlobalStoragesController.php189
-rw-r--r--apps/files_external/lib/Controller/StoragesController.php317
-rw-r--r--apps/files_external/lib/Controller/UserGlobalStoragesController.php193
-rw-r--r--apps/files_external/lib/Controller/UserStoragesController.php214
-rw-r--r--apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php31
-rw-r--r--apps/files_external/lib/Lib/Auth/AuthMechanism.php (renamed from apps/files_external/lib/auth/authmechanism.php)61
-rw-r--r--apps/files_external/lib/Lib/Auth/Builtin.php23
-rw-r--r--apps/files_external/lib/Lib/Auth/IUserProvided.php21
-rw-r--r--apps/files_external/lib/Lib/Auth/InvalidAuth.php29
-rw-r--r--apps/files_external/lib/Lib/Auth/NullMechanism.php23
-rw-r--r--apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php37
-rw-r--r--apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php32
-rw-r--r--apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php34
-rw-r--r--apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php30
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php80
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php113
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/Password.php29
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php67
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php73
-rw-r--r--apps/files_external/lib/Lib/Auth/Password/UserProvided.php77
-rw-r--r--apps/files_external/lib/Lib/Auth/PublicKey/RSA.php75
-rw-r--r--apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php54
-rw-r--r--apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php35
-rw-r--r--apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php19
-rw-r--r--apps/files_external/lib/Lib/Backend/AmazonS3.php54
-rw-r--r--apps/files_external/lib/Lib/Backend/Backend.php (renamed from apps/files_external/lib/backend/backend.php)66
-rw-r--r--apps/files_external/lib/Lib/Backend/DAV.php37
-rw-r--r--apps/files_external/lib/Lib/Backend/FTP.php39
-rw-r--r--apps/files_external/lib/Lib/Backend/InvalidBackend.php50
-rw-r--r--apps/files_external/lib/Lib/Backend/LegacyBackend.php (renamed from apps/files_external/lib/backend/legacybackend.php)57
-rw-r--r--apps/files_external/lib/Lib/Backend/Local.php38
-rw-r--r--apps/files_external/lib/Lib/Backend/OwnCloud.php34
-rw-r--r--apps/files_external/lib/Lib/Backend/SFTP.php34
-rw-r--r--apps/files_external/lib/Lib/Backend/SFTP_Key.php31
-rw-r--r--apps/files_external/lib/Lib/Backend/SMB.php140
-rw-r--r--apps/files_external/lib/Lib/Backend/SMB_OC.php57
-rw-r--r--apps/files_external/lib/Lib/Backend/Swift.php43
-rw-r--r--apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php23
-rw-r--r--apps/files_external/lib/Lib/Config/IBackendProvider.php23
-rw-r--r--apps/files_external/lib/Lib/DefinitionParameter.php217
-rw-r--r--apps/files_external/lib/Lib/DependencyTrait.php23
-rw-r--r--apps/files_external/lib/Lib/FrontendDefinitionTrait.php107
-rw-r--r--apps/files_external/lib/Lib/IFrontendDefinition.php43
-rw-r--r--apps/files_external/lib/Lib/IIdentifier.php14
-rw-r--r--apps/files_external/lib/Lib/IdentifierTrait.php63
-rw-r--r--apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php27
-rw-r--r--apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php (renamed from apps/files_external/lib/legacydependencycheckpolyfill.php)30
-rw-r--r--apps/files_external/lib/Lib/MissingDependency.php51
-rw-r--r--apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php130
-rw-r--r--apps/files_external/lib/Lib/PersonalMount.php70
-rw-r--r--apps/files_external/lib/Lib/PriorityTrait.php35
-rw-r--r--apps/files_external/lib/Lib/SessionStorageWrapper.php25
-rw-r--r--apps/files_external/lib/Lib/Storage/AmazonS3.php760
-rw-r--r--apps/files_external/lib/Lib/Storage/FTP.php364
-rw-r--r--apps/files_external/lib/Lib/Storage/FtpConnection.php222
-rw-r--r--apps/files_external/lib/Lib/Storage/OwnCloud.php63
-rw-r--r--apps/files_external/lib/Lib/Storage/SFTP.php523
-rw-r--r--apps/files_external/lib/Lib/Storage/SFTPReadStream.php217
-rw-r--r--apps/files_external/lib/Lib/Storage/SFTPWriteStream.php165
-rw-r--r--apps/files_external/lib/Lib/Storage/SMB.php727
-rw-r--r--apps/files_external/lib/Lib/Storage/StreamWrapper.php99
-rw-r--r--apps/files_external/lib/Lib/Storage/Swift.php593
-rw-r--r--apps/files_external/lib/Lib/Storage/SystemBridge.php27
-rw-r--r--apps/files_external/lib/Lib/StorageConfig.php (renamed from apps/files_external/lib/storageconfig.php)110
-rw-r--r--apps/files_external/lib/Lib/StorageModifierTrait.php50
-rw-r--r--apps/files_external/lib/Lib/VisibilityTrait.php (renamed from apps/files_external/lib/visibilitytrait.php)25
-rw-r--r--apps/files_external/lib/Listener/GroupDeletedListener.php29
-rw-r--r--apps/files_external/lib/Listener/LoadAdditionalListener.php41
-rw-r--r--apps/files_external/lib/Listener/StorePasswordListener.php57
-rw-r--r--apps/files_external/lib/Listener/UserDeletedListener.php29
-rw-r--r--apps/files_external/lib/Migration/DummyUserSession.php57
-rw-r--r--apps/files_external/lib/Migration/Version1011Date20200630192246.php136
-rw-r--r--apps/files_external/lib/Migration/Version1015Date20211104103506.php90
-rw-r--r--apps/files_external/lib/Migration/Version1016Date20220324154536.php38
-rw-r--r--apps/files_external/lib/Migration/Version22000Date20210216084416.php47
-rw-r--r--apps/files_external/lib/MountConfig.php250
-rw-r--r--apps/files_external/lib/NotFoundException.php14
-rw-r--r--apps/files_external/lib/ResponseDefinitions.php42
-rw-r--r--apps/files_external/lib/Service/BackendService.php342
-rw-r--r--apps/files_external/lib/Service/DBConfigService.php501
-rw-r--r--apps/files_external/lib/Service/GlobalStoragesService.php165
-rw-r--r--apps/files_external/lib/Service/ImportLegacyStoragesService.php31
-rw-r--r--apps/files_external/lib/Service/LegacyStoragesService.php193
-rw-r--r--apps/files_external/lib/Service/StoragesService.php476
-rw-r--r--apps/files_external/lib/Service/UserGlobalStoragesService.php187
-rw-r--r--apps/files_external/lib/Service/UserStoragesService.php134
-rw-r--r--apps/files_external/lib/Service/UserTrait.php59
-rw-r--r--apps/files_external/lib/Settings/Admin.php63
-rw-r--r--apps/files_external/lib/Settings/Personal.php67
-rw-r--r--apps/files_external/lib/Settings/PersonalSection.php25
-rw-r--r--apps/files_external/lib/Settings/Section.php61
-rw-r--r--apps/files_external/lib/amazons3.php634
-rw-r--r--apps/files_external/lib/api.php87
-rw-r--r--apps/files_external/lib/auth/amazons3/accesskey.php47
-rw-r--r--apps/files_external/lib/auth/builtin.php41
-rw-r--r--apps/files_external/lib/auth/iuserprovided.php36
-rw-r--r--apps/files_external/lib/auth/nullmechanism.php41
-rw-r--r--apps/files_external/lib/auth/oauth1/oauth1.php53
-rw-r--r--apps/files_external/lib/auth/oauth2/oauth2.php51
-rw-r--r--apps/files_external/lib/auth/openstack/openstack.php48
-rw-r--r--apps/files_external/lib/auth/openstack/rackspace.php46
-rw-r--r--apps/files_external/lib/auth/password/password.php45
-rw-r--r--apps/files_external/lib/auth/password/sessioncredentials.php86
-rw-r--r--apps/files_external/lib/auth/publickey/rsa.php81
-rw-r--r--apps/files_external/lib/backend/amazons3.php61
-rw-r--r--apps/files_external/lib/backend/dav.php55
-rw-r--r--apps/files_external/lib/backend/dropbox.php51
-rw-r--r--apps/files_external/lib/backend/ftp.php55
-rw-r--r--apps/files_external/lib/backend/google.php51
-rw-r--r--apps/files_external/lib/backend/local.php49
-rw-r--r--apps/files_external/lib/backend/owncloud.php52
-rw-r--r--apps/files_external/lib/backend/sftp.php51
-rw-r--r--apps/files_external/lib/backend/sftp_key.php50
-rw-r--r--apps/files_external/lib/backend/smb.php69
-rw-r--r--apps/files_external/lib/backend/smb_oc.php72
-rw-r--r--apps/files_external/lib/backend/swift.php62
-rw-r--r--apps/files_external/lib/config.php413
-rw-r--r--apps/files_external/lib/config/configadapter.php183
-rw-r--r--apps/files_external/lib/definitionparameter.php193
-rw-r--r--apps/files_external/lib/dependencytrait.php41
-rw-r--r--apps/files_external/lib/dropbox.php352
-rw-r--r--apps/files_external/lib/frontenddefinitiontrait.php150
-rw-r--r--apps/files_external/lib/ftp.php155
-rw-r--r--apps/files_external/lib/google.php710
-rw-r--r--apps/files_external/lib/identifiertrait.php102
-rw-r--r--apps/files_external/lib/insufficientdataformeaningfulanswerexception.php42
-rw-r--r--apps/files_external/lib/missingdependency.php64
-rw-r--r--apps/files_external/lib/notfoundexception.php28
-rw-r--r--apps/files_external/lib/owncloud.php74
-rw-r--r--apps/files_external/lib/personalmount.php88
-rw-r--r--apps/files_external/lib/prioritytrait.php60
-rw-r--r--apps/files_external/lib/sessionstoragewrapper.php43
-rw-r--r--apps/files_external/lib/sftp.php467
-rw-r--r--apps/files_external/lib/smb.php396
-rw-r--r--apps/files_external/lib/storagemodifiertrait.php69
-rw-r--r--apps/files_external/lib/streamwrapper.php128
-rw-r--r--apps/files_external/lib/swift.php599
163 files changed, 12950 insertions, 6546 deletions
diff --git a/apps/files_external/lib/AppInfo/Application.php b/apps/files_external/lib/AppInfo/Application.php
new file mode 100644
index 00000000000..a6c2aff947b
--- /dev/null
+++ b/apps/files_external/lib/AppInfo/Application.php
@@ -0,0 +1,156 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\AppInfo;
+
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\Files_External\Config\ConfigAdapter;
+use OCA\Files_External\Config\UserPlaceholderHandler;
+use OCA\Files_External\ConfigLexicon;
+use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey;
+use OCA\Files_External\Lib\Auth\Builtin;
+use OCA\Files_External\Lib\Auth\NullMechanism;
+use OCA\Files_External\Lib\Auth\OAuth2\OAuth2;
+use OCA\Files_External\Lib\Auth\OpenStack\OpenStackV2;
+use OCA\Files_External\Lib\Auth\OpenStack\OpenStackV3;
+use OCA\Files_External\Lib\Auth\OpenStack\Rackspace;
+use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
+use OCA\Files_External\Lib\Auth\Password\LoginCredentials;
+use OCA\Files_External\Lib\Auth\Password\Password;
+use OCA\Files_External\Lib\Auth\Password\SessionCredentials;
+use OCA\Files_External\Lib\Auth\Password\UserGlobalAuth;
+use OCA\Files_External\Lib\Auth\Password\UserProvided;
+use OCA\Files_External\Lib\Auth\PublicKey\RSA;
+use OCA\Files_External\Lib\Auth\PublicKey\RSAPrivateKey;
+use OCA\Files_External\Lib\Auth\SMB\KerberosApacheAuth;
+use OCA\Files_External\Lib\Auth\SMB\KerberosAuth;
+use OCA\Files_External\Lib\Backend\AmazonS3;
+use OCA\Files_External\Lib\Backend\DAV;
+use OCA\Files_External\Lib\Backend\FTP;
+use OCA\Files_External\Lib\Backend\Local;
+use OCA\Files_External\Lib\Backend\OwnCloud;
+use OCA\Files_External\Lib\Backend\SFTP;
+use OCA\Files_External\Lib\Backend\SFTP_Key;
+use OCA\Files_External\Lib\Backend\SMB;
+use OCA\Files_External\Lib\Backend\SMB_OC;
+use OCA\Files_External\Lib\Backend\Swift;
+use OCA\Files_External\Lib\Config\IAuthMechanismProvider;
+use OCA\Files_External\Lib\Config\IBackendProvider;
+use OCA\Files_External\Listener\GroupDeletedListener;
+use OCA\Files_External\Listener\LoadAdditionalListener;
+use OCA\Files_External\Listener\UserDeletedListener;
+use OCA\Files_External\Service\BackendService;
+use OCP\AppFramework\App;
+use OCP\AppFramework\Bootstrap\IBootContext;
+use OCP\AppFramework\Bootstrap\IBootstrap;
+use OCP\AppFramework\Bootstrap\IRegistrationContext;
+use OCP\AppFramework\QueryException;
+use OCP\Files\Config\IMountProviderCollection;
+use OCP\Group\Events\GroupDeletedEvent;
+use OCP\User\Events\UserDeletedEvent;
+
+/**
+ * @package OCA\Files_External\AppInfo
+ */
+class Application extends App implements IBackendProvider, IAuthMechanismProvider, IBootstrap {
+ public const APP_ID = 'files_external';
+
+ /**
+ * Application constructor.
+ *
+ * @throws QueryException
+ */
+ public function __construct(array $urlParams = []) {
+ parent::__construct(self::APP_ID, $urlParams);
+ }
+
+ public function register(IRegistrationContext $context): void {
+ $context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
+ $context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class);
+ $context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
+ $context->registerConfigLexicon(ConfigLexicon::class);
+ }
+
+ public function boot(IBootContext $context): void {
+ $context->injectFn(function (IMountProviderCollection $mountProviderCollection, ConfigAdapter $configAdapter): void {
+ $mountProviderCollection->registerProvider($configAdapter);
+ });
+ $context->injectFn(function (BackendService $backendService, UserPlaceholderHandler $userConfigHandler): void {
+ $backendService->registerBackendProvider($this);
+ $backendService->registerAuthMechanismProvider($this);
+ $backendService->registerConfigHandler('user', function () use ($userConfigHandler) {
+ return $userConfigHandler;
+ });
+ });
+
+ // force-load auth mechanisms since some will register hooks
+ // TODO: obsolete these and use the TokenProvider to get the user's password from the session
+ $this->getAuthMechanisms();
+ }
+
+ /**
+ * @{inheritdoc}
+ */
+ public function getBackends() {
+ $container = $this->getContainer();
+
+ $backends = [
+ $container->get(Local::class),
+ $container->get(FTP::class),
+ $container->get(DAV::class),
+ $container->get(OwnCloud::class),
+ $container->get(SFTP::class),
+ $container->get(AmazonS3::class),
+ $container->get(Swift::class),
+ $container->get(SFTP_Key::class),
+ $container->get(SMB::class),
+ $container->get(SMB_OC::class),
+ ];
+
+ return $backends;
+ }
+
+ /**
+ * @{inheritdoc}
+ */
+ public function getAuthMechanisms() {
+ $container = $this->getContainer();
+
+ return [
+ // AuthMechanism::SCHEME_NULL mechanism
+ $container->get(NullMechanism::class),
+
+ // AuthMechanism::SCHEME_BUILTIN mechanism
+ $container->get(Builtin::class),
+
+ // AuthMechanism::SCHEME_PASSWORD mechanisms
+ $container->get(Password::class),
+ $container->get(SessionCredentials::class),
+ $container->get(LoginCredentials::class),
+ $container->get(UserProvided::class),
+ $container->get(GlobalAuth::class),
+ $container->get(UserGlobalAuth::class),
+
+ // AuthMechanism::SCHEME_OAUTH2 mechanisms
+ $container->get(OAuth2::class),
+
+ // AuthMechanism::SCHEME_PUBLICKEY mechanisms
+ $container->get(RSA::class),
+ $container->get(RSAPrivateKey::class),
+
+ // AuthMechanism::SCHEME_OPENSTACK mechanisms
+ $container->get(OpenStackV2::class),
+ $container->get(OpenStackV3::class),
+ $container->get(Rackspace::class),
+
+ // Specialized mechanisms
+ $container->get(AccessKey::class),
+ $container->get(KerberosAuth::class),
+ $container->get(KerberosApacheAuth::class),
+ ];
+ }
+}
diff --git a/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php b/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php
new file mode 100644
index 00000000000..90a5ae17ab2
--- /dev/null
+++ b/apps/files_external/lib/BackgroundJob/CredentialsCleanup.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\BackgroundJob;
+
+use OCA\Files_External\Lib\Auth\Password\LoginCredentials;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCP\AppFramework\Utility\ITimeFactory;
+use OCP\BackgroundJob\TimedJob;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\Security\ICredentialsManager;
+
+class CredentialsCleanup extends TimedJob {
+ public function __construct(
+ ITimeFactory $time,
+ private ICredentialsManager $credentialsManager,
+ private UserGlobalStoragesService $userGlobalStoragesService,
+ private IUserManager $userManager,
+ ) {
+ parent::__construct($time);
+
+ // run every day
+ $this->setInterval(24 * 60 * 60);
+ $this->setTimeSensitivity(self::TIME_INSENSITIVE);
+ }
+
+ protected function run($argument) {
+ $this->userManager->callForSeenUsers(function (IUser $user): void {
+ $storages = $this->userGlobalStoragesService->getAllStoragesForUser($user);
+
+ $usesLoginCredentials = array_reduce($storages, function (bool $uses, StorageConfig $storage) {
+ return $uses || $storage->getAuthMechanism() instanceof LoginCredentials;
+ }, false);
+
+ if (!$usesLoginCredentials) {
+ $this->credentialsManager->delete($user->getUID(), LoginCredentials::CREDENTIALS_IDENTIFIER);
+ }
+ });
+ }
+}
diff --git a/apps/files_external/lib/Command/Applicable.php b/apps/files_external/lib/Command/Applicable.php
new file mode 100644
index 00000000000..4d5e264bfaf
--- /dev/null
+++ b/apps/files_external/lib/Command/Applicable.php
@@ -0,0 +1,124 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\AppFramework\Http;
+use OCP\IGroupManager;
+use OCP\IUserManager;
+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 Applicable extends Base {
+ public function __construct(
+ protected GlobalStoragesService $globalService,
+ private IUserManager $userManager,
+ private IGroupManager $groupManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:applicable')
+ ->setDescription('Manage applicable users and groups for a mount')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'The id of the mount to edit'
+ )->addOption(
+ 'add-user',
+ '',
+ InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+ 'user to add as applicable'
+ )->addOption(
+ 'remove-user',
+ '',
+ InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+ 'user to remove as applicable'
+ )->addOption(
+ 'add-group',
+ '',
+ InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+ 'group to add as applicable'
+ )->addOption(
+ 'remove-group',
+ '',
+ InputOption::VALUE_IS_ARRAY | InputOption::VALUE_REQUIRED,
+ 'group to remove as applicable'
+ )->addOption(
+ 'remove-all',
+ '',
+ InputOption::VALUE_NONE,
+ 'Set the mount to be globally applicable'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $mountId = $input->getArgument('mount_id');
+ try {
+ $mount = $this->globalService->getStorage($mountId);
+ } catch (NotFoundException $e) {
+ $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+
+ if ($mount->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) {
+ $output->writeln('<error>Can\'t change applicables on personal mounts</error>');
+ return self::FAILURE;
+ }
+
+ $addUsers = $input->getOption('add-user');
+ $removeUsers = $input->getOption('remove-user');
+ $addGroups = $input->getOption('add-group');
+ $removeGroups = $input->getOption('remove-group');
+
+ $applicableUsers = $mount->getApplicableUsers();
+ $applicableGroups = $mount->getApplicableGroups();
+
+ if ((count($addUsers) + count($removeUsers) + count($addGroups) + count($removeGroups) > 0) || $input->getOption('remove-all')) {
+ foreach ($addUsers as $addUser) {
+ if (!$this->userManager->userExists($addUser)) {
+ $output->writeln('<error>User "' . $addUser . '" not found</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+ }
+ foreach ($addGroups as $addGroup) {
+ if (!$this->groupManager->groupExists($addGroup)) {
+ $output->writeln('<error>Group "' . $addGroup . '" not found</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+ }
+
+ if ($input->getOption('remove-all')) {
+ $applicableUsers = [];
+ $applicableGroups = [];
+ } else {
+ $applicableUsers = array_unique(array_merge($applicableUsers, $addUsers));
+ $applicableUsers = array_values(array_diff($applicableUsers, $removeUsers));
+ $applicableGroups = array_unique(array_merge($applicableGroups, $addGroups));
+ $applicableGroups = array_values(array_diff($applicableGroups, $removeGroups));
+ }
+ $mount->setApplicableUsers($applicableUsers);
+ $mount->setApplicableGroups($applicableGroups);
+ $this->globalService->updateStorage($mount);
+ }
+
+ $this->writeArrayInOutputFormat($input, $output, [
+ 'users' => $applicableUsers,
+ 'groups' => $applicableGroups
+ ]);
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files_external/lib/Command/Backends.php b/apps/files_external/lib/Command/Backends.php
new file mode 100644
index 00000000000..7fab0477adf
--- /dev/null
+++ b/apps/files_external/lib/Command/Backends.php
@@ -0,0 +1,104 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Service\BackendService;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Backends extends Base {
+ public function __construct(
+ private BackendService $backendService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:backends')
+ ->setDescription('Show available authentication and storage backends')
+ ->addArgument(
+ 'type',
+ InputArgument::OPTIONAL,
+ 'only show backends of a certain type. Possible values are "authentication" or "storage"'
+ )->addArgument(
+ 'backend',
+ InputArgument::OPTIONAL,
+ 'only show information of a specific backend'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $authBackends = $this->backendService->getAuthMechanisms();
+ $storageBackends = $this->backendService->getBackends();
+
+ $data = [
+ 'authentication' => array_map([$this, 'serializeAuthBackend'], $authBackends),
+ 'storage' => array_map([$this, 'serializeAuthBackend'], $storageBackends)
+ ];
+
+ $type = $input->getArgument('type');
+ $backend = $input->getArgument('backend');
+ if ($type) {
+ if (!isset($data[$type])) {
+ $output->writeln('<error>Invalid type "' . $type . '". Possible values are "authentication" or "storage"</error>');
+ return self::FAILURE;
+ }
+ $data = $data[$type];
+
+ if ($backend) {
+ if (!isset($data[$backend])) {
+ $output->writeln('<error>Unknown backend "' . $backend . '" of type "' . $type . '"</error>');
+ return self::FAILURE;
+ }
+ $data = $data[$backend];
+ }
+ }
+
+ $this->writeArrayInOutputFormat($input, $output, $data);
+ return self::SUCCESS;
+ }
+
+ private function serializeAuthBackend(\JsonSerializable $backend): array {
+ $data = $backend->jsonSerialize();
+ $result = [
+ 'name' => $data['name'],
+ 'identifier' => $data['identifier'],
+ 'configuration' => $this->formatConfiguration($data['configuration'])
+ ];
+ if ($backend instanceof Backend) {
+ $result['storage_class'] = $backend->getStorageClass();
+ $authBackends = $this->backendService->getAuthMechanismsByScheme(array_keys($backend->getAuthSchemes()));
+ $result['supported_authentication_backends'] = array_keys($authBackends);
+ $authConfig = array_map(function (AuthMechanism $auth) {
+ return $this->serializeAuthBackend($auth)['configuration'];
+ }, $authBackends);
+ $result['authentication_configuration'] = array_combine(array_keys($authBackends), $authConfig);
+ }
+ return $result;
+ }
+
+ /**
+ * @param DefinitionParameter[] $parameters
+ * @return string[]
+ */
+ private function formatConfiguration(array $parameters): array {
+ $configuration = array_filter($parameters, function (DefinitionParameter $parameter) {
+ return $parameter->isFlagSet(DefinitionParameter::FLAG_HIDDEN);
+ });
+ return array_map(function (DefinitionParameter $parameter) {
+ return $parameter->getTypeName();
+ }, $configuration);
+ }
+}
diff --git a/apps/files_external/lib/Command/Config.php b/apps/files_external/lib/Command/Config.php
new file mode 100644
index 00000000000..883b4a2f3e7
--- /dev/null
+++ b/apps/files_external/lib/Command/Config.php
@@ -0,0 +1,96 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\AppFramework\Http;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Config extends Base {
+ public function __construct(
+ protected GlobalStoragesService $globalService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:config')
+ ->setDescription('Manage backend configuration for a mount')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'The id of the mount to edit'
+ )->addArgument(
+ 'key',
+ InputArgument::REQUIRED,
+ 'key of the config option to set/get'
+ )->addArgument(
+ 'value',
+ InputArgument::OPTIONAL,
+ 'value to set the config option to, when no value is provided the existing value will be printed'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $mountId = $input->getArgument('mount_id');
+ $key = $input->getArgument('key');
+ try {
+ $mount = $this->globalService->getStorage($mountId);
+ } catch (NotFoundException $e) {
+ $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+
+ $value = $input->getArgument('value');
+ if ($value !== null) {
+ $this->setOption($mount, $key, $value, $output);
+ } else {
+ $this->getOption($mount, $key, $output);
+ }
+ return self::SUCCESS;
+ }
+
+ /**
+ * @param string $key
+ */
+ protected function getOption(StorageConfig $mount, $key, OutputInterface $output): void {
+ if ($key === 'mountpoint' || $key === 'mount_point') {
+ $value = $mount->getMountPoint();
+ } else {
+ $value = $mount->getBackendOption($key);
+ }
+ if (!is_string($value) && json_decode(json_encode($value)) === $value) { // show bools and objects correctly
+ $value = json_encode($value);
+ }
+ $output->writeln((string)$value);
+ }
+
+ /**
+ * @param string $key
+ * @param string $value
+ */
+ protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output): void {
+ $decoded = json_decode($value, true);
+ if (!is_null($decoded) && json_encode($decoded) === $value) {
+ $value = $decoded;
+ }
+ if ($key === 'mountpoint' || $key === 'mount_point') {
+ $mount->setMountPoint($value);
+ } else {
+ $mount->setBackendOption($key, $value);
+ }
+ $this->globalService->updateStorage($mount);
+ }
+}
diff --git a/apps/files_external/lib/Command/Create.php b/apps/files_external/lib/Command/Create.php
new file mode 100644
index 00000000000..3307015518a
--- /dev/null
+++ b/apps/files_external/lib/Command/Create.php
@@ -0,0 +1,184 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OC\Files\Filesystem;
+use OC\User\NoUserException;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCA\Files_External\Service\StoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\AppFramework\Http;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Symfony\Component\Console\Input\ArrayInput;
+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 Create extends Base {
+ public function __construct(
+ private GlobalStoragesService $globalService,
+ private UserStoragesService $userService,
+ private IUserManager $userManager,
+ private IUserSession $userSession,
+ private BackendService $backendService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:create')
+ ->setDescription('Create a new mount configuration')
+ ->addOption(
+ 'user',
+ '',
+ InputOption::VALUE_OPTIONAL,
+ 'user to add the mount configuration for, if not set the mount will be added as system mount'
+ )
+ ->addArgument(
+ 'mount_point',
+ InputArgument::REQUIRED,
+ 'mount point for the new mount'
+ )
+ ->addArgument(
+ 'storage_backend',
+ InputArgument::REQUIRED,
+ 'storage backend identifier for the new mount, see `occ files_external:backends` for possible values'
+ )
+ ->addArgument(
+ 'authentication_backend',
+ InputArgument::REQUIRED,
+ 'authentication backend identifier for the new mount, see `occ files_external:backends` for possible values'
+ )
+ ->addOption(
+ 'config',
+ 'c',
+ InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
+ 'Mount configuration option in key=value format'
+ )
+ ->addOption(
+ 'dry',
+ '',
+ InputOption::VALUE_NONE,
+ 'Don\'t save the created mount, only list the new mount'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getOption('user');
+ $mountPoint = $input->getArgument('mount_point');
+ $storageIdentifier = $input->getArgument('storage_backend');
+ $authIdentifier = $input->getArgument('authentication_backend');
+ $configInput = $input->getOption('config');
+
+ $storageBackend = $this->backendService->getBackend($storageIdentifier);
+ $authBackend = $this->backendService->getAuthMechanism($authIdentifier);
+
+ if (!Filesystem::isValidPath($mountPoint)) {
+ $output->writeln('<error>Invalid mountpoint "' . $mountPoint . '"</error>');
+ return self::FAILURE;
+ }
+ if (is_null($storageBackend)) {
+ $output->writeln('<error>Storage backend with identifier "' . $storageIdentifier . '" not found (see `occ files_external:backends` for possible values)</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+ if (is_null($authBackend)) {
+ $output->writeln('<error>Authentication backend with identifier "' . $authIdentifier . '" not found (see `occ files_external:backends` for possible values)</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+ $supportedSchemes = array_keys($storageBackend->getAuthSchemes());
+ if (!in_array($authBackend->getScheme(), $supportedSchemes)) {
+ $output->writeln('<error>Authentication backend "' . $authIdentifier . '" not valid for storage backend "' . $storageIdentifier . '" (see `occ files_external:backends storage ' . $storageIdentifier . '` for possible values)</error>');
+ return self::FAILURE;
+ }
+
+ $config = [];
+ foreach ($configInput as $configOption) {
+ if (!str_contains($configOption, '=')) {
+ $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>');
+ return self::FAILURE;
+ }
+ [$key, $value] = explode('=', $configOption, 2);
+ if (!$this->validateParam($key, $value, $storageBackend, $authBackend)) {
+ $output->writeln('<error>Unknown configuration for backends "' . $key . '"</error>');
+ return self::FAILURE;
+ }
+ $config[$key] = $value;
+ }
+
+ $mount = new StorageConfig();
+ $mount->setMountPoint($mountPoint);
+ $mount->setBackend($storageBackend);
+ $mount->setAuthMechanism($authBackend);
+ $mount->setBackendOptions($config);
+
+ if ($user) {
+ if (!$this->userManager->userExists($user)) {
+ $output->writeln('<error>User "' . $user . '" not found</error>');
+ return self::FAILURE;
+ }
+ $mount->setApplicableUsers([$user]);
+ }
+
+ if ($input->getOption('dry')) {
+ $this->showMount($user, $mount, $input, $output);
+ } else {
+ $this->getStorageService($user)->addStorage($mount);
+ if ($input->getOption('output') === self::OUTPUT_FORMAT_PLAIN) {
+ $output->writeln('<info>Storage created with id ' . $mount->getId() . '</info>');
+ } else {
+ $output->writeln((string)$mount->getId());
+ }
+ }
+ return self::SUCCESS;
+ }
+
+ private function validateParam(string $key, &$value, Backend $storageBackend, AuthMechanism $authBackend): bool {
+ $params = array_merge($storageBackend->getParameters(), $authBackend->getParameters());
+ foreach ($params as $param) {
+ /** @var DefinitionParameter $param */
+ if ($param->getName() === $key) {
+ if ($param->getType() === DefinitionParameter::VALUE_BOOLEAN) {
+ $value = ($value === 'true');
+ }
+ return true;
+ }
+ }
+ return false;
+ }
+
+ private function showMount(string $user, StorageConfig $mount, InputInterface $input, OutputInterface $output): void {
+ $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager);
+ $listInput = new ArrayInput([], $listCommand->getDefinition());
+ $listInput->setOption('output', $input->getOption('output'));
+ $listInput->setOption('show-password', true);
+ $listCommand->listMounts($user, [$mount], $listInput, $output);
+ }
+
+ protected function getStorageService(string $userId): StoragesService {
+ if (empty($userId)) {
+ return $this->globalService;
+ }
+
+ $user = $this->userManager->get($userId);
+ if (is_null($user)) {
+ throw new NoUserException("user $userId not found");
+ }
+ $this->userSession->setUser($user);
+ return $this->userService;
+ }
+}
diff --git a/apps/files_external/lib/Command/Delete.php b/apps/files_external/lib/Command/Delete.php
new file mode 100644
index 00000000000..9f121250f7d
--- /dev/null
+++ b/apps/files_external/lib/Command/Delete.php
@@ -0,0 +1,82 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\AppFramework\Http;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Symfony\Component\Console\Helper\QuestionHelper;
+use Symfony\Component\Console\Input\ArrayInput;
+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 Delete extends Base {
+ public function __construct(
+ protected GlobalStoragesService $globalService,
+ protected UserStoragesService $userService,
+ protected IUserSession $userSession,
+ protected IUserManager $userManager,
+ protected QuestionHelper $questionHelper,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:delete')
+ ->setDescription('Delete an external mount')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'The id of the mount to edit'
+ )->addOption(
+ 'yes',
+ 'y',
+ InputOption::VALUE_NONE,
+ 'Skip confirmation'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $mountId = $input->getArgument('mount_id');
+ try {
+ $mount = $this->globalService->getStorage($mountId);
+ } catch (NotFoundException $e) {
+ $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+
+ $noConfirm = $input->getOption('yes');
+
+ if (!$noConfirm) {
+ $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager);
+ $listInput = new ArrayInput([], $listCommand->getDefinition());
+ $listInput->setOption('output', $input->getOption('output'));
+ $listCommand->listMounts(null, [$mount], $listInput, $output);
+
+ /** @var QuestionHelper $questionHelper */
+ $questionHelper = $this->getHelper('question');
+ $question = new ConfirmationQuestion('Delete this mount? [y/N] ', false);
+
+ if (!$questionHelper->ask($input, $output, $question)) {
+ return self::FAILURE;
+ }
+ }
+
+ $this->globalService->removeStorage($mountId);
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files_external/lib/Command/Dependencies.php b/apps/files_external/lib/Command/Dependencies.php
new file mode 100644
index 00000000000..d2db8a8c9af
--- /dev/null
+++ b/apps/files_external/lib/Command/Dependencies.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Service\BackendService;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Dependencies extends Base {
+ public function __construct(
+ private readonly BackendService $backendService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:dependencies')
+ ->setDescription('Show information about the backend dependencies');
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $storageBackends = $this->backendService->getBackends();
+
+ $anyMissing = false;
+
+ foreach ($storageBackends as $backend) {
+ if ($backend->getDeprecateTo() !== null) {
+ continue;
+ }
+ $missingDependencies = $backend->checkDependencies();
+ if ($missingDependencies) {
+ $anyMissing = true;
+ $output->writeln($backend->getText() . ':');
+ foreach ($missingDependencies as $missingDependency) {
+ if ($missingDependency->getMessage()) {
+ $output->writeln(" - <comment>{$missingDependency->getDependency()}</comment>: {$missingDependency->getMessage()}");
+ } else {
+ $output->writeln(" - <comment>{$missingDependency->getDependency()}</comment>");
+ }
+ }
+ }
+ }
+
+ if (!$anyMissing) {
+ $output->writeln('<info>All dependencies are met</info>');
+ }
+
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files_external/lib/Command/Export.php b/apps/files_external/lib/Command/Export.php
new file mode 100644
index 00000000000..59484d0e788
--- /dev/null
+++ b/apps/files_external/lib/Command/Export.php
@@ -0,0 +1,44 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use Symfony\Component\Console\Input\ArrayInput;
+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 Export extends ListCommand {
+ protected function configure(): void {
+ $this
+ ->setName('files_external:export')
+ ->setDescription('Export mount configurations')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL,
+ 'user id to export the personal mounts for, if no user is provided admin mounts will be exported'
+ )->addOption(
+ 'all',
+ 'a',
+ InputOption::VALUE_NONE,
+ 'show both system wide mounts and all personal mounts'
+ );
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager);
+ $listInput = new ArrayInput([], $listCommand->getDefinition());
+ $listInput->setArgument('user_id', $input->getArgument('user_id'));
+ $listInput->setOption('all', $input->getOption('all'));
+ $listInput->setOption('output', 'json_pretty');
+ $listInput->setOption('show-password', true);
+ $listInput->setOption('full', true);
+ $listCommand->execute($listInput, $output);
+ return self::SUCCESS;
+ }
+}
diff --git a/apps/files_external/lib/Command/Import.php b/apps/files_external/lib/Command/Import.php
new file mode 100644
index 00000000000..a9ed76fbe40
--- /dev/null
+++ b/apps/files_external/lib/Command/Import.php
@@ -0,0 +1,179 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OC\User\NoUserException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCA\Files_External\Service\ImportLegacyStoragesService;
+use OCA\Files_External\Service\StoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Symfony\Component\Console\Input\ArrayInput;
+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 Import extends Base {
+ public function __construct(
+ private GlobalStoragesService $globalService,
+ private UserStoragesService $userService,
+ private IUserSession $userSession,
+ private IUserManager $userManager,
+ private ImportLegacyStoragesService $importLegacyStorageService,
+ private BackendService $backendService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:import')
+ ->setDescription('Import mount configurations')
+ ->addOption(
+ 'user',
+ '',
+ InputOption::VALUE_OPTIONAL,
+ 'user to add the mount configurations for, if not set the mount will be added as system mount'
+ )
+ ->addArgument(
+ 'path',
+ InputArgument::REQUIRED,
+ 'path to a json file containing the mounts to import, use "-" to read from stdin'
+ )
+ ->addOption(
+ 'dry',
+ '',
+ InputOption::VALUE_NONE,
+ 'Don\'t save the imported mounts, only list the new mounts'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $user = (string)$input->getOption('user');
+ $path = $input->getArgument('path');
+ if ($path === '-') {
+ $json = file_get_contents('php://stdin');
+ } else {
+ if (!file_exists($path)) {
+ $output->writeln('<error>File not found: ' . $path . '</error>');
+ return self::FAILURE;
+ }
+ $json = file_get_contents($path);
+ }
+ if (!is_string($json) || strlen($json) < 2) {
+ $output->writeln('<error>Error while reading json</error>');
+ return self::FAILURE;
+ }
+ $data = json_decode($json, true);
+ if (!is_array($data)) {
+ $output->writeln('<error>Error while parsing json</error>');
+ return self::FAILURE;
+ }
+
+ $isLegacy = isset($data['user']) || isset($data['group']);
+ if ($isLegacy) {
+ $this->importLegacyStorageService->setData($data);
+ $mounts = $this->importLegacyStorageService->getAllStorages();
+ foreach ($mounts as $mount) {
+ if ($mount->getBackendOption('password') === false) {
+ $output->writeln('<error>Failed to decrypt password</error>');
+ return self::FAILURE;
+ }
+ }
+ } else {
+ if (!isset($data[0])) { //normalize to an array of mounts
+ $data = [$data];
+ }
+ $mounts = array_map([$this, 'parseData'], $data);
+ }
+
+ if ($user) {
+ // ensure applicables are correct for personal mounts
+ foreach ($mounts as $mount) {
+ $mount->setApplicableGroups([]);
+ $mount->setApplicableUsers([$user]);
+ }
+ }
+
+ $storageService = $this->getStorageService($user);
+
+ $existingMounts = $storageService->getAllStorages();
+
+ foreach ($mounts as $mount) {
+ foreach ($existingMounts as $existingMount) {
+ if (
+ $existingMount->getMountPoint() === $mount->getMountPoint()
+ && $existingMount->getApplicableGroups() === $mount->getApplicableGroups()
+ && $existingMount->getApplicableUsers() === $mount->getApplicableUsers()
+ && $existingMount->getBackendOptions() === $mount->getBackendOptions()
+ ) {
+ $output->writeln('<error>Duplicate mount (' . $mount->getMountPoint() . ')</error>');
+ return self::FAILURE;
+ }
+ }
+ }
+
+ if ($input->getOption('dry')) {
+ if (count($mounts) === 0) {
+ $output->writeln('<error>No mounts to be imported</error>');
+ return self::FAILURE;
+ }
+ $listCommand = new ListCommand($this->globalService, $this->userService, $this->userSession, $this->userManager);
+ $listInput = new ArrayInput([], $listCommand->getDefinition());
+ $listInput->setOption('output', $input->getOption('output'));
+ $listInput->setOption('show-password', true);
+ $listCommand->listMounts($user, $mounts, $listInput, $output);
+ } else {
+ foreach ($mounts as $mount) {
+ $storageService->addStorage($mount);
+ }
+ }
+ return self::SUCCESS;
+ }
+
+ private function parseData(array $data): StorageConfig {
+ $mount = new StorageConfig($data['mount_id']);
+ $mount->setMountPoint($data['mount_point']);
+ $mount->setBackend($this->getBackendByClass($data['storage']));
+ $authBackend = $this->backendService->getAuthMechanism($data['authentication_type']);
+ $mount->setAuthMechanism($authBackend);
+ $mount->setBackendOptions($data['configuration']);
+ $mount->setMountOptions($data['options']);
+ $mount->setApplicableUsers($data['applicable_users'] ?? []);
+ $mount->setApplicableGroups($data['applicable_groups'] ?? []);
+ return $mount;
+ }
+
+ private function getBackendByClass(string $className) {
+ $backends = $this->backendService->getBackends();
+ foreach ($backends as $backend) {
+ if ($backend->getStorageClass() === $className) {
+ return $backend;
+ }
+ }
+ }
+
+ protected function getStorageService(string $userId): StoragesService {
+ if (empty($userId)) {
+ return $this->globalService;
+ }
+
+ $user = $this->userManager->get($userId);
+ if (is_null($user)) {
+ throw new NoUserException("user $userId not found");
+ }
+ $this->userSession->setUser($user);
+ return $this->userService;
+ }
+}
diff --git a/apps/files_external/lib/Command/ListCommand.php b/apps/files_external/lib/Command/ListCommand.php
new file mode 100644
index 00000000000..350916b6c2c
--- /dev/null
+++ b/apps/files_external/lib/Command/ListCommand.php
@@ -0,0 +1,239 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OC\User\NoUserException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCA\Files_External\Service\StoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use Symfony\Component\Console\Helper\Table;
+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 ListCommand extends Base {
+ public const ALL = -1;
+
+ public function __construct(
+ protected GlobalStoragesService $globalService,
+ protected UserStoragesService $userService,
+ protected IUserSession $userSession,
+ protected IUserManager $userManager,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:list')
+ ->setDescription('List configured admin or personal mounts')
+ ->addArgument(
+ 'user_id',
+ InputArgument::OPTIONAL,
+ 'user id to list the personal mounts for, if no user is provided admin mounts will be listed'
+ )->addOption(
+ 'show-password',
+ '',
+ InputOption::VALUE_NONE,
+ 'show passwords and secrets'
+ )->addOption(
+ 'full',
+ null,
+ InputOption::VALUE_NONE,
+ 'don\'t truncate long values in table output'
+ )->addOption(
+ 'all',
+ 'a',
+ InputOption::VALUE_NONE,
+ 'show both system wide mounts and all personal mounts'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ /** @var StorageConfig[] $mounts */
+ if ($input->getOption('all')) {
+ $mounts = $this->globalService->getStorageForAllUsers();
+ $userId = self::ALL;
+ } else {
+ $userId = (string)$input->getArgument('user_id');
+ $storageService = $this->getStorageService($userId);
+ $mounts = $storageService->getAllStorages();
+ }
+
+ $this->listMounts($userId, $mounts, $input, $output);
+ return self::SUCCESS;
+ }
+
+ /**
+ * @param ?string|ListCommand::ALL $userId
+ * @param StorageConfig[] $mounts
+ */
+ public function listMounts($userId, array $mounts, InputInterface $input, OutputInterface $output): void {
+ $outputType = $input->getOption('output');
+ if (count($mounts) === 0) {
+ if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) {
+ $output->writeln('[]');
+ } else {
+ if ($userId === self::ALL) {
+ $output->writeln('<info>No mounts configured</info>');
+ } elseif ($userId) {
+ $output->writeln("<info>No mounts configured by $userId</info>");
+ } else {
+ $output->writeln('<info>No admin mounts configured</info>');
+ }
+ }
+ return;
+ }
+
+ $headers = ['Mount ID', 'Mount Point', 'Storage', 'Authentication Type', 'Configuration', 'Options'];
+
+ if (!$userId || $userId === self::ALL) {
+ $headers[] = 'Applicable Users';
+ $headers[] = 'Applicable Groups';
+ }
+ if ($userId === self::ALL) {
+ $headers[] = 'Type';
+ }
+
+ if (!$input->getOption('show-password')) {
+ $hideKeys = ['key', 'bucket', 'secret', 'password', 'refresh_token', 'token', 'client_secret', 'public_key', 'private_key'];
+ foreach ($mounts as $mount) {
+ $config = $mount->getBackendOptions();
+ foreach ($config as $key => $value) {
+ if (in_array($key, $hideKeys)) {
+ $mount->setBackendOption($key, '***REMOVED SENSITIVE VALUE***');
+ }
+ }
+ }
+ }
+
+ if ($outputType === self::OUTPUT_FORMAT_JSON || $outputType === self::OUTPUT_FORMAT_JSON_PRETTY) {
+ $keys = array_map(function ($header) {
+ return strtolower(str_replace(' ', '_', $header));
+ }, $headers);
+
+ $pairs = array_map(function (StorageConfig $config) use ($keys, $userId) {
+ $values = [
+ $config->getId(),
+ $config->getMountPoint(),
+ $config->getBackend()->getStorageClass(),
+ $config->getAuthMechanism()->getIdentifier(),
+ $config->getBackendOptions(),
+ $config->getMountOptions()
+ ];
+ if (!$userId || $userId === self::ALL) {
+ $values[] = $config->getApplicableUsers();
+ $values[] = $config->getApplicableGroups();
+ }
+ if ($userId === self::ALL) {
+ $values[] = $config->getType() === StorageConfig::MOUNT_TYPE_ADMIN ? 'admin' : 'personal';
+ }
+
+ return array_combine($keys, $values);
+ }, $mounts);
+ if ($outputType === self::OUTPUT_FORMAT_JSON) {
+ $output->writeln(json_encode(array_values($pairs)));
+ } else {
+ $output->writeln(json_encode(array_values($pairs), JSON_PRETTY_PRINT));
+ }
+ } else {
+ $full = $input->getOption('full');
+ $defaultMountOptions = [
+ 'encrypt' => true,
+ 'previews' => true,
+ 'filesystem_check_changes' => 1,
+ 'enable_sharing' => false,
+ 'encoding_compatibility' => false,
+ 'readonly' => false,
+ ];
+ $rows = array_map(function (StorageConfig $config) use ($userId, $defaultMountOptions, $full) {
+ $storageConfig = $config->getBackendOptions();
+ $keys = array_keys($storageConfig);
+ $values = array_values($storageConfig);
+
+ if (!$full) {
+ $values = array_map(function ($value) {
+ if (is_string($value) && strlen($value) > 32) {
+ return substr($value, 0, 6) . '...' . substr($value, -6, 6);
+ } else {
+ return $value;
+ }
+ }, $values);
+ }
+
+ $configStrings = array_map(function ($key, $value) {
+ return $key . ': ' . json_encode($value);
+ }, $keys, $values);
+ $configString = implode(', ', $configStrings);
+
+ $mountOptions = $config->getMountOptions();
+ // hide defaults
+ foreach ($mountOptions as $key => $value) {
+ if ($value === $defaultMountOptions[$key]) {
+ unset($mountOptions[$key]);
+ }
+ }
+ $keys = array_keys($mountOptions);
+ $values = array_values($mountOptions);
+
+ $optionsStrings = array_map(function ($key, $value) {
+ return $key . ': ' . json_encode($value);
+ }, $keys, $values);
+ $optionsString = implode(', ', $optionsStrings);
+
+ $values = [
+ $config->getId(),
+ $config->getMountPoint(),
+ $config->getBackend()->getText(),
+ $config->getAuthMechanism()->getText(),
+ $configString,
+ $optionsString
+ ];
+
+ if (!$userId || $userId === self::ALL) {
+ $applicableUsers = implode(', ', $config->getApplicableUsers());
+ $applicableGroups = implode(', ', $config->getApplicableGroups());
+ if ($applicableUsers === '' && $applicableGroups === '') {
+ $applicableUsers = 'All';
+ }
+ $values[] = $applicableUsers;
+ $values[] = $applicableGroups;
+ }
+ if ($userId === self::ALL) {
+ $values[] = $config->getType() === StorageConfig::MOUNT_TYPE_ADMIN ? 'Admin' : 'Personal';
+ }
+
+ return $values;
+ }, $mounts);
+
+ $table = new Table($output);
+ $table->setHeaders($headers);
+ $table->setRows($rows);
+ $table->render();
+ }
+ }
+
+ protected function getStorageService(string $userId): StoragesService {
+ if (empty($userId)) {
+ return $this->globalService;
+ }
+
+ $user = $this->userManager->get($userId);
+ if (is_null($user)) {
+ throw new NoUserException("user $userId not found");
+ }
+ $this->userSession->setUser($user);
+ return $this->userService;
+ }
+}
diff --git a/apps/files_external/lib/Command/Notify.php b/apps/files_external/lib/Command/Notify.php
new file mode 100644
index 00000000000..0982aa5598b
--- /dev/null
+++ b/apps/files_external/lib/Command/Notify.php
@@ -0,0 +1,253 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Command;
+
+use Doctrine\DBAL\Exception\DriverException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\Files\Notify\IChange;
+use OCP\Files\Notify\INotifyHandler;
+use OCP\Files\Notify\IRenameChange;
+use OCP\Files\Storage\INotifyStorage;
+use OCP\Files\Storage\IStorage;
+use OCP\IDBConnection;
+use OCP\IUserManager;
+use Psr\Log\LoggerInterface;
+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 Notify extends StorageAuthBase {
+ public function __construct(
+ private IDBConnection $connection,
+ private LoggerInterface $logger,
+ GlobalStoragesService $globalService,
+ IUserManager $userManager,
+ ) {
+ parent::__construct($globalService, $userManager);
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:notify')
+ ->setDescription('Listen for active update notifications for a configured external mount')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'the mount id of the mount to listen to'
+ )->addOption(
+ 'user',
+ 'u',
+ InputOption::VALUE_REQUIRED,
+ 'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
+ )->addOption(
+ 'password',
+ 'p',
+ InputOption::VALUE_REQUIRED,
+ 'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
+ )->addOption(
+ 'path',
+ '',
+ InputOption::VALUE_REQUIRED,
+ 'The directory in the storage to listen for updates in',
+ '/'
+ )->addOption(
+ 'no-self-check',
+ '',
+ InputOption::VALUE_NONE,
+ 'Disable self check on startup'
+ )->addOption(
+ 'dry-run',
+ '',
+ InputOption::VALUE_NONE,
+ 'Don\'t make any changes, only log detected changes'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ [$mount, $storage] = $this->createStorage($input, $output);
+ if ($storage === null) {
+ return self::FAILURE;
+ }
+
+ if (!$storage instanceof INotifyStorage) {
+ $output->writeln('<error>Mount of type "' . $mount->getBackend()->getText() . '" does not support active update notifications</error>');
+ return self::FAILURE;
+ }
+
+ $dryRun = $input->getOption('dry-run');
+ if ($dryRun && $output->getVerbosity() < OutputInterface::VERBOSITY_VERBOSE) {
+ $output->setVerbosity(OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ $path = trim($input->getOption('path'), '/');
+ $notifyHandler = $storage->notify($path);
+
+ if (!$input->getOption('no-self-check')) {
+ $this->selfTest($storage, $notifyHandler, $output);
+ }
+
+ $notifyHandler->listen(function (IChange $change) use ($mount, $output, $dryRun): void {
+ $this->logUpdate($change, $output);
+ if ($change instanceof IRenameChange) {
+ $this->markParentAsOutdated($mount->getId(), $change->getTargetPath(), $output, $dryRun);
+ }
+ $this->markParentAsOutdated($mount->getId(), $change->getPath(), $output, $dryRun);
+ });
+ return self::SUCCESS;
+ }
+
+ private function markParentAsOutdated($mountId, $path, OutputInterface $output, bool $dryRun): void {
+ $parent = ltrim(dirname($path), '/');
+ if ($parent === '.') {
+ $parent = '';
+ }
+
+ try {
+ $storages = $this->getStorageIds($mountId, $parent);
+ } catch (DriverException $ex) {
+ $this->logger->warning('Error while trying to find correct storage ids.', ['exception' => $ex]);
+ $this->connection = $this->reconnectToDatabase($this->connection, $output);
+ $output->writeln('<info>Needed to reconnect to the database</info>');
+ $storages = $this->getStorageIds($mountId, $path);
+ }
+ if (count($storages) === 0) {
+ $output->writeln(" no users found with access to '$parent', skipping", OutputInterface::VERBOSITY_VERBOSE);
+ return;
+ }
+
+ $users = array_map(function (array $storage) {
+ return $storage['user_id'];
+ }, $storages);
+
+ $output->writeln(" marking '$parent' as outdated for " . implode(', ', $users), OutputInterface::VERBOSITY_VERBOSE);
+
+ $storageIds = array_map(function (array $storage) {
+ return intval($storage['storage_id']);
+ }, $storages);
+ $storageIds = array_values(array_unique($storageIds));
+
+ if ($dryRun) {
+ $output->writeln(' dry-run: skipping database write');
+ } else {
+ $result = $this->updateParent($storageIds, $parent);
+ if ($result === 0) {
+ //TODO: Find existing parent further up the tree in the database and register that folder instead.
+ $this->logger->info('Failed updating parent for "' . $path . '" while trying to register change. It may not exist in the filecache.');
+ }
+ }
+ }
+
+ private function logUpdate(IChange $change, OutputInterface $output): void {
+ $text = match ($change->getType()) {
+ INotifyStorage::NOTIFY_ADDED => 'added',
+ INotifyStorage::NOTIFY_MODIFIED => 'modified',
+ INotifyStorage::NOTIFY_REMOVED => 'removed',
+ INotifyStorage::NOTIFY_RENAMED => 'renamed',
+ default => '',
+ };
+
+ if ($text === '') {
+ return;
+ }
+
+ $text .= ' ' . $change->getPath();
+ if ($change instanceof IRenameChange) {
+ $text .= ' to ' . $change->getTargetPath();
+ }
+
+ $output->writeln($text, OutputInterface::VERBOSITY_VERBOSE);
+ }
+
+ private function getStorageIds(int $mountId, string $path): array {
+ $pathHash = md5(trim(\OC_Util::normalizeUnicode($path), '/'));
+ $qb = $this->connection->getQueryBuilder();
+ return $qb
+ ->select('storage_id', 'user_id')
+ ->from('mounts', 'm')
+ ->innerJoin('m', 'filecache', 'f', $qb->expr()->eq('m.storage_id', 'f.storage'))
+ ->where($qb->expr()->eq('mount_id', $qb->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR)))
+ ->execute()
+ ->fetchAll();
+ }
+
+ private function updateParent(array $storageIds, string $parent): int {
+ $pathHash = md5(trim(\OC_Util::normalizeUnicode($parent), '/'));
+ $qb = $this->connection->getQueryBuilder();
+ return $qb
+ ->update('filecache')
+ ->set('size', $qb->createNamedParameter(-1, IQueryBuilder::PARAM_INT))
+ ->where($qb->expr()->in('storage', $qb->createNamedParameter($storageIds, IQueryBuilder::PARAM_INT_ARRAY, ':storage_ids')))
+ ->andWhere($qb->expr()->eq('path_hash', $qb->createNamedParameter($pathHash, IQueryBuilder::PARAM_STR)))
+ ->executeStatement();
+ }
+
+ private function reconnectToDatabase(IDBConnection $connection, OutputInterface $output): IDBConnection {
+ try {
+ $connection->close();
+ } catch (\Exception $ex) {
+ $this->logger->warning('Error while disconnecting from DB', ['exception' => $ex]);
+ $output->writeln("<info>Error while disconnecting from database: {$ex->getMessage()}</info>");
+ }
+ $connected = false;
+ while (!$connected) {
+ try {
+ $connected = $connection->connect();
+ } catch (\Exception $ex) {
+ $this->logger->warning('Error while re-connecting to database', ['exception' => $ex]);
+ $output->writeln("<info>Error while re-connecting to database: {$ex->getMessage()}</info>");
+ sleep(60);
+ }
+ }
+ return $connection;
+ }
+
+
+ private function selfTest(IStorage $storage, INotifyHandler $notifyHandler, OutputInterface $output): void {
+ usleep(100 * 1000); //give time for the notify to start
+ if (!$storage->file_put_contents('/.nc_test_file.txt', 'test content')) {
+ $output->writeln('Failed to create test file for self-test');
+ return;
+ }
+ $storage->mkdir('/.nc_test_folder');
+ $storage->file_put_contents('/.nc_test_folder/subfile.txt', 'test content');
+
+ usleep(100 * 1000); //time for all changes to be processed
+ $changes = $notifyHandler->getChanges();
+
+ $storage->unlink('/.nc_test_file.txt');
+ $storage->unlink('/.nc_test_folder/subfile.txt');
+ $storage->rmdir('/.nc_test_folder');
+
+ usleep(100 * 1000); //time for all changes to be processed
+ $notifyHandler->getChanges(); // flush
+
+ $foundRootChange = false;
+ $foundSubfolderChange = false;
+
+ foreach ($changes as $change) {
+ if ($change->getPath() === '/.nc_test_file.txt' || $change->getPath() === '.nc_test_file.txt') {
+ $foundRootChange = true;
+ } elseif ($change->getPath() === '/.nc_test_folder/subfile.txt' || $change->getPath() === '.nc_test_folder/subfile.txt') {
+ $foundSubfolderChange = true;
+ }
+ }
+
+ if ($foundRootChange && $foundSubfolderChange) {
+ $output->writeln('<info>Self-test successful</info>', OutputInterface::VERBOSITY_VERBOSE);
+ } elseif ($foundRootChange) {
+ $output->writeln('<error>Error while running self-test, change is subfolder not detected</error>');
+ } else {
+ $output->writeln('<error>Error while running self-test, no changes detected</error>');
+ }
+ }
+}
diff --git a/apps/files_external/lib/Command/Option.php b/apps/files_external/lib/Command/Option.php
new file mode 100644
index 00000000000..3fda3fcb3cf
--- /dev/null
+++ b/apps/files_external/lib/Command/Option.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OCA\Files_External\Lib\StorageConfig;
+use Symfony\Component\Console\Input\InputArgument;
+use Symfony\Component\Console\Output\OutputInterface;
+
+class Option extends Config {
+ protected function configure(): void {
+ $this
+ ->setName('files_external:option')
+ ->setDescription('Manage mount options for a mount')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'The id of the mount to edit'
+ )->addArgument(
+ 'key',
+ InputArgument::REQUIRED,
+ 'key of the mount option to set/get'
+ )->addArgument(
+ 'value',
+ InputArgument::OPTIONAL,
+ 'value to set the mount option to, when no value is provided the existing value will be printed'
+ );
+ }
+
+ /**
+ * @param string $key
+ */
+ protected function getOption(StorageConfig $mount, $key, OutputInterface $output): void {
+ $value = $mount->getMountOption($key);
+ if (!is_string($value)) { // show bools and objects correctly
+ $value = json_encode($value);
+ }
+ $output->writeln((string)$value);
+ }
+
+ /**
+ * @param string $key
+ * @param string $value
+ */
+ protected function setOption(StorageConfig $mount, $key, $value, OutputInterface $output): void {
+ $decoded = json_decode($value, true);
+ if (!is_null($decoded)) {
+ $value = $decoded;
+ }
+ $mount->setMountOption($key, $value);
+ $this->globalService->updateStorage($mount);
+ }
+}
diff --git a/apps/files_external/lib/Command/Scan.php b/apps/files_external/lib/Command/Scan.php
new file mode 100644
index 00000000000..75d98878baa
--- /dev/null
+++ b/apps/files_external/lib/Command/Scan.php
@@ -0,0 +1,166 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Command;
+
+use OC\Files\Cache\Scanner;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\IUserManager;
+use OCP\Lock\LockedException;
+use Symfony\Component\Console\Helper\Table;
+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 Scan extends StorageAuthBase {
+ protected float $execTime = 0;
+ protected int $foldersCounter = 0;
+ protected int $filesCounter = 0;
+
+ public function __construct(
+ GlobalStoragesService $globalService,
+ IUserManager $userManager,
+ ) {
+ parent::__construct($globalService, $userManager);
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:scan')
+ ->setDescription('Scan an external storage for changed files')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'the mount id of the mount to scan'
+ )->addOption(
+ 'user',
+ 'u',
+ InputOption::VALUE_REQUIRED,
+ 'The username for the remote mount (required only for some mount configuration that don\'t store credentials)'
+ )->addOption(
+ 'password',
+ 'p',
+ InputOption::VALUE_REQUIRED,
+ 'The password for the remote mount (required only for some mount configuration that don\'t store credentials)'
+ )->addOption(
+ 'path',
+ '',
+ InputOption::VALUE_OPTIONAL,
+ 'The path in the storage to scan',
+ ''
+ )->addOption(
+ 'unscanned',
+ '',
+ InputOption::VALUE_NONE,
+ 'only scan files which are marked as not fully scanned'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ [, $storage] = $this->createStorage($input, $output);
+ if ($storage === null) {
+ return 1;
+ }
+
+ $path = $input->getOption('path');
+
+ $this->execTime = -microtime(true);
+
+ /** @var Scanner $scanner */
+ $scanner = $storage->getScanner();
+
+ $scanner->listen('\OC\Files\Cache\Scanner', 'scanFile', function (string $path) use ($output): void {
+ $output->writeln("\tFile\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
+ ++$this->filesCounter;
+ $this->abortIfInterrupted();
+ });
+
+ $scanner->listen('\OC\Files\Cache\Scanner', 'scanFolder', function (string $path) use ($output): void {
+ $output->writeln("\tFolder\t<info>$path</info>", OutputInterface::VERBOSITY_VERBOSE);
+ ++$this->foldersCounter;
+ $this->abortIfInterrupted();
+ });
+
+ try {
+ if ($input->getOption('unscanned')) {
+ if ($path !== '') {
+ $output->writeln('<error>--unscanned is mutually exclusive with --path</error>');
+ return 1;
+ }
+ $scanner->backgroundScan();
+ } else {
+ $scanner->scan($path);
+ }
+ } catch (LockedException $e) {
+ if (is_string($e->getReadablePath()) && str_starts_with($e->getReadablePath(), 'scanner::')) {
+ if ($e->getReadablePath() === 'scanner::') {
+ $output->writeln('<error>Another process is already scanning this storage</error>');
+ } else {
+ $output->writeln('<error>Another process is already scanning \'' . substr($e->getReadablePath(), strlen('scanner::')) . '\' in this storage</error>');
+ }
+ } else {
+ throw $e;
+ }
+ }
+
+ $this->presentStats($output);
+
+ return 0;
+ }
+
+ /**
+ * @param OutputInterface $output
+ */
+ protected function presentStats(OutputInterface $output): void {
+ // Stop the timer
+ $this->execTime += microtime(true);
+
+ $headers = [
+ 'Folders', 'Files', 'Elapsed time'
+ ];
+
+ $this->showSummary($headers, [], $output);
+ }
+
+ /**
+ * Shows a summary of operations
+ *
+ * @param string[] $headers
+ * @param string[] $rows
+ * @param OutputInterface $output
+ */
+ protected function showSummary(array $headers, array $rows, OutputInterface $output): void {
+ $niceDate = $this->formatExecTime();
+ if (!$rows) {
+ $rows = [
+ $this->foldersCounter,
+ $this->filesCounter,
+ $niceDate,
+ ];
+ }
+ $table = new Table($output);
+ $table
+ ->setHeaders($headers)
+ ->setRows([$rows]);
+ $table->render();
+ }
+
+
+ /**
+ * Formats microtime into a human readable format
+ *
+ * @return string
+ */
+ protected function formatExecTime(): string {
+ $secs = round($this->execTime);
+ # convert seconds into HH:MM:SS form
+ return sprintf('%02d:%02d:%02d', ($secs / 3600), ($secs / 60 % 60), $secs % 60);
+ }
+}
diff --git a/apps/files_external/lib/Command/StorageAuthBase.php b/apps/files_external/lib/Command/StorageAuthBase.php
new file mode 100644
index 00000000000..6f830a08a60
--- /dev/null
+++ b/apps/files_external/lib/Command/StorageAuthBase.php
@@ -0,0 +1,114 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IUserManager;
+use Symfony\Component\Console\Input\InputInterface;
+use Symfony\Component\Console\Output\OutputInterface;
+
+abstract class StorageAuthBase extends Base {
+ public function __construct(
+ protected GlobalStoragesService $globalService,
+ protected IUserManager $userManager,
+ ) {
+ parent::__construct();
+ }
+
+ private function getUserOption(InputInterface $input): ?string {
+ if ($input->getOption('user')) {
+ return (string)$input->getOption('user');
+ }
+
+ return $_ENV['NOTIFY_USER'] ?? $_SERVER['NOTIFY_USER'] ?? null;
+ }
+
+ private function getPasswordOption(InputInterface $input): ?string {
+ if ($input->getOption('password')) {
+ return (string)$input->getOption('password');
+ }
+
+ return $_ENV['NOTIFY_PASSWORD'] ?? $_SERVER['NOTIFY_PASSWORD'] ?? null;
+ }
+
+ /**
+ * @param InputInterface $input
+ * @param OutputInterface $output
+ * @return array
+ * @psalm-return array{0: StorageConfig, 1: IStorage}|array{0: null, 1: null}
+ */
+ protected function createStorage(InputInterface $input, OutputInterface $output): array {
+ try {
+ /** @var StorageConfig|null $mount */
+ $mount = $this->globalService->getStorage((int)$input->getArgument('mount_id'));
+ } catch (NotFoundException $e) {
+ $output->writeln('<error>Mount not found</error>');
+ return [null, null];
+ }
+ if (is_null($mount)) {
+ $output->writeln('<error>Mount not found</error>');
+ return [null, null];
+ }
+ $noAuth = false;
+
+ $userOption = $this->getUserOption($input);
+ $passwordOption = $this->getPasswordOption($input);
+
+ // if only the user is provided, we get the user object to pass along to the auth backend
+ // this allows using saved user credentials
+ $user = ($userOption && !$passwordOption) ? $this->userManager->get($userOption) : null;
+
+ try {
+ $authBackend = $mount->getAuthMechanism();
+ $authBackend->manipulateStorageConfig($mount, $user);
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ $noAuth = true;
+ } catch (StorageNotAvailableException $e) {
+ $noAuth = true;
+ }
+
+ if ($userOption) {
+ $mount->setBackendOption('user', $userOption);
+ }
+ if ($passwordOption) {
+ $mount->setBackendOption('password', $passwordOption);
+ }
+
+ try {
+ $backend = $mount->getBackend();
+ $backend->manipulateStorageConfig($mount, $user);
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ $noAuth = true;
+ } catch (StorageNotAvailableException $e) {
+ $noAuth = true;
+ }
+
+ try {
+ $class = $mount->getBackend()->getStorageClass();
+ /** @var IStorage $storage */
+ $storage = new $class($mount->getBackendOptions());
+ if (!$storage->test()) {
+ throw new \Exception();
+ }
+ return [$mount, $storage];
+ } catch (\Exception $e) {
+ $output->writeln('<error>Error while trying to create storage</error>');
+ if ($noAuth) {
+ $output->writeln('<error>Username and/or password required</error>');
+ }
+ return [null, null];
+ }
+ }
+}
diff --git a/apps/files_external/lib/Command/Verify.php b/apps/files_external/lib/Command/Verify.php
new file mode 100644
index 00000000000..ecebbe0f7e6
--- /dev/null
+++ b/apps/files_external/lib/Command/Verify.php
@@ -0,0 +1,121 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Command;
+
+use OC\Core\Command\Base;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\MountConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\AppFramework\Http;
+use OCP\Files\StorageNotAvailableException;
+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 Verify extends Base {
+ public function __construct(
+ protected GlobalStoragesService $globalService,
+ ) {
+ parent::__construct();
+ }
+
+ protected function configure(): void {
+ $this
+ ->setName('files_external:verify')
+ ->setDescription('Verify mount configuration')
+ ->addArgument(
+ 'mount_id',
+ InputArgument::REQUIRED,
+ 'The id of the mount to check'
+ )->addOption(
+ 'config',
+ 'c',
+ InputOption::VALUE_REQUIRED | InputOption::VALUE_IS_ARRAY,
+ 'Additional config option to set before checking in key=value pairs, required for certain auth backends such as login credentails'
+ );
+ parent::configure();
+ }
+
+ protected function execute(InputInterface $input, OutputInterface $output): int {
+ $mountId = $input->getArgument('mount_id');
+ $configInput = $input->getOption('config');
+
+ try {
+ $mount = $this->globalService->getStorage($mountId);
+ } catch (NotFoundException $e) {
+ $output->writeln('<error>Mount with id "' . $mountId . ' not found, check "occ files_external:list" to get available mounts"</error>');
+ return Http::STATUS_NOT_FOUND;
+ }
+
+ $this->updateStorageStatus($mount, $configInput, $output);
+
+ $this->writeArrayInOutputFormat($input, $output, [
+ 'status' => StorageNotAvailableException::getStateCodeName($mount->getStatus()),
+ 'code' => $mount->getStatus(),
+ 'message' => $mount->getStatusMessage()
+ ]);
+ return self::SUCCESS;
+ }
+
+ private function manipulateStorageConfig(StorageConfig $storage): void {
+ $authMechanism = $storage->getAuthMechanism();
+ $authMechanism->manipulateStorageConfig($storage);
+ $backend = $storage->getBackend();
+ $backend->manipulateStorageConfig($storage);
+ }
+
+ private function updateStorageStatus(StorageConfig &$storage, $configInput, OutputInterface $output): void {
+ try {
+ try {
+ $this->manipulateStorageConfig($storage);
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ if (count($configInput) === 0) { // extra config options might solve the error
+ throw $e;
+ }
+ }
+
+ foreach ($configInput as $configOption) {
+ if (!strpos($configOption, '=')) {
+ $output->writeln('<error>Invalid mount configuration option "' . $configOption . '"</error>');
+ return;
+ }
+ [$key, $value] = explode('=', $configOption, 2);
+ $storage->setBackendOption($key, $value);
+ }
+
+ $backend = $storage->getBackend();
+ // update status (can be time-consuming)
+ $storage->setStatus(
+ MountConfig::getBackendStatus(
+ $backend->getStorageClass(),
+ $storage->getBackendOptions(),
+ )
+ );
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ $status = $e->getCode() ?: StorageNotAvailableException::STATUS_INDETERMINATE;
+ $storage->setStatus(
+ $status,
+ $e->getMessage()
+ );
+ } catch (StorageNotAvailableException $e) {
+ $storage->setStatus(
+ $e->getCode(),
+ $e->getMessage()
+ );
+ } catch (\Exception $e) {
+ // FIXME: convert storage exceptions to StorageNotAvailableException
+ $storage->setStatus(
+ StorageNotAvailableException::STATUS_ERROR,
+ get_class($e) . ': ' . $e->getMessage()
+ );
+ }
+ }
+}
diff --git a/apps/files_external/lib/Config/ConfigAdapter.php b/apps/files_external/lib/Config/ConfigAdapter.php
new file mode 100644
index 00000000000..a46c0fd5c66
--- /dev/null
+++ b/apps/files_external/lib/Config/ConfigAdapter.php
@@ -0,0 +1,171 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Config;
+
+use OC\Files\Cache\Storage;
+use OC\Files\Storage\FailedStorage;
+use OC\Files\Storage\Wrapper\Availability;
+use OC\Files\Storage\Wrapper\KnownMtime;
+use OCA\Files_External\Lib\PersonalMount;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\MountConfig;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\AppFramework\QueryException;
+use OCP\Files\Config\IMountProvider;
+use OCP\Files\Mount\IMountPoint;
+use OCP\Files\ObjectStore\IObjectStore;
+use OCP\Files\Storage\IConstructableStorage;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\Storage\IStorageFactory;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IUser;
+use OCP\Server;
+use Psr\Clock\ClockInterface;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Make the old files_external config work with the new public mount config api
+ */
+class ConfigAdapter implements IMountProvider {
+ public function __construct(
+ private UserStoragesService $userStoragesService,
+ private UserGlobalStoragesService $userGlobalStoragesService,
+ private ClockInterface $clock,
+ ) {
+ }
+
+ /**
+ * @param class-string $class
+ * @return class-string<IObjectStore>
+ * @throws \InvalidArgumentException
+ * @psalm-taint-escape callable
+ */
+ private function validateObjectStoreClassString(string $class): string {
+ if (!\is_subclass_of($class, IObjectStore::class)) {
+ throw new \InvalidArgumentException('Invalid object store');
+ }
+ return $class;
+ }
+
+ /**
+ * Process storage ready for mounting
+ *
+ * @throws QueryException
+ */
+ private function prepareStorageConfig(StorageConfig &$storage, IUser $user): void {
+ foreach ($storage->getBackendOptions() as $option => $value) {
+ $storage->setBackendOption($option, MountConfig::substitutePlaceholdersInConfig($value, $user->getUID()));
+ }
+
+ $objectStore = $storage->getBackendOption('objectstore');
+ if ($objectStore) {
+ $objectClass = $this->validateObjectStoreClassString($objectStore['class']);
+ $storage->setBackendOption('objectstore', new $objectClass($objectStore));
+ }
+
+ $storage->getAuthMechanism()->manipulateStorageConfig($storage, $user);
+ $storage->getBackend()->manipulateStorageConfig($storage, $user);
+ }
+
+ /**
+ * Construct the storage implementation
+ *
+ * @param StorageConfig $storageConfig
+ */
+ private function constructStorage(StorageConfig $storageConfig): IStorage {
+ $class = $storageConfig->getBackend()->getStorageClass();
+ if (!is_a($class, IConstructableStorage::class, true)) {
+ Server::get(LoggerInterface::class)->warning('Building a storage not implementing IConstructableStorage is deprecated since 31.0.0', ['class' => $class]);
+ }
+ $storage = new $class($storageConfig->getBackendOptions());
+
+ // auth mechanism should fire first
+ $storage = $storageConfig->getBackend()->wrapStorage($storage);
+ $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
+
+ return $storage;
+ }
+
+ /**
+ * Get all mountpoints applicable for the user
+ *
+ * @return IMountPoint[]
+ */
+ public function getMountsForUser(IUser $user, IStorageFactory $loader) {
+ $this->userStoragesService->setUser($user);
+ $this->userGlobalStoragesService->setUser($user);
+
+ $storageConfigs = $this->userGlobalStoragesService->getAllStoragesForUser();
+
+ $storages = array_map(function (StorageConfig $storageConfig) use ($user) {
+ try {
+ $this->prepareStorageConfig($storageConfig, $user);
+ return $this->constructStorage($storageConfig);
+ } catch (\Exception $e) {
+ // propagate exception into filesystem
+ return new FailedStorage(['exception' => $e]);
+ }
+ }, $storageConfigs);
+
+
+ Storage::getGlobalCache()->loadForStorageIds(array_map(function (IStorage $storage) {
+ return $storage->getId();
+ }, $storages));
+
+ $availableStorages = array_map(function (IStorage $storage, StorageConfig $storageConfig): IStorage {
+ try {
+ $availability = $storage->getAvailability();
+ if (!$availability['available'] && !Availability::shouldRecheck($availability)) {
+ $storage = new FailedStorage([
+ 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available')
+ ]);
+ }
+ } catch (\Exception $e) {
+ // propagate exception into filesystem
+ $storage = new FailedStorage(['exception' => $e]);
+ }
+ return $storage;
+ }, $storages, $storageConfigs);
+
+ $mounts = array_map(function (StorageConfig $storageConfig, IStorage $storage) use ($user, $loader) {
+ $storage->setOwner($user->getUID());
+ if ($storageConfig->getType() === StorageConfig::MOUNT_TYPE_PERSONAL) {
+ return new PersonalMount(
+ $this->userStoragesService,
+ $storageConfig,
+ $storageConfig->getId(),
+ new KnownMtime([
+ 'storage' => $storage,
+ 'clock' => $this->clock,
+ ]),
+ '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(),
+ null,
+ $loader,
+ $storageConfig->getMountOptions(),
+ $storageConfig->getId()
+ );
+ } else {
+ return new SystemMountPoint(
+ $storageConfig,
+ $storage,
+ '/' . $user->getUID() . '/files' . $storageConfig->getMountPoint(),
+ null,
+ $loader,
+ $storageConfig->getMountOptions(),
+ $storageConfig->getId()
+ );
+ }
+ }, $storageConfigs, $availableStorages);
+
+ $this->userStoragesService->resetUser();
+ $this->userGlobalStoragesService->resetUser();
+
+ return $mounts;
+ }
+}
diff --git a/apps/files_external/lib/Config/ExternalMountPoint.php b/apps/files_external/lib/Config/ExternalMountPoint.php
new file mode 100644
index 00000000000..97569ed2913
--- /dev/null
+++ b/apps/files_external/lib/Config/ExternalMountPoint.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Config;
+
+use OC\Files\Mount\MountPoint;
+use OCA\Files_External\Lib\Auth\Password\SessionCredentials;
+use OCA\Files_External\Lib\StorageConfig;
+
+class ExternalMountPoint extends MountPoint {
+
+ public function __construct(
+ protected StorageConfig $storageConfig,
+ $storage,
+ $mountpoint,
+ $arguments = null,
+ $loader = null,
+ $mountOptions = null,
+ $mountId = null,
+ ) {
+ parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId, ConfigAdapter::class);
+ }
+
+ public function getMountType() {
+ return ($this->storageConfig->getAuthMechanism() instanceof SessionCredentials) ? 'external-session' : 'external';
+ }
+
+ public function getStorageConfig(): StorageConfig {
+ return $this->storageConfig;
+ }
+}
diff --git a/apps/files_external/lib/Config/IConfigHandler.php b/apps/files_external/lib/Config/IConfigHandler.php
new file mode 100644
index 00000000000..9e8283cc58b
--- /dev/null
+++ b/apps/files_external/lib/Config/IConfigHandler.php
@@ -0,0 +1,22 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Config;
+
+/**
+ * Interface IConfigHandler
+ *
+ * @package OCA\Files_External\Config
+ * @since 16.0.0
+ */
+interface IConfigHandler {
+ /**
+ * @param mixed $optionValue
+ * @return mixed the same type as $optionValue
+ * @since 16.0.0
+ */
+ public function handle($optionValue);
+}
diff --git a/apps/files_external/lib/Config/SimpleSubstitutionTrait.php b/apps/files_external/lib/Config/SimpleSubstitutionTrait.php
new file mode 100644
index 00000000000..85a76054fa8
--- /dev/null
+++ b/apps/files_external/lib/Config/SimpleSubstitutionTrait.php
@@ -0,0 +1,69 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Config;
+
+/**
+ * Trait SimpleSubstitutionTrait
+ *
+ * @package OCA\Files_External\Config
+ * @since 16.0.0
+ */
+trait SimpleSubstitutionTrait {
+ /**
+ * @var string the placeholder without $ prefix
+ * @since 16.0.0
+ */
+ protected $placeholder;
+
+ /** @var string */
+ protected $sanitizedPlaceholder;
+
+ /**
+ * @param mixed $optionValue
+ * @param string $replacement
+ * @return mixed
+ * @since 16.0.0
+ */
+ private function processInput($optionValue, string $replacement) {
+ $this->checkPlaceholder();
+ if (is_array($optionValue)) {
+ foreach ($optionValue as &$value) {
+ $value = $this->substituteIfString($value, $replacement);
+ }
+ } else {
+ $optionValue = $this->substituteIfString($optionValue, $replacement);
+ }
+ return $optionValue;
+ }
+
+ /**
+ * @throws \RuntimeException
+ */
+ protected function checkPlaceholder(): void {
+ $this->sanitizedPlaceholder = trim(strtolower($this->placeholder));
+ if (!(bool)\preg_match('/^[a-z0-9]*$/', $this->sanitizedPlaceholder)) {
+ throw new \RuntimeException(sprintf(
+ 'Invalid placeholder %s, only [a-z0-9] are allowed', $this->sanitizedPlaceholder
+ ));
+ }
+ if ($this->sanitizedPlaceholder === '') {
+ throw new \RuntimeException('Invalid empty placeholder');
+ }
+ }
+
+ /**
+ * @param mixed $value
+ * @param string $replacement
+ * @return mixed
+ */
+ protected function substituteIfString($value, string $replacement) {
+ if (is_string($value)) {
+ return str_ireplace('$' . $this->sanitizedPlaceholder, $replacement, $value);
+ }
+ return $value;
+ }
+}
diff --git a/apps/files_external/lib/Config/SystemMountPoint.php b/apps/files_external/lib/Config/SystemMountPoint.php
new file mode 100644
index 00000000000..af0bf792140
--- /dev/null
+++ b/apps/files_external/lib/Config/SystemMountPoint.php
@@ -0,0 +1,15 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Config;
+
+use OCP\Files\Mount\IShareOwnerlessMount;
+use OCP\Files\Mount\ISystemMountPoint;
+
+class SystemMountPoint extends ExternalMountPoint implements ISystemMountPoint, IShareOwnerlessMount {
+}
diff --git a/apps/files_external/lib/Config/UserContext.php b/apps/files_external/lib/Config/UserContext.php
new file mode 100644
index 00000000000..fb5c79a9329
--- /dev/null
+++ b/apps/files_external/lib/Config/UserContext.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Config;
+
+use OCP\IRequest;
+use OCP\IUser;
+use OCP\IUserManager;
+use OCP\IUserSession;
+use OCP\Share\Exceptions\ShareNotFound;
+use OCP\Share\IManager as ShareManager;
+
+class UserContext {
+
+ /** @var string */
+ private $userId;
+
+ public function __construct(
+ private IUserSession $session,
+ private ShareManager $shareManager,
+ private IRequest $request,
+ private IUserManager $userManager,
+ ) {
+ }
+
+ public function getSession(): IUserSession {
+ return $this->session;
+ }
+
+ public function setUserId(string $userId): void {
+ $this->userId = $userId;
+ }
+
+ protected function getUserId(): ?string {
+ if ($this->userId !== null) {
+ return $this->userId;
+ }
+ if ($this->session->getUser() !== null) {
+ return $this->session->getUser()->getUID();
+ }
+ try {
+ $shareToken = $this->request->getParam('token');
+ $share = $this->shareManager->getShareByToken($shareToken);
+ return $share->getShareOwner();
+ } catch (ShareNotFound $e) {
+ }
+
+ return null;
+ }
+
+ protected function getUser(): ?IUser {
+ $userId = $this->getUserId();
+ if ($userId !== null) {
+ return $this->userManager->get($userId);
+ }
+ return null;
+ }
+}
diff --git a/apps/files_external/lib/Config/UserPlaceholderHandler.php b/apps/files_external/lib/Config/UserPlaceholderHandler.php
new file mode 100644
index 00000000000..d158e6923c1
--- /dev/null
+++ b/apps/files_external/lib/Config/UserPlaceholderHandler.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Config;
+
+class UserPlaceholderHandler extends UserContext implements IConfigHandler {
+ use SimpleSubstitutionTrait;
+
+ /**
+ * @param mixed $optionValue
+ * @return mixed the same type as $optionValue
+ * @since 16.0.0
+ */
+ public function handle($optionValue) {
+ $this->placeholder = 'user';
+ $uid = $this->getUserId();
+ if ($uid === null) {
+ return $optionValue;
+ }
+ return $this->processInput($optionValue, $uid);
+ }
+}
diff --git a/apps/files_external/lib/ConfigLexicon.php b/apps/files_external/lib/ConfigLexicon.php
new file mode 100644
index 00000000000..154f76c1989
--- /dev/null
+++ b/apps/files_external/lib/ConfigLexicon.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2025 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External;
+
+use OCP\Config\Lexicon\Entry;
+use OCP\Config\Lexicon\ILexicon;
+use OCP\Config\Lexicon\Strictness;
+use OCP\Config\ValueType;
+
+/**
+ * Config Lexicon for files_sharing.
+ *
+ * Please Add & Manage your Config Keys in that file and keep the Lexicon up to date!
+ *
+ * {@see ILexicon}
+ */
+class ConfigLexicon implements ILexicon {
+ public const ALLOW_USER_MOUNTING = 'allow_user_mounting';
+ public const USER_MOUNTING_BACKENDS = 'user_mounting_backends';
+
+ public function getStrictness(): Strictness {
+ return Strictness::NOTICE;
+ }
+
+ public function getAppConfigs(): array {
+ return [
+ new Entry(self::ALLOW_USER_MOUNTING, ValueType::BOOL, false, 'allow users to mount their own external filesystems', true),
+ new Entry(self::USER_MOUNTING_BACKENDS, ValueType::STRING, '', 'list of mounting backends available for users', true),
+ ];
+ }
+
+ public function getUserConfigs(): array {
+ return [];
+ }
+}
diff --git a/apps/files_external/lib/Controller/AjaxController.php b/apps/files_external/lib/Controller/AjaxController.php
new file mode 100644
index 00000000000..5cee6422530
--- /dev/null
+++ b/apps/files_external/lib/Controller/AjaxController.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Controller;
+
+use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
+use OCA\Files_External\Lib\Auth\PublicKey\RSA;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+use OCP\AppFramework\Http\JSONResponse;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUserSession;
+
+class AjaxController extends Controller {
+ /**
+ * @param string $appName
+ * @param IRequest $request
+ * @param RSA $rsaMechanism
+ * @param GlobalAuth $globalAuth
+ * @param IUserSession $userSession
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(
+ $appName,
+ IRequest $request,
+ private RSA $rsaMechanism,
+ private GlobalAuth $globalAuth,
+ private IUserSession $userSession,
+ private IGroupManager $groupManager,
+ private IL10N $l10n,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * @param int $keyLength
+ * @return array
+ */
+ private function generateSshKeys($keyLength) {
+ $key = $this->rsaMechanism->createKey($keyLength);
+ // Replace the placeholder label with a more meaningful one
+ $key['publickey'] = str_replace('phpseclib-generated-key', gethostname(), $key['publickey']);
+
+ return $key;
+ }
+
+ /**
+ * Generates an SSH public/private key pair.
+ *
+ * @param int $keyLength
+ */
+ #[NoAdminRequired]
+ public function getSshKeys($keyLength = 1024) {
+ $key = $this->generateSshKeys($keyLength);
+ return new JSONResponse([
+ 'data' => [
+ 'private_key' => $key['privatekey'],
+ 'public_key' => $key['publickey']
+ ],
+ 'status' => 'success',
+ ]);
+ }
+
+ /**
+ * @param string $uid
+ * @param string $user
+ * @param string $password
+ * @return JSONResponse
+ */
+ #[NoAdminRequired]
+ #[PasswordConfirmationRequired(strict: true)]
+ public function saveGlobalCredentials($uid, $user, $password): JSONResponse {
+ $currentUser = $this->userSession->getUser();
+ if ($currentUser === null) {
+ return new JSONResponse([
+ 'status' => 'error',
+ 'message' => $this->l10n->t('You are not logged in'),
+ ], Http::STATUS_UNAUTHORIZED);
+ }
+
+ // Non-admins can only edit their own credentials
+ // Admin can edit global credentials
+ $allowedToEdit = $uid === ''
+ ? $this->groupManager->isAdmin($currentUser->getUID())
+ : $currentUser->getUID() === $uid;
+
+ if ($allowedToEdit) {
+ $this->globalAuth->saveAuth($uid, $user, $password);
+ return new JSONResponse([
+ 'status' => 'success',
+ ]);
+ }
+
+ return new JSONResponse([
+ 'status' => 'success',
+ 'message' => $this->l10n->t('Permission denied'),
+ ], Http::STATUS_FORBIDDEN);
+ }
+}
diff --git a/apps/files_external/lib/Controller/ApiController.php b/apps/files_external/lib/Controller/ApiController.php
new file mode 100644
index 00000000000..49547357e6b
--- /dev/null
+++ b/apps/files_external/lib/Controller/ApiController.php
@@ -0,0 +1,101 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Controller;
+
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\ResponseDefinitions;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\AppFramework\OCSController;
+use OCP\Constants;
+use OCP\IRequest;
+
+/**
+ * @psalm-import-type Files_ExternalMount from ResponseDefinitions
+ */
+class ApiController extends OCSController {
+
+ public function __construct(
+ string $appName,
+ IRequest $request,
+ private UserGlobalStoragesService $userGlobalStoragesService,
+ private UserStoragesService $userStoragesService,
+ ) {
+ parent::__construct($appName, $request);
+ }
+
+ /**
+ * Formats the given mount config to a mount entry.
+ *
+ * @param string $mountPoint mount point name, relative to the data dir
+ * @param StorageConfig $mountConfig mount config to format
+ *
+ * @return Files_ExternalMount
+ */
+ private function formatMount(string $mountPoint, StorageConfig $mountConfig): array {
+ // split path from mount point
+ $path = \dirname($mountPoint);
+ if ($path === '.' || $path === '/') {
+ $path = '';
+ }
+
+ $isSystemMount = $mountConfig->getType() === StorageConfig::MOUNT_TYPE_ADMIN;
+
+ $permissions = Constants::PERMISSION_READ;
+ // personal mounts can be deleted
+ if (!$isSystemMount) {
+ $permissions |= Constants::PERMISSION_DELETE;
+ }
+
+ $entry = [
+ 'id' => $mountConfig->getId(),
+ 'type' => 'dir',
+ 'name' => basename($mountPoint),
+ 'path' => $path,
+ 'permissions' => $permissions,
+ 'scope' => $isSystemMount ? 'system' : 'personal',
+ 'backend' => $mountConfig->getBackend()->getText(),
+ 'class' => $mountConfig->getBackend()->getIdentifier(),
+ 'config' => $mountConfig->jsonSerialize(true),
+ ];
+ return $entry;
+ }
+
+ /**
+ * Get the mount points visible for this user
+ *
+ * @return DataResponse<Http::STATUS_OK, list<Files_ExternalMount>, array{}>
+ *
+ * 200: User mounts returned
+ */
+ #[NoAdminRequired]
+ public function getUserMounts(): DataResponse {
+ $entries = [];
+ $mountPoints = [];
+
+ foreach ($this->userGlobalStoragesService->getStorages() as $storage) {
+ $mountPoint = $storage->getMountPoint();
+ $mountPoints[$mountPoint] = $storage;
+ }
+
+ foreach ($this->userStoragesService->getStorages() as $storage) {
+ $mountPoint = $storage->getMountPoint();
+ $mountPoints[$mountPoint] = $storage;
+ }
+ foreach ($mountPoints as $mountPoint => $mount) {
+ $entries[] = $this->formatMount($mountPoint, $mount);
+ }
+
+ return new DataResponse($entries);
+ }
+}
diff --git a/apps/files_external/lib/Controller/GlobalStoragesController.php b/apps/files_external/lib/Controller/GlobalStoragesController.php
new file mode 100644
index 00000000000..e7274c9cfb6
--- /dev/null
+++ b/apps/files_external/lib/Controller/GlobalStoragesController.php
@@ -0,0 +1,189 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Controller;
+
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Global storages controller
+ */
+class GlobalStoragesController extends StoragesController {
+ /**
+ * Creates a new global storages controller.
+ *
+ * @param string $AppName application name
+ * @param IRequest $request request object
+ * @param IL10N $l10n l10n service
+ * @param GlobalStoragesService $globalStoragesService storage service
+ * @param LoggerInterface $logger
+ * @param IUserSession $userSession
+ * @param IGroupManager $groupManager
+ * @param IConfig $config
+ */
+ public function __construct(
+ $AppName,
+ IRequest $request,
+ IL10N $l10n,
+ GlobalStoragesService $globalStoragesService,
+ LoggerInterface $logger,
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ IConfig $config,
+ ) {
+ parent::__construct(
+ $AppName,
+ $request,
+ $l10n,
+ $globalStoragesService,
+ $logger,
+ $userSession,
+ $groupManager,
+ $config
+ );
+ }
+
+ /**
+ * Create an external storage entry.
+ *
+ * @param string $mountPoint storage mount point
+ * @param string $backend backend identifier
+ * @param string $authMechanism authentication mechanism identifier
+ * @param array $backendOptions backend-specific options
+ * @param array $mountOptions mount-specific options
+ * @param array $applicableUsers users for which to mount the storage
+ * @param array $applicableGroups groups for which to mount the storage
+ * @param int $priority priority
+ *
+ * @return DataResponse
+ */
+ #[PasswordConfirmationRequired(strict: true)]
+ public function create(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ $applicableUsers,
+ $applicableGroups,
+ $priority,
+ ) {
+ $canCreateNewLocalStorage = $this->config->getSystemValue('files_external_allow_create_new_local', true);
+ if (!$canCreateNewLocalStorage && $backend === 'local') {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Forbidden to manage local mounts')
+ ],
+ Http::STATUS_FORBIDDEN
+ );
+ }
+
+ $newStorage = $this->createStorage(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ $applicableUsers,
+ $applicableGroups,
+ $priority
+ );
+ if ($newStorage instanceof DataResponse) {
+ return $newStorage;
+ }
+
+ $response = $this->validate($newStorage);
+ if (!empty($response)) {
+ return $response;
+ }
+
+ $newStorage = $this->service->addStorage($newStorage);
+
+ $this->updateStorageStatus($newStorage);
+
+ return new DataResponse(
+ $newStorage->jsonSerialize(true),
+ Http::STATUS_CREATED
+ );
+ }
+
+ /**
+ * Update an external storage entry.
+ *
+ * @param int $id storage id
+ * @param string $mountPoint storage mount point
+ * @param string $backend backend identifier
+ * @param string $authMechanism authentication mechanism identifier
+ * @param array $backendOptions backend-specific options
+ * @param array $mountOptions mount-specific options
+ * @param array $applicableUsers users for which to mount the storage
+ * @param array $applicableGroups groups for which to mount the storage
+ * @param int $priority priority
+ *
+ * @return DataResponse
+ */
+ #[PasswordConfirmationRequired(strict: true)]
+ public function update(
+ $id,
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ $applicableUsers,
+ $applicableGroups,
+ $priority,
+ ) {
+ $storage = $this->createStorage(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ $applicableUsers,
+ $applicableGroups,
+ $priority
+ );
+ if ($storage instanceof DataResponse) {
+ return $storage;
+ }
+ $storage->setId($id);
+
+ $response = $this->validate($storage);
+ if (!empty($response)) {
+ return $response;
+ }
+
+ try {
+ $storage = $this->service->updateStorage($storage);
+ } catch (NotFoundException $e) {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id])
+ ],
+ Http::STATUS_NOT_FOUND
+ );
+ }
+
+ $this->updateStorageStatus($storage);
+
+ return new DataResponse(
+ $storage->jsonSerialize(true),
+ Http::STATUS_OK
+ );
+ }
+}
diff --git a/apps/files_external/lib/Controller/StoragesController.php b/apps/files_external/lib/Controller/StoragesController.php
new file mode 100644
index 00000000000..df3a4528054
--- /dev/null
+++ b/apps/files_external/lib/Controller/StoragesController.php
@@ -0,0 +1,317 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Controller;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\MountConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\StoragesService;
+use OCP\AppFramework\Controller;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Base class for storages controllers
+ */
+abstract class StoragesController extends Controller {
+ /**
+ * Creates a new storages controller.
+ *
+ * @param string $AppName application name
+ * @param IRequest $request request object
+ * @param IL10N $l10n l10n service
+ * @param StoragesService $storagesService storage service
+ * @param LoggerInterface $logger
+ */
+ public function __construct(
+ $AppName,
+ IRequest $request,
+ protected IL10N $l10n,
+ protected StoragesService $service,
+ protected LoggerInterface $logger,
+ protected IUserSession $userSession,
+ protected IGroupManager $groupManager,
+ protected IConfig $config,
+ ) {
+ parent::__construct($AppName, $request);
+ }
+
+ /**
+ * Create a storage from its parameters
+ *
+ * @param string $mountPoint storage mount point
+ * @param string $backend backend identifier
+ * @param string $authMechanism authentication mechanism identifier
+ * @param array $backendOptions backend-specific options
+ * @param array|null $mountOptions mount-specific options
+ * @param array|null $applicableUsers users for which to mount the storage
+ * @param array|null $applicableGroups groups for which to mount the storage
+ * @param int|null $priority priority
+ *
+ * @return StorageConfig|DataResponse
+ */
+ protected function createStorage(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions = null,
+ $applicableUsers = null,
+ $applicableGroups = null,
+ $priority = null,
+ ) {
+ $canCreateNewLocalStorage = $this->config->getSystemValue('files_external_allow_create_new_local', true);
+ if (!$canCreateNewLocalStorage && $backend === 'local') {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Forbidden to manage local mounts')
+ ],
+ Http::STATUS_FORBIDDEN
+ );
+ }
+
+ try {
+ return $this->service->createStorage(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ $applicableUsers,
+ $applicableGroups,
+ $priority
+ );
+ } catch (\InvalidArgumentException $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Invalid backend or authentication mechanism class')
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+ }
+
+ /**
+ * Validate storage config
+ *
+ * @param StorageConfig $storage storage config
+ *1
+ * @return DataResponse|null returns response in case of validation error
+ */
+ protected function validate(StorageConfig $storage) {
+ $mountPoint = $storage->getMountPoint();
+ if ($mountPoint === '') {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Invalid mount point'),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+
+ if ($storage->getBackendOption('objectstore')) {
+ // objectstore must not be sent from client side
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Objectstore forbidden'),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+
+ /** @var Backend */
+ $backend = $storage->getBackend();
+ /** @var AuthMechanism */
+ $authMechanism = $storage->getAuthMechanism();
+ if ($backend->checkDependencies()) {
+ // invalid backend
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Invalid storage backend "%s"', [
+ $backend->getIdentifier(),
+ ]),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+
+ if (!$backend->isVisibleFor($this->service->getVisibilityType())) {
+ // not permitted to use backend
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Not permitted to use backend "%s"', [
+ $backend->getIdentifier(),
+ ]),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+ if (!$authMechanism->isVisibleFor($this->service->getVisibilityType())) {
+ // not permitted to use auth mechanism
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Not permitted to use authentication mechanism "%s"', [
+ $authMechanism->getIdentifier(),
+ ]),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+
+ if (!$backend->validateStorage($storage)) {
+ // unsatisfied parameters
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Unsatisfied backend parameters'),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+ if (!$authMechanism->validateStorage($storage)) {
+ // unsatisfied parameters
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Unsatisfied authentication mechanism parameters'),
+ ],
+ Http::STATUS_UNPROCESSABLE_ENTITY
+ );
+ }
+
+ return null;
+ }
+
+ protected function manipulateStorageConfig(StorageConfig $storage) {
+ /** @var AuthMechanism */
+ $authMechanism = $storage->getAuthMechanism();
+ $authMechanism->manipulateStorageConfig($storage);
+ /** @var Backend */
+ $backend = $storage->getBackend();
+ $backend->manipulateStorageConfig($storage);
+ }
+
+ /**
+ * Check whether the given storage is available / valid.
+ *
+ * Note that this operation can be time consuming depending
+ * on whether the remote storage is available or not.
+ *
+ * @param StorageConfig $storage storage configuration
+ */
+ protected function updateStorageStatus(StorageConfig &$storage) {
+ try {
+ $this->manipulateStorageConfig($storage);
+
+ /** @var Backend */
+ $backend = $storage->getBackend();
+ // update status (can be time-consuming)
+ $storage->setStatus(
+ MountConfig::getBackendStatus(
+ $backend->getStorageClass(),
+ $storage->getBackendOptions(),
+ )
+ );
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ $status = $e->getCode() ?: StorageNotAvailableException::STATUS_INDETERMINATE;
+ $storage->setStatus(
+ (int)$status,
+ $this->l10n->t('Insufficient data: %s', [$e->getMessage()])
+ );
+ } catch (StorageNotAvailableException $e) {
+ $storage->setStatus(
+ (int)$e->getCode(),
+ $this->l10n->t('%s', [$e->getMessage()])
+ );
+ } catch (\Exception $e) {
+ // FIXME: convert storage exceptions to StorageNotAvailableException
+ $storage->setStatus(
+ StorageNotAvailableException::STATUS_ERROR,
+ get_class($e) . ': ' . $e->getMessage()
+ );
+ }
+ }
+
+ /**
+ * Get all storage entries
+ *
+ * @return DataResponse
+ */
+ public function index() {
+ $storages = array_map(static fn ($storage) => $storage->jsonSerialize(true), $this->service->getStorages());
+
+ return new DataResponse(
+ $storages,
+ Http::STATUS_OK
+ );
+ }
+
+ /**
+ * Get an external storage entry.
+ *
+ * @param int $id storage id
+ *
+ * @return DataResponse
+ */
+ public function show(int $id) {
+ try {
+ $storage = $this->service->getStorage($id);
+
+ $this->updateStorageStatus($storage);
+ } catch (NotFoundException $e) {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]),
+ ],
+ Http::STATUS_NOT_FOUND
+ );
+ }
+
+ $data = $storage->jsonSerialize(true);
+ $isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID());
+ $data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAL || $isAdmin;
+
+ return new DataResponse(
+ $data,
+ Http::STATUS_OK
+ );
+ }
+
+ /**
+ * Deletes the storage with the given id.
+ *
+ * @param int $id storage id
+ *
+ * @return DataResponse
+ */
+ #[PasswordConfirmationRequired(strict: true)]
+ public function destroy(int $id) {
+ try {
+ $this->service->removeStorage($id);
+ } catch (NotFoundException $e) {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id]),
+ ],
+ Http::STATUS_NOT_FOUND
+ );
+ }
+
+ return new DataResponse([], Http::STATUS_NO_CONTENT);
+ }
+}
diff --git a/apps/files_external/lib/Controller/UserGlobalStoragesController.php b/apps/files_external/lib/Controller/UserGlobalStoragesController.php
new file mode 100644
index 00000000000..88a9f936401
--- /dev/null
+++ b/apps/files_external/lib/Controller/UserGlobalStoragesController.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Controller;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\IUserProvided;
+use OCA\Files_External\Lib\Auth\Password\UserGlobalAuth;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+/**
+ * User global storages controller
+ */
+class UserGlobalStoragesController extends StoragesController {
+ /**
+ * Creates a new user global storages controller.
+ *
+ * @param string $AppName application name
+ * @param IRequest $request request object
+ * @param IL10N $l10n l10n service
+ * @param UserGlobalStoragesService $userGlobalStoragesService storage service
+ * @param LoggerInterface $logger
+ * @param IUserSession $userSession
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(
+ $AppName,
+ IRequest $request,
+ IL10N $l10n,
+ UserGlobalStoragesService $userGlobalStoragesService,
+ LoggerInterface $logger,
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ IConfig $config,
+ ) {
+ parent::__construct(
+ $AppName,
+ $request,
+ $l10n,
+ $userGlobalStoragesService,
+ $logger,
+ $userSession,
+ $groupManager,
+ $config
+ );
+ }
+
+ /**
+ * Get all storage entries
+ *
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function index() {
+ /** @var UserGlobalStoragesService */
+ $service = $this->service;
+ $storages = array_map(function ($storage) {
+ // remove configuration data, this must be kept private
+ $this->sanitizeStorage($storage);
+ return $storage->jsonSerialize(true);
+ }, $service->getUniqueStorages());
+
+ return new DataResponse(
+ $storages,
+ Http::STATUS_OK
+ );
+ }
+
+ protected function manipulateStorageConfig(StorageConfig $storage) {
+ /** @var AuthMechanism */
+ $authMechanism = $storage->getAuthMechanism();
+ $authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser());
+ /** @var Backend */
+ $backend = $storage->getBackend();
+ $backend->manipulateStorageConfig($storage, $this->userSession->getUser());
+ }
+
+ /**
+ * Get an external storage entry.
+ *
+ * @param int $id storage id
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function show($id) {
+ try {
+ $storage = $this->service->getStorage($id);
+
+ $this->updateStorageStatus($storage);
+ } catch (NotFoundException $e) {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id])
+ ],
+ Http::STATUS_NOT_FOUND
+ );
+ }
+
+ $this->sanitizeStorage($storage);
+
+ $data = $storage->jsonSerialize(true);
+ $isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID());
+ $data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAL || $isAdmin;
+
+ return new DataResponse(
+ $data,
+ Http::STATUS_OK
+ );
+ }
+
+ /**
+ * Update an external storage entry.
+ * Only allows setting user provided backend fields
+ *
+ * @param int $id storage id
+ * @param array $backendOptions backend-specific options
+ *
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[PasswordConfirmationRequired(strict: true)]
+ public function update(
+ $id,
+ $backendOptions,
+ ) {
+ try {
+ $storage = $this->service->getStorage($id);
+ $authMechanism = $storage->getAuthMechanism();
+ if ($authMechanism instanceof IUserProvided || $authMechanism instanceof UserGlobalAuth) {
+ $authMechanism->saveBackendOptions($this->userSession->getUser(), $id, $backendOptions);
+ $authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser());
+ } else {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" is not editable by non-admins', [$id])
+ ],
+ Http::STATUS_FORBIDDEN
+ );
+ }
+ } catch (NotFoundException $e) {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id])
+ ],
+ Http::STATUS_NOT_FOUND
+ );
+ }
+
+ $this->updateStorageStatus($storage);
+ $this->sanitizeStorage($storage);
+
+ return new DataResponse(
+ $storage->jsonSerialize(true),
+ Http::STATUS_OK
+ );
+ }
+
+ /**
+ * Remove sensitive data from a StorageConfig before returning it to the user
+ *
+ * @param StorageConfig $storage
+ */
+ protected function sanitizeStorage(StorageConfig $storage) {
+ $storage->setBackendOptions([]);
+ $storage->setMountOptions([]);
+
+ if ($storage->getAuthMechanism() instanceof IUserProvided) {
+ try {
+ $storage->getAuthMechanism()->manipulateStorageConfig($storage, $this->userSession->getUser());
+ } catch (InsufficientDataForMeaningfulAnswerException $e) {
+ // not configured yet
+ }
+ }
+ }
+}
diff --git a/apps/files_external/lib/Controller/UserStoragesController.php b/apps/files_external/lib/Controller/UserStoragesController.php
new file mode 100644
index 00000000000..7b564d57f7e
--- /dev/null
+++ b/apps/files_external/lib/Controller/UserStoragesController.php
@@ -0,0 +1,214 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Controller;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\NotFoundException;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\AppFramework\Http;
+use OCP\AppFramework\Http\Attribute\NoAdminRequired;
+use OCP\AppFramework\Http\Attribute\PasswordConfirmationRequired;
+use OCP\AppFramework\Http\DataResponse;
+use OCP\IConfig;
+use OCP\IGroupManager;
+use OCP\IL10N;
+use OCP\IRequest;
+use OCP\IUserSession;
+use Psr\Log\LoggerInterface;
+
+/**
+ * User storages controller
+ */
+class UserStoragesController extends StoragesController {
+ /**
+ * Creates a new user storages controller.
+ *
+ * @param string $AppName application name
+ * @param IRequest $request request object
+ * @param IL10N $l10n l10n service
+ * @param UserStoragesService $userStoragesService storage service
+ * @param LoggerInterface $logger
+ * @param IUserSession $userSession
+ * @param IGroupManager $groupManager
+ */
+ public function __construct(
+ $AppName,
+ IRequest $request,
+ IL10N $l10n,
+ UserStoragesService $userStoragesService,
+ LoggerInterface $logger,
+ IUserSession $userSession,
+ IGroupManager $groupManager,
+ IConfig $config,
+ ) {
+ parent::__construct(
+ $AppName,
+ $request,
+ $l10n,
+ $userStoragesService,
+ $logger,
+ $userSession,
+ $groupManager,
+ $config
+ );
+ }
+
+ protected function manipulateStorageConfig(StorageConfig $storage) {
+ /** @var AuthMechanism */
+ $authMechanism = $storage->getAuthMechanism();
+ $authMechanism->manipulateStorageConfig($storage, $this->userSession->getUser());
+ /** @var Backend */
+ $backend = $storage->getBackend();
+ $backend->manipulateStorageConfig($storage, $this->userSession->getUser());
+ }
+
+ /**
+ * Get all storage entries
+ *
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ public function index() {
+ return parent::index();
+ }
+
+ /**
+ * Return storage
+ *
+ * {@inheritdoc}
+ */
+ #[NoAdminRequired]
+ public function show(int $id) {
+ return parent::show($id);
+ }
+
+ /**
+ * Create an external storage entry.
+ *
+ * @param string $mountPoint storage mount point
+ * @param string $backend backend identifier
+ * @param string $authMechanism authentication mechanism identifier
+ * @param array $backendOptions backend-specific options
+ * @param array $mountOptions backend-specific mount options
+ *
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[PasswordConfirmationRequired(strict: true)]
+ public function create(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ ) {
+ $canCreateNewLocalStorage = $this->config->getSystemValue('files_external_allow_create_new_local', true);
+ if (!$canCreateNewLocalStorage && $backend === 'local') {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Forbidden to manage local mounts')
+ ],
+ Http::STATUS_FORBIDDEN
+ );
+ }
+ $newStorage = $this->createStorage(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions
+ );
+ if ($newStorage instanceof DataResponse) {
+ return $newStorage;
+ }
+
+ $response = $this->validate($newStorage);
+ if (!empty($response)) {
+ return $response;
+ }
+
+ $newStorage = $this->service->addStorage($newStorage);
+ $this->updateStorageStatus($newStorage);
+
+ return new DataResponse(
+ $newStorage->jsonSerialize(true),
+ Http::STATUS_CREATED
+ );
+ }
+
+ /**
+ * Update an external storage entry.
+ *
+ * @param int $id storage id
+ * @param string $mountPoint storage mount point
+ * @param string $backend backend identifier
+ * @param string $authMechanism authentication mechanism identifier
+ * @param array $backendOptions backend-specific options
+ * @param array $mountOptions backend-specific mount options
+ *
+ * @return DataResponse
+ */
+ #[NoAdminRequired]
+ #[PasswordConfirmationRequired(strict: true)]
+ public function update(
+ $id,
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions,
+ ) {
+ $storage = $this->createStorage(
+ $mountPoint,
+ $backend,
+ $authMechanism,
+ $backendOptions,
+ $mountOptions
+ );
+ if ($storage instanceof DataResponse) {
+ return $storage;
+ }
+ $storage->setId($id);
+
+ $response = $this->validate($storage);
+ if (!empty($response)) {
+ return $response;
+ }
+
+ try {
+ $storage = $this->service->updateStorage($storage);
+ } catch (NotFoundException $e) {
+ return new DataResponse(
+ [
+ 'message' => $this->l10n->t('Storage with ID "%d" not found', [$id])
+ ],
+ Http::STATUS_NOT_FOUND
+ );
+ }
+
+ $this->updateStorageStatus($storage);
+
+ return new DataResponse(
+ $storage->jsonSerialize(true),
+ Http::STATUS_OK
+ );
+ }
+
+ /**
+ * Delete storage
+ *
+ * {@inheritdoc}
+ */
+ #[NoAdminRequired]
+ #[PasswordConfirmationRequired(strict: true)]
+ public function destroy(int $id) {
+ return parent::destroy($id);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php
new file mode 100644
index 00000000000..c86c88a13d7
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/AmazonS3/AccessKey.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\AmazonS3;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+/**
+ * Amazon S3 access key authentication
+ */
+class AccessKey extends AuthMechanism {
+ public const SCHEME_AMAZONS3_ACCESSKEY = 'amazons3_accesskey';
+
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('amazons3::accesskey')
+ ->setScheme(self::SCHEME_AMAZONS3_ACCESSKEY)
+ ->setText($l->t('Access key'))
+ ->addParameters([
+ new DefinitionParameter('key', $l->t('Access key')),
+ (new DefinitionParameter('secret', $l->t('Secret key')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ ]);
+ }
+}
diff --git a/apps/files_external/lib/auth/authmechanism.php b/apps/files_external/lib/Lib/Auth/AuthMechanism.php
index 68d6f023487..7b0544100fb 100644
--- a/apps/files_external/lib/auth/authmechanism.php
+++ b/apps/files_external/lib/Lib/Auth/AuthMechanism.php
@@ -1,32 +1,19 @@
<?php
+
/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_External\Lib\Auth;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCA\Files_External\Lib\VisibilityTrait;
-use \OCA\Files_External\Lib\IdentifierTrait;
-use \OCA\Files_External\Lib\FrontendDefinitionTrait;
-use \OCA\Files_External\Lib\StorageModifierTrait;
+use OCA\Files_External\Lib\FrontendDefinitionTrait;
+use OCA\Files_External\Lib\IdentifierTrait;
+use OCA\Files_External\Lib\IFrontendDefinition;
+use OCA\Files_External\Lib\IIdentifier;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Lib\StorageModifierTrait;
+use OCA\Files_External\Lib\VisibilityTrait;
/**
* Authentication mechanism
@@ -35,7 +22,7 @@ use \OCA\Files_External\Lib\StorageModifierTrait;
* such as \OCP\IDB for database operations. This allows an authentication
* mechanism to perform advanced operations based on provided information.
*
- * An authenication scheme defines the parameter interface, common to the
+ * An authentication scheme defines the parameter interface, common to the
* storage implementation, the backend and the authentication mechanism.
* A storage implementation expects parameters according to the authentication
* scheme, which are provided from the authentication mechanism.
@@ -48,16 +35,15 @@ use \OCA\Files_External\Lib\StorageModifierTrait;
* - StorageModifierTrait
* Object can affect storage mounting
*/
-class AuthMechanism implements \JsonSerializable {
-
+class AuthMechanism implements \JsonSerializable, IIdentifier, IFrontendDefinition {
/** Standard authentication schemes */
- const SCHEME_NULL = 'null';
- const SCHEME_BUILTIN = 'builtin';
- const SCHEME_PASSWORD = 'password';
- const SCHEME_OAUTH1 = 'oauth1';
- const SCHEME_OAUTH2 = 'oauth2';
- const SCHEME_PUBLICKEY = 'publickey';
- const SCHEME_OPENSTACK = 'openstack';
+ public const SCHEME_NULL = 'null';
+ public const SCHEME_BUILTIN = 'builtin';
+ public const SCHEME_PASSWORD = 'password';
+ public const SCHEME_OAUTH2 = 'oauth2';
+ public const SCHEME_PUBLICKEY = 'publickey';
+ public const SCHEME_OPENSTACK = 'openstack';
+ public const SCHEME_SMB = 'smb';
use VisibilityTrait;
use FrontendDefinitionTrait;
@@ -79,7 +65,7 @@ class AuthMechanism implements \JsonSerializable {
/**
* @param string $scheme
- * @return self
+ * @return $this
*/
public function setScheme($scheme) {
$this->scheme = $scheme;
@@ -88,10 +74,8 @@ class AuthMechanism implements \JsonSerializable {
/**
* Serialize into JSON for client-side JS
- *
- * @return array
*/
- public function jsonSerialize() {
+ public function jsonSerialize(): array {
$data = $this->jsonSerializeDefinition();
$data += $this->jsonSerializeIdentifier();
@@ -116,5 +100,4 @@ class AuthMechanism implements \JsonSerializable {
return $this->validateStorageDefinition($storage);
}
-
}
diff --git a/apps/files_external/lib/Lib/Auth/Builtin.php b/apps/files_external/lib/Lib/Auth/Builtin.php
new file mode 100644
index 00000000000..8e12a6daca6
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Builtin.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth;
+
+use OCP\IL10N;
+
+/**
+ * Builtin authentication mechanism, for legacy backends
+ */
+class Builtin extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('builtin::builtin')
+ ->setScheme(self::SCHEME_BUILTIN)
+ ->setText($l->t('Builtin'))
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/IUserProvided.php b/apps/files_external/lib/Lib/Auth/IUserProvided.php
new file mode 100644
index 00000000000..2350d7f6db4
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/IUserProvided.php
@@ -0,0 +1,21 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth;
+
+use OCP\IUser;
+
+/**
+ * For auth mechanisms where the user needs to provide credentials
+ */
+interface IUserProvided {
+ /**
+ * @param IUser $user the user for which to save the user provided options
+ * @param int $mountId the mount id to save the options for
+ * @param array $options the user provided options
+ */
+ public function saveBackendOptions(IUser $user, $mountId, array $options);
+}
diff --git a/apps/files_external/lib/Lib/Auth/InvalidAuth.php b/apps/files_external/lib/Lib/Auth/InvalidAuth.php
new file mode 100644
index 00000000000..2af24f1ea07
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/InvalidAuth.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth;
+
+/**
+ * Invalid authentication representing an auth mechanism
+ * that could not be resolved0
+ */
+class InvalidAuth extends AuthMechanism {
+
+ /**
+ * Constructs a new InvalidAuth with the id of the invalid auth
+ * for display purposes
+ *
+ * @param string $invalidId invalid id
+ */
+ public function __construct($invalidId) {
+ $this
+ ->setIdentifier($invalidId)
+ ->setScheme(self::SCHEME_NULL)
+ ->setText('Unknown auth mechanism backend ' . $invalidId)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/NullMechanism.php b/apps/files_external/lib/Lib/Auth/NullMechanism.php
new file mode 100644
index 00000000000..8e2e5b656b2
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/NullMechanism.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth;
+
+use OCP\IL10N;
+
+/**
+ * Null authentication mechanism
+ */
+class NullMechanism extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('null::null')
+ ->setScheme(self::SCHEME_NULL)
+ ->setText($l->t('None'))
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php
new file mode 100644
index 00000000000..beaf73c2344
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/OAuth2/OAuth2.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\OAuth2;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+/**
+ * OAuth2 authentication
+ */
+class OAuth2 extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('oauth2::oauth2')
+ ->setScheme(self::SCHEME_OAUTH2)
+ ->setText($l->t('OAuth2'))
+ ->addParameters([
+ (new DefinitionParameter('configured', 'configured'))
+ ->setType(DefinitionParameter::VALUE_TEXT)
+ ->setFlag(DefinitionParameter::FLAG_HIDDEN),
+ new DefinitionParameter('client_id', $l->t('Client ID')),
+ (new DefinitionParameter('client_secret', $l->t('Client secret')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ (new DefinitionParameter('token', 'token'))
+ ->setType(DefinitionParameter::VALUE_PASSWORD)
+ ->setFlag(DefinitionParameter::FLAG_HIDDEN),
+ ])
+ ->addCustomJs('oauth2')
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php
new file mode 100644
index 00000000000..3b1c9f123af
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV2.php
@@ -0,0 +1,32 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\OpenStack;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+/**
+ * OpenStack Keystone authentication
+ */
+class OpenStackV2 extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('openstack::openstack')
+ ->setScheme(self::SCHEME_OPENSTACK)
+ ->setText($l->t('OpenStack v2'))
+ ->addParameters([
+ new DefinitionParameter('user', $l->t('Login')),
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ new DefinitionParameter('tenant', $l->t('Tenant name')),
+ new DefinitionParameter('url', $l->t('Identity endpoint URL')),
+ ])
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php
new file mode 100644
index 00000000000..b5d185fd374
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/OpenStack/OpenStackV3.php
@@ -0,0 +1,34 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Auth\OpenStack;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+/**
+ * OpenStack Keystone authentication
+ */
+class OpenStackV3 extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('openstack::openstackv3')
+ ->setScheme(self::SCHEME_OPENSTACK)
+ ->setText($l->t('OpenStack v3'))
+ ->addParameters([
+ new DefinitionParameter('user', $l->t('Login')),
+ new DefinitionParameter('domain', $l->t('Domain')),
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ new DefinitionParameter('tenant', $l->t('Tenant name')),
+ new DefinitionParameter('url', $l->t('Identity endpoint URL'))
+ ])
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php
new file mode 100644
index 00000000000..b1d1068e586
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/OpenStack/Rackspace.php
@@ -0,0 +1,30 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\OpenStack;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+/**
+ * Rackspace authentication
+ */
+class Rackspace extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('openstack::rackspace')
+ ->setScheme(self::SCHEME_OPENSTACK)
+ ->setText($l->t('Rackspace'))
+ ->addParameters([
+ new DefinitionParameter('user', $l->t('Login')),
+ (new DefinitionParameter('key', $l->t('API key')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ ])
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php b/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php
new file mode 100644
index 00000000000..916b496b506
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Password/GlobalAuth.php
@@ -0,0 +1,80 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\Password;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Security\ICredentialsManager;
+
+/**
+ * Global Username and Password
+ */
+class GlobalAuth extends AuthMechanism {
+ public const CREDENTIALS_IDENTIFIER = 'password::global';
+ private const PWD_PLACEHOLDER = '************************';
+
+ public function __construct(
+ IL10N $l,
+ protected ICredentialsManager $credentialsManager,
+ ) {
+ $this
+ ->setIdentifier('password::global')
+ ->setVisibility(BackendService::VISIBILITY_DEFAULT)
+ ->setScheme(self::SCHEME_PASSWORD)
+ ->setText($l->t('Global credentials'));
+ }
+
+ public function getAuth($uid) {
+ $auth = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER);
+ if (!is_array($auth)) {
+ return [
+ 'user' => '',
+ 'password' => ''
+ ];
+ } else {
+ $auth['password'] = self::PWD_PLACEHOLDER;
+ return $auth;
+ }
+ }
+
+ public function saveAuth($uid, $user, $password) {
+ // Use old password if it has not changed.
+ if ($password === self::PWD_PLACEHOLDER) {
+ $auth = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER);
+ $password = $auth['password'];
+ }
+
+ $this->credentialsManager->store($uid, self::CREDENTIALS_IDENTIFIER, [
+ 'user' => $user,
+ 'password' => $password
+ ]);
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ if ($storage->getType() === StorageConfig::MOUNT_TYPE_ADMIN) {
+ $uid = '';
+ } elseif (is_null($user)) {
+ throw new InsufficientDataForMeaningfulAnswerException('No credentials saved');
+ } else {
+ $uid = $user->getUID();
+ }
+ $credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER);
+
+ if (is_array($credentials)) {
+ $storage->setBackendOption('user', $credentials['user']);
+ $storage->setBackendOption('password', $credentials['password']);
+ }
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php
new file mode 100644
index 00000000000..ce38140b6ee
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Password/LoginCredentials.php
@@ -0,0 +1,113 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\Password;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Listener\StorePasswordListener;
+use OCP\Authentication\Exceptions\CredentialsUnavailableException;
+use OCP\Authentication\LoginCredentials\IStore as CredentialsStore;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IL10N;
+use OCP\ISession;
+use OCP\IUser;
+use OCP\IUserBackend;
+use OCP\LDAP\ILDAPProviderFactory;
+use OCP\Security\ICredentialsManager;
+use OCP\User\Events\PasswordUpdatedEvent;
+use OCP\User\Events\UserLoggedInEvent;
+
+/**
+ * Username and password from login credentials, saved in DB
+ */
+class LoginCredentials extends AuthMechanism {
+ public const CREDENTIALS_IDENTIFIER = 'password::logincredentials/credentials';
+
+ public function __construct(
+ IL10N $l,
+ protected ISession $session,
+ protected ICredentialsManager $credentialsManager,
+ private CredentialsStore $credentialsStore,
+ IEventDispatcher $eventDispatcher,
+ private ILDAPProviderFactory $ldapFactory,
+ ) {
+ $this
+ ->setIdentifier('password::logincredentials')
+ ->setScheme(self::SCHEME_PASSWORD)
+ ->setText($l->t('Log-in credentials, save in database'))
+ ->addParameters([
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD)
+ ->setFlag(DefinitionParameter::FLAG_HIDDEN)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ]);
+
+ $eventDispatcher->addServiceListener(UserLoggedInEvent::class, StorePasswordListener::class);
+ $eventDispatcher->addServiceListener(PasswordUpdatedEvent::class, StorePasswordListener::class);
+ }
+
+ private function getCredentials(IUser $user): array {
+ $credentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER);
+
+ if (is_null($credentials)) {
+ // nothing saved in db, try to get it from the session and save it
+ try {
+ $sessionCredentials = $this->credentialsStore->getLoginCredentials();
+
+ if ($sessionCredentials->getUID() !== $user->getUID()) {
+ // Can't take the credentials from the session as they are not the same user
+ throw new CredentialsUnavailableException();
+ }
+
+ $credentials = [
+ 'user' => $sessionCredentials->getLoginName(),
+ 'password' => $sessionCredentials->getPassword(),
+ ];
+
+ $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials);
+ } catch (CredentialsUnavailableException $e) {
+ throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved');
+ }
+ }
+
+ return $credentials;
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ if (!isset($user)) {
+ throw new InsufficientDataForMeaningfulAnswerException('No login credentials saved');
+ }
+ $credentials = $this->getCredentials($user);
+
+ $loginKey = $storage->getBackendOption('login_ldap_attr');
+ if ($loginKey) {
+ $backend = $user->getBackend();
+ if ($backend instanceof IUserBackend && $backend->getBackendName() === 'LDAP') {
+ $value = $this->getLdapPropertyForUser($user, $loginKey);
+ if ($value === null) {
+ throw new InsufficientDataForMeaningfulAnswerException('Custom ldap attribute not set for user ' . $user->getUID());
+ }
+ $storage->setBackendOption('user', $value);
+ } else {
+ throw new InsufficientDataForMeaningfulAnswerException('Custom ldap attribute configured but user ' . $user->getUID() . ' is not an ldap user');
+ }
+ } else {
+ $storage->setBackendOption('user', $credentials['user']);
+ }
+ $storage->setBackendOption('password', $credentials['password']);
+ }
+
+ private function getLdapPropertyForUser(IUser $user, string $property): ?string {
+ return $this->ldapFactory->getLDAPProvider()->getUserAttribute($user->getUID(), $property);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/Password/Password.php b/apps/files_external/lib/Lib/Auth/Password/Password.php
new file mode 100644
index 00000000000..d4291148e3e
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Password/Password.php
@@ -0,0 +1,29 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\Password;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+/**
+ * Basic password authentication mechanism
+ */
+class Password extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('password::password')
+ ->setScheme(self::SCHEME_PASSWORD)
+ ->setText($l->t('Login and password'))
+ ->addParameters([
+ new DefinitionParameter('user', $l->t('Login')),
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ ]);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php
new file mode 100644
index 00000000000..8f161073771
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Password/SessionCredentials.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\Password;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\SessionStorageWrapper;
+use OCA\Files_External\Lib\StorageConfig;
+use OCP\Authentication\Exceptions\CredentialsUnavailableException;
+use OCP\Authentication\LoginCredentials\IStore as CredentialsStore;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\StorageAuthException;
+use OCP\IL10N;
+use OCP\IUser;
+
+/**
+ * Username and password from login credentials, saved in session
+ */
+class SessionCredentials extends AuthMechanism {
+
+ public function __construct(
+ IL10N $l,
+ private CredentialsStore $credentialsStore,
+ ) {
+ $this->setIdentifier('password::sessioncredentials')
+ ->setScheme(self::SCHEME_PASSWORD)
+ ->setText($l->t('Log-in credentials, save in session'))
+ ->addParameters([
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD)
+ ->setFlag(DefinitionParameter::FLAG_HIDDEN)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ]);
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ try {
+ $credentials = $this->credentialsStore->getLoginCredentials();
+ } catch (CredentialsUnavailableException $e) {
+ throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
+ }
+
+ if ($user === null) {
+ throw new StorageAuthException('Session unavailable');
+ }
+
+ if ($credentials->getUID() !== $user->getUID()) {
+ throw new StorageAuthException('Session credentials for storage owner not available');
+ }
+
+ $storage->setBackendOption('user', $credentials->getLoginName());
+ $storage->setBackendOption('password', $credentials->getPassword());
+ }
+
+ public function wrapStorage(IStorage $storage): IStorage {
+ return new SessionStorageWrapper(['storage' => $storage]);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php
new file mode 100644
index 00000000000..cb7165261ac
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php
@@ -0,0 +1,73 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Auth\Password;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Security\ICredentialsManager;
+
+/**
+ * User provided Global Username and Password
+ */
+class UserGlobalAuth extends AuthMechanism {
+ private const CREDENTIALS_IDENTIFIER = 'password::global';
+
+ public function __construct(
+ IL10N $l,
+ protected ICredentialsManager $credentialsManager,
+ ) {
+ $this
+ ->setIdentifier('password::global::user')
+ ->setVisibility(BackendService::VISIBILITY_DEFAULT)
+ ->setScheme(self::SCHEME_PASSWORD)
+ ->setText($l->t('Global credentials, manually entered'));
+ }
+
+ public function saveBackendOptions(IUser $user, $id, $backendOptions) {
+ // backendOptions are set when invoked via Files app
+ // but they are not set when invoked via ext storage settings
+ if (!isset($backendOptions['user']) && !isset($backendOptions['password'])) {
+ return;
+ }
+
+ if ($backendOptions['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) {
+ $oldCredentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER);
+ $backendOptions['password'] = $oldCredentials['password'];
+ }
+
+ // make sure we're not setting any unexpected keys
+ $credentials = [
+ 'user' => $backendOptions['user'],
+ 'password' => $backendOptions['password'],
+ ];
+ $this->credentialsManager->store($user->getUID(), self::CREDENTIALS_IDENTIFIER, $credentials);
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ if ($user === null) {
+ throw new InsufficientDataForMeaningfulAnswerException('No credentials saved');
+ }
+
+ $uid = $user->getUID();
+ $credentials = $this->credentialsManager->retrieve($uid, self::CREDENTIALS_IDENTIFIER);
+
+ if (is_array($credentials)) {
+ $storage->setBackendOption('user', $credentials['user']);
+ $storage->setBackendOption('password', $credentials['password']);
+ }
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/Password/UserProvided.php b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php
new file mode 100644
index 00000000000..b158392f6eb
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/Password/UserProvided.php
@@ -0,0 +1,77 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2015 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\Password;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\IUserProvided;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCP\IL10N;
+use OCP\IUser;
+use OCP\Security\ICredentialsManager;
+
+/**
+ * User provided Username and Password
+ */
+class UserProvided extends AuthMechanism implements IUserProvided {
+ public const CREDENTIALS_IDENTIFIER_PREFIX = 'password::userprovided/';
+
+ public function __construct(
+ IL10N $l,
+ protected ICredentialsManager $credentialsManager,
+ ) {
+ $this
+ ->setIdentifier('password::userprovided')
+ ->setVisibility(BackendService::VISIBILITY_ADMIN)
+ ->setScheme(self::SCHEME_PASSWORD)
+ ->setText($l->t('Manually entered, store in database'))
+ ->addParameters([
+ (new DefinitionParameter('user', $l->t('Login')))
+ ->setFlag(DefinitionParameter::FLAG_USER_PROVIDED),
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD)
+ ->setFlag(DefinitionParameter::FLAG_USER_PROVIDED),
+ ]);
+ }
+
+ private function getCredentialsIdentifier($storageId) {
+ return self::CREDENTIALS_IDENTIFIER_PREFIX . $storageId;
+ }
+
+ public function saveBackendOptions(IUser $user, $mountId, array $options) {
+ if ($options['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) {
+ $oldCredentials = $this->credentialsManager->retrieve($user->getUID(), $this->getCredentialsIdentifier($mountId));
+ $options['password'] = $oldCredentials['password'];
+ }
+
+ $this->credentialsManager->store($user->getUID(), $this->getCredentialsIdentifier($mountId), [
+ 'user' => $options['user'], // explicitly copy the fields we want instead of just passing the entire $options array
+ 'password' => $options['password'] // this way we prevent users from being able to modify any other field
+ ]);
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ if (!isset($user)) {
+ throw new InsufficientDataForMeaningfulAnswerException('No credentials saved');
+ }
+ $uid = $user->getUID();
+ $credentials = $this->credentialsManager->retrieve($uid, $this->getCredentialsIdentifier($storage->getId()));
+
+ if (!isset($credentials)) {
+ throw new InsufficientDataForMeaningfulAnswerException('No credentials saved');
+ }
+
+ $storage->setBackendOption('user', $credentials['user']);
+ $storage->setBackendOption('password', $credentials['password']);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php
new file mode 100644
index 00000000000..ad95c743d2d
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSA.php
@@ -0,0 +1,75 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Auth\PublicKey;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\StorageConfig;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IUser;
+use phpseclib\Crypt\RSA as RSACrypt;
+
+/**
+ * RSA public key authentication
+ */
+class RSA extends AuthMechanism {
+
+ public function __construct(
+ IL10N $l,
+ private IConfig $config,
+ ) {
+ $this
+ ->setIdentifier('publickey::rsa')
+ ->setScheme(self::SCHEME_PUBLICKEY)
+ ->setText($l->t('RSA public key'))
+ ->addParameters([
+ new DefinitionParameter('user', $l->t('Login')),
+ new DefinitionParameter('public_key', $l->t('Public key')),
+ (new DefinitionParameter('private_key', 'private_key'))
+ ->setType(DefinitionParameter::VALUE_PASSWORD)
+ ->setFlag(DefinitionParameter::FLAG_HIDDEN),
+ ])
+ ->addCustomJs('public_key')
+ ;
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ $auth = new RSACrypt();
+ $auth->setPassword($this->config->getSystemValue('secret', ''));
+ if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
+ // Add fallback routine for a time where secret was not enforced to be exists
+ $auth->setPassword('');
+ if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
+ throw new \RuntimeException('unable to load private key');
+ }
+ }
+ $storage->setBackendOption('public_key_auth', $auth);
+ }
+
+ /**
+ * Generate a keypair
+ *
+ * @param int $keyLenth
+ * @return array ['privatekey' => $privateKey, 'publickey' => $publicKey]
+ */
+ public function createKey($keyLength) {
+ $rsa = new RSACrypt();
+ $rsa->setPublicKeyFormat(RSACrypt::PUBLIC_FORMAT_OPENSSH);
+ $rsa->setPassword($this->config->getSystemValue('secret', ''));
+
+ if ($keyLength !== 1024 && $keyLength !== 2048 && $keyLength !== 4096) {
+ $keyLength = 1024;
+ }
+
+ return $rsa->createKey($keyLength);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php
new file mode 100644
index 00000000000..8f58b71d5ac
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/PublicKey/RSAPrivateKey.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Auth\PublicKey;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\StorageConfig;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\IUser;
+use phpseclib\Crypt\RSA as RSACrypt;
+
+/**
+ * RSA public key authentication
+ */
+class RSAPrivateKey extends AuthMechanism {
+
+ public function __construct(
+ IL10N $l,
+ private IConfig $config,
+ ) {
+ $this
+ ->setIdentifier('publickey::rsa_private')
+ ->setScheme(self::SCHEME_PUBLICKEY)
+ ->setText($l->t('RSA private key'))
+ ->addParameters([
+ new DefinitionParameter('user', $l->t('Login')),
+ (new DefinitionParameter('password', $l->t('Password')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL)
+ ->setType(DefinitionParameter::VALUE_PASSWORD),
+ new DefinitionParameter('private_key', $l->t('Private key')),
+ ]);
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ $auth = new RSACrypt();
+ $auth->setPassword($this->config->getSystemValue('secret', ''));
+ if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
+ // Add fallback routine for a time where secret was not enforced to be exists
+ $auth->setPassword('');
+ if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
+ throw new \RuntimeException('unable to load private key');
+ }
+ }
+ $storage->setBackendOption('public_key_auth', $auth);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php
new file mode 100644
index 00000000000..26671110294
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosApacheAuth.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Lib\Auth\SMB;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\Authentication\LoginCredentials\IStore;
+use OCP\IL10N;
+
+class KerberosApacheAuth extends AuthMechanism {
+ public function __construct(
+ IL10N $l,
+ private IStore $credentialsStore,
+ ) {
+ $realm = new DefinitionParameter('default_realm', 'Default realm');
+ $realm
+ ->setType(DefinitionParameter::VALUE_TEXT)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL)
+ ->setTooltip($l->t('Kerberos default realm, defaults to "WORKGROUP"'));
+ $this
+ ->setIdentifier('smb::kerberosapache')
+ ->setScheme(self::SCHEME_SMB)
+ ->setText($l->t('Kerberos ticket Apache mode'))
+ ->addParameter($realm);
+ }
+
+ public function getCredentialsStore(): IStore {
+ return $this->credentialsStore;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php
new file mode 100644
index 00000000000..9210209192a
--- /dev/null
+++ b/apps/files_external/lib/Lib/Auth/SMB/KerberosAuth.php
@@ -0,0 +1,19 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Auth\SMB;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCP\IL10N;
+
+class KerberosAuth extends AuthMechanism {
+ public function __construct(IL10N $l) {
+ $this
+ ->setIdentifier('smb::kerberos')
+ ->setScheme(self::SCHEME_SMB)
+ ->setText($l->t('Kerberos ticket'));
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/AmazonS3.php b/apps/files_external/lib/Lib/Backend/AmazonS3.php
new file mode 100644
index 00000000000..464b03b55e0
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/AmazonS3.php
@@ -0,0 +1,54 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCP\IL10N;
+
+class AmazonS3 extends Backend {
+ use LegacyDependencyCheckPolyfill;
+
+ public function __construct(IL10N $l, AccessKey $legacyAuth) {
+ $this
+ ->setIdentifier('amazons3')
+ ->addIdentifierAlias('\OC\Files\Storage\AmazonS3') // legacy compat
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\AmazonS3')
+ ->setText($l->t('Amazon S3'))
+ ->addParameters([
+ new DefinitionParameter('bucket', $l->t('Bucket')),
+ (new DefinitionParameter('hostname', $l->t('Hostname')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('port', $l->t('Port')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('region', $l->t('Region')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('storageClass', $l->t('Storage Class')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('use_ssl', $l->t('Enable SSL')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setDefaultValue(true),
+ (new DefinitionParameter('use_path_style', $l->t('Enable Path Style')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN),
+ (new DefinitionParameter('legacy_auth', $l->t('Legacy (v2) authentication')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN),
+ (new DefinitionParameter('useMultipartCopy', $l->t('Enable multipart copy')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setDefaultValue(true),
+ (new DefinitionParameter('sse_c_key', $l->t('SSE-C encryption key')))
+ ->setType(DefinitionParameter::VALUE_PASSWORD)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ])
+ ->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY)
+ ->addAuthScheme(AuthMechanism::SCHEME_NULL)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/backend/backend.php b/apps/files_external/lib/Lib/Backend/Backend.php
index 8fb84b0e835..f7500ee24a4 100644
--- a/apps/files_external/lib/backend/backend.php
+++ b/apps/files_external/lib/Lib/Backend/Backend.php
@@ -1,34 +1,23 @@
<?php
+
/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_External\Lib\Backend;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCA\Files_External\Lib\VisibilityTrait;
-use \OCA\Files_External\Lib\FrontendDefinitionTrait;
-use \OCA\Files_External\Lib\PriorityTrait;
-use \OCA\Files_External\Lib\DependencyTrait;
-use \OCA\Files_External\Lib\StorageModifierTrait;
-use \OCA\Files_External\Lib\IdentifierTrait;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\DependencyTrait;
+use OCA\Files_External\Lib\FrontendDefinitionTrait;
+use OCA\Files_External\Lib\IdentifierTrait;
+use OCA\Files_External\Lib\IFrontendDefinition;
+use OCA\Files_External\Lib\IIdentifier;
+use OCA\Files_External\Lib\PriorityTrait;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Lib\StorageModifierTrait;
+use OCA\Files_External\Lib\VisibilityTrait;
+use OCP\Files\Storage\IStorage;
/**
* Storage backend
@@ -37,7 +26,7 @@ use \OCA\Files_External\Lib\Auth\AuthMechanism;
* such as \OCP\IDB for database operations. This allows a backend
* to perform advanced operations based on provided information.
*
- * An authenication scheme defines the parameter interface, common to the
+ * An authentication scheme defines the parameter interface, common to the
* storage implementation, the backend and the authentication mechanism.
* A storage implementation expects parameters according to the authentication
* scheme, which are provided from the authentication mechanism.
@@ -54,8 +43,7 @@ use \OCA\Files_External\Lib\Auth\AuthMechanism;
* - StorageModifierTrait
* Object can affect storage mounting
*/
-class Backend implements \JsonSerializable {
-
+class Backend implements \JsonSerializable, IIdentifier, IFrontendDefinition {
use VisibilityTrait;
use FrontendDefinitionTrait;
use PriorityTrait;
@@ -73,7 +61,7 @@ class Backend implements \JsonSerializable {
private $legacyAuthMechanism;
/**
- * @return string
+ * @return class-string<IStorage>
*/
public function getStorageClass() {
return $this->storageClass;
@@ -81,7 +69,7 @@ class Backend implements \JsonSerializable {
/**
* @param string $class
- * @return self
+ * @return $this
*/
public function setStorageClass($class) {
$this->storageClass = $class;
@@ -118,29 +106,23 @@ class Backend implements \JsonSerializable {
return $this->legacyAuthMechanism;
}
- /**
- * @param AuthMechanism $authMechanism
- * @return self
- */
- public function setLegacyAuthMechanism(AuthMechanism $authMechanism) {
+ public function setLegacyAuthMechanism(AuthMechanism $authMechanism): self {
$this->legacyAuthMechanism = $authMechanism;
return $this;
}
/**
* @param callable $callback dynamic auth mechanism selection
- * @return self
*/
- public function setLegacyAuthMechanismCallback(callable $callback) {
+ public function setLegacyAuthMechanismCallback(callable $callback): self {
$this->legacyAuthMechanism = $callback;
+ return $this;
}
/**
* Serialize into JSON for client-side JS
- *
- * @return array
*/
- public function jsonSerialize() {
+ public function jsonSerialize(): array {
$data = $this->jsonSerializeDefinition();
$data += $this->jsonSerializeIdentifier();
@@ -160,6 +142,4 @@ class Backend implements \JsonSerializable {
public function validateStorage(StorageConfig $storage) {
return $this->validateStorageDefinition($storage);
}
-
}
-
diff --git a/apps/files_external/lib/Lib/Backend/DAV.php b/apps/files_external/lib/Lib/Backend/DAV.php
new file mode 100644
index 00000000000..dea9e7c5e77
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/DAV.php
@@ -0,0 +1,37 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\Password\Password;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCP\IL10N;
+
+class DAV extends Backend {
+ use LegacyDependencyCheckPolyfill;
+
+ public function __construct(IL10N $l, Password $legacyAuth) {
+ $this
+ ->setIdentifier('dav')
+ ->addIdentifierAlias('\OC\Files\Storage\DAV') // legacy compat
+ ->setStorageClass('\OC\Files\Storage\DAV')
+ ->setText($l->t('WebDAV'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('URL')),
+ (new DefinitionParameter('root', $l->t('Remote subfolder')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('secure', $l->t('Secure https://')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setDefaultValue(true),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/FTP.php b/apps/files_external/lib/Lib/Backend/FTP.php
new file mode 100644
index 00000000000..72a8184c9b9
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/FTP.php
@@ -0,0 +1,39 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\Password\Password;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCP\IL10N;
+
+class FTP extends Backend {
+ use LegacyDependencyCheckPolyfill;
+
+ public function __construct(IL10N $l, Password $legacyAuth) {
+ $this
+ ->setIdentifier('ftp')
+ ->addIdentifierAlias('\OC\Files\Storage\FTP') // legacy compat
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\FTP')
+ ->setText($l->t('FTP'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('Host')),
+ (new DefinitionParameter('port', $l->t('Port')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('root', $l->t('Remote subfolder')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('secure', $l->t('Secure ftps://')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setDefaultValue(true),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/InvalidBackend.php b/apps/files_external/lib/Lib/Backend/InvalidBackend.php
new file mode 100644
index 00000000000..48912c0e49e
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/InvalidBackend.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud GmbH.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\StorageConfig;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IUser;
+
+/**
+ * Invalid storage backend representing a backend
+ * that could not be resolved
+ */
+class InvalidBackend extends Backend {
+
+ /**
+ * Constructs a new InvalidBackend with the id of the invalid backend
+ * for display purposes
+ *
+ * @param string $invalidId id of the backend that did not exist
+ */
+ public function __construct(
+ private $invalidId,
+ ) {
+ $this
+ ->setIdentifier($this->invalidId)
+ ->setStorageClass('\OC\Files\Storage\FailedStorage')
+ ->setText('Unknown storage backend ' . $this->invalidId);
+ }
+
+ /**
+ * Returns the invalid backend id
+ *
+ * @return string invalid backend id
+ */
+ public function getInvalidId() {
+ return $this->invalidId;
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ $storage->setBackendOption('exception', new \Exception('Unknown storage backend "' . $this->invalidId . '"', StorageNotAvailableException::STATUS_ERROR));
+ }
+}
diff --git a/apps/files_external/lib/backend/legacybackend.php b/apps/files_external/lib/Lib/Backend/LegacyBackend.php
index 084758ff78a..9c7e5b01bc3 100644
--- a/apps/files_external/lib/backend/legacybackend.php
+++ b/apps/files_external/lib/Lib/Backend/LegacyBackend.php
@@ -1,37 +1,21 @@
<?php
+
/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_External\Lib\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\Auth\Builtin;
-use \OCA\Files_External\Lib\MissingDependency;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCA\Files_External\Lib\Auth\Builtin;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCA\Files_External\Lib\MissingDependency;
/**
- * Legacy compatibility for OC_Mount_Config::registerBackend()
+ * Legacy compatibility for OCA\Files_External\MountConfig::registerBackend()
*/
class LegacyBackend extends Backend {
-
use LegacyDependencyCheckPolyfill {
LegacyDependencyCheckPolyfill::checkDependencies as doCheckDependencies;
}
@@ -61,18 +45,14 @@ class LegacyBackend extends Backend {
$placeholder = substr($placeholder, 1);
}
switch ($placeholder[0]) {
- case '!':
- $type = DefinitionParameter::VALUE_BOOLEAN;
- $placeholder = substr($placeholder, 1);
- break;
- case '*':
- $type = DefinitionParameter::VALUE_PASSWORD;
- $placeholder = substr($placeholder, 1);
- break;
- case '#':
- $type = DefinitionParameter::VALUE_HIDDEN;
- $placeholder = substr($placeholder, 1);
- break;
+ case '!':
+ $type = DefinitionParameter::VALUE_BOOLEAN;
+ $placeholder = substr($placeholder, 1);
+ break;
+ case '*':
+ $type = DefinitionParameter::VALUE_PASSWORD;
+ $placeholder = substr($placeholder, 1);
+ break;
}
$this->addParameter((new DefinitionParameter($name, $placeholder))
->setType($type)
@@ -84,7 +64,7 @@ class LegacyBackend extends Backend {
$this->setPriority($definition['priority']);
}
if (isset($definition['custom'])) {
- $this->setCustomJs($definition['custom']);
+ $this->addCustomJs($definition['custom']);
}
if (isset($definition['has_dependencies']) && $definition['has_dependencies']) {
$this->hasDependencies = true;
@@ -100,5 +80,4 @@ class LegacyBackend extends Backend {
}
return [];
}
-
}
diff --git a/apps/files_external/lib/Lib/Backend/Local.php b/apps/files_external/lib/Lib/Backend/Local.php
new file mode 100644
index 00000000000..56940b8e83b
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/Local.php
@@ -0,0 +1,38 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\NullMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCP\IL10N;
+use OCP\IUser;
+
+class Local extends Backend {
+ public function __construct(IL10N $l, NullMechanism $legacyAuth) {
+ $this
+ ->setIdentifier('local')
+ ->addIdentifierAlias('\OC\Files\Storage\Local') // legacy compat
+ ->setStorageClass('\OC\Files\Storage\Local')
+ ->setText($l->t('Local'))
+ ->addParameters([
+ new DefinitionParameter('datadir', $l->t('Location')),
+ ])
+ ->setAllowedVisibility(BackendService::VISIBILITY_ADMIN)
+ ->setPriority(BackendService::PRIORITY_DEFAULT + 50)
+ ->addAuthScheme(AuthMechanism::SCHEME_NULL)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ;
+ }
+
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null): void {
+ $storage->setBackendOption('isExternal', true);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/OwnCloud.php b/apps/files_external/lib/Lib/Backend/OwnCloud.php
new file mode 100644
index 00000000000..0c0e2c6d300
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/OwnCloud.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\Password\Password;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+class OwnCloud extends Backend {
+ public function __construct(IL10N $l, Password $legacyAuth) {
+ $this
+ ->setIdentifier('owncloud')
+ ->addIdentifierAlias('\OC\Files\Storage\OwnCloud') // legacy compat
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\OwnCloud')
+ ->setText($l->t('Nextcloud'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('URL')),
+ (new DefinitionParameter('root', $l->t('Remote subfolder')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('secure', $l->t('Secure https://')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setDefaultValue(true),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/SFTP.php b/apps/files_external/lib/Lib/Backend/SFTP.php
new file mode 100644
index 00000000000..0926cf7fd93
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/SFTP.php
@@ -0,0 +1,34 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\Password\Password;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+class SFTP extends Backend {
+ public function __construct(IL10N $l, Password $legacyAuth) {
+ $this
+ ->setIdentifier('sftp')
+ ->addIdentifierAlias('\OC\Files\Storage\SFTP') // legacy compat
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\SFTP')
+ ->setText($l->t('SFTP'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('Host')),
+ (new DefinitionParameter('port', $l->t('Port')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('root', $l->t('Root')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
+ ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/SFTP_Key.php b/apps/files_external/lib/Lib/Backend/SFTP_Key.php
new file mode 100644
index 00000000000..278fae3fba7
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/SFTP_Key.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\PublicKey\RSA;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCP\IL10N;
+
+class SFTP_Key extends Backend {
+ public function __construct(IL10N $l, RSA $legacyAuth, SFTP $sftpBackend) {
+ $this
+ ->setIdentifier('\OC\Files\Storage\SFTP_Key')
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\SFTP')
+ ->setText($l->t('SFTP with secret key login'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('Host')),
+ (new DefinitionParameter('root', $l->t('Remote subfolder')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ->deprecateTo($sftpBackend)
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/SMB.php b/apps/files_external/lib/Lib/Backend/SMB.php
new file mode 100644
index 00000000000..e86ad98880c
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/SMB.php
@@ -0,0 +1,140 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\Files_External\Lib\Backend;
+
+use Icewind\SMB\BasicAuth;
+use Icewind\SMB\KerberosAuth;
+use Icewind\SMB\KerberosTicket;
+use Icewind\SMB\Native\NativeServer;
+use Icewind\SMB\Wrapped\Server;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\Password\Password;
+use OCA\Files_External\Lib\Auth\SMB\KerberosApacheAuth as KerberosApacheAuthMechanism;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
+use OCA\Files_External\Lib\MissingDependency;
+use OCA\Files_External\Lib\Storage\SystemBridge;
+use OCA\Files_External\Lib\StorageConfig;
+use OCP\IL10N;
+use OCP\IUser;
+
+class SMB extends Backend {
+ public function __construct(IL10N $l, Password $legacyAuth) {
+ $this
+ ->setIdentifier('smb')
+ ->addIdentifierAlias('\OC\Files\Storage\SMB')// legacy compat
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\SMB')
+ ->setText($l->t('SMB/CIFS'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('Host')),
+ new DefinitionParameter('share', $l->t('Share')),
+ (new DefinitionParameter('root', $l->t('Remote subfolder')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('domain', $l->t('Domain')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('show_hidden', $l->t('Show hidden files')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('case_sensitive', $l->t('Case sensitive file system')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL)
+ ->setDefaultValue(true)
+ ->setTooltip($l->t('Disabling it will allow to use a case insensitive file system, but comes with a performance penalty')),
+ (new DefinitionParameter('check_acl', $l->t('Verify ACL access when listing files')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL)
+ ->setTooltip($l->t("Check the ACL's of each file or folder inside a directory to filter out items where the account has no read permissions, comes with a performance penalty")),
+ (new DefinitionParameter('timeout', $l->t('Timeout')))
+ ->setType(DefinitionParameter::VALUE_TEXT)
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL)
+ ->setFlag(DefinitionParameter::FLAG_HIDDEN),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
+ ->addAuthScheme(AuthMechanism::SCHEME_SMB)
+ ->setLegacyAuthMechanism($legacyAuth);
+ }
+
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null): void {
+ $auth = $storage->getAuthMechanism();
+ if ($auth->getScheme() === AuthMechanism::SCHEME_PASSWORD) {
+ if (!is_string($storage->getBackendOption('user')) || !is_string($storage->getBackendOption('password'))) {
+ throw new \InvalidArgumentException('user or password is not set');
+ }
+
+ $smbAuth = new BasicAuth(
+ $storage->getBackendOption('user'),
+ $storage->getBackendOption('domain'),
+ $storage->getBackendOption('password')
+ );
+ } else {
+ switch ($auth->getIdentifier()) {
+ case 'smb::kerberos':
+ $smbAuth = new KerberosAuth();
+ break;
+ case 'smb::kerberosapache':
+ if (!$auth instanceof KerberosApacheAuthMechanism) {
+ throw new \InvalidArgumentException('invalid authentication backend');
+ }
+ $credentialsStore = $auth->getCredentialsStore();
+ $kerbAuth = new KerberosAuth();
+ $kerbAuth->setTicket(KerberosTicket::fromEnv());
+ // check if a kerberos ticket is available, else fallback to session credentials
+ if ($kerbAuth->getTicket()?->isValid()) {
+ $smbAuth = $kerbAuth;
+ } else {
+ try {
+ $credentials = $credentialsStore->getLoginCredentials();
+ $loginName = $credentials->getLoginName();
+ $pass = $credentials->getPassword();
+ preg_match('/(.*)@(.*)/', $loginName, $matches);
+ $realm = $storage->getBackendOption('default_realm');
+ if (empty($realm)) {
+ $realm = 'WORKGROUP';
+ }
+ if (count($matches) === 0) {
+ $username = $loginName;
+ $workgroup = $realm;
+ } else {
+ [, $username, $workgroup] = $matches;
+ }
+ $smbAuth = new BasicAuth(
+ $username,
+ $workgroup,
+ $pass
+ );
+ } catch (\Exception) {
+ throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
+ }
+ }
+
+ break;
+ default:
+ throw new \InvalidArgumentException('unknown authentication backend');
+ }
+ }
+
+ $storage->setBackendOption('auth', $smbAuth);
+ }
+
+ public function checkDependencies(): array {
+ $system = \OCP\Server::get(SystemBridge::class);
+ if (NativeServer::available($system)) {
+ return [];
+ } elseif (Server::available($system)) {
+ $missing = new MissingDependency('php-smbclient');
+ $missing->setOptional(true);
+ $missing->setMessage('The php-smbclient library provides improved compatibility and performance for SMB storages.');
+ return [$missing];
+ } else {
+ $missing = new MissingDependency('php-smbclient');
+ $missing->setMessage('Either the php-smbclient library (preferred) or the smbclient binary is required for SMB storages.');
+ return [$missing, new MissingDependency('smbclient')];
+ }
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/SMB_OC.php b/apps/files_external/lib/Lib/Backend/SMB_OC.php
new file mode 100644
index 00000000000..bcb8d0fbf16
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/SMB_OC.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\Password\SessionCredentials;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\Service\BackendService;
+use OCP\IL10N;
+use OCP\IUser;
+
+/**
+ * Deprecated SMB_OC class - use SMB with the password::sessioncredentials auth mechanism
+ */
+class SMB_OC extends Backend {
+ use LegacyDependencyCheckPolyfill;
+
+ public function __construct(IL10N $l, SessionCredentials $legacyAuth, SMB $smbBackend) {
+ $this
+ ->setIdentifier('\OC\Files\Storage\SMB_OC')
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\SMB')
+ ->setText($l->t('SMB/CIFS using OC login'))
+ ->addParameters([
+ new DefinitionParameter('host', $l->t('Host')),
+ (new DefinitionParameter('username_as_share', $l->t('Login as share')))
+ ->setType(DefinitionParameter::VALUE_BOOLEAN),
+ (new DefinitionParameter('share', $l->t('Share')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ (new DefinitionParameter('root', $l->t('Remote subfolder')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ])
+ ->setPriority(BackendService::PRIORITY_DEFAULT - 10)
+ ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
+ ->setLegacyAuthMechanism($legacyAuth)
+ ->deprecateTo($smbBackend)
+ ;
+ }
+
+ /**
+ * @return void
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ $username_as_share = ($storage->getBackendOption('username_as_share') === true);
+
+ if ($username_as_share) {
+ $share = '/' . $storage->getBackendOption('user');
+ $storage->setBackendOption('share', $share);
+ }
+ }
+}
diff --git a/apps/files_external/lib/Lib/Backend/Swift.php b/apps/files_external/lib/Lib/Backend/Swift.php
new file mode 100644
index 00000000000..37527ba3dbb
--- /dev/null
+++ b/apps/files_external/lib/Lib/Backend/Swift.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Backend;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\OpenStack\OpenStackV2;
+use OCA\Files_External\Lib\Auth\OpenStack\Rackspace;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
+use OCP\IL10N;
+
+class Swift extends Backend {
+ use LegacyDependencyCheckPolyfill;
+
+ public function __construct(IL10N $l, OpenStackV2 $openstackAuth, Rackspace $rackspaceAuth) {
+ $this
+ ->setIdentifier('swift')
+ ->addIdentifierAlias('\OC\Files\Storage\Swift') // legacy compat
+ ->setStorageClass('\OCA\Files_External\Lib\Storage\Swift')
+ ->setText($l->t('OpenStack Object Storage'))
+ ->addParameters([
+ (new DefinitionParameter('service_name', $l->t('Service name')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ new DefinitionParameter('region', $l->t('Region')),
+ new DefinitionParameter('bucket', $l->t('Bucket')),
+ (new DefinitionParameter('timeout', $l->t('Request timeout (seconds)')))
+ ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
+ ])
+ ->addAuthScheme(AuthMechanism::SCHEME_OPENSTACK)
+ ->setLegacyAuthMechanismCallback(function (array $params) use ($openstackAuth, $rackspaceAuth) {
+ if (isset($params['options']['key']) && $params['options']['key']) {
+ return $rackspaceAuth;
+ }
+ return $openstackAuth;
+ })
+ ;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php
new file mode 100644
index 00000000000..0c2e90a243c
--- /dev/null
+++ b/apps/files_external/lib/Lib/Config/IAuthMechanismProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Config;
+
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+
+/**
+ * Provider of external storage auth mechanisms
+ * @since 9.1.0
+ */
+interface IAuthMechanismProvider {
+
+ /**
+ * @since 9.1.0
+ * @return AuthMechanism[]
+ */
+ public function getAuthMechanisms();
+}
diff --git a/apps/files_external/lib/Lib/Config/IBackendProvider.php b/apps/files_external/lib/Lib/Config/IBackendProvider.php
new file mode 100644
index 00000000000..44c460c3138
--- /dev/null
+++ b/apps/files_external/lib/Lib/Config/IBackendProvider.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Config;
+
+use OCA\Files_External\Lib\Backend\Backend;
+
+/**
+ * Provider of external storage backends
+ * @since 9.1.0
+ */
+interface IBackendProvider {
+
+ /**
+ * @since 9.1.0
+ * @return Backend[]
+ */
+ public function getBackends();
+}
diff --git a/apps/files_external/lib/Lib/DefinitionParameter.php b/apps/files_external/lib/Lib/DefinitionParameter.php
new file mode 100644
index 00000000000..a73dd2df967
--- /dev/null
+++ b/apps/files_external/lib/Lib/DefinitionParameter.php
@@ -0,0 +1,217 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+/**
+ * Parameter for an external storage definition
+ */
+class DefinitionParameter implements \JsonSerializable {
+ // placeholder value for password fields, when the client updates a storage configuration
+ // placeholder values are ignored and the field is left unmodified
+ public const UNMODIFIED_PLACEHOLDER = '__unmodified__';
+
+ /** Value constants */
+ public const VALUE_TEXT = 0;
+ public const VALUE_BOOLEAN = 1;
+ public const VALUE_PASSWORD = 2;
+
+ /** Flag constants */
+ public const FLAG_NONE = 0;
+ public const FLAG_OPTIONAL = 1;
+ public const FLAG_USER_PROVIDED = 2;
+ public const FLAG_HIDDEN = 4;
+
+ /** @var string human-readable parameter tooltip */
+ private string $tooltip = '';
+
+ /** @var int value type, see self::VALUE_* constants */
+ private int $type = self::VALUE_TEXT;
+
+ /** @var int flags, see self::FLAG_* constants */
+ private int $flags = self::FLAG_NONE;
+
+ /**
+ * @param string $name parameter name
+ * @param string $text parameter description
+ * @param mixed $defaultValue default value
+ */
+ public function __construct(
+ private string $name,
+ private string $text,
+ private $defaultValue = null,
+ ) {
+ }
+
+ /**
+ * @return string
+ */
+ public function getName(): string {
+ return $this->name;
+ }
+
+ /**
+ * @return string
+ */
+ public function getText(): string {
+ return $this->text;
+ }
+
+ /**
+ * Get value type
+ *
+ * @return int
+ */
+ public function getType(): int {
+ return $this->type;
+ }
+
+ /**
+ * Set value type
+ *
+ * @param int $type
+ * @return self
+ */
+ public function setType(int $type) {
+ $this->type = $type;
+ return $this;
+ }
+
+ /**
+ * @return mixed default value
+ */
+ public function getDefaultValue() {
+ return $this->defaultValue;
+ }
+
+ /**
+ * @param mixed $defaultValue default value
+ * @return self
+ */
+ public function setDefaultValue($defaultValue) {
+ $this->defaultValue = $defaultValue;
+ return $this;
+ }
+
+ /**
+ * @return string
+ */
+ public function getTypeName(): string {
+ switch ($this->type) {
+ case self::VALUE_BOOLEAN:
+ return 'boolean';
+ case self::VALUE_TEXT:
+ return 'text';
+ case self::VALUE_PASSWORD:
+ return 'password';
+ default:
+ return 'unknown';
+ }
+ }
+
+ /**
+ * @return int
+ */
+ public function getFlags(): int {
+ return $this->flags;
+ }
+
+ /**
+ * @param int $flags
+ * @return self
+ */
+ public function setFlags(int $flags) {
+ $this->flags = $flags;
+ return $this;
+ }
+
+ /**
+ * @param int $flag
+ * @return self
+ */
+ public function setFlag(int $flag) {
+ $this->flags |= $flag;
+ return $this;
+ }
+
+ /**
+ * @param int $flag
+ * @return bool
+ */
+ public function isFlagSet(int $flag): bool {
+ return (bool)($this->flags & $flag);
+ }
+
+ /**
+ * @return string
+ */
+ public function getTooltip(): string {
+ return $this->tooltip;
+ }
+
+ /**
+ * @param string $tooltip
+ * @return self
+ */
+ public function setTooltip(string $tooltip) {
+ $this->tooltip = $tooltip;
+ return $this;
+ }
+
+ /**
+ * Serialize into JSON for client-side JS
+ */
+ public function jsonSerialize(): array {
+ $result = [
+ 'value' => $this->getText(),
+ 'flags' => $this->getFlags(),
+ 'type' => $this->getType(),
+ 'tooltip' => $this->getTooltip(),
+ ];
+ $defaultValue = $this->getDefaultValue();
+ if ($defaultValue) {
+ $result['defaultValue'] = $defaultValue;
+ }
+ return $result;
+ }
+
+ public function isOptional(): bool {
+ return $this->isFlagSet(self::FLAG_OPTIONAL) || $this->isFlagSet(self::FLAG_USER_PROVIDED);
+ }
+
+ /**
+ * Validate a parameter value against this
+ * Convert type as necessary
+ *
+ * @param mixed $value Value to check
+ * @return bool success
+ */
+ public function validateValue(&$value): bool {
+ switch ($this->getType()) {
+ case self::VALUE_BOOLEAN:
+ if (!is_bool($value)) {
+ switch ($value) {
+ case 'true':
+ $value = true;
+ break;
+ case 'false':
+ $value = false;
+ break;
+ default:
+ return false;
+ }
+ }
+ break;
+ default:
+ if (!$value && !$this->isOptional()) {
+ return false;
+ }
+ break;
+ }
+ return true;
+ }
+}
diff --git a/apps/files_external/lib/Lib/DependencyTrait.php b/apps/files_external/lib/Lib/DependencyTrait.php
new file mode 100644
index 00000000000..644132b82bc
--- /dev/null
+++ b/apps/files_external/lib/Lib/DependencyTrait.php
@@ -0,0 +1,23 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+/**
+ * Trait for objects that have dependencies for use
+ */
+trait DependencyTrait {
+
+ /**
+ * Check if object is valid for use
+ *
+ * @return MissingDependency[] Unsatisfied dependencies
+ */
+ public function checkDependencies() {
+ return []; // no dependencies by default
+ }
+}
diff --git a/apps/files_external/lib/Lib/FrontendDefinitionTrait.php b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php
new file mode 100644
index 00000000000..0f280d1d486
--- /dev/null
+++ b/apps/files_external/lib/Lib/FrontendDefinitionTrait.php
@@ -0,0 +1,107 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+/**
+ * Trait for objects that have a frontend representation
+ */
+trait FrontendDefinitionTrait {
+
+ /** @var string human-readable mechanism name */
+ private string $text = '';
+
+ /** @var array<string, DefinitionParameter> parameters for mechanism */
+ private array $parameters = [];
+
+ /** @var string[] custom JS */
+ private array $customJs = [];
+
+ public function getText(): string {
+ return $this->text;
+ }
+
+ public function setText(string $text): self {
+ $this->text = $text;
+ return $this;
+ }
+
+ public static function lexicalCompare(IFrontendDefinition $a, IFrontendDefinition $b): int {
+ return strcmp($a->getText(), $b->getText());
+ }
+
+ /**
+ * @return array<string, DefinitionParameter>
+ */
+ public function getParameters(): array {
+ return $this->parameters;
+ }
+
+ /**
+ * @param list<DefinitionParameter> $parameters
+ */
+ public function addParameters(array $parameters): self {
+ foreach ($parameters as $parameter) {
+ $this->addParameter($parameter);
+ }
+ return $this;
+ }
+
+ public function addParameter(DefinitionParameter $parameter): self {
+ $this->parameters[$parameter->getName()] = $parameter;
+ return $this;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getCustomJs(): array {
+ return $this->customJs;
+ }
+
+ /**
+ * @param string $custom
+ * @return self
+ */
+ public function addCustomJs(string $custom): self {
+ $this->customJs[] = $custom;
+ return $this;
+ }
+
+ /**
+ * Serialize into JSON for client-side JS
+ */
+ public function jsonSerializeDefinition(): array {
+ $configuration = [];
+ foreach ($this->getParameters() as $parameter) {
+ $configuration[$parameter->getName()] = $parameter;
+ }
+
+ $data = [
+ 'name' => $this->getText(),
+ 'configuration' => $configuration,
+ 'custom' => $this->getCustomJs(),
+ ];
+ return $data;
+ }
+
+ /**
+ * Check if parameters are satisfied in a StorageConfig
+ */
+ public function validateStorageDefinition(StorageConfig $storage): bool {
+ foreach ($this->getParameters() as $name => $parameter) {
+ $value = $storage->getBackendOption($name);
+ if (!is_null($value) || !$parameter->isOptional()) {
+ if (!$parameter->validateValue($value)) {
+ return false;
+ }
+ $storage->setBackendOption($name, $value);
+ }
+ }
+ return true;
+ }
+}
diff --git a/apps/files_external/lib/Lib/IFrontendDefinition.php b/apps/files_external/lib/Lib/IFrontendDefinition.php
new file mode 100644
index 00000000000..c8b06a1c30b
--- /dev/null
+++ b/apps/files_external/lib/Lib/IFrontendDefinition.php
@@ -0,0 +1,43 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib;
+
+interface IFrontendDefinition {
+
+ public function getText(): string;
+
+ public function setText(string $text): self;
+
+ /**
+ * @return array<string, DefinitionParameter>
+ */
+ public function getParameters(): array;
+
+ /**
+ * @param list<DefinitionParameter> $parameters
+ */
+ public function addParameters(array $parameters): self;
+
+ public function addParameter(DefinitionParameter $parameter): self;
+
+ /**
+ * @return string[]
+ */
+ public function getCustomJs(): array;
+
+ public function addCustomJs(string $custom): self;
+
+ /**
+ * Serialize into JSON for client-side JS
+ */
+ public function jsonSerializeDefinition(): array;
+
+ /**
+ * Check if parameters are satisfied in a StorageConfig
+ */
+ public function validateStorageDefinition(StorageConfig $storage): bool;
+}
diff --git a/apps/files_external/lib/Lib/IIdentifier.php b/apps/files_external/lib/Lib/IIdentifier.php
new file mode 100644
index 00000000000..0677409a3cf
--- /dev/null
+++ b/apps/files_external/lib/Lib/IIdentifier.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib;
+
+interface IIdentifier {
+
+ public function getIdentifier(): string;
+
+ public function setIdentifier(string $identifier): self;
+}
diff --git a/apps/files_external/lib/Lib/IdentifierTrait.php b/apps/files_external/lib/Lib/IdentifierTrait.php
new file mode 100644
index 00000000000..f5ffde32307
--- /dev/null
+++ b/apps/files_external/lib/Lib/IdentifierTrait.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+/**
+ * Trait for objects requiring an identifier (and/or identifier aliases)
+ * Also supports deprecation to a different object, linking the objects
+ */
+trait IdentifierTrait {
+
+ protected string $identifier = '';
+
+ /** @var string[] */
+ protected array $identifierAliases = [];
+ protected ?IIdentifier $deprecateTo = null;
+
+ public function getIdentifier(): string {
+ return $this->identifier;
+ }
+
+ public function setIdentifier(string $identifier): self {
+ $this->identifier = $identifier;
+ $this->identifierAliases[] = $identifier;
+ return $this;
+ }
+
+ /**
+ * @return string[]
+ */
+ public function getIdentifierAliases(): array {
+ return $this->identifierAliases;
+ }
+
+ public function addIdentifierAlias(string $alias): self {
+ $this->identifierAliases[] = $alias;
+ return $this;
+ }
+
+ public function getDeprecateTo(): ?IIdentifier {
+ return $this->deprecateTo;
+ }
+
+ public function deprecateTo(IIdentifier $destinationObject): self {
+ $this->deprecateTo = $destinationObject;
+ return $this;
+ }
+
+ public function jsonSerializeIdentifier(): array {
+ $data = [
+ 'identifier' => $this->identifier,
+ 'identifierAliases' => $this->identifierAliases,
+ ];
+ if ($this->deprecateTo) {
+ $data['deprecateTo'] = $this->deprecateTo->getIdentifier();
+ }
+ return $data;
+ }
+}
diff --git a/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php
new file mode 100644
index 00000000000..1e872b35072
--- /dev/null
+++ b/apps/files_external/lib/Lib/InsufficientDataForMeaningfulAnswerException.php
@@ -0,0 +1,27 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+use OCP\Files\StorageNotAvailableException;
+
+/**
+ * Authentication mechanism or backend has insufficient data
+ */
+class InsufficientDataForMeaningfulAnswerException extends StorageNotAvailableException {
+ /**
+ * StorageNotAvailableException constructor.
+ *
+ * @param string $message
+ * @param int $code
+ * @param \Exception|null $previous
+ * @since 6.0.0
+ */
+ public function __construct($message = '', $code = self::STATUS_INDETERMINATE, ?\Exception $previous = null) {
+ parent::__construct($message, $code, $previous);
+ }
+}
diff --git a/apps/files_external/lib/legacydependencycheckpolyfill.php b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php
index 7d6c0c4b45b..f6311fae83e 100644
--- a/apps/files_external/lib/legacydependencycheckpolyfill.php
+++ b/apps/files_external/lib/Lib/LegacyDependencyCheckPolyfill.php
@@ -1,27 +1,13 @@
<?php
+
/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_External\Lib;
-use \OCA\Files_External\Lib\MissingDependency;
+use OCP\Files\Storage\IStorage;
/**
* Polyfill for checking dependencies using legacy Storage::checkDependencies()
@@ -29,7 +15,7 @@ use \OCA\Files_External\Lib\MissingDependency;
trait LegacyDependencyCheckPolyfill {
/**
- * @return string
+ * @return class-string<IStorage>
*/
abstract public function getStorageClass();
@@ -56,7 +42,7 @@ trait LegacyDependencyCheckPolyfill {
$module = $key;
$message = $value;
}
- $value = new MissingDependency($module, $this);
+ $value = new MissingDependency($module);
$value->setMessage($message);
}
$ret[] = $value;
@@ -65,6 +51,4 @@ trait LegacyDependencyCheckPolyfill {
return $ret;
}
-
}
-
diff --git a/apps/files_external/lib/Lib/MissingDependency.php b/apps/files_external/lib/Lib/MissingDependency.php
new file mode 100644
index 00000000000..c2da7fcadbf
--- /dev/null
+++ b/apps/files_external/lib/Lib/MissingDependency.php
@@ -0,0 +1,51 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+/**
+ * External storage backend dependency
+ */
+class MissingDependency {
+
+ /** @var string|null Custom message */
+ private ?string $message = null;
+ private bool $optional = false;
+
+ /**
+ * @param string $dependency
+ */
+ public function __construct(
+ private readonly string $dependency,
+ ) {
+ }
+
+ public function getDependency(): string {
+ return $this->dependency;
+ }
+
+ public function getMessage(): ?string {
+ return $this->message;
+ }
+
+ /**
+ * @param string $message
+ * @return self
+ */
+ public function setMessage($message) {
+ $this->message = $message;
+ return $this;
+ }
+
+ public function isOptional(): bool {
+ return $this->optional;
+ }
+
+ public function setOptional(bool $optional): void {
+ $this->optional = $optional;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php b/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php
new file mode 100644
index 00000000000..2812df6ad6a
--- /dev/null
+++ b/apps/files_external/lib/Lib/Notify/SMBNotifyHandler.php
@@ -0,0 +1,130 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Notify;
+
+use OC\Files\Notify\Change;
+use OC\Files\Notify\RenameChange;
+use OCP\Files\Notify\IChange;
+use OCP\Files\Notify\INotifyHandler;
+
+class SMBNotifyHandler implements INotifyHandler {
+ /**
+ * @var string
+ */
+ private $root;
+
+ private $oldRenamePath = null;
+
+ /**
+ * SMBNotifyHandler constructor.
+ *
+ * @param \Icewind\SMB\INotifyHandler $shareNotifyHandler
+ * @param string $root
+ */
+ public function __construct(
+ private \Icewind\SMB\INotifyHandler $shareNotifyHandler,
+ $root,
+ ) {
+ $this->root = str_replace('\\', '/', $root);
+ }
+
+ private function relativePath($fullPath) {
+ if ($fullPath === $this->root) {
+ return '';
+ } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
+ return substr($fullPath, strlen($this->root));
+ } else {
+ return null;
+ }
+ }
+
+ public function listen(callable $callback) {
+ $oldRenamePath = null;
+ $this->shareNotifyHandler->listen(function (\Icewind\SMB\Change $shareChange) use ($callback) {
+ $change = $this->mapChange($shareChange);
+ if (!is_null($change)) {
+ return $callback($change);
+ } else {
+ return true;
+ }
+ });
+ }
+
+ /**
+ * Get all changes detected since the start of the notify process or the last call to getChanges
+ *
+ * @return IChange[]
+ */
+ public function getChanges() {
+ $shareChanges = $this->shareNotifyHandler->getChanges();
+ $changes = [];
+ foreach ($shareChanges as $shareChange) {
+ $change = $this->mapChange($shareChange);
+ if ($change) {
+ $changes[] = $change;
+ }
+ }
+ return $changes;
+ }
+
+ /**
+ * Stop listening for changes
+ *
+ * Note that any pending changes will be discarded
+ */
+ public function stop() {
+ $this->shareNotifyHandler->stop();
+ }
+
+ /**
+ * @param \Icewind\SMB\Change $change
+ * @return IChange|null
+ */
+ private function mapChange(\Icewind\SMB\Change $change) {
+ $path = $this->relativePath($change->getPath());
+ if (is_null($path)) {
+ return null;
+ }
+ if ($change->getCode() === \Icewind\SMB\INotifyHandler::NOTIFY_RENAMED_OLD) {
+ $this->oldRenamePath = $path;
+ return null;
+ }
+ $type = $this->mapNotifyType($change->getCode());
+ if (is_null($type)) {
+ return null;
+ }
+ if ($type === IChange::RENAMED) {
+ if (!is_null($this->oldRenamePath)) {
+ $result = new RenameChange($type, $this->oldRenamePath, $path);
+ $this->oldRenamePath = null;
+ } else {
+ $result = null;
+ }
+ } else {
+ $result = new Change($type, $path);
+ }
+ return $result;
+ }
+
+ private function mapNotifyType($smbType) {
+ switch ($smbType) {
+ case \Icewind\SMB\INotifyHandler::NOTIFY_ADDED:
+ return IChange::ADDED;
+ case \Icewind\SMB\INotifyHandler::NOTIFY_REMOVED:
+ return IChange::REMOVED;
+ case \Icewind\SMB\INotifyHandler::NOTIFY_MODIFIED:
+ case \Icewind\SMB\INotifyHandler::NOTIFY_ADDED_STREAM:
+ case \Icewind\SMB\INotifyHandler::NOTIFY_MODIFIED_STREAM:
+ case \Icewind\SMB\INotifyHandler::NOTIFY_REMOVED_STREAM:
+ return IChange::MODIFIED;
+ case \Icewind\SMB\INotifyHandler::NOTIFY_RENAMED_NEW:
+ return IChange::RENAMED;
+ default:
+ return null;
+ }
+ }
+}
diff --git a/apps/files_external/lib/Lib/PersonalMount.php b/apps/files_external/lib/Lib/PersonalMount.php
new file mode 100644
index 00000000000..d9dbddd1449
--- /dev/null
+++ b/apps/files_external/lib/Lib/PersonalMount.php
@@ -0,0 +1,70 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+use OC\Files\Mount\MoveableMount;
+use OCA\Files_External\Config\ExternalMountPoint;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\Files\Storage\IStorage;
+use OCP\Files\Storage\IStorageFactory;
+
+/**
+ * Person mount points can be moved by the user
+ */
+class PersonalMount extends ExternalMountPoint implements MoveableMount {
+ /**
+ * @param UserStoragesService $storagesService
+ * @param int $storageId
+ * @param IStorage $storage
+ * @param string $mountpoint
+ * @param array $arguments (optional) configuration for the storage backend
+ * @param IStorageFactory $loader
+ * @param array $mountOptions mount specific options
+ * @param int $externalStorageId
+ */
+ public function __construct(
+ protected UserStoragesService $storagesService,
+ StorageConfig $storageConfig,
+ /** @var int id of the external storage (mount) (not the numeric id of the resulting storage!) */
+ protected $numericExternalStorageId,
+ $storage,
+ $mountpoint,
+ $arguments = null,
+ $loader = null,
+ $mountOptions = null,
+ $mountId = null,
+ ) {
+ parent::__construct($storageConfig, $storage, $mountpoint, $arguments, $loader, $mountOptions, $mountId);
+ }
+
+ /**
+ * Move the mount point to $target
+ *
+ * @param string $target the target mount point
+ * @return bool
+ */
+ public function moveMount($target) {
+ $storage = $this->storagesService->getStorage($this->numericExternalStorageId);
+ // remove "/$user/files" prefix
+ $targetParts = explode('/', trim($target, '/'), 3);
+ $storage->setMountPoint($targetParts[2]);
+ $this->storagesService->updateStorage($storage);
+ $this->setMountPoint($target);
+ return true;
+ }
+
+ /**
+ * Remove the mount points
+ *
+ * @return bool
+ */
+ public function removeMount() {
+ $this->storagesService->removeStorage($this->numericExternalStorageId);
+ return true;
+ }
+}
diff --git a/apps/files_external/lib/Lib/PriorityTrait.php b/apps/files_external/lib/Lib/PriorityTrait.php
new file mode 100644
index 00000000000..fad2c07e58c
--- /dev/null
+++ b/apps/files_external/lib/Lib/PriorityTrait.php
@@ -0,0 +1,35 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+use OCA\Files_External\Service\BackendService;
+
+/**
+ * Trait to implement priority mechanics for a configuration class
+ */
+trait PriorityTrait {
+
+ /** @var int initial priority */
+ protected $priority = BackendService::PRIORITY_DEFAULT;
+
+ /**
+ * @return int
+ */
+ public function getPriority() {
+ return $this->priority;
+ }
+
+ /**
+ * @param int $priority
+ * @return self
+ */
+ public function setPriority($priority) {
+ $this->priority = $priority;
+ return $this;
+ }
+}
diff --git a/apps/files_external/lib/Lib/SessionStorageWrapper.php b/apps/files_external/lib/Lib/SessionStorageWrapper.php
new file mode 100644
index 00000000000..8754041b2fa
--- /dev/null
+++ b/apps/files_external/lib/Lib/SessionStorageWrapper.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+use OC\Files\Storage\Wrapper\PermissionsMask;
+use OCP\Constants;
+
+/**
+ * Wrap Storage in PermissionsMask for session ephemeral use
+ */
+class SessionStorageWrapper extends PermissionsMask {
+ /**
+ * @param array $parameters ['storage' => $storage]
+ */
+ public function __construct(array $parameters) {
+ // disable sharing permission
+ $parameters['mask'] = Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE;
+ parent::__construct($parameters);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/AmazonS3.php b/apps/files_external/lib/Lib/Storage/AmazonS3.php
new file mode 100644
index 00000000000..5dc9e114532
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/AmazonS3.php
@@ -0,0 +1,760 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use Aws\S3\Exception\S3Exception;
+use Icewind\Streams\CallbackWrapper;
+use Icewind\Streams\CountWrapper;
+use Icewind\Streams\IteratorDirectory;
+use OC\Files\Cache\CacheEntry;
+use OC\Files\ObjectStore\S3ConnectionTrait;
+use OC\Files\ObjectStore\S3ObjectTrait;
+use OC\Files\Storage\Common;
+use OCP\Cache\CappedMemoryCache;
+use OCP\Constants;
+use OCP\Files\FileInfo;
+use OCP\Files\IMimeTypeDetector;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\ITempManager;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
+class AmazonS3 extends Common {
+ use S3ConnectionTrait;
+ use S3ObjectTrait;
+
+ private LoggerInterface $logger;
+
+ public function needsPartFile(): bool {
+ return false;
+ }
+
+ /** @var CappedMemoryCache<array|false> */
+ private CappedMemoryCache $objectCache;
+
+ /** @var CappedMemoryCache<bool> */
+ private CappedMemoryCache $directoryCache;
+
+ /** @var CappedMemoryCache<array> */
+ private CappedMemoryCache $filesCache;
+
+ private IMimeTypeDetector $mimeDetector;
+ private ?bool $versioningEnabled = null;
+ private ICache $memCache;
+
+ public function __construct(array $parameters) {
+ parent::__construct($parameters);
+ $this->parseParams($parameters);
+ $this->id = 'amazon::external::' . md5($this->params['hostname'] . ':' . $this->params['bucket'] . ':' . $this->params['key']);
+ $this->objectCache = new CappedMemoryCache();
+ $this->directoryCache = new CappedMemoryCache();
+ $this->filesCache = new CappedMemoryCache();
+ $this->mimeDetector = Server::get(IMimeTypeDetector::class);
+ /** @var ICacheFactory $cacheFactory */
+ $cacheFactory = Server::get(ICacheFactory::class);
+ $this->memCache = $cacheFactory->createLocal('s3-external');
+ $this->logger = Server::get(LoggerInterface::class);
+ }
+
+ private function normalizePath(string $path): string {
+ $path = trim($path, '/');
+
+ if (!$path) {
+ $path = '.';
+ }
+
+ return $path;
+ }
+
+ private function isRoot(string $path): bool {
+ return $path === '.';
+ }
+
+ private function cleanKey(string $path): string {
+ if ($this->isRoot($path)) {
+ return '/';
+ }
+ return $path;
+ }
+
+ private function clearCache(): void {
+ $this->objectCache = new CappedMemoryCache();
+ $this->directoryCache = new CappedMemoryCache();
+ $this->filesCache = new CappedMemoryCache();
+ }
+
+ private function invalidateCache(string $key): void {
+ unset($this->objectCache[$key]);
+ $keys = array_keys($this->objectCache->getData());
+ $keyLength = strlen($key);
+ foreach ($keys as $existingKey) {
+ if (substr($existingKey, 0, $keyLength) === $key) {
+ unset($this->objectCache[$existingKey]);
+ }
+ }
+ unset($this->filesCache[$key]);
+ $keys = array_keys($this->directoryCache->getData());
+ $keyLength = strlen($key);
+ foreach ($keys as $existingKey) {
+ if (substr($existingKey, 0, $keyLength) === $key) {
+ unset($this->directoryCache[$existingKey]);
+ }
+ }
+ unset($this->directoryCache[$key]);
+ }
+
+ private function headObject(string $key): array|false {
+ if (!isset($this->objectCache[$key])) {
+ try {
+ $this->objectCache[$key] = $this->getConnection()->headObject([
+ 'Bucket' => $this->bucket,
+ 'Key' => $key
+ ] + $this->getSSECParameters())->toArray();
+ } catch (S3Exception $e) {
+ if ($e->getStatusCode() >= 500) {
+ throw $e;
+ }
+ $this->objectCache[$key] = false;
+ }
+ }
+
+ if (is_array($this->objectCache[$key]) && !isset($this->objectCache[$key]['Key'])) {
+ /** @psalm-suppress InvalidArgument Psalm doesn't understand nested arrays well */
+ $this->objectCache[$key]['Key'] = $key;
+ }
+ return $this->objectCache[$key];
+ }
+
+ /**
+ * Return true if directory exists
+ *
+ * There are no folders in s3. A folder like structure could be archived
+ * by prefixing files with the folder name.
+ *
+ * Implementation from flysystem-aws-s3-v3:
+ * https://github.com/thephpleague/flysystem-aws-s3-v3/blob/8241e9cc5b28f981e0d24cdaf9867f14c7498ae4/src/AwsS3Adapter.php#L670-L694
+ *
+ * @throws \Exception
+ */
+ private function doesDirectoryExist(string $path): bool {
+ if ($path === '.' || $path === '') {
+ return true;
+ }
+ $path = rtrim($path, '/') . '/';
+
+ if (isset($this->directoryCache[$path])) {
+ return $this->directoryCache[$path];
+ }
+ try {
+ // Maybe this isn't an actual key, but a prefix.
+ // Do a prefix listing of objects to determine.
+ $result = $this->getConnection()->listObjectsV2([
+ 'Bucket' => $this->bucket,
+ 'Prefix' => $path,
+ 'MaxKeys' => 1,
+ ]);
+
+ if (isset($result['Contents'])) {
+ $this->directoryCache[$path] = true;
+ return true;
+ }
+
+ // empty directories have their own object
+ $object = $this->headObject($path);
+
+ if ($object) {
+ $this->directoryCache[$path] = true;
+ return true;
+ }
+ } catch (S3Exception $e) {
+ if ($e->getStatusCode() >= 400 && $e->getStatusCode() < 500) {
+ $this->directoryCache[$path] = false;
+ }
+ throw $e;
+ }
+
+
+ $this->directoryCache[$path] = false;
+ return false;
+ }
+
+ protected function remove(string $path): bool {
+ // remember fileType to reduce http calls
+ $fileType = $this->filetype($path);
+ if ($fileType === 'dir') {
+ return $this->rmdir($path);
+ } elseif ($fileType === 'file') {
+ return $this->unlink($path);
+ } else {
+ return false;
+ }
+ }
+
+ public function mkdir(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if ($this->is_dir($path)) {
+ return false;
+ }
+
+ try {
+ $this->getConnection()->putObject([
+ 'Bucket' => $this->bucket,
+ 'Key' => $path . '/',
+ 'Body' => '',
+ 'ContentType' => FileInfo::MIMETYPE_FOLDER
+ ] + $this->getSSECParameters());
+ $this->testTimeout();
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+
+ $this->invalidateCache($path);
+
+ return true;
+ }
+
+ public function file_exists(string $path): bool {
+ return $this->filetype($path) !== false;
+ }
+
+
+ public function rmdir(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if ($this->isRoot($path)) {
+ return $this->clearBucket();
+ }
+
+ if (!$this->file_exists($path)) {
+ return false;
+ }
+
+ $this->invalidateCache($path);
+ return $this->batchDelete($path);
+ }
+
+ protected function clearBucket(): bool {
+ $this->clearCache();
+ return $this->batchDelete();
+ }
+
+ private function batchDelete(?string $path = null): bool {
+ // TODO explore using https://docs.aws.amazon.com/aws-sdk-php/v3/api/class-Aws.S3.BatchDelete.html
+ $params = [
+ 'Bucket' => $this->bucket
+ ];
+ if ($path !== null) {
+ $params['Prefix'] = $path . '/';
+ }
+ try {
+ $connection = $this->getConnection();
+ // Since there are no real directories on S3, we need
+ // to delete all objects prefixed with the path.
+ do {
+ // instead of the iterator, manually loop over the list ...
+ $objects = $connection->listObjects($params);
+ // ... so we can delete the files in batches
+ if (isset($objects['Contents'])) {
+ $connection->deleteObjects([
+ 'Bucket' => $this->bucket,
+ 'Delete' => [
+ 'Objects' => $objects['Contents']
+ ]
+ ]);
+ $this->testTimeout();
+ }
+ // we reached the end when the list is no longer truncated
+ } while ($objects['IsTruncated']);
+ if ($path !== '' && $path !== null) {
+ $this->deleteObject($path);
+ }
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+ return true;
+ }
+
+ public function opendir(string $path) {
+ try {
+ $content = iterator_to_array($this->getDirectoryContent($path));
+ return IteratorDirectory::wrap(array_map(function (array $item) {
+ return $item['name'];
+ }, $content));
+ } catch (S3Exception $e) {
+ return false;
+ }
+ }
+
+ public function stat(string $path): array|false {
+ $path = $this->normalizePath($path);
+
+ if ($this->is_dir($path)) {
+ $stat = $this->getDirectoryMetaData($path);
+ } else {
+ $object = $this->headObject($path);
+ if ($object === false) {
+ return false;
+ }
+ $stat = $this->objectToMetaData($object);
+ }
+ $stat['atime'] = time();
+
+ return $stat;
+ }
+
+ /**
+ * Return content length for object
+ *
+ * When the information is already present (e.g. opendir has been called before)
+ * this value is return. Otherwise a headObject is emitted.
+ */
+ private function getContentLength(string $path): int {
+ if (isset($this->filesCache[$path])) {
+ return (int)$this->filesCache[$path]['ContentLength'];
+ }
+
+ $result = $this->headObject($path);
+ if (isset($result['ContentLength'])) {
+ return (int)$result['ContentLength'];
+ }
+
+ return 0;
+ }
+
+ /**
+ * Return last modified for object
+ *
+ * When the information is already present (e.g. opendir has been called before)
+ * this value is return. Otherwise a headObject is emitted.
+ */
+ private function getLastModified(string $path): string {
+ if (isset($this->filesCache[$path])) {
+ return $this->filesCache[$path]['LastModified'];
+ }
+
+ $result = $this->headObject($path);
+ if (isset($result['LastModified'])) {
+ return $result['LastModified'];
+ }
+
+ return 'now';
+ }
+
+ public function is_dir(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if (isset($this->filesCache[$path])) {
+ return false;
+ }
+
+ try {
+ return $this->doesDirectoryExist($path);
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+ }
+
+ public function filetype(string $path): string|false {
+ $path = $this->normalizePath($path);
+
+ if ($this->isRoot($path)) {
+ return 'dir';
+ }
+
+ try {
+ if (isset($this->directoryCache[$path]) && $this->directoryCache[$path]) {
+ return 'dir';
+ }
+ if (isset($this->filesCache[$path]) || $this->headObject($path)) {
+ return 'file';
+ }
+ if ($this->doesDirectoryExist($path)) {
+ return 'dir';
+ }
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+
+ return false;
+ }
+
+ public function getPermissions(string $path): int {
+ $type = $this->filetype($path);
+ if (!$type) {
+ return 0;
+ }
+ return $type === 'dir' ? Constants::PERMISSION_ALL : Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
+ }
+
+ public function unlink(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if ($this->is_dir($path)) {
+ return $this->rmdir($path);
+ }
+
+ try {
+ $this->deleteObject($path);
+ $this->invalidateCache($path);
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+
+ return true;
+ }
+
+ public function fopen(string $path, string $mode) {
+ $path = $this->normalizePath($path);
+
+ switch ($mode) {
+ case 'r':
+ case 'rb':
+ // Don't try to fetch empty files
+ $stat = $this->stat($path);
+ if (is_array($stat) && isset($stat['size']) && $stat['size'] === 0) {
+ return fopen('php://memory', $mode);
+ }
+
+ try {
+ return $this->readObject($path);
+ } catch (\Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+ case 'w':
+ case 'wb':
+ $tmpFile = Server::get(ITempManager::class)->getTemporaryFile();
+
+ $handle = fopen($tmpFile, 'w');
+ return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void {
+ $this->writeBack($tmpFile, $path);
+ });
+ case 'a':
+ case 'ab':
+ case 'r+':
+ case 'w+':
+ case 'wb+':
+ case 'a+':
+ case 'x':
+ case 'x+':
+ case 'c':
+ case 'c+':
+ if (strrpos($path, '.') !== false) {
+ $ext = substr($path, strrpos($path, '.'));
+ } else {
+ $ext = '';
+ }
+ $tmpFile = Server::get(ITempManager::class)->getTemporaryFile($ext);
+ if ($this->file_exists($path)) {
+ $source = $this->readObject($path);
+ file_put_contents($tmpFile, $source);
+ }
+
+ $handle = fopen($tmpFile, $mode);
+ return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void {
+ $this->writeBack($tmpFile, $path);
+ });
+ }
+ return false;
+ }
+
+ public function touch(string $path, ?int $mtime = null): bool {
+ if (is_null($mtime)) {
+ $mtime = time();
+ }
+ $metadata = [
+ 'lastmodified' => gmdate(\DateTime::RFC1123, $mtime)
+ ];
+
+ try {
+ if ($this->file_exists($path)) {
+ return false;
+ }
+
+ $mimeType = $this->mimeDetector->detectPath($path);
+ $this->getConnection()->putObject([
+ 'Bucket' => $this->bucket,
+ 'Key' => $this->cleanKey($path),
+ 'Metadata' => $metadata,
+ 'Body' => '',
+ 'ContentType' => $mimeType,
+ 'MetadataDirective' => 'REPLACE',
+ ] + $this->getSSECParameters());
+ $this->testTimeout();
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+
+ $this->invalidateCache($path);
+ return true;
+ }
+
+ public function copy(string $source, string $target, ?bool $isFile = null): bool {
+ $source = $this->normalizePath($source);
+ $target = $this->normalizePath($target);
+
+ if ($isFile === true || $this->is_file($source)) {
+ try {
+ $this->copyObject($source, $target, [
+ 'StorageClass' => $this->storageClass,
+ ]);
+ $this->testTimeout();
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+ } else {
+ $this->remove($target);
+
+ try {
+ $this->mkdir($target);
+ $this->testTimeout();
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+
+ foreach ($this->getDirectoryContent($source) as $item) {
+ $childSource = $source . '/' . $item['name'];
+ $childTarget = $target . '/' . $item['name'];
+ $this->copy($childSource, $childTarget, $item['mimetype'] !== FileInfo::MIMETYPE_FOLDER);
+ }
+ }
+
+ $this->invalidateCache($target);
+
+ return true;
+ }
+
+ public function rename(string $source, string $target): bool {
+ $source = $this->normalizePath($source);
+ $target = $this->normalizePath($target);
+
+ if ($this->is_file($source)) {
+ if ($this->copy($source, $target) === false) {
+ return false;
+ }
+
+ if ($this->unlink($source) === false) {
+ $this->unlink($target);
+ return false;
+ }
+ } else {
+ if ($this->copy($source, $target) === false) {
+ return false;
+ }
+
+ if ($this->rmdir($source) === false) {
+ $this->rmdir($target);
+ return false;
+ }
+ }
+
+ return true;
+ }
+
+ public function test(): bool {
+ $this->getConnection()->headBucket([
+ 'Bucket' => $this->bucket
+ ]);
+ return true;
+ }
+
+ public function getId(): string {
+ return $this->id;
+ }
+
+ public function writeBack(string $tmpFile, string $path): bool {
+ try {
+ $source = fopen($tmpFile, 'r');
+ $this->writeObject($path, $source, $this->mimeDetector->detectPath($path));
+ $this->invalidateCache($path);
+
+ unlink($tmpFile);
+ return true;
+ } catch (S3Exception $e) {
+ $this->logger->error($e->getMessage(), [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return false;
+ }
+ }
+
+ /**
+ * check if curl is installed
+ */
+ public static function checkDependencies(): bool {
+ return true;
+ }
+
+ public function getDirectoryContent(string $directory): \Traversable {
+ $path = $this->normalizePath($directory);
+
+ if ($this->isRoot($path)) {
+ $path = '';
+ } else {
+ $path .= '/';
+ }
+
+ $results = $this->getConnection()->getPaginator('ListObjectsV2', [
+ 'Bucket' => $this->bucket,
+ 'Delimiter' => '/',
+ 'Prefix' => $path,
+ ]);
+
+ foreach ($results as $result) {
+ // sub folders
+ if (is_array($result['CommonPrefixes'])) {
+ foreach ($result['CommonPrefixes'] as $prefix) {
+ $dir = $this->getDirectoryMetaData($prefix['Prefix']);
+ if ($dir) {
+ yield $dir;
+ }
+ }
+ }
+ if (is_array($result['Contents'])) {
+ foreach ($result['Contents'] as $object) {
+ $this->objectCache[$object['Key']] = $object;
+ if ($object['Key'] !== $path) {
+ yield $this->objectToMetaData($object);
+ }
+ }
+ }
+ }
+ }
+
+ private function objectToMetaData(array $object): array {
+ return [
+ 'name' => basename($object['Key']),
+ 'mimetype' => $this->mimeDetector->detectPath($object['Key']),
+ 'mtime' => strtotime($object['LastModified']),
+ 'storage_mtime' => strtotime($object['LastModified']),
+ 'etag' => trim($object['ETag'], '"'),
+ 'permissions' => Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE,
+ 'size' => (int)($object['Size'] ?? $object['ContentLength']),
+ ];
+ }
+
+ private function getDirectoryMetaData(string $path): ?array {
+ $path = trim($path, '/');
+ // when versioning is enabled, delete markers are returned as part of CommonPrefixes
+ // resulting in "ghost" folders, verify that each folder actually exists
+ if ($this->versioningEnabled() && !$this->doesDirectoryExist($path)) {
+ return null;
+ }
+ $cacheEntry = $this->getCache()->get($path);
+ if ($cacheEntry instanceof CacheEntry) {
+ return $cacheEntry->getData();
+ } else {
+ return [
+ 'name' => basename($path),
+ 'mimetype' => FileInfo::MIMETYPE_FOLDER,
+ 'mtime' => time(),
+ 'storage_mtime' => time(),
+ 'etag' => uniqid(),
+ 'permissions' => Constants::PERMISSION_ALL,
+ 'size' => -1,
+ ];
+ }
+ }
+
+ public function versioningEnabled(): bool {
+ if ($this->versioningEnabled === null) {
+ $cached = $this->memCache->get('versioning-enabled::' . $this->getBucket());
+ if ($cached === null) {
+ $this->versioningEnabled = $this->getVersioningStatusFromBucket();
+ $this->memCache->set('versioning-enabled::' . $this->getBucket(), $this->versioningEnabled, 60);
+ } else {
+ $this->versioningEnabled = $cached;
+ }
+ }
+ return $this->versioningEnabled;
+ }
+
+ protected function getVersioningStatusFromBucket(): bool {
+ try {
+ $result = $this->getConnection()->getBucketVersioning(['Bucket' => $this->getBucket()]);
+ return $result->get('Status') === 'Enabled';
+ } catch (S3Exception $s3Exception) {
+ // This is needed for compatibility with Storj gateway which does not support versioning yet
+ if ($s3Exception->getAwsErrorCode() === 'NotImplemented' || $s3Exception->getAwsErrorCode() === 'AccessDenied') {
+ return false;
+ }
+ throw $s3Exception;
+ }
+ }
+
+ public function hasUpdated(string $path, int $time): bool {
+ // for files we can get the proper mtime
+ if ($path !== '' && $object = $this->headObject($path)) {
+ $stat = $this->objectToMetaData($object);
+ return $stat['mtime'] > $time;
+ } else {
+ // for directories, the only real option we have is to do a prefix listing and iterate over all objects
+ // however, since this is just as expensive as just re-scanning the directory, we can simply return true
+ // and have the scanner figure out if anything has actually changed
+ return true;
+ }
+ }
+
+ public function writeStream(string $path, $stream, ?int $size = null): int {
+ if ($size === null) {
+ $size = 0;
+ // track the number of bytes read from the input stream to return as the number of written bytes.
+ $stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void {
+ $size = $writtenSize;
+ });
+ }
+
+ if (!is_resource($stream)) {
+ throw new \InvalidArgumentException('Invalid stream provided');
+ }
+
+ $path = $this->normalizePath($path);
+ $this->writeObject($path, $stream, $this->mimeDetector->detectPath($path));
+ $this->invalidateCache($path);
+
+ return $size;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/FTP.php b/apps/files_external/lib/Lib/Storage/FTP.php
new file mode 100644
index 00000000000..944964de7a6
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/FTP.php
@@ -0,0 +1,364 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use Icewind\Streams\CallbackWrapper;
+use Icewind\Streams\CountWrapper;
+use Icewind\Streams\IteratorDirectory;
+use OC\Files\Storage\Common;
+use OC\Files\Storage\PolyFill\CopyDirectory;
+use OCP\Constants;
+use OCP\Files\FileInfo;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\StorageNotAvailableException;
+use OCP\ITempManager;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
+class FTP extends Common {
+ use CopyDirectory;
+
+ private $root;
+ private $host;
+ private $password;
+ private $username;
+ private $secure;
+ private $port;
+ private $utf8Mode;
+
+ /** @var FtpConnection|null */
+ private $connection;
+
+ public function __construct(array $parameters) {
+ if (isset($parameters['host']) && isset($parameters['user']) && isset($parameters['password'])) {
+ $this->host = $parameters['host'];
+ $this->username = $parameters['user'];
+ $this->password = $parameters['password'];
+ if (isset($parameters['secure'])) {
+ if (is_string($parameters['secure'])) {
+ $this->secure = ($parameters['secure'] === 'true');
+ } else {
+ $this->secure = (bool)$parameters['secure'];
+ }
+ } else {
+ $this->secure = false;
+ }
+ $this->root = isset($parameters['root']) ? '/' . ltrim($parameters['root']) : '/';
+ $this->port = $parameters['port'] ?? 21;
+ $this->utf8Mode = isset($parameters['utf8']) && $parameters['utf8'];
+ } else {
+ throw new \Exception('Creating ' . self::class . ' storage failed, required parameters not set');
+ }
+ }
+
+ public function __destruct() {
+ $this->connection = null;
+ }
+
+ protected function getConnection(): FtpConnection {
+ if (!$this->connection) {
+ try {
+ $this->connection = new FtpConnection(
+ $this->secure,
+ $this->host,
+ $this->port,
+ $this->username,
+ $this->password
+ );
+ } catch (\Exception $e) {
+ throw new StorageNotAvailableException('Failed to create ftp connection', 0, $e);
+ }
+ if ($this->utf8Mode) {
+ if (!$this->connection->setUtf8Mode()) {
+ throw new StorageNotAvailableException('Could not set UTF-8 mode');
+ }
+ }
+ }
+
+ return $this->connection;
+ }
+
+ public function getId(): string {
+ return 'ftp::' . $this->username . '@' . $this->host . '/' . $this->root;
+ }
+
+ protected function buildPath(string $path): string {
+ return rtrim($this->root . '/' . $path, '/');
+ }
+
+ public static function checkDependencies(): array|bool {
+ if (function_exists('ftp_login')) {
+ return true;
+ } else {
+ return ['ftp'];
+ }
+ }
+
+ public function filemtime(string $path): int|false {
+ $result = $this->getConnection()->mdtm($this->buildPath($path));
+
+ if ($result === -1) {
+ if ($this->is_dir($path)) {
+ $list = $this->getConnection()->mlsd($this->buildPath($path));
+ if (!$list) {
+ Server::get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), failed to list folder contents");
+ return time();
+ }
+ $currentDir = current(array_filter($list, function ($item) {
+ return $item['type'] === 'cdir';
+ }));
+ if ($currentDir) {
+ [$modify] = explode('.', $currentDir['modify'] ?? '', 2);
+ $time = \DateTime::createFromFormat('YmdHis', $modify);
+ if ($time === false) {
+ throw new \Exception("Invalid date format for directory: $currentDir");
+ }
+ return $time->getTimestamp();
+ } else {
+ Server::get(LoggerInterface::class)->warning("Unable to get last modified date for ftp folder ($path), folder contents doesn't include current folder");
+ return time();
+ }
+ } else {
+ return false;
+ }
+ } else {
+ return $result;
+ }
+ }
+
+ public function filesize(string $path): false|int|float {
+ $result = $this->getConnection()->size($this->buildPath($path));
+ if ($result === -1) {
+ return false;
+ } else {
+ return $result;
+ }
+ }
+
+ public function rmdir(string $path): bool {
+ if ($this->is_dir($path)) {
+ $result = $this->getConnection()->rmdir($this->buildPath($path));
+ // recursive rmdir support depends on the ftp server
+ if ($result) {
+ return $result;
+ } else {
+ return $this->recursiveRmDir($path);
+ }
+ } elseif ($this->is_file($path)) {
+ return $this->unlink($path);
+ } else {
+ return false;
+ }
+ }
+
+ private function recursiveRmDir(string $path): bool {
+ $contents = $this->getDirectoryContent($path);
+ $result = true;
+ foreach ($contents as $content) {
+ if ($content['mimetype'] === FileInfo::MIMETYPE_FOLDER) {
+ $result = $result && $this->recursiveRmDir($path . '/' . $content['name']);
+ } else {
+ $result = $result && $this->getConnection()->delete($this->buildPath($path . '/' . $content['name']));
+ }
+ }
+ $result = $result && $this->getConnection()->rmdir($this->buildPath($path));
+
+ return $result;
+ }
+
+ public function test(): bool {
+ try {
+ return $this->getConnection()->systype() !== false;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function stat(string $path): array|false {
+ if (!$this->file_exists($path)) {
+ return false;
+ }
+ return [
+ 'mtime' => $this->filemtime($path),
+ 'size' => $this->filesize($path),
+ ];
+ }
+
+ public function file_exists(string $path): bool {
+ if ($path === '' || $path === '.' || $path === '/') {
+ return true;
+ }
+ return $this->filetype($path) !== false;
+ }
+
+ public function unlink(string $path): bool {
+ switch ($this->filetype($path)) {
+ case 'dir':
+ return $this->rmdir($path);
+ case 'file':
+ return $this->getConnection()->delete($this->buildPath($path));
+ default:
+ return false;
+ }
+ }
+
+ public function opendir(string $path) {
+ $files = $this->getConnection()->nlist($this->buildPath($path));
+ return IteratorDirectory::wrap($files);
+ }
+
+ public function mkdir(string $path): bool {
+ if ($this->is_dir($path)) {
+ return false;
+ }
+ return $this->getConnection()->mkdir($this->buildPath($path)) !== false;
+ }
+
+ public function is_dir(string $path): bool {
+ if ($path === '') {
+ return true;
+ }
+ if ($this->getConnection()->chdir($this->buildPath($path)) === true) {
+ $this->getConnection()->chdir('/');
+ return true;
+ } else {
+ return false;
+ }
+ }
+
+ public function is_file(string $path): bool {
+ return $this->filesize($path) !== false;
+ }
+
+ public function filetype(string $path): string|false {
+ if ($this->is_dir($path)) {
+ return 'dir';
+ } elseif ($this->is_file($path)) {
+ return 'file';
+ } else {
+ return false;
+ }
+ }
+
+ public function fopen(string $path, string $mode) {
+ $useExisting = true;
+ switch ($mode) {
+ case 'r':
+ case 'rb':
+ return $this->readStream($path);
+ case 'w':
+ case 'w+':
+ case 'wb':
+ case 'wb+':
+ $useExisting = false;
+ // no break
+ case 'a':
+ case 'ab':
+ case 'r+':
+ case 'a+':
+ case 'x':
+ case 'x+':
+ case 'c':
+ case 'c+':
+ //emulate these
+ if ($useExisting and $this->file_exists($path)) {
+ if (!$this->isUpdatable($path)) {
+ return false;
+ }
+ $tmpFile = $this->getCachedFile($path);
+ } else {
+ if (!$this->isCreatable(dirname($path))) {
+ return false;
+ }
+ $tmpFile = Server::get(ITempManager::class)->getTemporaryFile();
+ }
+ $source = fopen($tmpFile, $mode);
+ return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $path): void {
+ $this->writeStream($path, fopen($tmpFile, 'r'));
+ unlink($tmpFile);
+ });
+ }
+ return false;
+ }
+
+ public function writeStream(string $path, $stream, ?int $size = null): int {
+ if ($size === null) {
+ $stream = CountWrapper::wrap($stream, function ($writtenSize) use (&$size): void {
+ $size = $writtenSize;
+ });
+ }
+
+ $this->getConnection()->fput($this->buildPath($path), $stream);
+ fclose($stream);
+
+ return $size;
+ }
+
+ public function readStream(string $path) {
+ $stream = fopen('php://temp', 'w+');
+ $result = $this->getConnection()->fget($stream, $this->buildPath($path));
+ rewind($stream);
+
+ if (!$result) {
+ fclose($stream);
+ return false;
+ }
+ return $stream;
+ }
+
+ public function touch(string $path, ?int $mtime = null): bool {
+ if ($this->file_exists($path)) {
+ return false;
+ } else {
+ $this->file_put_contents($path, '');
+ return true;
+ }
+ }
+
+ public function rename(string $source, string $target): bool {
+ $this->unlink($target);
+ return $this->getConnection()->rename($this->buildPath($source), $this->buildPath($target));
+ }
+
+ public function getDirectoryContent(string $directory): \Traversable {
+ $files = $this->getConnection()->mlsd($this->buildPath($directory));
+ $mimeTypeDetector = Server::get(IMimeTypeDetector::class);
+
+ foreach ($files as $file) {
+ $name = $file['name'];
+ if ($file['type'] === 'cdir' || $file['type'] === 'pdir') {
+ continue;
+ }
+ $permissions = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
+ $isDir = $file['type'] === 'dir';
+ if ($isDir) {
+ $permissions += Constants::PERMISSION_CREATE;
+ }
+
+ $data = [];
+ $data['mimetype'] = $isDir ? FileInfo::MIMETYPE_FOLDER : $mimeTypeDetector->detectPath($name);
+
+ // strip fractional seconds
+ [$modify] = explode('.', $file['modify'], 2);
+ $mtime = \DateTime::createFromFormat('YmdGis', $modify);
+ $data['mtime'] = $mtime === false ? time() : $mtime->getTimestamp();
+ if ($isDir) {
+ $data['size'] = -1; //unknown
+ } elseif (isset($file['size'])) {
+ $data['size'] = $file['size'];
+ } else {
+ $data['size'] = $this->filesize($directory . '/' . $name);
+ }
+ $data['etag'] = uniqid();
+ $data['storage_mtime'] = $data['mtime'];
+ $data['permissions'] = $permissions;
+ $data['name'] = $name;
+
+ yield $data;
+ }
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/FtpConnection.php b/apps/files_external/lib/Lib/Storage/FtpConnection.php
new file mode 100644
index 00000000000..a064bf9b100
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/FtpConnection.php
@@ -0,0 +1,222 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Lib\Storage;
+
+/**
+ * Low level wrapper around the ftp functions that smooths over some difference between servers
+ */
+class FtpConnection {
+ private \FTP\Connection $connection;
+
+ public function __construct(bool $secure, string $hostname, int $port, string $username, string $password) {
+ if ($secure) {
+ $connection = ftp_ssl_connect($hostname, $port);
+ } else {
+ $connection = ftp_connect($hostname, $port);
+ }
+
+ if ($connection === false) {
+ throw new \Exception('Failed to connect to ftp');
+ }
+
+ if (ftp_login($connection, $username, $password) === false) {
+ throw new \Exception('Failed to connect to login to ftp');
+ }
+
+ ftp_pasv($connection, true);
+ $this->connection = $connection;
+ }
+
+ public function __destruct() {
+ ftp_close($this->connection);
+ }
+
+ public function setUtf8Mode(): bool {
+ $response = ftp_raw($this->connection, 'OPTS UTF8 ON');
+ return substr($response[0], 0, 3) === '200';
+ }
+
+ public function fput(string $path, $handle) {
+ return @ftp_fput($this->connection, $path, $handle, FTP_BINARY);
+ }
+
+ public function fget($handle, string $path) {
+ return @ftp_fget($this->connection, $handle, $path, FTP_BINARY);
+ }
+
+ public function mkdir(string $path) {
+ return @ftp_mkdir($this->connection, $path);
+ }
+
+ public function chdir(string $path) {
+ return @ftp_chdir($this->connection, $path);
+ }
+
+ public function delete(string $path) {
+ return @ftp_delete($this->connection, $path);
+ }
+
+ public function rmdir(string $path) {
+ return @ftp_rmdir($this->connection, $path);
+ }
+
+ public function rename(string $source, string $target) {
+ return @ftp_rename($this->connection, $source, $target);
+ }
+
+ public function mdtm(string $path): int {
+ $result = @ftp_mdtm($this->connection, $path);
+
+ // filezilla doesn't like empty path with mdtm
+ if ($result === -1 && $path === '') {
+ $result = @ftp_mdtm($this->connection, '/');
+ }
+ return $result;
+ }
+
+ public function size(string $path) {
+ return @ftp_size($this->connection, $path);
+ }
+
+ public function systype() {
+ return @ftp_systype($this->connection);
+ }
+
+ public function nlist(string $path) {
+ $files = @ftp_nlist($this->connection, $path);
+ return array_map(function ($name) {
+ if (str_contains($name, '/')) {
+ $name = basename($name);
+ }
+ return $name;
+ }, $files);
+ }
+
+ public function mlsd(string $path) {
+ $files = @ftp_mlsd($this->connection, $path);
+
+ if ($files !== false) {
+ return array_map(function ($file) {
+ if (str_contains($file['name'], '/')) {
+ $file['name'] = basename($file['name']);
+ }
+ return $file;
+ }, $files);
+ } else {
+ // not all servers support mlsd, in those cases we parse the raw list ourselves
+ $rawList = @ftp_rawlist($this->connection, '-aln ' . $path);
+ if ($rawList === false) {
+ return false;
+ }
+ return $this->parseRawList($rawList, $path);
+ }
+ }
+
+ // rawlist parsing logic is based on the ftp implementation from https://github.com/thephpleague/flysystem
+ private function parseRawList(array $rawList, string $directory): array {
+ return array_map(function ($item) use ($directory) {
+ return $this->parseRawListItem($item, $directory);
+ }, $rawList);
+ }
+
+ private function parseRawListItem(string $item, string $directory): array {
+ $isWindows = preg_match('/^[0-9]{2,4}-[0-9]{2}-[0-9]{2}/', $item);
+
+ return $isWindows ? $this->parseWindowsItem($item, $directory) : $this->parseUnixItem($item, $directory);
+ }
+
+ private function parseUnixItem(string $item, string $directory): array {
+ $item = preg_replace('#\s+#', ' ', $item, 7);
+
+ if (count(explode(' ', $item, 9)) !== 9) {
+ throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
+ }
+
+ [$permissions, /* $number */, /* $owner */, /* $group */, $size, $month, $day, $time, $name] = explode(' ', $item, 9);
+ if ($name === '.') {
+ $type = 'cdir';
+ } elseif ($name === '..') {
+ $type = 'pdir';
+ } else {
+ $type = substr($permissions, 0, 1) === 'd' ? 'dir' : 'file';
+ }
+
+ $parsedDate = (new \DateTime())
+ ->setTimestamp(strtotime("$month $day $time"));
+ $tomorrow = (new \DateTime())->add(new \DateInterval('P1D'));
+
+ // since the provided date doesn't include the year, we either set it to the correct year
+ // or when the date would otherwise be in the future (by more then 1 day to account for timezone errors)
+ // we use last year
+ if ($parsedDate > $tomorrow) {
+ $parsedDate = $parsedDate->sub(new \DateInterval('P1Y'));
+ }
+
+ $formattedDate = $parsedDate
+ ->format('YmdHis');
+
+ return [
+ 'type' => $type,
+ 'name' => $name,
+ 'modify' => $formattedDate,
+ 'perm' => $this->normalizePermissions($permissions),
+ 'size' => (int)$size,
+ ];
+ }
+
+ private function normalizePermissions(string $permissions) {
+ $isDir = substr($permissions, 0, 1) === 'd';
+ // remove the type identifier and only use owner permissions
+ $permissions = substr($permissions, 1, 4);
+
+ // map the string rights to the ftp counterparts
+ $filePermissionsMap = ['r' => 'r', 'w' => 'fadfw'];
+ $dirPermissionsMap = ['r' => 'e', 'w' => 'flcdmp'];
+
+ $map = $isDir ? $dirPermissionsMap : $filePermissionsMap;
+
+ return array_reduce(str_split($permissions), function ($ftpPermissions, $permission) use ($map) {
+ if (isset($map[$permission])) {
+ $ftpPermissions .= $map[$permission];
+ }
+ return $ftpPermissions;
+ }, '');
+ }
+
+ private function parseWindowsItem(string $item, string $directory): array {
+ $item = preg_replace('#\s+#', ' ', trim($item), 3);
+
+ if (count(explode(' ', $item, 4)) !== 4) {
+ throw new \RuntimeException("Metadata can't be parsed from item '$item' , not enough parts.");
+ }
+
+ [$date, $time, $size, $name] = explode(' ', $item, 4);
+
+ // Check for the correct date/time format
+ $format = strlen($date) === 8 ? 'm-d-yH:iA' : 'Y-m-dH:i';
+ $formattedDate = \DateTime::createFromFormat($format, $date . $time)->format('YmdGis');
+
+ if ($name === '.') {
+ $type = 'cdir';
+ } elseif ($name === '..') {
+ $type = 'pdir';
+ } else {
+ $type = ($size === '<DIR>') ? 'dir' : 'file';
+ }
+
+ return [
+ 'type' => $type,
+ 'name' => $name,
+ 'modify' => $formattedDate,
+ 'perm' => ($type === 'file') ? 'adfrw' : 'flcdmpe',
+ 'size' => (int)$size,
+ ];
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/OwnCloud.php b/apps/files_external/lib/Lib/Storage/OwnCloud.php
new file mode 100644
index 00000000000..12c305de750
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/OwnCloud.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use OC\Files\Storage\DAV;
+use OCP\Files\Storage\IDisableEncryptionStorage;
+use Sabre\DAV\Client;
+
+/**
+ * Nextcloud backend for external storage based on DAV backend.
+ *
+ * The Nextcloud URL consists of three parts:
+ * http://%host/%context/remote.php/webdav/%root
+ *
+ */
+class OwnCloud extends DAV implements IDisableEncryptionStorage {
+ public const OC_URL_SUFFIX = 'remote.php/webdav';
+
+ public function __construct(array $parameters) {
+ // extract context path from host if specified
+ // (owncloud install path on host)
+ $host = $parameters['host'];
+ // strip protocol
+ if (substr($host, 0, 8) === 'https://') {
+ $host = substr($host, 8);
+ $parameters['secure'] = true;
+ } elseif (substr($host, 0, 7) === 'http://') {
+ $host = substr($host, 7);
+ $parameters['secure'] = false;
+ }
+ $contextPath = '';
+ $hostSlashPos = strpos($host, '/');
+ if ($hostSlashPos !== false) {
+ $contextPath = substr($host, $hostSlashPos);
+ $host = substr($host, 0, $hostSlashPos);
+ }
+
+ if (!str_ends_with($contextPath, '/')) {
+ $contextPath .= '/';
+ }
+
+ if (isset($parameters['root'])) {
+ $root = '/' . ltrim($parameters['root'], '/');
+ } else {
+ $root = '/';
+ }
+
+ $parameters['host'] = $host;
+ $parameters['root'] = $contextPath . self::OC_URL_SUFFIX . $root;
+ $parameters['authType'] = Client::AUTH_BASIC;
+
+ parent::__construct($parameters);
+ }
+
+ public function needsPartFile(): bool {
+ return false;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/SFTP.php b/apps/files_external/lib/Lib/Storage/SFTP.php
new file mode 100644
index 00000000000..a2f5bafcca1
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/SFTP.php
@@ -0,0 +1,523 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use Icewind\Streams\CallbackWrapper;
+use Icewind\Streams\CountWrapper;
+use Icewind\Streams\IteratorDirectory;
+use Icewind\Streams\RetryWrapper;
+use OC\Files\Storage\Common;
+use OC\Files\View;
+use OCP\Cache\CappedMemoryCache;
+use OCP\Constants;
+use OCP\Files\FileInfo;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Server;
+use phpseclib\Net\SFTP\Stream;
+
+/**
+ * Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
+ * provide access to SFTP servers.
+ */
+class SFTP extends Common {
+ private $host;
+ private $user;
+ private $root;
+ private $port = 22;
+
+ private $auth = [];
+
+ /**
+ * @var \phpseclib\Net\SFTP
+ */
+ protected $client;
+ private CappedMemoryCache $knownMTimes;
+
+ private IMimeTypeDetector $mimeTypeDetector;
+
+ public const COPY_CHUNK_SIZE = 8 * 1024 * 1024;
+
+ /**
+ * @param string $host protocol://server:port
+ * @return array [$server, $port]
+ */
+ private function splitHost(string $host): array {
+ $input = $host;
+ if (!str_contains($host, '://')) {
+ // add a protocol to fix parse_url behavior with ipv6
+ $host = 'http://' . $host;
+ }
+
+ $parsed = parse_url($host);
+ if (is_array($parsed) && isset($parsed['port'])) {
+ return [$parsed['host'], $parsed['port']];
+ } elseif (is_array($parsed)) {
+ return [$parsed['host'], 22];
+ } else {
+ return [$input, 22];
+ }
+ }
+
+ public function __construct(array $parameters) {
+ // Register sftp://
+ Stream::register();
+
+ $parsedHost = $this->splitHost($parameters['host']);
+
+ $this->host = $parsedHost[0];
+ $this->port = $parsedHost[1];
+
+ if (!isset($parameters['user'])) {
+ throw new \UnexpectedValueException('no authentication parameters specified');
+ }
+ $this->user = $parameters['user'];
+
+ if (isset($parameters['public_key_auth'])) {
+ $this->auth[] = $parameters['public_key_auth'];
+ }
+ if (isset($parameters['password']) && $parameters['password'] !== '') {
+ $this->auth[] = $parameters['password'];
+ }
+
+ if ($this->auth === []) {
+ throw new \UnexpectedValueException('no authentication parameters specified');
+ }
+
+ $this->root
+ = isset($parameters['root']) ? $this->cleanPath($parameters['root']) : '/';
+
+ $this->root = '/' . ltrim($this->root, '/');
+ $this->root = rtrim($this->root, '/') . '/';
+
+ $this->knownMTimes = new CappedMemoryCache();
+
+ $this->mimeTypeDetector = Server::get(IMimeTypeDetector::class);
+ }
+
+ /**
+ * Returns the connection.
+ *
+ * @return \phpseclib\Net\SFTP connected client instance
+ * @throws \Exception when the connection failed
+ */
+ public function getConnection(): \phpseclib\Net\SFTP {
+ if (!is_null($this->client)) {
+ return $this->client;
+ }
+
+ $hostKeys = $this->readHostKeys();
+ $this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
+
+ // The SSH Host Key MUST be verified before login().
+ $currentHostKey = $this->client->getServerPublicHostKey();
+ if (array_key_exists($this->host, $hostKeys)) {
+ if ($hostKeys[$this->host] !== $currentHostKey) {
+ throw new \Exception('Host public key does not match known key');
+ }
+ } else {
+ $hostKeys[$this->host] = $currentHostKey;
+ $this->writeHostKeys($hostKeys);
+ }
+
+ $login = false;
+ foreach ($this->auth as $auth) {
+ /** @psalm-suppress TooManyArguments */
+ $login = $this->client->login($this->user, $auth);
+ if ($login === true) {
+ break;
+ }
+ }
+
+ if ($login === false) {
+ throw new \Exception('Login failed');
+ }
+ return $this->client;
+ }
+
+ public function test(): bool {
+ if (
+ !isset($this->host)
+ || !isset($this->user)
+ ) {
+ return false;
+ }
+ return $this->getConnection()->nlist() !== false;
+ }
+
+ public function getId(): string {
+ $id = 'sftp::' . $this->user . '@' . $this->host;
+ if ($this->port !== 22) {
+ $id .= ':' . $this->port;
+ }
+ // note: this will double the root slash,
+ // we should not change it to keep compatible with
+ // old storage ids
+ $id .= '/' . $this->root;
+ return $id;
+ }
+
+ public function getHost(): string {
+ return $this->host;
+ }
+
+ public function getRoot(): string {
+ return $this->root;
+ }
+
+ public function getUser(): string {
+ return $this->user;
+ }
+
+ private function absPath(string $path): string {
+ return $this->root . $this->cleanPath($path);
+ }
+
+ private function hostKeysPath(): string|false {
+ try {
+ $userId = \OC_User::getUser();
+ if ($userId === false) {
+ return false;
+ }
+
+ $view = new View('/' . $userId . '/files_external');
+
+ return $view->getLocalFile('ssh_hostKeys');
+ } catch (\Exception $e) {
+ }
+ return false;
+ }
+
+ protected function writeHostKeys(array $keys): bool {
+ try {
+ $keyPath = $this->hostKeysPath();
+ if ($keyPath && file_exists($keyPath)) {
+ $fp = fopen($keyPath, 'w');
+ foreach ($keys as $host => $key) {
+ fwrite($fp, $host . '::' . $key . "\n");
+ }
+ fclose($fp);
+ return true;
+ }
+ } catch (\Exception $e) {
+ }
+ return false;
+ }
+
+ protected function readHostKeys(): array {
+ try {
+ $keyPath = $this->hostKeysPath();
+ if (file_exists($keyPath)) {
+ $hosts = [];
+ $keys = [];
+ $lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
+ if ($lines) {
+ foreach ($lines as $line) {
+ $hostKeyArray = explode('::', $line, 2);
+ if (count($hostKeyArray) === 2) {
+ $hosts[] = $hostKeyArray[0];
+ $keys[] = $hostKeyArray[1];
+ }
+ }
+ return array_combine($hosts, $keys);
+ }
+ }
+ } catch (\Exception $e) {
+ }
+ return [];
+ }
+
+ public function mkdir(string $path): bool {
+ try {
+ return $this->getConnection()->mkdir($this->absPath($path));
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function rmdir(string $path): bool {
+ try {
+ $result = $this->getConnection()->delete($this->absPath($path), true);
+ // workaround: stray stat cache entry when deleting empty folders
+ // see https://github.com/phpseclib/phpseclib/issues/706
+ $this->getConnection()->clearStatCache();
+ return $result;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function opendir(string $path) {
+ try {
+ $list = $this->getConnection()->nlist($this->absPath($path));
+ if ($list === false) {
+ return false;
+ }
+
+ $id = md5('sftp:' . $path);
+ $dirStream = [];
+ foreach ($list as $file) {
+ if ($file !== '.' && $file !== '..') {
+ $dirStream[] = $file;
+ }
+ }
+ return IteratorDirectory::wrap($dirStream);
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function filetype(string $path): string|false {
+ try {
+ $stat = $this->getConnection()->stat($this->absPath($path));
+ if (!is_array($stat) || !array_key_exists('type', $stat)) {
+ return false;
+ }
+ if ((int)$stat['type'] === NET_SFTP_TYPE_REGULAR) {
+ return 'file';
+ }
+
+ if ((int)$stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
+ return 'dir';
+ }
+ } catch (\Exception $e) {
+ }
+ return false;
+ }
+
+ public function file_exists(string $path): bool {
+ try {
+ return $this->getConnection()->stat($this->absPath($path)) !== false;
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function unlink(string $path): bool {
+ try {
+ return $this->getConnection()->delete($this->absPath($path), true);
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function fopen(string $path, string $mode) {
+ $path = $this->cleanPath($path);
+ try {
+ $absPath = $this->absPath($path);
+ $connection = $this->getConnection();
+ switch ($mode) {
+ case 'r':
+ case 'rb':
+ $stat = $this->stat($path);
+ if (!$stat) {
+ return false;
+ }
+ SFTPReadStream::register();
+ $context = stream_context_create(['sftp' => ['session' => $connection, 'size' => $stat['size']]]);
+ $handle = fopen('sftpread://' . trim($absPath, '/'), 'r', false, $context);
+ return RetryWrapper::wrap($handle);
+ case 'w':
+ case 'wb':
+ SFTPWriteStream::register();
+ // the SFTPWriteStream doesn't go through the "normal" methods so it doesn't clear the stat cache.
+ $connection->_remove_from_stat_cache($absPath);
+ $context = stream_context_create(['sftp' => ['session' => $connection]]);
+ $fh = fopen('sftpwrite://' . trim($absPath, '/'), 'w', false, $context);
+ if ($fh) {
+ $fh = CallbackWrapper::wrap($fh, null, null, function () use ($path): void {
+ $this->knownMTimes->set($path, time());
+ });
+ }
+ return $fh;
+ case 'a':
+ case 'ab':
+ case 'r+':
+ case 'w+':
+ case 'wb+':
+ case 'a+':
+ case 'x':
+ case 'x+':
+ case 'c':
+ case 'c+':
+ $context = stream_context_create(['sftp' => ['session' => $connection]]);
+ $handle = fopen($this->constructUrl($path), $mode, false, $context);
+ return RetryWrapper::wrap($handle);
+ }
+ } catch (\Exception $e) {
+ }
+ return false;
+ }
+
+ public function touch(string $path, ?int $mtime = null): bool {
+ try {
+ if (!is_null($mtime)) {
+ return false;
+ }
+ if (!$this->file_exists($path)) {
+ return $this->getConnection()->put($this->absPath($path), '');
+ } else {
+ return false;
+ }
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * @throws \Exception
+ */
+ public function getFile(string $path, string $target): void {
+ $this->getConnection()->get($path, $target);
+ }
+
+ public function rename(string $source, string $target): bool {
+ try {
+ if ($this->file_exists($target)) {
+ $this->unlink($target);
+ }
+ return $this->getConnection()->rename(
+ $this->absPath($source),
+ $this->absPath($target)
+ );
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ /**
+ * @return array{mtime: int, size: int, ctime: int}|false
+ */
+ public function stat(string $path): array|false {
+ try {
+ $path = $this->cleanPath($path);
+ $stat = $this->getConnection()->stat($this->absPath($path));
+
+ $mtime = isset($stat['mtime']) ? (int)$stat['mtime'] : -1;
+ $size = isset($stat['size']) ? (int)$stat['size'] : 0;
+
+ // the mtime can't be less than when we last touched it
+ if ($knownMTime = $this->knownMTimes->get($path)) {
+ $mtime = max($mtime, $knownMTime);
+ }
+
+ return [
+ 'mtime' => $mtime,
+ 'size' => $size,
+ 'ctime' => -1
+ ];
+ } catch (\Exception $e) {
+ return false;
+ }
+ }
+
+ public function constructUrl(string $path): string {
+ // Do not pass the password here. We want to use the Net_SFTP object
+ // supplied via stream context or fail. We only supply username and
+ // hostname because this might show up in logs (they are not used).
+ $url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path;
+ return $url;
+ }
+
+ public function file_put_contents(string $path, mixed $data): int|float|false {
+ /** @psalm-suppress InternalMethod */
+ $result = $this->getConnection()->put($this->absPath($path), $data);
+ if ($result) {
+ return strlen($data);
+ } else {
+ return false;
+ }
+ }
+
+ public function writeStream(string $path, $stream, ?int $size = null): int {
+ if ($size === null) {
+ $stream = CountWrapper::wrap($stream, function (int $writtenSize) use (&$size): void {
+ $size = $writtenSize;
+ });
+ if (!$stream) {
+ throw new \Exception('Failed to wrap stream');
+ }
+ }
+ /** @psalm-suppress InternalMethod */
+ $result = $this->getConnection()->put($this->absPath($path), $stream);
+ fclose($stream);
+ if ($result) {
+ if ($size === null) {
+ throw new \Exception('Failed to get written size from sftp storage wrapper');
+ }
+ return $size;
+ } else {
+ throw new \Exception('Failed to write steam to sftp storage');
+ }
+ }
+
+ public function copy(string $source, string $target): bool {
+ if ($this->is_dir($source) || $this->is_dir($target)) {
+ return parent::copy($source, $target);
+ } else {
+ $absSource = $this->absPath($source);
+ $absTarget = $this->absPath($target);
+
+ $connection = $this->getConnection();
+ $size = $connection->size($absSource);
+ if ($size === false) {
+ return false;
+ }
+ for ($i = 0; $i < $size; $i += self::COPY_CHUNK_SIZE) {
+ /** @psalm-suppress InvalidArgument */
+ $chunk = $connection->get($absSource, false, $i, self::COPY_CHUNK_SIZE);
+ if ($chunk === false) {
+ return false;
+ }
+ /** @psalm-suppress InternalMethod */
+ if (!$connection->put($absTarget, $chunk, \phpseclib\Net\SFTP::SOURCE_STRING, $i)) {
+ return false;
+ }
+ }
+ return true;
+ }
+ }
+
+ public function getPermissions(string $path): int {
+ $stat = $this->getConnection()->stat($this->absPath($path));
+ if (!$stat) {
+ return 0;
+ }
+ if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
+ return Constants::PERMISSION_ALL;
+ } else {
+ return Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
+ }
+ }
+
+ public function getMetaData(string $path): ?array {
+ $stat = $this->getConnection()->stat($this->absPath($path));
+ if (!$stat) {
+ return null;
+ }
+
+ if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
+ $stat['permissions'] = Constants::PERMISSION_ALL;
+ } else {
+ $stat['permissions'] = Constants::PERMISSION_ALL - Constants::PERMISSION_CREATE;
+ }
+
+ if ($stat['type'] === NET_SFTP_TYPE_DIRECTORY) {
+ $stat['size'] = -1;
+ $stat['mimetype'] = FileInfo::MIMETYPE_FOLDER;
+ } else {
+ $stat['mimetype'] = $this->mimeTypeDetector->detectPath($path);
+ }
+
+ $stat['etag'] = $this->getETag($path);
+ $stat['storage_mtime'] = $stat['mtime'];
+ $stat['name'] = basename($path);
+
+ $keys = ['size', 'mtime', 'mimetype', 'etag', 'storage_mtime', 'permissions', 'name'];
+ return array_intersect_key($stat, array_flip($keys));
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/SFTPReadStream.php b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php
new file mode 100644
index 00000000000..7dedbd7035a
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/SFTPReadStream.php
@@ -0,0 +1,217 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use Icewind\Streams\File;
+use phpseclib\Net\SSH2;
+
+class SFTPReadStream implements File {
+ /** @var resource */
+ public $context;
+
+ /** @var \phpseclib\Net\SFTP */
+ private $sftp;
+
+ /** @var string */
+ private $handle;
+
+ /** @var int */
+ private $internalPosition = 0;
+
+ /** @var int */
+ private $readPosition = 0;
+
+ /** @var bool */
+ private $eof = false;
+
+ private $buffer = '';
+ private bool $pendingRead = false;
+ private int $size = 0;
+
+ public static function register($protocol = 'sftpread') {
+ if (in_array($protocol, stream_get_wrappers(), true)) {
+ return false;
+ }
+ return stream_wrapper_register($protocol, get_called_class());
+ }
+
+ /**
+ * Load the source from the stream context and return the context options
+ *
+ * @throws \BadMethodCallException
+ */
+ protected function loadContext(string $name) {
+ $context = stream_context_get_options($this->context);
+ if (isset($context[$name])) {
+ $context = $context[$name];
+ } else {
+ throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
+ }
+ if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
+ $this->sftp = $context['session'];
+ } else {
+ throw new \BadMethodCallException('Invalid context, session not set');
+ }
+ if (isset($context['size'])) {
+ $this->size = $context['size'];
+ }
+ return $context;
+ }
+
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ [, $path] = explode('://', $path);
+ $path = '/' . ltrim($path);
+ $path = str_replace('//', '/', $path);
+
+ $this->loadContext('sftp');
+
+ if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
+ return false;
+ }
+
+ $remote_file = $this->sftp->_realpath($path);
+ if ($remote_file === false) {
+ return false;
+ }
+
+ $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_READ, 0);
+ if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
+ return false;
+ }
+
+ $response = $this->sftp->_get_sftp_packet();
+ switch ($this->sftp->packet_type) {
+ case NET_SFTP_HANDLE:
+ $this->handle = substr($response, 4);
+ break;
+ case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
+ $this->sftp->_logError($response);
+ return false;
+ default:
+ user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
+ return false;
+ }
+
+ $this->request_chunk(256 * 1024);
+
+ return true;
+ }
+
+ public function stream_seek($offset, $whence = SEEK_SET) {
+ switch ($whence) {
+ case SEEK_SET:
+ $this->seekTo($offset);
+ break;
+ case SEEK_CUR:
+ $this->seekTo($this->readPosition + $offset);
+ break;
+ case SEEK_END:
+ $this->seekTo($this->size + $offset);
+ break;
+ }
+ return true;
+ }
+
+ private function seekTo(int $offset): void {
+ $this->internalPosition = $offset;
+ $this->readPosition = $offset;
+ $this->buffer = '';
+ $this->request_chunk(256 * 1024);
+ }
+
+ public function stream_tell() {
+ return $this->readPosition;
+ }
+
+ public function stream_read($count) {
+ if (!$this->eof && strlen($this->buffer) < $count) {
+ $chunk = $this->read_chunk();
+ $this->buffer .= $chunk;
+ if (!$this->eof) {
+ $this->request_chunk(256 * 1024);
+ }
+ }
+
+ $data = substr($this->buffer, 0, $count);
+ $this->buffer = substr($this->buffer, $count);
+ $this->readPosition += strlen($data);
+
+ return $data;
+ }
+
+ private function request_chunk(int $size) {
+ if ($this->pendingRead) {
+ $this->sftp->_get_sftp_packet();
+ }
+
+ $packet = pack('Na*N3', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size);
+ $this->pendingRead = true;
+ return $this->sftp->_send_sftp_packet(NET_SFTP_READ, $packet);
+ }
+
+ private function read_chunk() {
+ $this->pendingRead = false;
+ $response = $this->sftp->_get_sftp_packet();
+
+ switch ($this->sftp->packet_type) {
+ case NET_SFTP_DATA:
+ $temp = substr($response, 4);
+ $len = strlen($temp);
+ $this->internalPosition += $len;
+ return $temp;
+ case NET_SFTP_STATUS:
+ [1 => $status] = unpack('N', substr($response, 0, 4));
+ if ($status == NET_SFTP_STATUS_EOF) {
+ $this->eof = true;
+ }
+ return '';
+ default:
+ return '';
+ }
+ }
+
+ public function stream_write($data) {
+ return false;
+ }
+
+ public function stream_set_option($option, $arg1, $arg2) {
+ return false;
+ }
+
+ public function stream_truncate($size) {
+ return false;
+ }
+
+ public function stream_stat() {
+ return false;
+ }
+
+ public function stream_lock($operation) {
+ return false;
+ }
+
+ public function stream_flush() {
+ return false;
+ }
+
+ public function stream_eof() {
+ return $this->eof;
+ }
+
+ public function stream_close() {
+ // we still have a read request incoming that needs to be handled before we can close
+ if ($this->pendingRead) {
+ $this->sftp->_get_sftp_packet();
+ }
+ if (!$this->sftp->_close_handle($this->handle)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php
new file mode 100644
index 00000000000..d64e89b5462
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/SFTPWriteStream.php
@@ -0,0 +1,165 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use Icewind\Streams\File;
+use phpseclib\Net\SSH2;
+
+class SFTPWriteStream implements File {
+ /** @var resource */
+ public $context;
+
+ /** @var \phpseclib\Net\SFTP */
+ private $sftp;
+
+ /** @var string */
+ private $handle;
+
+ /** @var int */
+ private $internalPosition = 0;
+
+ /** @var int */
+ private $writePosition = 0;
+
+ /** @var bool */
+ private $eof = false;
+
+ private $buffer = '';
+
+ public static function register($protocol = 'sftpwrite') {
+ if (in_array($protocol, stream_get_wrappers(), true)) {
+ return false;
+ }
+ return stream_wrapper_register($protocol, get_called_class());
+ }
+
+ /**
+ * Load the source from the stream context and return the context options
+ *
+ * @throws \BadMethodCallException
+ */
+ protected function loadContext(string $name) {
+ $context = stream_context_get_options($this->context);
+ if (isset($context[$name])) {
+ $context = $context[$name];
+ } else {
+ throw new \BadMethodCallException('Invalid context, "' . $name . '" options not set');
+ }
+ if (isset($context['session']) and $context['session'] instanceof \phpseclib\Net\SFTP) {
+ $this->sftp = $context['session'];
+ } else {
+ throw new \BadMethodCallException('Invalid context, session not set');
+ }
+ return $context;
+ }
+
+ public function stream_open($path, $mode, $options, &$opened_path) {
+ [, $path] = explode('://', $path);
+ $path = '/' . ltrim($path);
+ $path = str_replace('//', '/', $path);
+
+ $this->loadContext('sftp');
+
+ if (!($this->sftp->bitmap & SSH2::MASK_LOGIN)) {
+ return false;
+ }
+
+ $remote_file = $this->sftp->_realpath($path);
+ if ($remote_file === false) {
+ return false;
+ }
+
+ $packet = pack('Na*N2', strlen($remote_file), $remote_file, NET_SFTP_OPEN_WRITE | NET_SFTP_OPEN_CREATE | NET_SFTP_OPEN_TRUNCATE, 0);
+ if (!$this->sftp->_send_sftp_packet(NET_SFTP_OPEN, $packet)) {
+ return false;
+ }
+
+ $response = $this->sftp->_get_sftp_packet();
+ switch ($this->sftp->packet_type) {
+ case NET_SFTP_HANDLE:
+ $this->handle = substr($response, 4);
+ break;
+ case NET_SFTP_STATUS: // presumably SSH_FX_NO_SUCH_FILE or SSH_FX_PERMISSION_DENIED
+ $this->sftp->_logError($response);
+ return false;
+ default:
+ user_error('Expected SSH_FXP_HANDLE or SSH_FXP_STATUS');
+ return false;
+ }
+
+ return true;
+ }
+
+ public function stream_seek($offset, $whence = SEEK_SET) {
+ return false;
+ }
+
+ public function stream_tell() {
+ return $this->writePosition;
+ }
+
+ public function stream_read($count) {
+ return false;
+ }
+
+ public function stream_write($data) {
+ $written = strlen($data);
+ $this->writePosition += $written;
+
+ $this->buffer .= $data;
+
+ if (strlen($this->buffer) > 64 * 1024) {
+ if (!$this->stream_flush()) {
+ return false;
+ }
+ }
+
+ return $written;
+ }
+
+ public function stream_set_option($option, $arg1, $arg2) {
+ return false;
+ }
+
+ public function stream_truncate($size) {
+ return false;
+ }
+
+ public function stream_stat() {
+ return false;
+ }
+
+ public function stream_lock($operation) {
+ return false;
+ }
+
+ public function stream_flush() {
+ $size = strlen($this->buffer);
+ $packet = pack('Na*N3a*', strlen($this->handle), $this->handle, $this->internalPosition / 4294967296, $this->internalPosition, $size, $this->buffer);
+ if (!$this->sftp->_send_sftp_packet(NET_SFTP_WRITE, $packet)) {
+ return false;
+ }
+ $this->internalPosition += $size;
+ $this->buffer = '';
+
+ return $this->sftp->_read_put_responses(1);
+ }
+
+ public function stream_eof() {
+ return $this->eof;
+ }
+
+ public function stream_close() {
+ $this->stream_flush();
+ if (!$this->sftp->_close_handle($this->handle)) {
+ return false;
+ }
+ return true;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/SMB.php b/apps/files_external/lib/Lib/Storage/SMB.php
new file mode 100644
index 00000000000..8f8750864e1
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/SMB.php
@@ -0,0 +1,727 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+
+namespace OCA\Files_External\Lib\Storage;
+
+use Icewind\SMB\ACL;
+use Icewind\SMB\BasicAuth;
+use Icewind\SMB\Exception\AlreadyExistsException;
+use Icewind\SMB\Exception\ConnectException;
+use Icewind\SMB\Exception\Exception;
+use Icewind\SMB\Exception\ForbiddenException;
+use Icewind\SMB\Exception\InvalidArgumentException;
+use Icewind\SMB\Exception\InvalidTypeException;
+use Icewind\SMB\Exception\NotFoundException;
+use Icewind\SMB\Exception\OutOfSpaceException;
+use Icewind\SMB\Exception\TimedOutException;
+use Icewind\SMB\IFileInfo;
+use Icewind\SMB\Native\NativeServer;
+use Icewind\SMB\Options;
+use Icewind\SMB\ServerFactory;
+use Icewind\SMB\Wrapped\Server;
+use Icewind\Streams\CallbackWrapper;
+use Icewind\Streams\IteratorDirectory;
+use OC\Files\Filesystem;
+use OC\Files\Storage\Common;
+use OCA\Files_External\Lib\Notify\SMBNotifyHandler;
+use OCP\Cache\CappedMemoryCache;
+use OCP\Constants;
+use OCP\Files\EntityTooLargeException;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\Notify\IChange;
+use OCP\Files\Notify\IRenameChange;
+use OCP\Files\NotPermittedException;
+use OCP\Files\Storage\INotifyStorage;
+use OCP\Files\StorageAuthException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\ITempManager;
+use Psr\Log\LoggerInterface;
+
+class SMB extends Common implements INotifyStorage {
+ /**
+ * @var \Icewind\SMB\IServer
+ */
+ protected $server;
+
+ /**
+ * @var \Icewind\SMB\IShare
+ */
+ protected $share;
+
+ /**
+ * @var string
+ */
+ protected $root;
+
+ /** @var CappedMemoryCache<IFileInfo> */
+ protected CappedMemoryCache $statCache;
+
+ /** @var LoggerInterface */
+ protected $logger;
+
+ /** @var bool */
+ protected $showHidden;
+
+ private bool $caseSensitive;
+
+ /** @var bool */
+ protected $checkAcl;
+
+ public function __construct(array $parameters) {
+ if (!isset($parameters['host'])) {
+ throw new \Exception('Invalid configuration, no host provided');
+ }
+
+ if (isset($parameters['auth'])) {
+ $auth = $parameters['auth'];
+ } elseif (isset($parameters['user']) && isset($parameters['password']) && isset($parameters['share'])) {
+ [$workgroup, $user] = $this->splitUser($parameters['user']);
+ $auth = new BasicAuth($user, $workgroup, $parameters['password']);
+ } else {
+ throw new \Exception('Invalid configuration, no credentials provided');
+ }
+
+ if (isset($parameters['logger'])) {
+ if (!$parameters['logger'] instanceof LoggerInterface) {
+ throw new \Exception(
+ 'Invalid logger. Got '
+ . get_class($parameters['logger'])
+ . ' Expected ' . LoggerInterface::class
+ );
+ }
+ $this->logger = $parameters['logger'];
+ } else {
+ $this->logger = \OCP\Server::get(LoggerInterface::class);
+ }
+
+ $options = new Options();
+ if (isset($parameters['timeout'])) {
+ $timeout = (int)$parameters['timeout'];
+ if ($timeout > 0) {
+ $options->setTimeout($timeout);
+ }
+ }
+ $system = \OCP\Server::get(SystemBridge::class);
+ $serverFactory = new ServerFactory($options, $system);
+ $this->server = $serverFactory->createServer($parameters['host'], $auth);
+ $this->share = $this->server->getShare(trim($parameters['share'], '/'));
+
+ $this->root = $parameters['root'] ?? '/';
+ $this->root = '/' . ltrim($this->root, '/');
+ $this->root = rtrim($this->root, '/') . '/';
+
+ $this->showHidden = isset($parameters['show_hidden']) && $parameters['show_hidden'];
+ $this->caseSensitive = (bool)($parameters['case_sensitive'] ?? true);
+ $this->checkAcl = isset($parameters['check_acl']) && $parameters['check_acl'];
+
+ $this->statCache = new CappedMemoryCache();
+ parent::__construct($parameters);
+ }
+
+ private function splitUser(string $user): array {
+ if (str_contains($user, '/')) {
+ return explode('/', $user, 2);
+ } elseif (str_contains($user, '\\')) {
+ return explode('\\', $user);
+ }
+
+ return [null, $user];
+ }
+
+ public function getId(): string {
+ // FIXME: double slash to keep compatible with the old storage ids,
+ // failure to do so will lead to creation of a new storage id and
+ // loss of shares from the storage
+ return 'smb::' . $this->server->getAuth()->getUsername() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
+ }
+
+ protected function buildPath(string $path): string {
+ return Filesystem::normalizePath($this->root . '/' . $path, true, false, true);
+ }
+
+ protected function relativePath(string $fullPath): ?string {
+ if ($fullPath === $this->root) {
+ return '';
+ } elseif (substr($fullPath, 0, strlen($this->root)) === $this->root) {
+ return substr($fullPath, strlen($this->root));
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * @throws StorageAuthException
+ * @throws \OCP\Files\NotFoundException
+ * @throws \OCP\Files\ForbiddenException
+ */
+ protected function getFileInfo(string $path): IFileInfo {
+ try {
+ $path = $this->buildPath($path);
+ $cached = $this->statCache[$path] ?? null;
+ if ($cached instanceof IFileInfo) {
+ return $cached;
+ } else {
+ $stat = $this->share->stat($path);
+ $this->statCache[$path] = $stat;
+ return $stat;
+ }
+ } catch (ConnectException $e) {
+ $this->throwUnavailable($e);
+ } catch (NotFoundException $e) {
+ throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
+ } catch (ForbiddenException $e) {
+ // with php-smbclient, this exception is thrown when the provided password is invalid.
+ // Possible is also ForbiddenException with a different error code, so we check it.
+ if ($e->getCode() === 1) {
+ $this->throwUnavailable($e);
+ }
+ throw new \OCP\Files\ForbiddenException($e->getMessage(), false, $e);
+ }
+ }
+
+ /**
+ * @throws StorageAuthException
+ */
+ protected function throwUnavailable(\Exception $e): never {
+ $this->logger->error('Error while getting file info', ['exception' => $e]);
+ throw new StorageAuthException($e->getMessage(), $e);
+ }
+
+ /**
+ * get the acl from fileinfo that is relevant for the configured user
+ */
+ private function getACL(IFileInfo $file): ?ACL {
+ try {
+ $acls = $file->getAcls();
+ } catch (Exception $e) {
+ $this->logger->warning('Error while getting file acls', ['exception' => $e]);
+ return null;
+ }
+ foreach ($acls as $user => $acl) {
+ [, $user] = $this->splitUser($user); // strip domain
+ if ($user === $this->server->getAuth()->getUsername()) {
+ return $acl;
+ }
+ }
+
+ return null;
+ }
+
+ /**
+ * @return \Generator<IFileInfo>
+ * @throws StorageNotAvailableException
+ */
+ protected function getFolderContents(string $path): iterable {
+ try {
+ $path = ltrim($this->buildPath($path), '/');
+ try {
+ $files = $this->share->dir($path);
+ } catch (ForbiddenException $e) {
+ $this->logger->critical($e->getMessage(), ['exception' => $e]);
+ throw new NotPermittedException();
+ } catch (InvalidTypeException $e) {
+ return;
+ }
+ foreach ($files as $file) {
+ $this->statCache[$path . '/' . $file->getName()] = $file;
+ }
+
+ foreach ($files as $file) {
+ try {
+ // the isHidden check is done before checking the config boolean to ensure that the metadata is always fetch
+ // so we trigger the below exceptions where applicable
+ $hide = $file->isHidden() && !$this->showHidden;
+
+ if ($this->checkAcl && $acl = $this->getACL($file)) {
+ // if there is no explicit deny, we assume it's allowed
+ // this doesn't take inheritance fully into account but if read permissions is denied for a parent we wouldn't be in this folder
+ // additionally, it's better to have false negatives here then false positives
+ if ($acl->denies(ACL::MASK_READ) || $acl->denies(ACL::MASK_EXECUTE)) {
+ $this->logger->debug('Hiding non readable entry ' . $file->getName());
+ continue;
+ }
+ }
+
+ if ($hide) {
+ $this->logger->debug('hiding hidden file ' . $file->getName());
+ }
+ if (!$hide) {
+ yield $file;
+ }
+ } catch (ForbiddenException $e) {
+ $this->logger->debug($e->getMessage(), ['exception' => $e]);
+ } catch (NotFoundException $e) {
+ $this->logger->debug('Hiding forbidden entry ' . $file->getName(), ['exception' => $e]);
+ }
+ }
+ } catch (ConnectException $e) {
+ $this->logger->error('Error while getting folder content', ['exception' => $e]);
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ } catch (NotFoundException $e) {
+ throw new \OCP\Files\NotFoundException($e->getMessage(), 0, $e);
+ }
+ }
+
+ protected function formatInfo(IFileInfo $info): array {
+ $result = [
+ 'size' => $info->getSize(),
+ 'mtime' => $info->getMTime(),
+ ];
+ if ($info->isDirectory()) {
+ $result['type'] = 'dir';
+ } else {
+ $result['type'] = 'file';
+ }
+ return $result;
+ }
+
+ /**
+ * Rename the files. If the source or the target is the root, the rename won't happen.
+ *
+ * @param string $source the old name of the path
+ * @param string $target the new name of the path
+ */
+ public function rename(string $source, string $target, bool $retry = true): bool {
+ if ($this->isRootDir($source) || $this->isRootDir($target)) {
+ return false;
+ }
+ if ($this->caseSensitive === false
+ && mb_strtolower($target) === mb_strtolower($source)
+ ) {
+ // Forbid changing case only on case-insensitive file system
+ return false;
+ }
+
+ $absoluteSource = $this->buildPath($source);
+ $absoluteTarget = $this->buildPath($target);
+ try {
+ $result = $this->share->rename($absoluteSource, $absoluteTarget);
+ } catch (AlreadyExistsException $e) {
+ if ($retry) {
+ $this->remove($target);
+ $result = $this->share->rename($absoluteSource, $absoluteTarget);
+ } else {
+ $this->logger->warning($e->getMessage(), ['exception' => $e]);
+ return false;
+ }
+ } catch (InvalidArgumentException $e) {
+ if ($retry) {
+ $this->remove($target);
+ $result = $this->share->rename($absoluteSource, $absoluteTarget);
+ } else {
+ $this->logger->warning($e->getMessage(), ['exception' => $e]);
+ return false;
+ }
+ } catch (\Exception $e) {
+ $this->logger->warning($e->getMessage(), ['exception' => $e]);
+ return false;
+ }
+ unset($this->statCache[$absoluteSource], $this->statCache[$absoluteTarget]);
+ return $result;
+ }
+
+ public function stat(string $path, bool $retry = true): array|false {
+ try {
+ $result = $this->formatInfo($this->getFileInfo($path));
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return false;
+ } catch (\OCP\Files\NotFoundException $e) {
+ return false;
+ } catch (TimedOutException $e) {
+ if ($retry) {
+ return $this->stat($path, false);
+ } else {
+ throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
+ }
+ }
+ if ($this->remoteIsShare() && $this->isRootDir($path)) {
+ $result['mtime'] = $this->shareMTime();
+ }
+ return $result;
+ }
+
+ /**
+ * get the best guess for the modification time of the share
+ */
+ private function shareMTime(): int {
+ $highestMTime = 0;
+ $files = $this->share->dir($this->root);
+ foreach ($files as $fileInfo) {
+ try {
+ if ($fileInfo->getMTime() > $highestMTime) {
+ $highestMTime = $fileInfo->getMTime();
+ }
+ } catch (NotFoundException $e) {
+ // Ignore this, can happen on unavailable DFS shares
+ } catch (ForbiddenException $e) {
+ // Ignore this too - it's a symlink
+ }
+ }
+ return $highestMTime;
+ }
+
+ /**
+ * Check if the path is our root dir (not the smb one)
+ */
+ private function isRootDir(string $path): bool {
+ return $path === '' || $path === '/' || $path === '.';
+ }
+
+ /**
+ * Check if our root points to a smb share
+ */
+ private function remoteIsShare(): bool {
+ return $this->share->getName() && (!$this->root || $this->root === '/');
+ }
+
+ public function unlink(string $path): bool {
+ if ($this->isRootDir($path)) {
+ return false;
+ }
+
+ try {
+ if ($this->is_dir($path)) {
+ return $this->rmdir($path);
+ } else {
+ $path = $this->buildPath($path);
+ unset($this->statCache[$path]);
+ $this->share->del($path);
+ return true;
+ }
+ } catch (NotFoundException $e) {
+ return false;
+ } catch (ForbiddenException $e) {
+ return false;
+ } catch (ConnectException $e) {
+ $this->logger->error('Error while deleting file', ['exception' => $e]);
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ }
+ }
+
+ /**
+ * check if a file or folder has been updated since $time
+ */
+ public function hasUpdated(string $path, int $time): bool {
+ if (!$path and $this->root === '/') {
+ // mtime doesn't work for shares, but giving the nature of the backend,
+ // doing a full update is still just fast enough
+ return true;
+ } else {
+ $actualTime = $this->filemtime($path);
+ return $actualTime > $time || $actualTime === 0;
+ }
+ }
+
+ /**
+ * @return resource|false
+ */
+ public function fopen(string $path, string $mode) {
+ $fullPath = $this->buildPath($path);
+ try {
+ switch ($mode) {
+ case 'r':
+ case 'rb':
+ if (!$this->file_exists($path)) {
+ $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file doesn\'t exist.');
+ return false;
+ }
+ return $this->share->read($fullPath);
+ case 'w':
+ case 'wb':
+ $source = $this->share->write($fullPath);
+ return CallBackWrapper::wrap($source, null, null, function () use ($fullPath): void {
+ unset($this->statCache[$fullPath]);
+ });
+ case 'a':
+ case 'ab':
+ case 'r+':
+ case 'w+':
+ case 'wb+':
+ case 'a+':
+ case 'x':
+ case 'x+':
+ case 'c':
+ case 'c+':
+ //emulate these
+ if (strrpos($path, '.') !== false) {
+ $ext = substr($path, strrpos($path, '.'));
+ } else {
+ $ext = '';
+ }
+ if ($this->file_exists($path)) {
+ if (!$this->isUpdatable($path)) {
+ $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', file not updatable.');
+ return false;
+ }
+ $tmpFile = $this->getCachedFile($path);
+ } else {
+ if (!$this->isCreatable(dirname($path))) {
+ $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', parent directory not writable.');
+ return false;
+ }
+ $tmpFile = \OCP\Server::get(ITempManager::class)->getTemporaryFile($ext);
+ }
+ $source = fopen($tmpFile, $mode);
+ $share = $this->share;
+ return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share): void {
+ unset($this->statCache[$fullPath]);
+ $share->put($tmpFile, $fullPath);
+ unlink($tmpFile);
+ });
+ }
+ return false;
+ } catch (NotFoundException $e) {
+ $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', not found.', ['exception' => $e]);
+ return false;
+ } catch (ForbiddenException $e) {
+ $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', forbidden.', ['exception' => $e]);
+ return false;
+ } catch (OutOfSpaceException $e) {
+ $this->logger->warning('Failed to open ' . $path . ' on ' . $this->getId() . ', out of space.', ['exception' => $e]);
+ throw new EntityTooLargeException('not enough available space to create file', 0, $e);
+ } catch (ConnectException $e) {
+ $this->logger->error('Error while opening file ' . $path . ' on ' . $this->getId(), ['exception' => $e]);
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ }
+ }
+
+ public function rmdir(string $path): bool {
+ if ($this->isRootDir($path)) {
+ return false;
+ }
+
+ try {
+ $this->statCache = new CappedMemoryCache();
+ $content = $this->share->dir($this->buildPath($path));
+ foreach ($content as $file) {
+ if ($file->isDirectory()) {
+ $this->rmdir($path . '/' . $file->getName());
+ } else {
+ $this->share->del($file->getPath());
+ }
+ }
+ $this->share->rmdir($this->buildPath($path));
+ return true;
+ } catch (NotFoundException $e) {
+ return false;
+ } catch (ForbiddenException $e) {
+ return false;
+ } catch (ConnectException $e) {
+ $this->logger->error('Error while removing folder', ['exception' => $e]);
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ }
+ }
+
+ public function touch(string $path, ?int $mtime = null): bool {
+ try {
+ if (!$this->file_exists($path)) {
+ $fh = $this->share->write($this->buildPath($path));
+ fclose($fh);
+ return true;
+ }
+ return false;
+ } catch (OutOfSpaceException $e) {
+ throw new EntityTooLargeException('not enough available space to create file', 0, $e);
+ } catch (ConnectException $e) {
+ $this->logger->error('Error while creating file', ['exception' => $e]);
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ }
+ }
+
+ public function getMetaData(string $path): ?array {
+ try {
+ $fileInfo = $this->getFileInfo($path);
+ } catch (\OCP\Files\NotFoundException $e) {
+ return null;
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return null;
+ }
+
+ return $this->getMetaDataFromFileInfo($fileInfo);
+ }
+
+ private function getMetaDataFromFileInfo(IFileInfo $fileInfo): array {
+ $permissions = Constants::PERMISSION_READ + Constants::PERMISSION_SHARE;
+
+ if (
+ !$fileInfo->isReadOnly() || $fileInfo->isDirectory()
+ ) {
+ $permissions += Constants::PERMISSION_DELETE;
+ $permissions += Constants::PERMISSION_UPDATE;
+ if ($fileInfo->isDirectory()) {
+ $permissions += Constants::PERMISSION_CREATE;
+ }
+ }
+
+ $data = [];
+ if ($fileInfo->isDirectory()) {
+ $data['mimetype'] = 'httpd/unix-directory';
+ } else {
+ $data['mimetype'] = \OCP\Server::get(IMimeTypeDetector::class)->detectPath($fileInfo->getPath());
+ }
+ $data['mtime'] = $fileInfo->getMTime();
+ if ($fileInfo->isDirectory()) {
+ $data['size'] = -1; //unknown
+ } else {
+ $data['size'] = $fileInfo->getSize();
+ }
+ $data['etag'] = $this->getETag($fileInfo->getPath());
+ $data['storage_mtime'] = $data['mtime'];
+ $data['permissions'] = $permissions;
+ $data['name'] = $fileInfo->getName();
+
+ return $data;
+ }
+
+ public function opendir(string $path) {
+ try {
+ $files = $this->getFolderContents($path);
+ } catch (NotFoundException $e) {
+ return false;
+ } catch (NotPermittedException $e) {
+ return false;
+ }
+ $names = array_map(function ($info) {
+ /** @var IFileInfo $info */
+ return $info->getName();
+ }, iterator_to_array($files));
+ return IteratorDirectory::wrap($names);
+ }
+
+ public function getDirectoryContent(string $directory): \Traversable {
+ try {
+ $files = $this->getFolderContents($directory);
+ foreach ($files as $file) {
+ yield $this->getMetaDataFromFileInfo($file);
+ }
+ } catch (NotFoundException $e) {
+ return;
+ } catch (NotPermittedException $e) {
+ return;
+ }
+ }
+
+ public function filetype(string $path): string|false {
+ try {
+ return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
+ } catch (\OCP\Files\NotFoundException $e) {
+ return false;
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return false;
+ }
+ }
+
+ public function mkdir(string $path): bool {
+ $path = $this->buildPath($path);
+ try {
+ $this->share->mkdir($path);
+ return true;
+ } catch (ConnectException $e) {
+ $this->logger->error('Error while creating folder', ['exception' => $e]);
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ } catch (Exception $e) {
+ return false;
+ }
+ }
+
+ public function file_exists(string $path): bool {
+ try {
+ // Case sensitive filesystem doesn't matter for root directory
+ if ($this->caseSensitive === false && $path !== '') {
+ $filename = basename($path);
+ $siblings = $this->getDirectoryContent(dirname($path));
+ foreach ($siblings as $sibling) {
+ if ($sibling['name'] === $filename) {
+ return true;
+ }
+ }
+ return false;
+ }
+ $this->getFileInfo($path);
+ return true;
+ } catch (\OCP\Files\NotFoundException $e) {
+ return false;
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return false;
+ } catch (ConnectException $e) {
+ throw new StorageNotAvailableException($e->getMessage(), (int)$e->getCode(), $e);
+ }
+ }
+
+ public function isReadable(string $path): bool {
+ try {
+ $info = $this->getFileInfo($path);
+ return $this->showHidden || !$info->isHidden();
+ } catch (\OCP\Files\NotFoundException $e) {
+ return false;
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return false;
+ }
+ }
+
+ public function isUpdatable(string $path): bool {
+ try {
+ $info = $this->getFileInfo($path);
+ // following windows behaviour for read-only folders: they can be written into
+ // (https://support.microsoft.com/en-us/kb/326549 - "cause" section)
+ return ($this->showHidden || !$info->isHidden()) && (!$info->isReadOnly() || $info->isDirectory());
+ } catch (\OCP\Files\NotFoundException $e) {
+ return false;
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return false;
+ }
+ }
+
+ public function isDeletable(string $path): bool {
+ try {
+ $info = $this->getFileInfo($path);
+ return ($this->showHidden || !$info->isHidden()) && !$info->isReadOnly();
+ } catch (\OCP\Files\NotFoundException $e) {
+ return false;
+ } catch (\OCP\Files\ForbiddenException $e) {
+ return false;
+ }
+ }
+
+ /**
+ * check if smbclient is installed
+ */
+ public static function checkDependencies(): array|bool {
+ $system = \OCP\Server::get(SystemBridge::class);
+ return Server::available($system) || NativeServer::available($system) ?: ['smbclient'];
+ }
+
+ public function test(): bool {
+ try {
+ return parent::test();
+ } catch (StorageAuthException $e) {
+ return false;
+ } catch (ForbiddenException $e) {
+ return false;
+ } catch (Exception $e) {
+ $this->logger->error($e->getMessage(), ['exception' => $e]);
+ return false;
+ }
+ }
+
+ public function listen(string $path, callable $callback): void {
+ $this->notify($path)->listen(function (IChange $change) use ($callback) {
+ if ($change instanceof IRenameChange) {
+ return $callback($change->getType(), $change->getPath(), $change->getTargetPath());
+ } else {
+ return $callback($change->getType(), $change->getPath());
+ }
+ });
+ }
+
+ public function notify(string $path): SMBNotifyHandler {
+ $path = '/' . ltrim($path, '/');
+ $shareNotifyHandler = $this->share->notify($this->buildPath($path));
+ return new SMBNotifyHandler($shareNotifyHandler, $this->root);
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/StreamWrapper.php b/apps/files_external/lib/Lib/Storage/StreamWrapper.php
new file mode 100644
index 00000000000..1272b9d4d8a
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/StreamWrapper.php
@@ -0,0 +1,99 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2020-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use OC\Files\Storage\Common;
+
+abstract class StreamWrapper extends Common {
+
+ abstract public function constructUrl(string $path): ?string;
+
+ public function mkdir(string $path): bool {
+ return mkdir($this->constructUrl($path));
+ }
+
+ public function rmdir(string $path): bool {
+ if ($this->is_dir($path) && $this->isDeletable($path)) {
+ $dh = $this->opendir($path);
+ if (!is_resource($dh)) {
+ return false;
+ }
+ while (($file = readdir($dh)) !== false) {
+ if ($this->is_dir($path . '/' . $file)) {
+ $this->rmdir($path . '/' . $file);
+ } else {
+ $this->unlink($path . '/' . $file);
+ }
+ }
+ $url = $this->constructUrl($path);
+ $success = rmdir($url);
+ clearstatcache(false, $url);
+ return $success;
+ } else {
+ return false;
+ }
+ }
+
+ public function opendir(string $path) {
+ return opendir($this->constructUrl($path));
+ }
+
+ public function filetype(string $path): string|false {
+ return @filetype($this->constructUrl($path));
+ }
+
+ public function file_exists(string $path): bool {
+ return file_exists($this->constructUrl($path));
+ }
+
+ public function unlink(string $path): bool {
+ $url = $this->constructUrl($path);
+ $success = unlink($url);
+ // normally unlink() is supposed to do this implicitly,
+ // but doing it anyway just to be sure
+ clearstatcache(false, $url);
+ return $success;
+ }
+
+ public function fopen(string $path, string $mode) {
+ return fopen($this->constructUrl($path), $mode);
+ }
+
+ public function touch(string $path, ?int $mtime = null): bool {
+ if ($this->file_exists($path)) {
+ if (is_null($mtime)) {
+ $fh = $this->fopen($path, 'a');
+ fwrite($fh, '');
+ fclose($fh);
+
+ return true;
+ } else {
+ return false; //not supported
+ }
+ } else {
+ $this->file_put_contents($path, '');
+ return true;
+ }
+ }
+
+ public function getFile(string $path, string $target): bool {
+ return copy($this->constructUrl($path), $target);
+ }
+
+ public function uploadFile(string $path, string $target): bool {
+ return copy($path, $this->constructUrl($target));
+ }
+
+ public function rename(string $source, string $target): bool {
+ return rename($this->constructUrl($source), $this->constructUrl($target));
+ }
+
+ public function stat(string $path): array|false {
+ return stat($this->constructUrl($path));
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/Swift.php b/apps/files_external/lib/Lib/Storage/Swift.php
new file mode 100644
index 00000000000..e80570f14ba
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/Swift.php
@@ -0,0 +1,593 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib\Storage;
+
+use GuzzleHttp\Psr7\Uri;
+use Icewind\Streams\CallbackWrapper;
+use Icewind\Streams\IteratorDirectory;
+use OC\Files\Filesystem;
+use OC\Files\ObjectStore\SwiftFactory;
+use OC\Files\Storage\Common;
+use OCP\Cache\CappedMemoryCache;
+use OCP\Files\IMimeTypeDetector;
+use OCP\Files\StorageAuthException;
+use OCP\Files\StorageBadConfigException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\ICache;
+use OCP\ICacheFactory;
+use OCP\ITempManager;
+use OCP\Server;
+use OpenStack\Common\Error\BadResponseError;
+use OpenStack\ObjectStore\v1\Models\Container;
+use OpenStack\ObjectStore\v1\Models\StorageObject;
+use Psr\Log\LoggerInterface;
+
+class Swift extends Common {
+ /** @var SwiftFactory */
+ private $connectionFactory;
+ /**
+ * @var Container
+ */
+ private $container;
+ /**
+ * @var string
+ */
+ private $bucket;
+ /**
+ * Connection parameters
+ *
+ * @var array
+ */
+ private $params;
+
+ /** @var string */
+ private $id;
+
+ /** @var \OC\Files\ObjectStore\Swift */
+ private $objectStore;
+
+ /** @var IMimeTypeDetector */
+ private $mimeDetector;
+
+ /**
+ * Key value cache mapping path to data object. Maps path to
+ * \OpenCloud\OpenStack\ObjectStorage\Resource\DataObject for existing
+ * paths and path to false for not existing paths.
+ *
+ * @var ICache
+ */
+ private $objectCache;
+
+ private function normalizePath(string $path): string {
+ $path = trim($path, '/');
+
+ if (!$path) {
+ $path = '.';
+ }
+
+ $path = str_replace('#', '%23', $path);
+
+ return $path;
+ }
+
+ public const SUBCONTAINER_FILE = '.subcontainers';
+
+ /**
+ * Fetches an object from the API.
+ * If the object is cached already or a
+ * failed "doesn't exist" response was cached,
+ * that one will be returned.
+ *
+ * @return StorageObject|false object
+ * or false if the object did not exist
+ * @throws StorageAuthException
+ * @throws StorageNotAvailableException
+ */
+ private function fetchObject(string $path): StorageObject|false {
+ $cached = $this->objectCache->get($path);
+ if ($cached !== null) {
+ // might be "false" if object did not exist from last check
+ return $cached;
+ }
+ try {
+ $object = $this->getContainer()->getObject($path);
+ $object->retrieve();
+ $this->objectCache->set($path, $object);
+ return $object;
+ } catch (BadResponseError $e) {
+ // Expected response is "404 Not Found", so only log if it isn't
+ if ($e->getResponse()->getStatusCode() !== 404) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ }
+ $this->objectCache->set($path, false);
+ return false;
+ }
+ }
+
+ /**
+ * Returns whether the given path exists.
+ *
+ * @return bool true if the object exist, false otherwise
+ * @throws StorageAuthException
+ * @throws StorageNotAvailableException
+ */
+ private function doesObjectExist(string $path): bool {
+ return $this->fetchObject($path) !== false;
+ }
+
+ public function __construct(array $parameters) {
+ if ((empty($parameters['key']) and empty($parameters['password']))
+ or (empty($parameters['user']) && empty($parameters['userid'])) or empty($parameters['bucket'])
+ or empty($parameters['region'])
+ ) {
+ throw new StorageBadConfigException('API Key or password, Login, Bucket and Region have to be configured.');
+ }
+
+ $user = $parameters['user'];
+ $this->id = 'swift::' . $user . md5($parameters['bucket']);
+
+ $bucketUrl = new Uri($parameters['bucket']);
+ if ($bucketUrl->getHost()) {
+ $parameters['bucket'] = basename($bucketUrl->getPath());
+ $parameters['endpoint_url'] = (string)$bucketUrl->withPath(dirname($bucketUrl->getPath()));
+ }
+
+ if (empty($parameters['url'])) {
+ $parameters['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
+ }
+
+ if (empty($parameters['service_name'])) {
+ $parameters['service_name'] = 'cloudFiles';
+ }
+
+ $parameters['autocreate'] = true;
+
+ if (isset($parameters['domain'])) {
+ $parameters['user'] = [
+ 'name' => $parameters['user'],
+ 'password' => $parameters['password'],
+ 'domain' => [
+ 'name' => $parameters['domain'],
+ ]
+ ];
+ }
+
+ $this->params = $parameters;
+ // FIXME: private class...
+ $this->objectCache = new CappedMemoryCache();
+ $this->connectionFactory = new SwiftFactory(
+ Server::get(ICacheFactory::class)->createDistributed('swift/'),
+ $this->params,
+ Server::get(LoggerInterface::class)
+ );
+ $this->objectStore = new \OC\Files\ObjectStore\Swift($this->params, $this->connectionFactory);
+ $this->bucket = $parameters['bucket'];
+ $this->mimeDetector = Server::get(IMimeTypeDetector::class);
+ }
+
+ public function mkdir(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if ($this->is_dir($path)) {
+ return false;
+ }
+
+ if ($path !== '.') {
+ $path .= '/';
+ }
+
+ try {
+ $this->getContainer()->createObject([
+ 'name' => $path,
+ 'content' => '',
+ 'headers' => ['content-type' => 'httpd/unix-directory']
+ ]);
+ // invalidate so that the next access gets the real object
+ // with all properties
+ $this->objectCache->remove($path);
+ } catch (BadResponseError $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+
+ return true;
+ }
+
+ public function file_exists(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if ($path !== '.' && $this->is_dir($path)) {
+ $path .= '/';
+ }
+
+ return $this->doesObjectExist($path);
+ }
+
+ public function rmdir(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if (!$this->is_dir($path) || !$this->isDeletable($path)) {
+ return false;
+ }
+
+ $dh = $this->opendir($path);
+ while (($file = readdir($dh)) !== false) {
+ if (Filesystem::isIgnoredDir($file)) {
+ continue;
+ }
+
+ if ($this->is_dir($path . '/' . $file)) {
+ $this->rmdir($path . '/' . $file);
+ } else {
+ $this->unlink($path . '/' . $file);
+ }
+ }
+
+ try {
+ $this->objectStore->deleteObject($path . '/');
+ $this->objectCache->remove($path . '/');
+ } catch (BadResponseError $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+
+ return true;
+ }
+
+ public function opendir(string $path) {
+ $path = $this->normalizePath($path);
+
+ if ($path === '.') {
+ $path = '';
+ } else {
+ $path .= '/';
+ }
+
+ // $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of #
+
+ try {
+ $files = [];
+ $objects = $this->getContainer()->listObjects([
+ 'prefix' => $path,
+ 'delimiter' => '/'
+ ]);
+
+ /** @var StorageObject $object */
+ foreach ($objects as $object) {
+ $file = basename($object->name);
+ if ($file !== basename($path) && $file !== '.') {
+ $files[] = $file;
+ }
+ }
+
+ return IteratorDirectory::wrap($files);
+ } catch (\Exception $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+ }
+
+ public function stat(string $path): array|false {
+ $path = $this->normalizePath($path);
+ if ($path === '.') {
+ $path = '';
+ } elseif ($this->is_dir($path)) {
+ $path .= '/';
+ }
+
+ try {
+ $object = $this->fetchObject($path);
+ if (!$object) {
+ return false;
+ }
+ } catch (BadResponseError $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+
+ $mtime = null;
+ if (!empty($object->lastModified)) {
+ $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->lastModified);
+ if ($dateTime !== false) {
+ $mtime = $dateTime->getTimestamp();
+ }
+ }
+
+ if (is_numeric($object->getMetadata()['timestamp'] ?? null)) {
+ $mtime = (float)$object->getMetadata()['timestamp'];
+ }
+
+ return [
+ 'size' => (int)$object->contentLength,
+ 'mtime' => isset($mtime) ? (int)floor($mtime) : null,
+ 'atime' => time(),
+ ];
+ }
+
+ public function filetype(string $path) {
+ $path = $this->normalizePath($path);
+
+ if ($path !== '.' && $this->doesObjectExist($path)) {
+ return 'file';
+ }
+
+ if ($path !== '.') {
+ $path .= '/';
+ }
+
+ if ($this->doesObjectExist($path)) {
+ return 'dir';
+ }
+ }
+
+ public function unlink(string $path): bool {
+ $path = $this->normalizePath($path);
+
+ if ($this->is_dir($path)) {
+ return $this->rmdir($path);
+ }
+
+ try {
+ $this->objectStore->deleteObject($path);
+ $this->objectCache->remove($path);
+ $this->objectCache->remove($path . '/');
+ } catch (BadResponseError $e) {
+ if ($e->getResponse()->getStatusCode() !== 404) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ throw $e;
+ }
+ }
+
+ return true;
+ }
+
+ public function fopen(string $path, string $mode) {
+ $path = $this->normalizePath($path);
+
+ switch ($mode) {
+ case 'a':
+ case 'ab':
+ case 'a+':
+ return false;
+ case 'r':
+ case 'rb':
+ try {
+ return $this->objectStore->readObject($path);
+ } catch (BadResponseError $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+ case 'w':
+ case 'wb':
+ case 'r+':
+ case 'w+':
+ case 'wb+':
+ case 'x':
+ case 'x+':
+ case 'c':
+ case 'c+':
+ if (strrpos($path, '.') !== false) {
+ $ext = substr($path, strrpos($path, '.'));
+ } else {
+ $ext = '';
+ }
+ $tmpFile = Server::get(ITempManager::class)->getTemporaryFile($ext);
+ // Fetch existing file if required
+ if ($mode[0] !== 'w' && $this->file_exists($path)) {
+ if ($mode[0] === 'x') {
+ // File cannot already exist
+ return false;
+ }
+ $source = $this->fopen($path, 'r');
+ file_put_contents($tmpFile, $source);
+ }
+ $handle = fopen($tmpFile, $mode);
+ return CallbackWrapper::wrap($handle, null, null, function () use ($path, $tmpFile): void {
+ $this->writeBack($tmpFile, $path);
+ });
+ }
+ }
+
+ public function touch(string $path, ?int $mtime = null): bool {
+ $path = $this->normalizePath($path);
+ if (is_null($mtime)) {
+ $mtime = time();
+ }
+ $metadata = ['timestamp' => (string)$mtime];
+ if ($this->file_exists($path)) {
+ if ($this->is_dir($path) && $path !== '.') {
+ $path .= '/';
+ }
+
+ $object = $this->fetchObject($path);
+ if ($object->mergeMetadata($metadata)) {
+ // invalidate target object to force repopulation on fetch
+ $this->objectCache->remove($path);
+ }
+ return true;
+ } else {
+ $mimeType = $this->mimeDetector->detectPath($path);
+ $this->getContainer()->createObject([
+ 'name' => $path,
+ 'content' => '',
+ 'headers' => ['content-type' => 'httpd/unix-directory']
+ ]);
+ // invalidate target object to force repopulation on fetch
+ $this->objectCache->remove($path);
+ return true;
+ }
+ }
+
+ public function copy(string $source, string $target): bool {
+ $source = $this->normalizePath($source);
+ $target = $this->normalizePath($target);
+
+ $fileType = $this->filetype($source);
+ if ($fileType) {
+ // make way
+ $this->unlink($target);
+ }
+
+ if ($fileType === 'file') {
+ try {
+ $sourceObject = $this->fetchObject($source);
+ $sourceObject->copy([
+ 'destination' => $this->bucket . '/' . $target
+ ]);
+ // invalidate target object to force repopulation on fetch
+ $this->objectCache->remove($target);
+ $this->objectCache->remove($target . '/');
+ } catch (BadResponseError $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+ } elseif ($fileType === 'dir') {
+ try {
+ $sourceObject = $this->fetchObject($source . '/');
+ $sourceObject->copy([
+ 'destination' => $this->bucket . '/' . $target . '/'
+ ]);
+ // invalidate target object to force repopulation on fetch
+ $this->objectCache->remove($target);
+ $this->objectCache->remove($target . '/');
+ } catch (BadResponseError $e) {
+ Server::get(LoggerInterface::class)->error($e->getMessage(), [
+ 'exception' => $e,
+ 'app' => 'files_external',
+ ]);
+ return false;
+ }
+
+ $dh = $this->opendir($source);
+ while (($file = readdir($dh)) !== false) {
+ if (Filesystem::isIgnoredDir($file)) {
+ continue;
+ }
+
+ $source = $source . '/' . $file;
+ $target = $target . '/' . $file;
+ $this->copy($source, $target);
+ }
+ } else {
+ //file does not exist
+ return false;
+ }
+
+ return true;
+ }
+
+ public function rename(string $source, string $target): bool {
+ $source = $this->normalizePath($source);
+ $target = $this->normalizePath($target);
+
+ $fileType = $this->filetype($source);
+
+ if ($fileType === 'dir' || $fileType === 'file') {
+ // copy
+ if ($this->copy($source, $target) === false) {
+ return false;
+ }
+
+ // cleanup
+ if ($this->unlink($source) === false) {
+ throw new \Exception('failed to remove original');
+ $this->unlink($target);
+ return false;
+ }
+
+ return true;
+ }
+
+ return false;
+ }
+
+ public function getId(): string {
+ return $this->id;
+ }
+
+ /**
+ * Returns the initialized object store container.
+ *
+ * @return Container
+ * @throws StorageAuthException
+ * @throws StorageNotAvailableException
+ */
+ public function getContainer(): Container {
+ if (is_null($this->container)) {
+ $this->container = $this->connectionFactory->getContainer();
+
+ if (!$this->file_exists('.')) {
+ $this->mkdir('.');
+ }
+ }
+ return $this->container;
+ }
+
+ public function writeBack(string $tmpFile, string $path): void {
+ $fileData = fopen($tmpFile, 'r');
+ $this->objectStore->writeObject($path, $fileData, $this->mimeDetector->detectPath($path));
+ // invalidate target object to force repopulation on fetch
+ $this->objectCache->remove($path);
+ unlink($tmpFile);
+ }
+
+ public function hasUpdated(string $path, int $time): bool {
+ if ($this->is_file($path)) {
+ return parent::hasUpdated($path, $time);
+ }
+ $path = $this->normalizePath($path);
+ $dh = $this->opendir($path);
+ $content = [];
+ while (($file = readdir($dh)) !== false) {
+ $content[] = $file;
+ }
+ if ($path === '.') {
+ $path = '';
+ }
+ $cachedContent = $this->getCache()->getFolderContents($path);
+ $cachedNames = array_map(function ($content) {
+ return $content['name'];
+ }, $cachedContent);
+ sort($cachedNames);
+ sort($content);
+ return $cachedNames !== $content;
+ }
+
+ /**
+ * check if curl is installed
+ */
+ public static function checkDependencies(): bool {
+ return true;
+ }
+}
diff --git a/apps/files_external/lib/Lib/Storage/SystemBridge.php b/apps/files_external/lib/Lib/Storage/SystemBridge.php
new file mode 100644
index 00000000000..80449b2744b
--- /dev/null
+++ b/apps/files_external/lib/Lib/Storage/SystemBridge.php
@@ -0,0 +1,27 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2024 Robin Appelman <robin@icewind.nl>
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Lib\Storage;
+
+use Icewind\SMB\System;
+use OCP\IBinaryFinder;
+
+/**
+ * Bridge the NC and SMB binary finding logic
+ */
+class SystemBridge extends System {
+ public function __construct(
+ private IBinaryFinder $binaryFinder,
+ ) {
+ }
+
+ protected function getBinaryPath(string $binary): ?string {
+ $path = $this->binaryFinder->findBinaryPath($binary);
+ return $path !== false ? $path : null;
+ }
+}
diff --git a/apps/files_external/lib/storageconfig.php b/apps/files_external/lib/Lib/StorageConfig.php
index 590a5f53249..2cb82d3790a 100644
--- a/apps/files_external/lib/storageconfig.php
+++ b/apps/files_external/lib/Lib/StorageConfig.php
@@ -1,40 +1,28 @@
<?php
+
/**
- * @author Jesús Macias <jmacias@solidgear.es>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
+namespace OCA\Files_External\Lib;
-namespace OCA\Files_external\Lib;
-
+use OC\Files\Filesystem;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\Auth\IUserProvided;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\ResponseDefinitions;
/**
* External storage configuration
+ *
+ * @psalm-import-type Files_ExternalStorageConfig from ResponseDefinitions
*/
class StorageConfig implements \JsonSerializable {
- const MOUNT_TYPE_ADMIN = 1;
- const MOUNT_TYPE_PERSONAl = 2;
+ public const MOUNT_TYPE_ADMIN = 1;
+ public const MOUNT_TYPE_PERSONAL = 2;
+ /** @deprecated use MOUNT_TYPE_PERSONAL (full uppercase) instead */
+ public const MOUNT_TYPE_PERSONAl = 2;
/**
* Storage config id
@@ -60,7 +48,7 @@ class StorageConfig implements \JsonSerializable {
/**
* Backend options
*
- * @var array
+ * @var array<string, mixed>
*/
private $backendOptions = [];
@@ -95,21 +83,21 @@ class StorageConfig implements \JsonSerializable {
/**
* List of users who have access to this storage
*
- * @var array
+ * @var list<string>
*/
private $applicableUsers = [];
/**
* List of groups that have access to this storage
*
- * @var array
+ * @var list<string>
*/
private $applicableGroups = [];
/**
* Mount-specific options
*
- * @var array
+ * @var array<string, mixed>
*/
private $mountOptions = [];
@@ -123,10 +111,10 @@ class StorageConfig implements \JsonSerializable {
/**
* Creates a storage config
*
- * @param int|null $id config id or null for a new config
+ * @param int|string $id config id or null for a new config
*/
public function __construct($id = null) {
- $this->id = $id;
+ $this->id = $id ?? -1;
$this->mountOptions['enable_sharing'] = false;
}
@@ -144,7 +132,7 @@ class StorageConfig implements \JsonSerializable {
*
* @param int $id configuration id
*/
- public function setId($id) {
+ public function setId(int $id): void {
$this->id = $id;
}
@@ -166,7 +154,7 @@ class StorageConfig implements \JsonSerializable {
* @param string $mountPoint path
*/
public function setMountPoint($mountPoint) {
- $this->mountPoint = \OC\Files\Filesystem::normalizePath($mountPoint);
+ $this->mountPoint = Filesystem::normalizePath($mountPoint);
}
/**
@@ -180,7 +168,7 @@ class StorageConfig implements \JsonSerializable {
* @param Backend $backend
*/
public function setBackend(Backend $backend) {
- $this->backend= $backend;
+ $this->backend = $backend;
}
/**
@@ -212,12 +200,12 @@ class StorageConfig implements \JsonSerializable {
* @param array $backendOptions backend options
*/
public function setBackendOptions($backendOptions) {
- if($this->getBackend() instanceof Backend) {
+ if ($this->getBackend() instanceof Backend) {
$parameters = $this->getBackend()->getParameters();
- foreach($backendOptions as $key => $value) {
- if(isset($parameters[$key])) {
+ foreach ($backendOptions as $key => $value) {
+ if (isset($parameters[$key])) {
switch ($parameters[$key]->getType()) {
- case \OCA\Files_External\Lib\DefinitionParameter::VALUE_BOOLEAN:
+ case DefinitionParameter::VALUE_BOOLEAN:
$value = (bool)$value;
break;
}
@@ -258,7 +246,7 @@ class StorageConfig implements \JsonSerializable {
}
/**
- * Sets the mount priotity
+ * Sets the mount priority
*
* @param int $priority priority
*/
@@ -269,7 +257,7 @@ class StorageConfig implements \JsonSerializable {
/**
* Returns the users for which to mount this storage
*
- * @return array applicable users
+ * @return list<string> applicable users
*/
public function getApplicableUsers() {
return $this->applicableUsers;
@@ -278,7 +266,7 @@ class StorageConfig implements \JsonSerializable {
/**
* Sets the users for which to mount this storage
*
- * @param array|null $applicableUsers applicable users
+ * @param list<string>|null $applicableUsers applicable users
*/
public function setApplicableUsers($applicableUsers) {
if (is_null($applicableUsers)) {
@@ -290,7 +278,7 @@ class StorageConfig implements \JsonSerializable {
/**
* Returns the groups for which to mount this storage
*
- * @return array applicable groups
+ * @return list<string> applicable groups
*/
public function getApplicableGroups() {
return $this->applicableGroups;
@@ -299,7 +287,7 @@ class StorageConfig implements \JsonSerializable {
/**
* Sets the groups for which to mount this storage
*
- * @param array|null $applicableGroups applicable groups
+ * @param list<string>|null $applicableGroups applicable groups
*/
public function setApplicableGroups($applicableGroups) {
if (is_null($applicableGroups)) {
@@ -378,14 +366,14 @@ class StorageConfig implements \JsonSerializable {
}
/**
- * @return int self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAl
+ * @return int self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL
*/
public function getType() {
return $this->type;
}
/**
- * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAl
+ * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL
*/
public function setType($type) {
$this->type = $type;
@@ -393,14 +381,19 @@ class StorageConfig implements \JsonSerializable {
/**
* Serialize config to JSON
- *
- * @return array
+ * @return Files_ExternalStorageConfig
*/
- public function jsonSerialize() {
+ public function jsonSerialize(bool $obfuscate = false): array {
$result = [];
if (!is_null($this->id)) {
$result['id'] = $this->id;
}
+
+ // obfuscate sensitive data if requested
+ if ($obfuscate) {
+ $this->formatStorageForUI();
+ }
+
$result['mountPoint'] = $this->mountPoint;
$result['backend'] = $this->backend->getIdentifier();
$result['authMechanism'] = $this->authMechanism->getIdentifier();
@@ -424,7 +417,22 @@ class StorageConfig implements \JsonSerializable {
$result['statusMessage'] = $this->statusMessage;
}
$result['userProvided'] = $this->authMechanism instanceof IUserProvided;
- $result['type'] = ($this->getType() === self::MOUNT_TYPE_PERSONAl) ? 'personal': 'system';
+ $result['type'] = ($this->getType() === self::MOUNT_TYPE_PERSONAL) ? 'personal': 'system';
return $result;
}
+
+ protected function formatStorageForUI(): void {
+ /** @var DefinitionParameter[] $parameters */
+ $parameters = array_merge($this->getBackend()->getParameters(), $this->getAuthMechanism()->getParameters());
+
+ $options = $this->getBackendOptions();
+ foreach ($options as $key => $value) {
+ foreach ($parameters as $parameter) {
+ if ($parameter->getName() === $key && $parameter->getType() === DefinitionParameter::VALUE_PASSWORD) {
+ $this->setBackendOption($key, DefinitionParameter::UNMODIFIED_PLACEHOLDER);
+ break;
+ }
+ }
+ }
+ }
}
diff --git a/apps/files_external/lib/Lib/StorageModifierTrait.php b/apps/files_external/lib/Lib/StorageModifierTrait.php
new file mode 100644
index 00000000000..4062ff1635e
--- /dev/null
+++ b/apps/files_external/lib/Lib/StorageModifierTrait.php
@@ -0,0 +1,50 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Lib;
+
+use OCP\Files\Storage\IStorage;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IUser;
+
+/**
+ * Trait for objects that can modify StorageConfigs and wrap Storages
+ *
+ * When a storage implementation is being prepared for use, the StorageConfig
+ * is passed through manipulateStorageConfig() to update any parameters as
+ * necessary. After the storage implementation has been constructed, it is
+ * passed through wrapStorage(), potentially replacing the implementation with
+ * a wrapped storage that changes its behaviour.
+ *
+ * Certain configuration options need to be set before the implementation is
+ * constructed, while others are retrieved directly from the storage
+ * implementation and so need a wrapper to be modified.
+ */
+trait StorageModifierTrait {
+
+ /**
+ * Modify a StorageConfig parameters
+ *
+ * @param StorageConfig &$storage
+ * @param ?IUser $user User the storage is being used as
+ * @return void
+ * @throws InsufficientDataForMeaningfulAnswerException
+ * @throws StorageNotAvailableException
+ */
+ public function manipulateStorageConfig(StorageConfig &$storage, ?IUser $user = null) {
+ }
+
+ /**
+ * Wrap a storage if necessary
+ *
+ * @throws InsufficientDataForMeaningfulAnswerException
+ * @throws StorageNotAvailableException
+ */
+ public function wrapStorage(IStorage $storage): IStorage {
+ return $storage;
+ }
+}
diff --git a/apps/files_external/lib/visibilitytrait.php b/apps/files_external/lib/Lib/VisibilityTrait.php
index 916c8e69d9c..62b26f3edb1 100644
--- a/apps/files_external/lib/visibilitytrait.php
+++ b/apps/files_external/lib/Lib/VisibilityTrait.php
@@ -1,27 +1,13 @@
<?php
+
/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
*/
-
namespace OCA\Files_External\Lib;
-use \OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\BackendService;
/**
* Trait to implement visibility mechanics for a configuration class
@@ -132,5 +118,4 @@ trait VisibilityTrait {
public function removeAllowedVisibility($allowedVisibility) {
return $this->setAllowedVisibility($this->allowedVisibility & ~$allowedVisibility);
}
-
}
diff --git a/apps/files_external/lib/Listener/GroupDeletedListener.php b/apps/files_external/lib/Listener/GroupDeletedListener.php
new file mode 100644
index 00000000000..244b3b2371f
--- /dev/null
+++ b/apps/files_external/lib/Listener/GroupDeletedListener.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Listener;
+
+use OCA\Files_External\Service\DBConfigService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Group\Events\GroupDeletedEvent;
+
+/** @template-implements IEventListener<GroupDeletedEvent> */
+class GroupDeletedListener implements IEventListener {
+ public function __construct(
+ private DBConfigService $config,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof GroupDeletedEvent) {
+ return;
+ }
+ $this->config->modifyMountsOnGroupDelete($event->getGroup()->getGID());
+ }
+}
diff --git a/apps/files_external/lib/Listener/LoadAdditionalListener.php b/apps/files_external/lib/Listener/LoadAdditionalListener.php
new file mode 100644
index 00000000000..6ba917759c3
--- /dev/null
+++ b/apps/files_external/lib/Listener/LoadAdditionalListener.php
@@ -0,0 +1,41 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2019 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Listener;
+
+use OCA\Files\Event\LoadAdditionalScriptsEvent;
+use OCA\Files_External\AppInfo\Application;
+use OCA\Files_External\ConfigLexicon;
+use OCP\AppFramework\Services\IInitialState;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\IAppConfig;
+use OCP\Util;
+
+/**
+ * @template-implements IEventListener<LoadAdditionalScriptsEvent>
+ */
+class LoadAdditionalListener implements IEventListener {
+
+ public function __construct(
+ private readonly IAppConfig $appConfig,
+ private IInitialState $initialState,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!($event instanceof LoadAdditionalScriptsEvent)) {
+ return;
+ }
+
+ $allowUserMounting = $this->appConfig->getValueBool('files_external', ConfigLexicon::ALLOW_USER_MOUNTING);
+ $this->initialState->provideInitialState('allowUserMounting', $allowUserMounting);
+
+ Util::addInitScript(Application::APP_ID, 'init');
+ }
+}
diff --git a/apps/files_external/lib/Listener/StorePasswordListener.php b/apps/files_external/lib/Listener/StorePasswordListener.php
new file mode 100644
index 00000000000..8580176b014
--- /dev/null
+++ b/apps/files_external/lib/Listener/StorePasswordListener.php
@@ -0,0 +1,57 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Listener;
+
+use OCA\Files_External\Lib\Auth\Password\LoginCredentials;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\Security\ICredentialsManager;
+use OCP\User\Events\PasswordUpdatedEvent;
+use OCP\User\Events\UserLoggedInEvent;
+
+/** @template-implements IEventListener<PasswordUpdatedEvent|UserLoggedInEvent> */
+class StorePasswordListener implements IEventListener {
+ public function __construct(
+ private ICredentialsManager $credentialsManager,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof UserLoggedInEvent && !$event instanceof PasswordUpdatedEvent) {
+ return;
+ }
+
+ if ($event instanceof UserLoggedInEvent && $event->isTokenLogin()) {
+ return;
+ }
+
+ $storedCredentials = $this->credentialsManager->retrieve($event->getUser()->getUID(), LoginCredentials::CREDENTIALS_IDENTIFIER);
+
+ if (!$storedCredentials) {
+ return;
+ }
+
+ $newCredentials = $storedCredentials;
+ $shouldUpdate = false;
+
+ if (($storedCredentials['password'] ?? null) !== $event->getPassword() && $event->getPassword() !== null) {
+ $shouldUpdate = true;
+ $newCredentials['password'] = $event->getPassword();
+ }
+
+ if ($event instanceof UserLoggedInEvent && ($storedCredentials['user'] ?? null) !== $event->getLoginName()) {
+ $shouldUpdate = true;
+ $newCredentials['user'] = $event->getLoginName();
+ }
+
+ if ($shouldUpdate) {
+ $this->credentialsManager->store($event->getUser()->getUID(), LoginCredentials::CREDENTIALS_IDENTIFIER, $newCredentials);
+ }
+ }
+}
diff --git a/apps/files_external/lib/Listener/UserDeletedListener.php b/apps/files_external/lib/Listener/UserDeletedListener.php
new file mode 100644
index 00000000000..337fd12f311
--- /dev/null
+++ b/apps/files_external/lib/Listener/UserDeletedListener.php
@@ -0,0 +1,29 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Listener;
+
+use OCA\Files_External\Service\DBConfigService;
+use OCP\EventDispatcher\Event;
+use OCP\EventDispatcher\IEventListener;
+use OCP\User\Events\UserDeletedEvent;
+
+/** @template-implements IEventListener<UserDeletedEvent> */
+class UserDeletedListener implements IEventListener {
+ public function __construct(
+ private DBConfigService $config,
+ ) {
+ }
+
+ public function handle(Event $event): void {
+ if (!$event instanceof UserDeletedEvent) {
+ return;
+ }
+ $this->config->modifyMountsOnUserDelete($event->getUser()->getUID());
+ }
+}
diff --git a/apps/files_external/lib/Migration/DummyUserSession.php b/apps/files_external/lib/Migration/DummyUserSession.php
new file mode 100644
index 00000000000..1ebf0e1ec4f
--- /dev/null
+++ b/apps/files_external/lib/Migration/DummyUserSession.php
@@ -0,0 +1,57 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Migration;
+
+use OCP\IUser;
+use OCP\IUserSession;
+
+class DummyUserSession implements IUserSession {
+
+ private ?IUser $user = null;
+
+ public function login($uid, $password) {
+ }
+
+ public function logout() {
+ }
+
+ public function setUser($user) {
+ $this->user = $user;
+ }
+
+ public function setVolatileActiveUser(?IUser $user): void {
+ $this->user = $user;
+ }
+
+ public function getUser() {
+ return $this->user;
+ }
+
+ public function isLoggedIn() {
+ return !is_null($this->user);
+ }
+
+ /**
+ * get getImpersonatingUserID
+ *
+ * @return string|null
+ * @since 17.0.0
+ */
+ public function getImpersonatingUserID() : ?string {
+ return null;
+ }
+
+ /**
+ * set setImpersonatingUserID
+ *
+ * @since 17.0.0
+ */
+ public function setImpersonatingUserID(bool $useCurrentUser = true): void {
+ //no OP
+ }
+}
diff --git a/apps/files_external/lib/Migration/Version1011Date20200630192246.php b/apps/files_external/lib/Migration/Version1011Date20200630192246.php
new file mode 100644
index 00000000000..c87b1cfbc8b
--- /dev/null
+++ b/apps/files_external/lib/Migration/Version1011Date20200630192246.php
@@ -0,0 +1,136 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2020 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\DB\Types;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1011Date20200630192246 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options) {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ if (!$schema->hasTable('external_mounts')) {
+ $table = $schema->createTable('external_mounts');
+ $table->addColumn('mount_id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('mount_point', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 128,
+ ]);
+ $table->addColumn('storage_backend', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('auth_backend', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('priority', Types::INTEGER, [
+ 'notnull' => true,
+ 'length' => 4,
+ 'default' => 100,
+ ]);
+ $table->addColumn('type', Types::INTEGER, [
+ 'notnull' => true,
+ 'length' => 4,
+ ]);
+ $table->setPrimaryKey(['mount_id']);
+ }
+
+ if (!$schema->hasTable('external_applicable')) {
+ $table = $schema->createTable('external_applicable');
+ $table->addColumn('applicable_id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('mount_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('type', Types::INTEGER, [
+ 'notnull' => true,
+ 'length' => 4,
+ ]);
+ $table->addColumn('value', Types::STRING, [
+ 'notnull' => false,
+ 'length' => 64,
+ ]);
+ $table->setPrimaryKey(['applicable_id']);
+ $table->addIndex(['mount_id'], 'applicable_mount');
+ $table->addUniqueIndex(['type', 'value', 'mount_id'], 'applicable_type_value_mount');
+ }
+
+ if (!$schema->hasTable('external_config')) {
+ $table = $schema->createTable('external_config');
+ $table->addColumn('config_id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('mount_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('key', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('value', Types::STRING, [
+ 'notnull' => false,
+ 'length' => 4000,
+ ]);
+ $table->setPrimaryKey(['config_id']);
+ $table->addUniqueIndex(['mount_id', 'key'], 'config_mount_key');
+ } else {
+ $table = $schema->getTable('external_config');
+ $table->changeColumn('value', [
+ 'notnull' => false,
+ 'length' => 4000,
+ ]);
+ }
+
+ if (!$schema->hasTable('external_options')) {
+ $table = $schema->createTable('external_options');
+ $table->addColumn('option_id', Types::BIGINT, [
+ 'autoincrement' => true,
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('mount_id', Types::BIGINT, [
+ 'notnull' => true,
+ 'length' => 6,
+ ]);
+ $table->addColumn('key', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 64,
+ ]);
+ $table->addColumn('value', Types::STRING, [
+ 'notnull' => true,
+ 'length' => 256,
+ ]);
+ $table->setPrimaryKey(['option_id']);
+ $table->addUniqueIndex(['mount_id', 'key'], 'option_mount_key');
+ }
+ return $schema;
+ }
+}
diff --git a/apps/files_external/lib/Migration/Version1015Date20211104103506.php b/apps/files_external/lib/Migration/Version1015Date20211104103506.php
new file mode 100644
index 00000000000..6027c795cdf
--- /dev/null
+++ b/apps/files_external/lib/Migration/Version1015Date20211104103506.php
@@ -0,0 +1,90 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Migration;
+
+use Closure;
+use OC\Files\Cache\Storage;
+use OCP\DB\Exception;
+use OCP\DB\IResult;
+use OCP\DB\ISchemaWrapper;
+use OCP\IDBConnection;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+use Psr\Log\LoggerInterface;
+
+class Version1015Date20211104103506 extends SimpleMigrationStep {
+
+ public function __construct(
+ private IDBConnection $connection,
+ private LoggerInterface $logger,
+ ) {
+ }
+
+ public function postSchemaChange(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->update('storages')
+ ->set('id', $qb->createParameter('newId'))
+ ->where($qb->expr()->eq('id', $qb->createParameter('oldId')));
+
+ $mounts = $this->getS3Mounts();
+ if (!$mounts instanceof IResult) {
+ throw new \Exception('Could not fetch existing mounts for migration');
+ }
+
+ while ($mount = $mounts->fetch()) {
+ $config = $this->getStorageConfig((int)$mount['mount_id']);
+ $hostname = $config['hostname'];
+ $bucket = $config['bucket'];
+ $key = $config['key'];
+ $oldId = Storage::adjustStorageId('amazon::' . $bucket);
+ $newId = Storage::adjustStorageId('amazon::external::' . md5($hostname . ':' . $bucket . ':' . $key));
+ try {
+ $qb->setParameter('oldId', $oldId);
+ $qb->setParameter('newId', $newId);
+ $qb->execute();
+ $this->logger->info('Migrated s3 storage id for mount with id ' . $mount['mount_id'] . ' to ' . $newId);
+ } catch (Exception $e) {
+ $this->logger->error('Failed to migrate external s3 storage id for mount with id ' . $mount['mount_id'], [
+ 'exception' => $e
+ ]);
+ }
+ }
+ return null;
+ }
+
+ /**
+ * @throws Exception
+ * @return IResult|int
+ */
+ private function getS3Mounts() {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('m.mount_id')
+ ->selectAlias('c.value', 'bucket')
+ ->from('external_mounts', 'm')
+ ->innerJoin('m', 'external_config', 'c', 'c.mount_id = m.mount_id')
+ ->where($qb->expr()->eq('m.storage_backend', $qb->createPositionalParameter('amazons3')))
+ ->andWhere($qb->expr()->eq('c.key', $qb->createPositionalParameter('bucket')));
+ return $qb->execute();
+ }
+
+ /**
+ * @throws Exception
+ */
+ private function getStorageConfig(int $mountId): array {
+ $qb = $this->connection->getQueryBuilder();
+ $qb->select('key', 'value')
+ ->from('external_config')
+ ->where($qb->expr()->eq('mount_id', $qb->createPositionalParameter($mountId)));
+ $config = [];
+ foreach ($qb->execute()->fetchAll() as $row) {
+ $config[$row['key']] = $row['value'];
+ }
+ return $config;
+ }
+}
diff --git a/apps/files_external/lib/Migration/Version1016Date20220324154536.php b/apps/files_external/lib/Migration/Version1016Date20220324154536.php
new file mode 100644
index 00000000000..fb2cccfdd80
--- /dev/null
+++ b/apps/files_external/lib/Migration/Version1016Date20220324154536.php
@@ -0,0 +1,38 @@
+<?php
+
+declare(strict_types=1);
+/**
+ * SPDX-FileCopyrightText: 2022 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+class Version1016Date20220324154536 extends SimpleMigrationStep {
+
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $table = $schema->getTable('external_config');
+ $column = $table->getColumn('value');
+
+ if ($column->getLength() > 4000) {
+ $column->setLength(4000);
+ return $schema;
+ }
+
+ return null;
+ }
+}
diff --git a/apps/files_external/lib/Migration/Version22000Date20210216084416.php b/apps/files_external/lib/Migration/Version22000Date20210216084416.php
new file mode 100644
index 00000000000..c4878e602c0
--- /dev/null
+++ b/apps/files_external/lib/Migration/Version22000Date20210216084416.php
@@ -0,0 +1,47 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2021 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Migration;
+
+use Closure;
+use OCP\DB\ISchemaWrapper;
+use OCP\Migration\IOutput;
+use OCP\Migration\SimpleMigrationStep;
+
+/**
+ * Auto-generated migration step: Please modify to your needs!
+ */
+class Version22000Date20210216084416 extends SimpleMigrationStep {
+ /**
+ * @param IOutput $output
+ * @param Closure $schemaClosure The `\Closure` returns a `ISchemaWrapper`
+ * @param array $options
+ * @return null|ISchemaWrapper
+ */
+ public function changeSchema(IOutput $output, Closure $schemaClosure, array $options): ?ISchemaWrapper {
+ /** @var ISchemaWrapper $schema */
+ $schema = $schemaClosure();
+
+ $table = $schema->getTable('external_applicable');
+ if ($table->hasIndex('applicable_type_value')) {
+ $table->dropIndex('applicable_type_value');
+ }
+
+ $table = $schema->getTable('external_config');
+ if ($table->hasIndex('config_mount')) {
+ $table->dropIndex('config_mount');
+ }
+
+ $table = $schema->getTable('external_options');
+ if ($table->hasIndex('option_mount')) {
+ $table->dropIndex('option_mount');
+ }
+
+ return $schema;
+ }
+}
diff --git a/apps/files_external/lib/MountConfig.php b/apps/files_external/lib/MountConfig.php
new file mode 100644
index 00000000000..5637ee71ec1
--- /dev/null
+++ b/apps/files_external/lib/MountConfig.php
@@ -0,0 +1,250 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External;
+
+use OC\Files\Storage\Common;
+use OCA\Files_External\Config\IConfigHandler;
+use OCA\Files_External\Config\UserContext;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCA\Files_External\Service\UserStoragesService;
+use OCP\AppFramework\QueryException;
+use OCP\Files\StorageNotAvailableException;
+use OCP\IConfig;
+use OCP\IL10N;
+use OCP\Security\ISecureRandom;
+use OCP\Server;
+use OCP\Util;
+use phpseclib\Crypt\AES;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Class to configure mount.json globally and for users
+ */
+class MountConfig {
+ // TODO: make this class non-static and give it a proper namespace
+
+ public const MOUNT_TYPE_GLOBAL = 'global';
+ public const MOUNT_TYPE_GROUP = 'group';
+ public const MOUNT_TYPE_USER = 'user';
+ public const MOUNT_TYPE_PERSONAL = 'personal';
+
+ // whether to skip backend test (for unit tests, as this static class is not mockable)
+ public static $skipTest = false;
+
+ public function __construct(
+ private UserGlobalStoragesService $userGlobalStorageService,
+ private UserStoragesService $userStorageService,
+ private GlobalStoragesService $globalStorageService,
+ ) {
+ }
+
+ /**
+ * @param mixed $input
+ * @param string|null $userId
+ * @return mixed
+ * @throws QueryException
+ * @since 16.0.0
+ */
+ public static function substitutePlaceholdersInConfig($input, ?string $userId = null) {
+ /** @var BackendService $backendService */
+ $backendService = Server::get(BackendService::class);
+ /** @var IConfigHandler[] $handlers */
+ $handlers = $backendService->getConfigHandlers();
+ foreach ($handlers as $handler) {
+ if ($handler instanceof UserContext && $userId !== null) {
+ $handler->setUserId($userId);
+ }
+ $input = $handler->handle($input);
+ }
+ return $input;
+ }
+
+ /**
+ * Test connecting using the given backend configuration
+ *
+ * @param string $class backend class name
+ * @param array $options backend configuration options
+ * @param boolean $isPersonal
+ * @return int see self::STATUS_*
+ * @throws \Exception
+ */
+ public static function getBackendStatus($class, $options) {
+ if (self::$skipTest) {
+ return StorageNotAvailableException::STATUS_SUCCESS;
+ }
+ foreach ($options as $key => &$option) {
+ if ($key === 'password') {
+ // no replacements in passwords
+ continue;
+ }
+ $option = self::substitutePlaceholdersInConfig($option);
+ }
+ if (class_exists($class)) {
+ try {
+ /** @var Common $storage */
+ $storage = new $class($options);
+
+ try {
+ $result = $storage->test();
+ $storage->setAvailability($result);
+ if ($result) {
+ return StorageNotAvailableException::STATUS_SUCCESS;
+ }
+ } catch (\Exception $e) {
+ $storage->setAvailability(false);
+ throw $e;
+ }
+ } catch (\Exception $exception) {
+ Server::get(LoggerInterface::class)->error($exception->getMessage(), ['exception' => $exception, 'app' => 'files_external']);
+ throw $exception;
+ }
+ }
+ return StorageNotAvailableException::STATUS_ERROR;
+ }
+
+ /**
+ * Get backend dependency message
+ * TODO: move into AppFramework along with templates
+ *
+ * @param Backend[] $backends
+ */
+ public static function dependencyMessage(array $backends): string {
+ $l = Util::getL10N('files_external');
+ $message = '';
+ $dependencyGroups = [];
+
+ foreach ($backends as $backend) {
+ foreach ($backend->checkDependencies() as $dependency) {
+ $dependencyMessage = $dependency->getMessage();
+ if ($dependencyMessage !== null) {
+ $message .= '<p>' . $dependencyMessage . '</p>';
+ } else {
+ $dependencyGroups[$dependency->getDependency()][] = $backend;
+ }
+ }
+ }
+
+ foreach ($dependencyGroups as $module => $dependants) {
+ $backends = implode(', ', array_map(function (Backend $backend): string {
+ return '"' . $backend->getText() . '"';
+ }, $dependants));
+ $message .= '<p>' . MountConfig::getSingleDependencyMessage($l, $module, $backends) . '</p>';
+ }
+
+ return $message;
+ }
+
+ /**
+ * Returns a dependency missing message
+ */
+ private static function getSingleDependencyMessage(IL10N $l, string $module, string $backend): string {
+ switch (strtolower($module)) {
+ case 'curl':
+ return $l->t('The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]);
+ case 'ftp':
+ return $l->t('The FTP support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', [$backend]);
+ default:
+ return $l->t('"%1$s" is not installed. Mounting of %2$s is not possible. Please ask your system administrator to install it.', [$module, $backend]);
+ }
+ }
+
+ /**
+ * Encrypt passwords in the given config options
+ *
+ * @param array $options mount options
+ * @return array updated options
+ */
+ public static function encryptPasswords($options) {
+ if (isset($options['password'])) {
+ $options['password_encrypted'] = self::encryptPassword($options['password']);
+ // do not unset the password, we want to keep the keys order
+ // on load... because that's how the UI currently works
+ $options['password'] = '';
+ }
+ return $options;
+ }
+
+ /**
+ * Decrypt passwords in the given config options
+ *
+ * @param array $options mount options
+ * @return array updated options
+ */
+ public static function decryptPasswords($options) {
+ // note: legacy options might still have the unencrypted password in the "password" field
+ if (isset($options['password_encrypted'])) {
+ $options['password'] = self::decryptPassword($options['password_encrypted']);
+ unset($options['password_encrypted']);
+ }
+ return $options;
+ }
+
+ /**
+ * Encrypt a single password
+ *
+ * @param string $password plain text password
+ * @return string encrypted password
+ */
+ private static function encryptPassword($password) {
+ $cipher = self::getCipher();
+ $iv = Server::get(ISecureRandom::class)->generate(16);
+ $cipher->setIV($iv);
+ return base64_encode($iv . $cipher->encrypt($password));
+ }
+
+ /**
+ * Decrypts a single password
+ *
+ * @param string $encryptedPassword encrypted password
+ * @return string plain text password
+ */
+ private static function decryptPassword($encryptedPassword) {
+ $cipher = self::getCipher();
+ $binaryPassword = base64_decode($encryptedPassword);
+ $iv = substr($binaryPassword, 0, 16);
+ $cipher->setIV($iv);
+ $binaryPassword = substr($binaryPassword, 16);
+ return $cipher->decrypt($binaryPassword);
+ }
+
+ /**
+ * Returns the encryption cipher
+ *
+ * @return AES
+ */
+ private static function getCipher() {
+ $cipher = new AES(AES::MODE_CBC);
+ $cipher->setKey(Server::get(IConfig::class)->getSystemValue('passwordsalt', null));
+ return $cipher;
+ }
+
+ /**
+ * Computes a hash based on the given configuration.
+ * This is mostly used to find out whether configurations
+ * are the same.
+ *
+ * @param array $config
+ * @return string
+ */
+ public static function makeConfigHash($config) {
+ $data = json_encode(
+ [
+ 'c' => $config['backend'],
+ 'a' => $config['authMechanism'],
+ 'm' => $config['mountpoint'],
+ 'o' => $config['options'],
+ 'p' => $config['priority'] ?? -1,
+ 'mo' => $config['mountOptions'] ?? [],
+ ]
+ );
+ return hash('md5', $data);
+ }
+}
diff --git a/apps/files_external/lib/NotFoundException.php b/apps/files_external/lib/NotFoundException.php
new file mode 100644
index 00000000000..411a2212513
--- /dev/null
+++ b/apps/files_external/lib/NotFoundException.php
@@ -0,0 +1,14 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External;
+
+/**
+ * Storage is not found
+ */
+class NotFoundException extends \Exception {
+}
diff --git a/apps/files_external/lib/ResponseDefinitions.php b/apps/files_external/lib/ResponseDefinitions.php
new file mode 100644
index 00000000000..26a0965f1fc
--- /dev/null
+++ b/apps/files_external/lib/ResponseDefinitions.php
@@ -0,0 +1,42 @@
+<?php
+
+declare(strict_types=1);
+
+/**
+ * SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+
+namespace OCA\Files_External;
+
+/**
+ * @psalm-type Files_ExternalStorageConfig = array{
+ * applicableGroups?: list<string>,
+ * applicableUsers?: list<string>,
+ * authMechanism: string,
+ * backend: string,
+ * backendOptions: array<string, mixed>,
+ * id?: int,
+ * mountOptions?: array<string, mixed>,
+ * mountPoint: string,
+ * priority?: int,
+ * status?: int,
+ * statusMessage?: string,
+ * type: 'personal'|'system',
+ * userProvided: bool,
+ * }
+ *
+ * @psalm-type Files_ExternalMount = array{
+ * name: string,
+ * path: string,
+ * type: 'dir',
+ * backend: string,
+ * scope: 'system'|'personal',
+ * permissions: int,
+ * id: int,
+ * class: string,
+ * config: Files_ExternalStorageConfig,
+ * }
+ */
+class ResponseDefinitions {
+}
diff --git a/apps/files_external/lib/Service/BackendService.php b/apps/files_external/lib/Service/BackendService.php
new file mode 100644
index 00000000000..3a688ee66e6
--- /dev/null
+++ b/apps/files_external/lib/Service/BackendService.php
@@ -0,0 +1,342 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OCA\Files_External\Config\IConfigHandler;
+use OCA\Files_External\ConfigLexicon;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\Config\IAuthMechanismProvider;
+use OCA\Files_External\Lib\Config\IBackendProvider;
+use OCA\Files_External\Lib\MissingDependency;
+use OCP\EventDispatcher\GenericEvent;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\IAppConfig;
+use OCP\Server;
+
+/**
+ * Service class to manage backend definitions
+ */
+class BackendService {
+
+ /** Visibility constants for VisibilityTrait */
+ public const VISIBILITY_NONE = 0;
+ public const VISIBILITY_PERSONAL = 1;
+ public const VISIBILITY_ADMIN = 2;
+ //const VISIBILITY_ALIENS = 4;
+
+ public const VISIBILITY_DEFAULT = 3; // PERSONAL | ADMIN
+
+ /** Priority constants for PriorityTrait */
+ public const PRIORITY_DEFAULT = 100;
+
+ /** @var bool */
+ private $userMountingAllowed = true;
+
+ /** @var string[] */
+ private $userMountingBackends = [];
+
+ /** @var Backend[] */
+ private $backends = [];
+
+ /** @var IBackendProvider[] */
+ private $backendProviders = [];
+
+ /** @var AuthMechanism[] */
+ private $authMechanisms = [];
+
+ /** @var IAuthMechanismProvider[] */
+ private $authMechanismProviders = [];
+
+ /** @var callable[] */
+ private $configHandlerLoaders = [];
+
+ private $configHandlers = [];
+
+ public function __construct(
+ protected IAppConfig $appConfig,
+ ) {
+ // Load config values
+ $this->userMountingAllowed = $appConfig->getValueBool('files_external', ConfigLexicon::ALLOW_USER_MOUNTING);
+ $this->userMountingBackends = explode(',', $appConfig->getValueString('files_external', ConfigLexicon::USER_MOUNTING_BACKENDS));
+
+ // if no backend is in the list an empty string is in the array and user mounting is disabled
+ if ($this->userMountingBackends === ['']) {
+ $this->userMountingAllowed = false;
+ }
+ }
+
+ /**
+ * Register a backend provider
+ *
+ * @since 9.1.0
+ * @param IBackendProvider $provider
+ */
+ public function registerBackendProvider(IBackendProvider $provider) {
+ $this->backendProviders[] = $provider;
+ }
+
+ private function callForRegistrations() {
+ static $eventSent = false;
+ if (!$eventSent) {
+ Server::get(IEventDispatcher::class)->dispatch(
+ 'OCA\\Files_External::loadAdditionalBackends',
+ new GenericEvent()
+ );
+ $eventSent = true;
+ }
+ }
+
+ private function loadBackendProviders() {
+ $this->callForRegistrations();
+ foreach ($this->backendProviders as $provider) {
+ $this->registerBackends($provider->getBackends());
+ }
+ $this->backendProviders = [];
+ }
+
+ /**
+ * Register an auth mechanism provider
+ *
+ * @since 9.1.0
+ * @param IAuthMechanismProvider $provider
+ */
+ public function registerAuthMechanismProvider(IAuthMechanismProvider $provider) {
+ $this->authMechanismProviders[] = $provider;
+ }
+
+ private function loadAuthMechanismProviders() {
+ $this->callForRegistrations();
+ foreach ($this->authMechanismProviders as $provider) {
+ $this->registerAuthMechanisms($provider->getAuthMechanisms());
+ }
+ $this->authMechanismProviders = [];
+ }
+
+ /**
+ * Register a backend
+ *
+ * @deprecated 9.1.0 use registerBackendProvider()
+ * @param Backend $backend
+ */
+ public function registerBackend(Backend $backend) {
+ if (!$this->isAllowedUserBackend($backend)) {
+ $backend->removeVisibility(BackendService::VISIBILITY_PERSONAL);
+ }
+ foreach ($backend->getIdentifierAliases() as $alias) {
+ $this->backends[$alias] = $backend;
+ }
+ }
+
+ /**
+ * @deprecated 9.1.0 use registerBackendProvider()
+ * @param Backend[] $backends
+ */
+ public function registerBackends(array $backends) {
+ foreach ($backends as $backend) {
+ $this->registerBackend($backend);
+ }
+ }
+ /**
+ * Register an authentication mechanism
+ *
+ * @deprecated 9.1.0 use registerAuthMechanismProvider()
+ * @param AuthMechanism $authMech
+ */
+ public function registerAuthMechanism(AuthMechanism $authMech) {
+ if (!$this->isAllowedAuthMechanism($authMech)) {
+ $authMech->removeVisibility(BackendService::VISIBILITY_PERSONAL);
+ }
+ foreach ($authMech->getIdentifierAliases() as $alias) {
+ $this->authMechanisms[$alias] = $authMech;
+ }
+ }
+
+ /**
+ * @deprecated 9.1.0 use registerAuthMechanismProvider()
+ * @param AuthMechanism[] $mechanisms
+ */
+ public function registerAuthMechanisms(array $mechanisms) {
+ foreach ($mechanisms as $mechanism) {
+ $this->registerAuthMechanism($mechanism);
+ }
+ }
+
+ /**
+ * Get all backends
+ *
+ * @return Backend[]
+ */
+ public function getBackends() {
+ $this->loadBackendProviders();
+ // only return real identifiers, no aliases
+ $backends = [];
+ foreach ($this->backends as $backend) {
+ $backends[$backend->getIdentifier()] = $backend;
+ }
+ return $backends;
+ }
+
+ /**
+ * Get all available backends
+ *
+ * @return Backend[]
+ */
+ public function getAvailableBackends() {
+ return array_filter($this->getBackends(), function ($backend) {
+ $missing = array_filter($backend->checkDependencies(), fn (MissingDependency $dependency) => !$dependency->isOptional());
+ return count($missing) === 0;
+ });
+ }
+
+ /**
+ * @param string $identifier
+ * @return Backend|null
+ */
+ public function getBackend($identifier) {
+ $this->loadBackendProviders();
+ if (isset($this->backends[$identifier])) {
+ return $this->backends[$identifier];
+ }
+ return null;
+ }
+
+ /**
+ * Get all authentication mechanisms
+ *
+ * @return AuthMechanism[]
+ */
+ public function getAuthMechanisms() {
+ $this->loadAuthMechanismProviders();
+ // only return real identifiers, no aliases
+ $mechanisms = [];
+ foreach ($this->authMechanisms as $mechanism) {
+ $mechanisms[$mechanism->getIdentifier()] = $mechanism;
+ }
+ return $mechanisms;
+ }
+
+ /**
+ * Get all authentication mechanisms for schemes
+ *
+ * @param string[] $schemes
+ * @return AuthMechanism[]
+ */
+ public function getAuthMechanismsByScheme(array $schemes) {
+ return array_filter($this->getAuthMechanisms(), function ($authMech) use ($schemes) {
+ return in_array($authMech->getScheme(), $schemes, true);
+ });
+ }
+
+ /**
+ * @param string $identifier
+ * @return AuthMechanism|null
+ */
+ public function getAuthMechanism($identifier) {
+ $this->loadAuthMechanismProviders();
+ if (isset($this->authMechanisms[$identifier])) {
+ return $this->authMechanisms[$identifier];
+ }
+ return null;
+ }
+
+ /**
+ * @return bool
+ */
+ public function isUserMountingAllowed() {
+ return $this->userMountingAllowed;
+ }
+
+ /**
+ * Check a backend if a user is allowed to mount it
+ *
+ * @param Backend $backend
+ * @return bool
+ */
+ protected function isAllowedUserBackend(Backend $backend) {
+ if ($this->userMountingAllowed
+ && array_intersect($backend->getIdentifierAliases(), $this->userMountingBackends)
+ ) {
+ return true;
+ }
+ return false;
+ }
+
+ /**
+ * Check an authentication mechanism if a user is allowed to use it
+ *
+ * @param AuthMechanism $authMechanism
+ * @return bool
+ */
+ protected function isAllowedAuthMechanism(AuthMechanism $authMechanism) {
+ return true; // not implemented
+ }
+
+ /**
+ * registers a configuration handler
+ *
+ * The function of the provided $placeholder is mostly to act a sorting
+ * criteria, so longer placeholders are replaced first. This avoids
+ * "$user" overwriting parts of "$userMail" and "$userLang", for example.
+ * The provided value should not contain the $ prefix, only a-z0-9 are
+ * allowed. Upper case letters are lower cased, the replacement is case-
+ * insensitive.
+ *
+ * The configHandlerLoader should just instantiate the handler on demand.
+ * For now all handlers are instantiated when a mount is loaded, independent
+ * of whether the placeholder is present or not. This may change in future.
+ *
+ * @since 16.0.0
+ */
+ public function registerConfigHandler(string $placeholder, callable $configHandlerLoader) {
+ $placeholder = trim(strtolower($placeholder));
+ if (!(bool)\preg_match('/^[a-z0-9]*$/', $placeholder)) {
+ throw new \RuntimeException(sprintf(
+ 'Invalid placeholder %s, only [a-z0-9] are allowed', $placeholder
+ ));
+ }
+ if ($placeholder === '') {
+ throw new \RuntimeException('Invalid empty placeholder');
+ }
+ if (isset($this->configHandlerLoaders[$placeholder]) || isset($this->configHandlers[$placeholder])) {
+ throw new \RuntimeException(sprintf('A handler is already registered for %s', $placeholder));
+ }
+ $this->configHandlerLoaders[$placeholder] = $configHandlerLoader;
+ }
+
+ protected function loadConfigHandlers():void {
+ $this->callForRegistrations();
+ $newLoaded = false;
+ foreach ($this->configHandlerLoaders as $placeholder => $loader) {
+ $handler = $loader();
+ if (!$handler instanceof IConfigHandler) {
+ throw new \RuntimeException(sprintf(
+ 'Handler for %s is not an instance of IConfigHandler', $placeholder
+ ));
+ }
+ $this->configHandlers[$placeholder] = $handler;
+ $newLoaded = true;
+ }
+ $this->configHandlerLoaders = [];
+ if ($newLoaded) {
+ // ensure those with longest placeholders come first,
+ // to avoid substring matches
+ uksort($this->configHandlers, function ($phA, $phB) {
+ return strlen($phB) <=> strlen($phA);
+ });
+ }
+ }
+
+ /**
+ * @since 16.0.0
+ */
+ public function getConfigHandlers() {
+ $this->loadConfigHandlers();
+ return $this->configHandlers;
+ }
+}
diff --git a/apps/files_external/lib/Service/DBConfigService.php b/apps/files_external/lib/Service/DBConfigService.php
new file mode 100644
index 00000000000..41ec4512d70
--- /dev/null
+++ b/apps/files_external/lib/Service/DBConfigService.php
@@ -0,0 +1,501 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use Doctrine\DBAL\Exception\UniqueConstraintViolationException;
+use OCP\DB\QueryBuilder\IQueryBuilder;
+use OCP\IDBConnection;
+use OCP\Security\ICrypto;
+
+/**
+ * Stores the mount config in the database
+ */
+class DBConfigService {
+ public const MOUNT_TYPE_ADMIN = 1;
+ public const MOUNT_TYPE_PERSONAL = 2;
+ /** @deprecated use MOUNT_TYPE_PERSONAL (full uppercase) instead */
+ public const MOUNT_TYPE_PERSONAl = 2;
+
+ public const APPLICABLE_TYPE_GLOBAL = 1;
+ public const APPLICABLE_TYPE_GROUP = 2;
+ public const APPLICABLE_TYPE_USER = 3;
+
+ public function __construct(
+ private IDBConnection $connection,
+ private ICrypto $crypto,
+ ) {
+ }
+
+ public function getMountById(int $mountId): ?array {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
+ ->from('external_mounts', 'm')
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+ $mounts = $this->getMountsFromQuery($query);
+ if (count($mounts) > 0) {
+ return $mounts[0];
+ } else {
+ return null;
+ }
+ }
+
+ /**
+ * Get all configured mounts
+ *
+ * @return array
+ */
+ public function getAllMounts() {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
+ ->from('external_mounts');
+ return $this->getMountsFromQuery($query);
+ }
+
+ public function getMountsForUser($userId, $groupIds) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
+ ->from('external_mounts', 'm')
+ ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
+ ->where($builder->expr()->orX(
+ $builder->expr()->andX( // global mounts
+ $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
+ $builder->expr()->isNull('a.value')
+ ),
+ $builder->expr()->andX( // mounts for user
+ $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_USER, IQueryBuilder::PARAM_INT)),
+ $builder->expr()->eq('a.value', $builder->createNamedParameter($userId))
+ ),
+ $builder->expr()->andX( // mounts for group
+ $builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GROUP, IQueryBuilder::PARAM_INT)),
+ $builder->expr()->in('a.value', $builder->createNamedParameter($groupIds, IQueryBuilder::PARAM_STR_ARRAY))
+ )
+ ));
+
+ return $this->getMountsFromQuery($query);
+ }
+
+ public function modifyMountsOnUserDelete(string $uid): void {
+ $this->modifyMountsOnDelete($uid, self::APPLICABLE_TYPE_USER);
+ }
+
+ public function modifyMountsOnGroupDelete(string $gid): void {
+ $this->modifyMountsOnDelete($gid, self::APPLICABLE_TYPE_GROUP);
+ }
+
+ protected function modifyMountsOnDelete(string $applicableId, int $applicableType): void {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select(['a.mount_id', $builder->func()->count('a.mount_id', 'count')])
+ ->from('external_applicable', 'a')
+ ->leftJoin('a', 'external_applicable', 'b', $builder->expr()->eq('a.mount_id', 'b.mount_id'))
+ ->where($builder->expr()->andX(
+ $builder->expr()->eq('b.type', $builder->createNamedParameter($applicableType, IQueryBuilder::PARAM_INT)),
+ $builder->expr()->eq('b.value', $builder->createNamedParameter($applicableId))
+ )
+ )
+ ->groupBy(['a.mount_id']);
+ $stmt = $query->executeQuery();
+ $result = $stmt->fetchAll();
+ $stmt->closeCursor();
+
+ foreach ($result as $row) {
+ if ((int)$row['count'] > 1) {
+ $this->removeApplicable($row['mount_id'], $applicableType, $applicableId);
+ } else {
+ $this->removeMount($row['mount_id']);
+ }
+ }
+ }
+
+ /**
+ * Get admin defined mounts
+ *
+ * @return array
+ */
+ public function getAdminMounts() {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->select(['mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'type'])
+ ->from('external_mounts')
+ ->where($builder->expr()->eq('type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
+ return $this->getMountsFromQuery($query);
+ }
+
+ protected function getForQuery(IQueryBuilder $builder, $type, $value) {
+ $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
+ ->from('external_mounts', 'm')
+ ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
+ ->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
+
+ if (is_null($value)) {
+ $query = $query->andWhere($builder->expr()->isNull('a.value'));
+ } else {
+ $query = $query->andWhere($builder->expr()->eq('a.value', $builder->createNamedParameter($value)));
+ }
+
+ return $query;
+ }
+
+ /**
+ * Get mounts by applicable
+ *
+ * @param int $type any of the self::APPLICABLE_TYPE_ constants
+ * @param string|null $value user_id, group_id or null for global mounts
+ * @return array
+ */
+ public function getMountsFor($type, $value) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $this->getForQuery($builder, $type, $value);
+
+ return $this->getMountsFromQuery($query);
+ }
+
+ /**
+ * Get admin defined mounts by applicable
+ *
+ * @param int $type any of the self::APPLICABLE_TYPE_ constants
+ * @param string|null $value user_id, group_id or null for global mounts
+ * @return array
+ */
+ public function getAdminMountsFor($type, $value) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $this->getForQuery($builder, $type, $value);
+ $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
+
+ return $this->getMountsFromQuery($query);
+ }
+
+ /**
+ * Get admin defined mounts for multiple applicable
+ *
+ * @param int $type any of the self::APPLICABLE_TYPE_ constants
+ * @param string[] $values user_ids or group_ids
+ * @return array
+ */
+ public function getAdminMountsForMultiple($type, array $values) {
+ $builder = $this->connection->getQueryBuilder();
+ $params = array_map(function ($value) use ($builder) {
+ return $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR);
+ }, $values);
+
+ $query = $builder->select(['m.mount_id', 'mount_point', 'storage_backend', 'auth_backend', 'priority', 'm.type'])
+ ->from('external_mounts', 'm')
+ ->innerJoin('m', 'external_applicable', 'a', $builder->expr()->eq('m.mount_id', 'a.mount_id'))
+ ->where($builder->expr()->eq('a.type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)))
+ ->andWhere($builder->expr()->in('a.value', $params));
+ $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_ADMIN, IQueryBuilder::PARAM_INT)));
+
+ return $this->getMountsFromQuery($query);
+ }
+
+ /**
+ * Get user defined mounts by applicable
+ *
+ * @param int $type any of the self::APPLICABLE_TYPE_ constants
+ * @param string|null $value user_id, group_id or null for global mounts
+ * @return array
+ */
+ public function getUserMountsFor($type, $value) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $this->getForQuery($builder, $type, $value);
+ $query->andWhere($builder->expr()->eq('m.type', $builder->expr()->literal(self::MOUNT_TYPE_PERSONAL, IQueryBuilder::PARAM_INT)));
+
+ return $this->getMountsFromQuery($query);
+ }
+
+ /**
+ * Add a mount to the database
+ *
+ * @param string $mountPoint
+ * @param string $storageBackend
+ * @param string $authBackend
+ * @param int $priority
+ * @param int $type self::MOUNT_TYPE_ADMIN or self::MOUNT_TYPE_PERSONAL
+ * @return int the id of the new mount
+ */
+ public function addMount($mountPoint, $storageBackend, $authBackend, $priority, $type) {
+ if (!$priority) {
+ $priority = 100;
+ }
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->insert('external_mounts')
+ ->values([
+ 'mount_point' => $builder->createNamedParameter($mountPoint, IQueryBuilder::PARAM_STR),
+ 'storage_backend' => $builder->createNamedParameter($storageBackend, IQueryBuilder::PARAM_STR),
+ 'auth_backend' => $builder->createNamedParameter($authBackend, IQueryBuilder::PARAM_STR),
+ 'priority' => $builder->createNamedParameter($priority, IQueryBuilder::PARAM_INT),
+ 'type' => $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)
+ ]);
+ $query->executeStatement();
+ return $query->getLastInsertId();
+ }
+
+ /**
+ * Remove a mount from the database
+ *
+ * @param int $mountId
+ */
+ public function removeMount($mountId) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->delete('external_mounts')
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->delete('external_applicable')
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->delete('external_config')
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->delete('external_options')
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+ $query->executeStatement();
+ }
+
+ /**
+ * @param int $mountId
+ * @param string $newMountPoint
+ */
+ public function setMountPoint($mountId, $newMountPoint) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->update('external_mounts')
+ ->set('mount_point', $builder->createNamedParameter($newMountPoint))
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+
+ $query->executeStatement();
+ }
+
+ /**
+ * @param int $mountId
+ * @param string $newAuthBackend
+ */
+ public function setAuthBackend($mountId, $newAuthBackend) {
+ $builder = $this->connection->getQueryBuilder();
+
+ $query = $builder->update('external_mounts')
+ ->set('auth_backend', $builder->createNamedParameter($newAuthBackend))
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)));
+
+ $query->executeStatement();
+ }
+
+ /**
+ * @param int $mountId
+ * @param string $key
+ * @param string $value
+ */
+ public function setConfig($mountId, $key, $value) {
+ if ($key === 'password') {
+ $value = $this->encryptValue($value);
+ }
+
+ try {
+ $builder = $this->connection->getQueryBuilder();
+ $builder->insert('external_config')
+ ->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))
+ ->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))
+ ->setValue('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))
+ ->execute();
+ } catch (UniqueConstraintViolationException $e) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->update('external_config')
+ ->set('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR))
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)));
+ $query->executeStatement();
+ }
+ }
+
+ /**
+ * @param int $mountId
+ * @param string $key
+ * @param string $value
+ */
+ public function setOption($mountId, $key, $value) {
+ try {
+ $builder = $this->connection->getQueryBuilder();
+ $builder->insert('external_options')
+ ->setValue('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT))
+ ->setValue('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR))
+ ->setValue('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR))
+ ->execute();
+ } catch (UniqueConstraintViolationException $e) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->update('external_options')
+ ->set('value', $builder->createNamedParameter(json_encode($value), IQueryBuilder::PARAM_STR))
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($builder->expr()->eq('key', $builder->createNamedParameter($key, IQueryBuilder::PARAM_STR)));
+ $query->executeStatement();
+ }
+ }
+
+ public function addApplicable($mountId, $type, $value) {
+ try {
+ $builder = $this->connection->getQueryBuilder();
+ $builder->insert('external_applicable')
+ ->setValue('mount_id', $builder->createNamedParameter($mountId))
+ ->setValue('type', $builder->createNamedParameter($type))
+ ->setValue('value', $builder->createNamedParameter($value))
+ ->execute();
+ } catch (UniqueConstraintViolationException $e) {
+ // applicable exists already
+ }
+ }
+
+ public function removeApplicable($mountId, $type, $value) {
+ $builder = $this->connection->getQueryBuilder();
+ $query = $builder->delete('external_applicable')
+ ->where($builder->expr()->eq('mount_id', $builder->createNamedParameter($mountId, IQueryBuilder::PARAM_INT)))
+ ->andWhere($builder->expr()->eq('type', $builder->createNamedParameter($type, IQueryBuilder::PARAM_INT)));
+
+ if (is_null($value)) {
+ $query = $query->andWhere($builder->expr()->isNull('value'));
+ } else {
+ $query = $query->andWhere($builder->expr()->eq('value', $builder->createNamedParameter($value, IQueryBuilder::PARAM_STR)));
+ }
+
+ $query->executeStatement();
+ }
+
+ private function getMountsFromQuery(IQueryBuilder $query) {
+ $result = $query->executeQuery();
+ $mounts = $result->fetchAll();
+ $uniqueMounts = [];
+ foreach ($mounts as $mount) {
+ $id = $mount['mount_id'];
+ if (!isset($uniqueMounts[$id])) {
+ $uniqueMounts[$id] = $mount;
+ }
+ }
+ $uniqueMounts = array_values($uniqueMounts);
+
+ $mountIds = array_map(function ($mount) {
+ return $mount['mount_id'];
+ }, $uniqueMounts);
+ $mountIds = array_values(array_unique($mountIds));
+
+ $applicable = $this->getApplicableForMounts($mountIds);
+ $config = $this->getConfigForMounts($mountIds);
+ $options = $this->getOptionsForMounts($mountIds);
+
+ return array_map(function ($mount, $applicable, $config, $options) {
+ $mount['type'] = (int)$mount['type'];
+ $mount['priority'] = (int)$mount['priority'];
+ $mount['applicable'] = $applicable;
+ $mount['config'] = $config;
+ $mount['options'] = $options;
+ return $mount;
+ }, $uniqueMounts, $applicable, $config, $options);
+ }
+
+ /**
+ * Get mount options from a table grouped by mount id
+ *
+ * @param string $table
+ * @param string[] $fields
+ * @param int[] $mountIds
+ * @return array [$mountId => [['field1' => $value1, ...], ...], ...]
+ */
+ private function selectForMounts($table, array $fields, array $mountIds) {
+ if (count($mountIds) === 0) {
+ return [];
+ }
+ $builder = $this->connection->getQueryBuilder();
+ $fields[] = 'mount_id';
+ $placeHolders = array_map(function ($id) use ($builder) {
+ return $builder->createPositionalParameter($id, IQueryBuilder::PARAM_INT);
+ }, $mountIds);
+ $query = $builder->select($fields)
+ ->from($table)
+ ->where($builder->expr()->in('mount_id', $placeHolders));
+
+ $result = $query->executeQuery();
+ $rows = $result->fetchAll();
+ $result->closeCursor();
+
+ $result = [];
+ foreach ($mountIds as $mountId) {
+ $result[$mountId] = [];
+ }
+ foreach ($rows as $row) {
+ if (isset($row['type'])) {
+ $row['type'] = (int)$row['type'];
+ }
+ $result[$row['mount_id']][] = $row;
+ }
+ return $result;
+ }
+
+ /**
+ * @param int[] $mountIds
+ * @return array [$id => [['type' => $type, 'value' => $value], ...], ...]
+ */
+ public function getApplicableForMounts($mountIds) {
+ return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
+ }
+
+ /**
+ * @param int[] $mountIds
+ * @return array [$id => ['key1' => $value1, ...], ...]
+ */
+ public function getConfigForMounts($mountIds) {
+ $mountConfigs = $this->selectForMounts('external_config', ['key', 'value'], $mountIds);
+ return array_map([$this, 'createKeyValueMap'], $mountConfigs);
+ }
+
+ /**
+ * @param int[] $mountIds
+ * @return array [$id => ['key1' => $value1, ...], ...]
+ */
+ public function getOptionsForMounts($mountIds) {
+ $mountOptions = $this->selectForMounts('external_options', ['key', 'value'], $mountIds);
+ $optionsMap = array_map([$this, 'createKeyValueMap'], $mountOptions);
+ return array_map(function (array $options) {
+ return array_map(function ($option) {
+ return json_decode($option);
+ }, $options);
+ }, $optionsMap);
+ }
+
+ /**
+ * @param array $keyValuePairs [['key'=>$key, 'value=>$value], ...]
+ * @return array ['key1' => $value1, ...]
+ */
+ private function createKeyValueMap(array $keyValuePairs) {
+ $decryptedPairts = array_map(function ($pair) {
+ if ($pair['key'] === 'password') {
+ $pair['value'] = $this->decryptValue($pair['value']);
+ }
+ return $pair;
+ }, $keyValuePairs);
+ $keys = array_map(function ($pair) {
+ return $pair['key'];
+ }, $decryptedPairts);
+ $values = array_map(function ($pair) {
+ return $pair['value'];
+ }, $decryptedPairts);
+
+ return array_combine($keys, $values);
+ }
+
+ private function encryptValue($value) {
+ return $this->crypto->encrypt($value);
+ }
+
+ private function decryptValue($value) {
+ try {
+ return $this->crypto->decrypt($value);
+ } catch (\Exception $e) {
+ return $value;
+ }
+ }
+}
diff --git a/apps/files_external/lib/Service/GlobalStoragesService.php b/apps/files_external/lib/Service/GlobalStoragesService.php
new file mode 100644
index 00000000000..5b1a9f41e48
--- /dev/null
+++ b/apps/files_external/lib/Service/GlobalStoragesService.php
@@ -0,0 +1,165 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OC\Files\Filesystem;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\MountConfig;
+
+/**
+ * Service class to manage global external storage
+ */
+class GlobalStoragesService extends StoragesService {
+ /**
+ * Triggers $signal for all applicable users of the given
+ * storage
+ *
+ * @param StorageConfig $storage storage data
+ * @param string $signal signal to trigger
+ */
+ protected function triggerHooks(StorageConfig $storage, $signal) {
+ // FIXME: Use as expression in empty once PHP 5.4 support is dropped
+ $applicableUsers = $storage->getApplicableUsers();
+ $applicableGroups = $storage->getApplicableGroups();
+ if (empty($applicableUsers) && empty($applicableGroups)) {
+ // raise for user "all"
+ $this->triggerApplicableHooks(
+ $signal,
+ $storage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ ['all']
+ );
+ return;
+ }
+
+ $this->triggerApplicableHooks(
+ $signal,
+ $storage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ $applicableUsers
+ );
+ $this->triggerApplicableHooks(
+ $signal,
+ $storage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_GROUP,
+ $applicableGroups
+ );
+ }
+
+ /**
+ * Triggers signal_create_mount or signal_delete_mount to
+ * accommodate for additions/deletions in applicableUsers
+ * and applicableGroups fields.
+ *
+ * @param StorageConfig $oldStorage old storage config
+ * @param StorageConfig $newStorage new storage config
+ */
+ protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) {
+ // if mount point changed, it's like a deletion + creation
+ if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) {
+ $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount);
+ $this->triggerHooks($newStorage, Filesystem::signal_create_mount);
+ return;
+ }
+
+ $userAdditions = array_diff($newStorage->getApplicableUsers(), $oldStorage->getApplicableUsers());
+ $userDeletions = array_diff($oldStorage->getApplicableUsers(), $newStorage->getApplicableUsers());
+ $groupAdditions = array_diff($newStorage->getApplicableGroups(), $oldStorage->getApplicableGroups());
+ $groupDeletions = array_diff($oldStorage->getApplicableGroups(), $newStorage->getApplicableGroups());
+
+ // FIXME: Use as expression in empty once PHP 5.4 support is dropped
+ // if no applicable were set, raise a signal for "all"
+ $oldApplicableUsers = $oldStorage->getApplicableUsers();
+ $oldApplicableGroups = $oldStorage->getApplicableGroups();
+ if (empty($oldApplicableUsers) && empty($oldApplicableGroups)) {
+ $this->triggerApplicableHooks(
+ Filesystem::signal_delete_mount,
+ $oldStorage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ ['all']
+ );
+ }
+
+ // trigger delete for removed users
+ $this->triggerApplicableHooks(
+ Filesystem::signal_delete_mount,
+ $oldStorage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ $userDeletions
+ );
+
+ // trigger delete for removed groups
+ $this->triggerApplicableHooks(
+ Filesystem::signal_delete_mount,
+ $oldStorage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_GROUP,
+ $groupDeletions
+ );
+
+ // and now add the new users
+ $this->triggerApplicableHooks(
+ Filesystem::signal_create_mount,
+ $newStorage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ $userAdditions
+ );
+
+ // and now add the new groups
+ $this->triggerApplicableHooks(
+ Filesystem::signal_create_mount,
+ $newStorage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_GROUP,
+ $groupAdditions
+ );
+
+ // FIXME: Use as expression in empty once PHP 5.4 support is dropped
+ // if no applicable, raise a signal for "all"
+ $newApplicableUsers = $newStorage->getApplicableUsers();
+ $newApplicableGroups = $newStorage->getApplicableGroups();
+ if (empty($newApplicableUsers) && empty($newApplicableGroups)) {
+ $this->triggerApplicableHooks(
+ Filesystem::signal_create_mount,
+ $newStorage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ ['all']
+ );
+ }
+ }
+
+ /**
+ * Get the visibility type for this controller, used in validation
+ *
+ * @return int BackendService::VISIBILITY_* constants
+ */
+ public function getVisibilityType() {
+ return BackendService::VISIBILITY_ADMIN;
+ }
+
+ protected function isApplicable(StorageConfig $config) {
+ return true;
+ }
+
+ /**
+ * Get all configured admin and personal mounts
+ *
+ * @return StorageConfig[] map of storage id to storage config
+ */
+ public function getStorageForAllUsers() {
+ $mounts = $this->dbConfig->getAllMounts();
+ $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
+ $configs = array_filter($configs, function ($config) {
+ return $config instanceof StorageConfig;
+ });
+
+ $keys = array_map(function (StorageConfig $config) {
+ return $config->getId();
+ }, $configs);
+
+ return array_combine($keys, $configs);
+ }
+}
diff --git a/apps/files_external/lib/Service/ImportLegacyStoragesService.php b/apps/files_external/lib/Service/ImportLegacyStoragesService.php
new file mode 100644
index 00000000000..7d9840e9f5e
--- /dev/null
+++ b/apps/files_external/lib/Service/ImportLegacyStoragesService.php
@@ -0,0 +1,31 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+class ImportLegacyStoragesService extends LegacyStoragesService {
+ private $data;
+
+ /**
+ * @param BackendService $backendService
+ */
+ public function __construct(BackendService $backendService) {
+ $this->backendService = $backendService;
+ }
+
+ public function setData($data) {
+ $this->data = $data;
+ }
+
+ /**
+ * Read legacy config data
+ *
+ * @return array list of mount configs
+ */
+ protected function readLegacyConfig() {
+ return $this->data;
+ }
+}
diff --git a/apps/files_external/lib/Service/LegacyStoragesService.php b/apps/files_external/lib/Service/LegacyStoragesService.php
new file mode 100644
index 00000000000..9f199a89b3f
--- /dev/null
+++ b/apps/files_external/lib/Service/LegacyStoragesService.php
@@ -0,0 +1,193 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\MountConfig;
+use OCP\Server;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Read mount config from legacy mount.json
+ */
+abstract class LegacyStoragesService {
+ /** @var BackendService */
+ protected $backendService;
+
+ /**
+ * Read legacy config data
+ *
+ * @return array list of mount configs
+ */
+ abstract protected function readLegacyConfig();
+
+ /**
+ * Copy legacy storage options into the given storage config object.
+ *
+ * @param StorageConfig $storageConfig storage config to populate
+ * @param string $mountType mount type
+ * @param string $applicable applicable user or group
+ * @param array $storageOptions legacy storage options
+ *
+ * @return StorageConfig populated storage config
+ */
+ protected function populateStorageConfigWithLegacyOptions(
+ &$storageConfig,
+ $mountType,
+ $applicable,
+ $storageOptions,
+ ) {
+ $backend = $this->backendService->getBackend($storageOptions['backend']);
+ if (!$backend) {
+ throw new \UnexpectedValueException('Invalid backend ' . $storageOptions['backend']);
+ }
+ $storageConfig->setBackend($backend);
+ if (isset($storageOptions['authMechanism']) && $storageOptions['authMechanism'] !== 'builtin::builtin') {
+ $authMechanism = $this->backendService->getAuthMechanism($storageOptions['authMechanism']);
+ } else {
+ $authMechanism = $backend->getLegacyAuthMechanism($storageOptions);
+ $storageOptions['authMechanism'] = 'null'; // to make error handling easier
+ }
+ if (!$authMechanism) {
+ throw new \UnexpectedValueException('Invalid authentication mechanism ' . $storageOptions['authMechanism']);
+ }
+ $storageConfig->setAuthMechanism($authMechanism);
+ $storageConfig->setBackendOptions($storageOptions['options']);
+ if (isset($storageOptions['mountOptions'])) {
+ $storageConfig->setMountOptions($storageOptions['mountOptions']);
+ }
+ if (!isset($storageOptions['priority'])) {
+ $storageOptions['priority'] = $backend->getPriority();
+ }
+ $storageConfig->setPriority($storageOptions['priority']);
+ if ($mountType === MountConfig::MOUNT_TYPE_USER) {
+ $applicableUsers = $storageConfig->getApplicableUsers();
+ if ($applicable !== 'all') {
+ $applicableUsers[] = $applicable;
+ $storageConfig->setApplicableUsers($applicableUsers);
+ }
+ } elseif ($mountType === MountConfig::MOUNT_TYPE_GROUP) {
+ $applicableGroups = $storageConfig->getApplicableGroups();
+ $applicableGroups[] = $applicable;
+ $storageConfig->setApplicableGroups($applicableGroups);
+ }
+ return $storageConfig;
+ }
+
+ /**
+ * Read the external storage config
+ *
+ * @return StorageConfig[] map of storage id to storage config
+ */
+ public function getAllStorages() {
+ $mountPoints = $this->readLegacyConfig();
+ /**
+ * Here is the how the horribly messy mount point array looks like
+ * from the mount.json file:
+ *
+ * $storageOptions = $mountPoints[$mountType][$applicable][$mountPath]
+ *
+ * - $mountType is either "user" or "group"
+ * - $applicable is the name of a user or group (or the current user for personal mounts)
+ * - $mountPath is the mount point path (where the storage must be mounted)
+ * - $storageOptions is a map of storage options:
+ * - "priority": storage priority
+ * - "backend": backend identifier
+ * - "class": LEGACY backend class name
+ * - "options": backend-specific options
+ * - "authMechanism": authentication mechanism identifier
+ * - "mountOptions": mount-specific options (ex: disable previews, scanner, etc)
+ */
+ // group by storage id
+ /** @var StorageConfig[] $storages */
+ $storages = [];
+ // for storages without id (legacy), group by config hash for
+ // later processing
+ $storagesWithConfigHash = [];
+ foreach ($mountPoints as $mountType => $applicables) {
+ foreach ($applicables as $applicable => $mountPaths) {
+ foreach ($mountPaths as $rootMountPath => $storageOptions) {
+ $currentStorage = null;
+ /**
+ * Flag whether the config that was read already has an id.
+ * If not, it will use a config hash instead and generate
+ * a proper id later
+ *
+ * @var boolean
+ */
+ $hasId = false;
+ // the root mount point is in the format "/$user/files/the/mount/point"
+ // we remove the "/$user/files" prefix
+ $parts = explode('/', ltrim($rootMountPath, '/'), 3);
+ if (count($parts) < 3) {
+ // something went wrong, skip
+ Server::get(LoggerInterface::class)->error('Could not parse mount point "' . $rootMountPath . '"', ['app' => 'files_external']);
+ continue;
+ }
+ $relativeMountPath = rtrim($parts[2], '/');
+ // note: we cannot do this after the loop because the decrypted config
+ // options might be needed for the config hash
+ $storageOptions['options'] = MountConfig::decryptPasswords($storageOptions['options']);
+ if (!isset($storageOptions['backend'])) {
+ $storageOptions['backend'] = $storageOptions['class']; // legacy compat
+ }
+ if (!isset($storageOptions['authMechanism'])) {
+ $storageOptions['authMechanism'] = null; // ensure config hash works
+ }
+ if (isset($storageOptions['id'])) {
+ $configId = (int)$storageOptions['id'];
+ if (isset($storages[$configId])) {
+ $currentStorage = $storages[$configId];
+ }
+ $hasId = true;
+ } else {
+ // missing id in legacy config, need to generate
+ // but at this point we don't know the max-id, so use
+ // first group it by config hash
+ $storageOptions['mountpoint'] = $rootMountPath;
+ $configId = MountConfig::makeConfigHash($storageOptions);
+ if (isset($storagesWithConfigHash[$configId])) {
+ $currentStorage = $storagesWithConfigHash[$configId];
+ }
+ }
+ if (is_null($currentStorage)) {
+ // create new
+ $currentStorage = new StorageConfig($configId);
+ $currentStorage->setMountPoint($relativeMountPath);
+ }
+ try {
+ $this->populateStorageConfigWithLegacyOptions(
+ $currentStorage,
+ $mountType,
+ $applicable,
+ $storageOptions
+ );
+ if ($hasId) {
+ $storages[$configId] = $currentStorage;
+ } else {
+ $storagesWithConfigHash[$configId] = $currentStorage;
+ }
+ } catch (\UnexpectedValueException $e) {
+ // don't die if a storage backend doesn't exist
+ Server::get(LoggerInterface::class)->error('Could not load storage.', [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ }
+ }
+ }
+ }
+
+ // convert parameter values
+ foreach ($storages as $storage) {
+ $storage->getBackend()->validateStorageDefinition($storage);
+ $storage->getAuthMechanism()->validateStorageDefinition($storage);
+ }
+ return $storages;
+ }
+}
diff --git a/apps/files_external/lib/Service/StoragesService.php b/apps/files_external/lib/Service/StoragesService.php
new file mode 100644
index 00000000000..a12a8fc245a
--- /dev/null
+++ b/apps/files_external/lib/Service/StoragesService.php
@@ -0,0 +1,476 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OC\Files\Cache\Storage;
+use OC\Files\Filesystem;
+use OCA\Files_External\Lib\Auth\AuthMechanism;
+use OCA\Files_External\Lib\Auth\InvalidAuth;
+use OCA\Files_External\Lib\Backend\Backend;
+use OCA\Files_External\Lib\Backend\InvalidBackend;
+use OCA\Files_External\Lib\DefinitionParameter;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\NotFoundException;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Config\IUserMountCache;
+use OCP\Files\Events\InvalidateMountCacheEvent;
+use OCP\Files\StorageNotAvailableException;
+use OCP\Server;
+use OCP\Util;
+use Psr\Log\LoggerInterface;
+
+/**
+ * Service class to manage external storage
+ */
+abstract class StoragesService {
+
+ /**
+ * @param BackendService $backendService
+ * @param DBConfigService $dbConfig
+ * @param IUserMountCache $userMountCache
+ * @param IEventDispatcher $eventDispatcher
+ */
+ public function __construct(
+ protected BackendService $backendService,
+ protected DBConfigService $dbConfig,
+ protected IUserMountCache $userMountCache,
+ protected IEventDispatcher $eventDispatcher,
+ ) {
+ }
+
+ protected function readDBConfig() {
+ return $this->dbConfig->getAdminMounts();
+ }
+
+ protected function getStorageConfigFromDBMount(array $mount) {
+ $applicableUsers = array_filter($mount['applicable'], function ($applicable) {
+ return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_USER;
+ });
+ $applicableUsers = array_map(function ($applicable) {
+ return $applicable['value'];
+ }, $applicableUsers);
+
+ $applicableGroups = array_filter($mount['applicable'], function ($applicable) {
+ return $applicable['type'] === DBConfigService::APPLICABLE_TYPE_GROUP;
+ });
+ $applicableGroups = array_map(function ($applicable) {
+ return $applicable['value'];
+ }, $applicableGroups);
+
+ try {
+ $config = $this->createStorage(
+ $mount['mount_point'],
+ $mount['storage_backend'],
+ $mount['auth_backend'],
+ $mount['config'],
+ $mount['options'],
+ array_values($applicableUsers),
+ array_values($applicableGroups),
+ $mount['priority']
+ );
+ $config->setType($mount['type']);
+ $config->setId((int)$mount['mount_id']);
+ return $config;
+ } catch (\UnexpectedValueException $e) {
+ // don't die if a storage backend doesn't exist
+ Server::get(LoggerInterface::class)->error('Could not load storage.', [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return null;
+ } catch (\InvalidArgumentException $e) {
+ Server::get(LoggerInterface::class)->error('Could not load storage.', [
+ 'app' => 'files_external',
+ 'exception' => $e,
+ ]);
+ return null;
+ }
+ }
+
+ /**
+ * Read the external storage config
+ *
+ * @return array map of storage id to storage config
+ */
+ protected function readConfig() {
+ $mounts = $this->readDBConfig();
+ $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
+ $configs = array_filter($configs, function ($config) {
+ return $config instanceof StorageConfig;
+ });
+
+ $keys = array_map(function (StorageConfig $config) {
+ return $config->getId();
+ }, $configs);
+
+ return array_combine($keys, $configs);
+ }
+
+ /**
+ * Get a storage with status
+ *
+ * @param int $id storage id
+ *
+ * @return StorageConfig
+ * @throws NotFoundException if the storage with the given id was not found
+ */
+ public function getStorage(int $id) {
+ $mount = $this->dbConfig->getMountById($id);
+
+ if (!is_array($mount)) {
+ throw new NotFoundException('Storage with ID "' . $id . '" not found');
+ }
+
+ $config = $this->getStorageConfigFromDBMount($mount);
+ if ($this->isApplicable($config)) {
+ return $config;
+ } else {
+ throw new NotFoundException('Storage with ID "' . $id . '" not found');
+ }
+ }
+
+ /**
+ * Check whether this storage service should provide access to a storage
+ *
+ * @param StorageConfig $config
+ * @return bool
+ */
+ abstract protected function isApplicable(StorageConfig $config);
+
+ /**
+ * Gets all storages, valid or not
+ *
+ * @return StorageConfig[] array of storage configs
+ */
+ public function getAllStorages() {
+ return $this->readConfig();
+ }
+
+ /**
+ * Gets all valid storages
+ *
+ * @return StorageConfig[]
+ */
+ public function getStorages() {
+ return array_filter($this->getAllStorages(), [$this, 'validateStorage']);
+ }
+
+ /**
+ * Validate storage
+ * FIXME: De-duplicate with StoragesController::validate()
+ *
+ * @param StorageConfig $storage
+ * @return bool
+ */
+ protected function validateStorage(StorageConfig $storage) {
+ /** @var Backend */
+ $backend = $storage->getBackend();
+ /** @var AuthMechanism */
+ $authMechanism = $storage->getAuthMechanism();
+
+ if (!$backend->isVisibleFor($this->getVisibilityType())) {
+ // not permitted to use backend
+ return false;
+ }
+ if (!$authMechanism->isVisibleFor($this->getVisibilityType())) {
+ // not permitted to use auth mechanism
+ return false;
+ }
+
+ return true;
+ }
+
+ /**
+ * Get the visibility type for this controller, used in validation
+ *
+ * @return int BackendService::VISIBILITY_* constants
+ */
+ abstract public function getVisibilityType();
+
+ /**
+ * @return integer
+ */
+ protected function getType() {
+ return DBConfigService::MOUNT_TYPE_ADMIN;
+ }
+
+ /**
+ * Add new storage to the configuration
+ *
+ * @param StorageConfig $newStorage storage attributes
+ *
+ * @return StorageConfig storage config, with added id
+ */
+ public function addStorage(StorageConfig $newStorage) {
+ $allStorages = $this->readConfig();
+
+ $configId = $this->dbConfig->addMount(
+ $newStorage->getMountPoint(),
+ $newStorage->getBackend()->getIdentifier(),
+ $newStorage->getAuthMechanism()->getIdentifier(),
+ $newStorage->getPriority(),
+ $this->getType()
+ );
+
+ $newStorage->setId($configId);
+
+ foreach ($newStorage->getApplicableUsers() as $user) {
+ $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_USER, $user);
+ }
+ foreach ($newStorage->getApplicableGroups() as $group) {
+ $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
+ }
+ foreach ($newStorage->getBackendOptions() as $key => $value) {
+ $this->dbConfig->setConfig($configId, $key, $value);
+ }
+ foreach ($newStorage->getMountOptions() as $key => $value) {
+ $this->dbConfig->setOption($configId, $key, $value);
+ }
+
+ if (count($newStorage->getApplicableUsers()) === 0 && count($newStorage->getApplicableGroups()) === 0) {
+ $this->dbConfig->addApplicable($configId, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
+ }
+
+ // add new storage
+ $allStorages[$configId] = $newStorage;
+
+ $this->triggerHooks($newStorage, Filesystem::signal_create_mount);
+
+ $newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS);
+ return $newStorage;
+ }
+
+ /**
+ * Create a storage from its parameters
+ *
+ * @param string $mountPoint storage mount point
+ * @param string $backendIdentifier backend identifier
+ * @param string $authMechanismIdentifier authentication mechanism identifier
+ * @param array $backendOptions backend-specific options
+ * @param array|null $mountOptions mount-specific options
+ * @param array|null $applicableUsers users for which to mount the storage
+ * @param array|null $applicableGroups groups for which to mount the storage
+ * @param int|null $priority priority
+ *
+ * @return StorageConfig
+ */
+ public function createStorage(
+ $mountPoint,
+ $backendIdentifier,
+ $authMechanismIdentifier,
+ $backendOptions,
+ $mountOptions = null,
+ $applicableUsers = null,
+ $applicableGroups = null,
+ $priority = null,
+ ) {
+ $backend = $this->backendService->getBackend($backendIdentifier);
+ if (!$backend) {
+ $backend = new InvalidBackend($backendIdentifier);
+ }
+ $authMechanism = $this->backendService->getAuthMechanism($authMechanismIdentifier);
+ if (!$authMechanism) {
+ $authMechanism = new InvalidAuth($authMechanismIdentifier);
+ }
+ $newStorage = new StorageConfig();
+ $newStorage->setMountPoint($mountPoint);
+ $newStorage->setBackend($backend);
+ $newStorage->setAuthMechanism($authMechanism);
+ $newStorage->setBackendOptions($backendOptions);
+ if (isset($mountOptions)) {
+ $newStorage->setMountOptions($mountOptions);
+ }
+ if (isset($applicableUsers)) {
+ $newStorage->setApplicableUsers($applicableUsers);
+ }
+ if (isset($applicableGroups)) {
+ $newStorage->setApplicableGroups($applicableGroups);
+ }
+ if (isset($priority)) {
+ $newStorage->setPriority($priority);
+ }
+
+ return $newStorage;
+ }
+
+ /**
+ * Triggers the given hook signal for all the applicables given
+ *
+ * @param string $signal signal
+ * @param string $mountPoint hook mount point param
+ * @param string $mountType hook mount type param
+ * @param array $applicableArray array of applicable users/groups for which to trigger the hook
+ */
+ protected function triggerApplicableHooks($signal, $mountPoint, $mountType, $applicableArray): void {
+ $this->eventDispatcher->dispatchTyped(new InvalidateMountCacheEvent(null));
+ foreach ($applicableArray as $applicable) {
+ Util::emitHook(
+ Filesystem::CLASSNAME,
+ $signal,
+ [
+ Filesystem::signal_param_path => $mountPoint,
+ Filesystem::signal_param_mount_type => $mountType,
+ Filesystem::signal_param_users => $applicable,
+ ]
+ );
+ }
+ }
+
+ /**
+ * Triggers $signal for all applicable users of the given
+ * storage
+ *
+ * @param StorageConfig $storage storage data
+ * @param string $signal signal to trigger
+ */
+ abstract protected function triggerHooks(StorageConfig $storage, $signal);
+
+ /**
+ * Triggers signal_create_mount or signal_delete_mount to
+ * accommodate for additions/deletions in applicableUsers
+ * and applicableGroups fields.
+ *
+ * @param StorageConfig $oldStorage old storage data
+ * @param StorageConfig $newStorage new storage data
+ */
+ abstract protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage);
+
+ /**
+ * Update storage to the configuration
+ *
+ * @param StorageConfig $updatedStorage storage attributes
+ *
+ * @return StorageConfig storage config
+ * @throws NotFoundException if the given storage does not exist in the config
+ */
+ public function updateStorage(StorageConfig $updatedStorage) {
+ $id = $updatedStorage->getId();
+
+ $existingMount = $this->dbConfig->getMountById($id);
+
+ if (!is_array($existingMount)) {
+ throw new NotFoundException('Storage with ID "' . $id . '" not found while updating storage');
+ }
+
+ $oldStorage = $this->getStorageConfigFromDBMount($existingMount);
+
+ if ($oldStorage->getBackend() instanceof InvalidBackend) {
+ throw new NotFoundException('Storage with id "' . $id . '" cannot be edited due to missing backend');
+ }
+
+ $removedUsers = array_diff($oldStorage->getApplicableUsers(), $updatedStorage->getApplicableUsers());
+ $removedGroups = array_diff($oldStorage->getApplicableGroups(), $updatedStorage->getApplicableGroups());
+ $addedUsers = array_diff($updatedStorage->getApplicableUsers(), $oldStorage->getApplicableUsers());
+ $addedGroups = array_diff($updatedStorage->getApplicableGroups(), $oldStorage->getApplicableGroups());
+
+ $oldUserCount = count($oldStorage->getApplicableUsers());
+ $oldGroupCount = count($oldStorage->getApplicableGroups());
+ $newUserCount = count($updatedStorage->getApplicableUsers());
+ $newGroupCount = count($updatedStorage->getApplicableGroups());
+ $wasGlobal = ($oldUserCount + $oldGroupCount) === 0;
+ $isGlobal = ($newUserCount + $newGroupCount) === 0;
+
+ foreach ($removedUsers as $user) {
+ $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
+ }
+ foreach ($removedGroups as $group) {
+ $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
+ }
+ foreach ($addedUsers as $user) {
+ $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_USER, $user);
+ }
+ foreach ($addedGroups as $group) {
+ $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GROUP, $group);
+ }
+
+ if ($wasGlobal && !$isGlobal) {
+ $this->dbConfig->removeApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
+ } elseif (!$wasGlobal && $isGlobal) {
+ $this->dbConfig->addApplicable($id, DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
+ }
+
+ $changedConfig = array_diff_assoc($updatedStorage->getBackendOptions(), $oldStorage->getBackendOptions());
+ $changedOptions = array_diff_assoc($updatedStorage->getMountOptions(), $oldStorage->getMountOptions());
+
+ foreach ($changedConfig as $key => $value) {
+ if ($value !== DefinitionParameter::UNMODIFIED_PLACEHOLDER) {
+ $this->dbConfig->setConfig($id, $key, $value);
+ }
+ }
+ foreach ($changedOptions as $key => $value) {
+ $this->dbConfig->setOption($id, $key, $value);
+ }
+
+ if ($updatedStorage->getMountPoint() !== $oldStorage->getMountPoint()) {
+ $this->dbConfig->setMountPoint($id, $updatedStorage->getMountPoint());
+ }
+
+ if ($updatedStorage->getAuthMechanism()->getIdentifier() !== $oldStorage->getAuthMechanism()->getIdentifier()) {
+ $this->dbConfig->setAuthBackend($id, $updatedStorage->getAuthMechanism()->getIdentifier());
+ }
+
+ $this->triggerChangeHooks($oldStorage, $updatedStorage);
+
+ if (($wasGlobal && !$isGlobal) || count($removedGroups) > 0) { // to expensive to properly handle these on the fly
+ $this->userMountCache->remoteStorageMounts($this->getStorageId($updatedStorage));
+ } else {
+ $storageId = $this->getStorageId($updatedStorage);
+ foreach ($removedUsers as $userId) {
+ $this->userMountCache->removeUserStorageMount($storageId, $userId);
+ }
+ }
+
+ return $this->getStorage($id);
+ }
+
+ /**
+ * Delete the storage with the given id.
+ *
+ * @param int $id storage id
+ *
+ * @throws NotFoundException if no storage was found with the given id
+ */
+ public function removeStorage(int $id) {
+ $existingMount = $this->dbConfig->getMountById($id);
+
+ if (!is_array($existingMount)) {
+ throw new NotFoundException('Storage with ID "' . $id . '" not found');
+ }
+
+ $this->dbConfig->removeMount($id);
+
+ $deletedStorage = $this->getStorageConfigFromDBMount($existingMount);
+ $this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount);
+
+ // delete oc_storages entries and oc_filecache
+ Storage::cleanByMountId($id);
+ }
+
+ /**
+ * Construct the storage implementation
+ *
+ * @param StorageConfig $storageConfig
+ * @return int
+ */
+ private function getStorageId(StorageConfig $storageConfig) {
+ try {
+ $class = $storageConfig->getBackend()->getStorageClass();
+ /** @var \OC\Files\Storage\Storage $storage */
+ $storage = new $class($storageConfig->getBackendOptions());
+
+ // auth mechanism should fire first
+ $storage = $storageConfig->getBackend()->wrapStorage($storage);
+ $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
+
+ /** @var \OC\Files\Storage\Storage $storage */
+ return $storage->getStorageCache()->getNumericId();
+ } catch (\Exception $e) {
+ return -1;
+ }
+ }
+}
diff --git a/apps/files_external/lib/Service/UserGlobalStoragesService.php b/apps/files_external/lib/Service/UserGlobalStoragesService.php
new file mode 100644
index 00000000000..aaa59c85d62
--- /dev/null
+++ b/apps/files_external/lib/Service/UserGlobalStoragesService.php
@@ -0,0 +1,187 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OCA\Files_External\Lib\StorageConfig;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Config\IUserMountCache;
+use OCP\IGroupManager;
+use OCP\IUser;
+use OCP\IUserSession;
+
+/**
+ * Service class to read global storages applicable to the user
+ * Read-only access available, attempting to write will throw DomainException
+ */
+class UserGlobalStoragesService extends GlobalStoragesService {
+ use UserTrait;
+
+ /**
+ * @param BackendService $backendService
+ * @param DBConfigService $dbConfig
+ * @param IUserSession $userSession
+ * @param IGroupManager $groupManager
+ * @param IUserMountCache $userMountCache
+ * @param IEventDispatcher $eventDispatcher
+ */
+ public function __construct(
+ BackendService $backendService,
+ DBConfigService $dbConfig,
+ IUserSession $userSession,
+ protected IGroupManager $groupManager,
+ IUserMountCache $userMountCache,
+ IEventDispatcher $eventDispatcher,
+ ) {
+ parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher);
+ $this->userSession = $userSession;
+ }
+
+ /**
+ * Replace config hash ID with real IDs, for migrating legacy storages
+ *
+ * @param StorageConfig[] $storages Storages with real IDs
+ * @param StorageConfig[] $storagesWithConfigHash Storages with config hash IDs
+ */
+ protected function setRealStorageIds(array &$storages, array $storagesWithConfigHash) {
+ // as a read-only view, storage IDs don't need to be real
+ foreach ($storagesWithConfigHash as $storage) {
+ $storages[$storage->getId()] = $storage;
+ }
+ }
+
+ protected function readDBConfig() {
+ $userMounts = $this->dbConfig->getAdminMountsFor(DBConfigService::APPLICABLE_TYPE_USER, $this->getUser()->getUID());
+ $globalMounts = $this->dbConfig->getAdminMountsFor(DBConfigService::APPLICABLE_TYPE_GLOBAL, null);
+ $groups = $this->groupManager->getUserGroupIds($this->getUser());
+ if (count($groups) !== 0) {
+ $groupMounts = $this->dbConfig->getAdminMountsForMultiple(DBConfigService::APPLICABLE_TYPE_GROUP, $groups);
+ } else {
+ $groupMounts = [];
+ }
+ return array_merge($userMounts, $groupMounts, $globalMounts);
+ }
+
+ public function addStorage(StorageConfig $newStorage) {
+ throw new \DomainException('UserGlobalStoragesService writing disallowed');
+ }
+
+ public function updateStorage(StorageConfig $updatedStorage) {
+ throw new \DomainException('UserGlobalStoragesService writing disallowed');
+ }
+
+ /**
+ * @param integer $id
+ */
+ public function removeStorage($id) {
+ throw new \DomainException('UserGlobalStoragesService writing disallowed');
+ }
+
+ /**
+ * Get unique storages, in case two are defined with the same mountpoint
+ * Higher priority storages take precedence
+ *
+ * @return StorageConfig[]
+ */
+ public function getUniqueStorages() {
+ $storages = $this->getStorages();
+
+ $storagesByMountpoint = [];
+ foreach ($storages as $storage) {
+ $storagesByMountpoint[$storage->getMountPoint()][] = $storage;
+ }
+
+ $result = [];
+ foreach ($storagesByMountpoint as $storageList) {
+ $storage = array_reduce($storageList, function ($carry, $item) {
+ if (isset($carry)) {
+ $carryPriorityType = $this->getPriorityType($carry);
+ $itemPriorityType = $this->getPriorityType($item);
+ if ($carryPriorityType > $itemPriorityType) {
+ return $carry;
+ } elseif ($carryPriorityType === $itemPriorityType) {
+ if ($carry->getPriority() > $item->getPriority()) {
+ return $carry;
+ }
+ }
+ }
+ return $item;
+ });
+ $result[$storage->getID()] = $storage;
+ }
+
+ return $result;
+ }
+
+ /**
+ * Get a priority 'type', where a bigger number means higher priority
+ * user applicable > group applicable > 'all'
+ *
+ * @param StorageConfig $storage
+ * @return int
+ */
+ protected function getPriorityType(StorageConfig $storage) {
+ $applicableUsers = $storage->getApplicableUsers();
+ $applicableGroups = $storage->getApplicableGroups();
+
+ if ($applicableUsers && $applicableUsers[0] !== 'all') {
+ return 2;
+ }
+ if ($applicableGroups) {
+ return 1;
+ }
+ return 0;
+ }
+
+ protected function isApplicable(StorageConfig $config) {
+ $applicableUsers = $config->getApplicableUsers();
+ $applicableGroups = $config->getApplicableGroups();
+
+ if (count($applicableUsers) === 0 && count($applicableGroups) === 0) {
+ return true;
+ }
+ if (in_array($this->getUser()->getUID(), $applicableUsers, true)) {
+ return true;
+ }
+ $groupIds = $this->groupManager->getUserGroupIds($this->getUser());
+ foreach ($groupIds as $groupId) {
+ if (in_array($groupId, $applicableGroups, true)) {
+ return true;
+ }
+ }
+ return false;
+ }
+
+
+ /**
+ * Gets all storages for the user, admin, personal, global, etc
+ *
+ * @param IUser|null $user user to get the storages for, if not set the currently logged in user will be used
+ * @return StorageConfig[] array of storage configs
+ */
+ public function getAllStoragesForUser(?IUser $user = null) {
+ if (is_null($user)) {
+ $user = $this->getUser();
+ }
+ if (is_null($user)) {
+ return [];
+ }
+ $groupIds = $this->groupManager->getUserGroupIds($user);
+ $mounts = $this->dbConfig->getMountsForUser($user->getUID(), $groupIds);
+ $configs = array_map([$this, 'getStorageConfigFromDBMount'], $mounts);
+ $configs = array_filter($configs, function ($config) {
+ return $config instanceof StorageConfig;
+ });
+
+ $keys = array_map(function (StorageConfig $config) {
+ return $config->getId();
+ }, $configs);
+
+ $storages = array_combine($keys, $configs);
+ return array_filter($storages, [$this, 'validateStorage']);
+ }
+}
diff --git a/apps/files_external/lib/Service/UserStoragesService.php b/apps/files_external/lib/Service/UserStoragesService.php
new file mode 100644
index 00000000000..9d4192734b6
--- /dev/null
+++ b/apps/files_external/lib/Service/UserStoragesService.php
@@ -0,0 +1,134 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OC\Files\Filesystem;
+use OCA\Files_External\Lib\StorageConfig;
+use OCA\Files_External\MountConfig;
+use OCA\Files_External\NotFoundException;
+use OCP\EventDispatcher\IEventDispatcher;
+use OCP\Files\Config\IUserMountCache;
+use OCP\IUserSession;
+
+/**
+ * Service class to manage user external storage
+ * (aka personal storages)
+ */
+class UserStoragesService extends StoragesService {
+ use UserTrait;
+
+ /**
+ * Create a user storages service
+ *
+ * @param BackendService $backendService
+ * @param DBConfigService $dbConfig
+ * @param IUserSession $userSession user session
+ * @param IUserMountCache $userMountCache
+ * @param IEventDispatcher $eventDispatcher
+ */
+ public function __construct(
+ BackendService $backendService,
+ DBConfigService $dbConfig,
+ IUserSession $userSession,
+ IUserMountCache $userMountCache,
+ IEventDispatcher $eventDispatcher,
+ ) {
+ $this->userSession = $userSession;
+ parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher);
+ }
+
+ protected function readDBConfig() {
+ return $this->dbConfig->getUserMountsFor(DBConfigService::APPLICABLE_TYPE_USER, $this->getUser()->getUID());
+ }
+
+ /**
+ * Triggers $signal for all applicable users of the given
+ * storage
+ *
+ * @param StorageConfig $storage storage data
+ * @param string $signal signal to trigger
+ */
+ protected function triggerHooks(StorageConfig $storage, $signal) {
+ $user = $this->getUser()->getUID();
+
+ // trigger hook for the current user
+ $this->triggerApplicableHooks(
+ $signal,
+ $storage->getMountPoint(),
+ MountConfig::MOUNT_TYPE_USER,
+ [$user]
+ );
+ }
+
+ /**
+ * Triggers signal_create_mount or signal_delete_mount to
+ * accommodate for additions/deletions in applicableUsers
+ * and applicableGroups fields.
+ *
+ * @param StorageConfig $oldStorage old storage data
+ * @param StorageConfig $newStorage new storage data
+ */
+ protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) {
+ // if mount point changed, it's like a deletion + creation
+ if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) {
+ $this->triggerHooks($oldStorage, Filesystem::signal_delete_mount);
+ $this->triggerHooks($newStorage, Filesystem::signal_create_mount);
+ }
+ }
+
+ protected function getType() {
+ return DBConfigService::MOUNT_TYPE_PERSONAL;
+ }
+
+ /**
+ * Add new storage to the configuration
+ *
+ * @param StorageConfig $newStorage storage attributes
+ *
+ * @return StorageConfig storage config, with added id
+ */
+ public function addStorage(StorageConfig $newStorage) {
+ $newStorage->setApplicableUsers([$this->getUser()->getUID()]);
+ return parent::addStorage($newStorage);
+ }
+
+ /**
+ * Update storage to the configuration
+ *
+ * @param StorageConfig $updatedStorage storage attributes
+ *
+ * @return StorageConfig storage config
+ * @throws NotFoundException if the given storage does not exist in the config
+ */
+ public function updateStorage(StorageConfig $updatedStorage) {
+ // verify ownership through $this->isApplicable() and otherwise throws an exception
+ $this->getStorage($updatedStorage->getId());
+
+ $updatedStorage->setApplicableUsers([$this->getUser()->getUID()]);
+ return parent::updateStorage($updatedStorage);
+ }
+
+ /**
+ * Get the visibility type for this controller, used in validation
+ *
+ * @return int BackendService::VISIBILITY_* constants
+ */
+ public function getVisibilityType() {
+ return BackendService::VISIBILITY_PERSONAL;
+ }
+
+ protected function isApplicable(StorageConfig $config) {
+ return ($config->getApplicableUsers() === [$this->getUser()->getUID()]) && $config->getType() === StorageConfig::MOUNT_TYPE_PERSONAL;
+ }
+
+ public function removeStorage($id) {
+ // verify ownership through $this->isApplicable() and otherwise throws an exception
+ $this->getStorage($id);
+ parent::removeStorage($id);
+ }
+}
diff --git a/apps/files_external/lib/Service/UserTrait.php b/apps/files_external/lib/Service/UserTrait.php
new file mode 100644
index 00000000000..679066283a5
--- /dev/null
+++ b/apps/files_external/lib/Service/UserTrait.php
@@ -0,0 +1,59 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2019-2024 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-FileCopyrightText: 2016 ownCloud, Inc.
+ * SPDX-License-Identifier: AGPL-3.0-only
+ */
+namespace OCA\Files_External\Service;
+
+use OCP\IUser;
+use OCP\IUserSession;
+
+/**
+ * Trait for getting user information in a service
+ */
+trait UserTrait {
+
+ /** @var IUserSession */
+ protected $userSession;
+
+ /**
+ * User override
+ *
+ * @var IUser|null
+ */
+ private $user = null;
+
+ /**
+ * @return IUser|null
+ */
+ protected function getUser() {
+ if ($this->user) {
+ return $this->user;
+ }
+ return $this->userSession->getUser();
+ }
+
+ /**
+ * Override the user from the session
+ * Unset with ->resetUser() when finished!
+ *
+ * @param IUser $user
+ * @return self
+ */
+ public function setUser(IUser $user) {
+ $this->user = $user;
+ return $this;
+ }
+
+ /**
+ * Reset the user override
+ *
+ * @return self
+ */
+ public function resetUser() {
+ $this->user = null;
+ return $this;
+ }
+}
diff --git a/apps/files_external/lib/Settings/Admin.php b/apps/files_external/lib/Settings/Admin.php
new file mode 100644
index 00000000000..9af0f3c61c1
--- /dev/null
+++ b/apps/files_external/lib/Settings/Admin.php
@@ -0,0 +1,63 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Settings;
+
+use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
+use OCA\Files_External\MountConfig;
+use OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\GlobalStoragesService;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Encryption\IManager;
+use OCP\Settings\ISettings;
+
+class Admin implements ISettings {
+
+ public function __construct(
+ private IManager $encryptionManager,
+ private GlobalStoragesService $globalStoragesService,
+ private BackendService $backendService,
+ private GlobalAuth $globalAuth,
+ ) {
+ }
+
+ /**
+ * @return TemplateResponse
+ */
+ public function getForm() {
+ $parameters = [
+ 'encryptionEnabled' => $this->encryptionManager->isEnabled(),
+ 'visibilityType' => BackendService::VISIBILITY_ADMIN,
+ 'storages' => $this->globalStoragesService->getStorages(),
+ 'backends' => $this->backendService->getAvailableBackends(),
+ 'authMechanisms' => $this->backendService->getAuthMechanisms(),
+ 'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()),
+ 'allowUserMounting' => $this->backendService->isUserMountingAllowed(),
+ 'globalCredentials' => $this->globalAuth->getAuth(''),
+ 'globalCredentialsUid' => '',
+ ];
+
+ return new TemplateResponse('files_external', 'settings', $parameters, '');
+ }
+
+ /**
+ * @return string the section ID, e.g. 'sharing'
+ */
+ public function getSection() {
+ return 'externalstorages';
+ }
+
+ /**
+ * @return int whether the form should be rather on the top or bottom of
+ * the admin section. The forms are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
+ *
+ * E.g.: 70
+ */
+ public function getPriority() {
+ return 40;
+ }
+}
diff --git a/apps/files_external/lib/Settings/Personal.php b/apps/files_external/lib/Settings/Personal.php
new file mode 100644
index 00000000000..8478badb842
--- /dev/null
+++ b/apps/files_external/lib/Settings/Personal.php
@@ -0,0 +1,67 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Settings;
+
+use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
+use OCA\Files_External\MountConfig;
+use OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCP\AppFramework\Http\TemplateResponse;
+use OCP\Encryption\IManager;
+use OCP\IUserSession;
+use OCP\Settings\ISettings;
+
+class Personal implements ISettings {
+
+ public function __construct(
+ private IManager $encryptionManager,
+ private UserGlobalStoragesService $userGlobalStoragesService,
+ private BackendService $backendService,
+ private GlobalAuth $globalAuth,
+ private IUserSession $userSession,
+ ) {
+ }
+
+ /**
+ * @return TemplateResponse
+ */
+ public function getForm() {
+ $uid = $this->userSession->getUser()->getUID();
+
+ $parameters = [
+ 'encryptionEnabled' => $this->encryptionManager->isEnabled(),
+ 'visibilityType' => BackendService::VISIBILITY_PERSONAL,
+ 'storages' => $this->userGlobalStoragesService->getStorages(),
+ 'backends' => $this->backendService->getAvailableBackends(),
+ 'authMechanisms' => $this->backendService->getAuthMechanisms(),
+ 'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()),
+ 'allowUserMounting' => $this->backendService->isUserMountingAllowed(),
+ 'globalCredentials' => $this->globalAuth->getAuth($uid),
+ 'globalCredentialsUid' => $uid,
+ ];
+
+ return new TemplateResponse('files_external', 'settings', $parameters, '');
+ }
+
+ /**
+ * @return string the section ID, e.g. 'sharing'
+ */
+ public function getSection() {
+ return 'externalstorages';
+ }
+
+ /**
+ * @return int whether the form should be rather on the top or bottom of
+ * the admin section. The forms are arranged in ascending order of the
+ * priority values. It is required to return a value between 0 and 100.
+ *
+ * E.g.: 70
+ */
+ public function getPriority() {
+ return 40;
+ }
+}
diff --git a/apps/files_external/lib/Settings/PersonalSection.php b/apps/files_external/lib/Settings/PersonalSection.php
new file mode 100644
index 00000000000..c6eb1c6b889
--- /dev/null
+++ b/apps/files_external/lib/Settings/PersonalSection.php
@@ -0,0 +1,25 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2017 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Settings;
+
+use OCA\Files_External\Service\BackendService;
+use OCA\Files_External\Service\UserGlobalStoragesService;
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\IUserSession;
+
+class PersonalSection extends Section {
+ public function __construct(
+ IURLGenerator $url,
+ IL10N $l,
+ private IUserSession $userSession,
+ private UserGlobalStoragesService $userGlobalStoragesService,
+ private BackendService $backendService,
+ ) {
+ parent::__construct($url, $l);
+ }
+}
diff --git a/apps/files_external/lib/Settings/Section.php b/apps/files_external/lib/Settings/Section.php
new file mode 100644
index 00000000000..cf3b73472d7
--- /dev/null
+++ b/apps/files_external/lib/Settings/Section.php
@@ -0,0 +1,61 @@
+<?php
+
+/**
+ * SPDX-FileCopyrightText: 2016 Nextcloud GmbH and Nextcloud contributors
+ * SPDX-License-Identifier: AGPL-3.0-or-later
+ */
+namespace OCA\Files_External\Settings;
+
+use OCP\IL10N;
+use OCP\IURLGenerator;
+use OCP\Settings\IIconSection;
+
+class Section implements IIconSection {
+ /**
+ * @param IURLGenerator $url
+ * @param IL10N $l
+ */
+ public function __construct(
+ private IURLGenerator $url,
+ private IL10N $l,
+ ) {
+ }
+
+ /**
+ * returns the ID of the section. It is supposed to be a lower case string,
+ * e.g. 'ldap'
+ *
+ * @returns string
+ */
+ public function getID() {
+ return 'externalstorages';
+ }
+
+ /**
+ * returns the translated name as it should be displayed, e.g. 'LDAP / AD
+ * integration'. Use the L10N service to translate it.
+ *
+ * @return string
+ */
+ public function getName() {
+ return $this->l->t('External storage');
+ }
+
+ /**
+ * @return int whether the form should be rather on the top or bottom of
+ * the settings navigation. The sections are arranged in ascending order of
+ * the priority values. It is required to return a value between 0 and 99.
+ *
+ * E.g.: 70
+ */
+ public function getPriority() {
+ return 10;
+ }
+
+ /**
+ * {@inheritdoc}
+ */
+ public function getIcon() {
+ return $this->url->imagePath('files_external', 'app-dark.svg');
+ }
+}
diff --git a/apps/files_external/lib/amazons3.php b/apps/files_external/lib/amazons3.php
deleted file mode 100644
index cb2082ee38b..00000000000
--- a/apps/files_external/lib/amazons3.php
+++ /dev/null
@@ -1,634 +0,0 @@
-<?php
-/**
- * @author André Gaul <gaul@web-yard.de>
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Christian Berendt <berendt@b1-systems.de>
- * @author Christopher T. Johnson <ctjctj@gmail.com>
- * @author Johan Björk <johanimon@gmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Martin Mattel <martin.mattel@diemattels.at>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-set_include_path(get_include_path() . PATH_SEPARATOR .
- \OC_App::getAppPath('files_external') . '/3rdparty/aws-sdk-php');
-require 'aws-autoloader.php';
-
-use Aws\S3\S3Client;
-use Aws\S3\Exception\S3Exception;
-use Icewind\Streams\IteratorDirectory;
-
-class AmazonS3 extends \OC\Files\Storage\Common {
-
- /**
- * @var \Aws\S3\S3Client
- */
- private $connection;
- /**
- * @var string
- */
- private $bucket;
- /**
- * @var array
- */
- private static $tmpFiles = array();
- /**
- * @var array
- */
- private $params;
- /**
- * @var bool
- */
- private $test = false;
- /**
- * @var int
- */
- private $timeout = 15;
- /**
- * @var int in seconds
- */
- private $rescanDelay = 10;
-
- /**
- * @param string $path
- * @return string correctly encoded path
- */
- private function normalizePath($path) {
- $path = trim($path, '/');
-
- if (!$path) {
- $path = '.';
- }
-
- return $path;
- }
-
- /**
- * when running the tests wait to let the buckets catch up
- */
- private function testTimeout() {
- if ($this->test) {
- sleep($this->timeout);
- }
- }
-
- private function isRoot($path) {
- return $path === '.';
- }
-
- private function cleanKey($path) {
- if ($this->isRoot($path)) {
- return '/';
- }
- return $path;
- }
-
- public function __construct($params) {
- if (empty($params['key']) || empty($params['secret']) || empty($params['bucket'])) {
- throw new \Exception("Access Key, Secret and Bucket have to be configured.");
- }
-
- $this->id = 'amazon::' . $params['bucket'];
- $this->updateLegacyId($params);
-
- $this->bucket = $params['bucket'];
- $this->test = isset($params['test']);
- $this->timeout = (!isset($params['timeout'])) ? 15 : $params['timeout'];
- $this->rescanDelay = (!isset($params['rescanDelay'])) ? 10 : $params['rescanDelay'];
- $params['region'] = empty($params['region']) ? 'eu-west-1' : $params['region'];
- $params['hostname'] = empty($params['hostname']) ? 's3.amazonaws.com' : $params['hostname'];
- if (!isset($params['port']) || $params['port'] === '') {
- $params['port'] = ($params['use_ssl'] === false) ? 80 : 443;
- }
- $this->params = $params;
- }
-
- /**
- * Updates old storage ids (v0.2.1 and older) that are based on key and secret to new ones based on the bucket name.
- * TODO Do this in an update.php. requires iterating over all users and loading the mount.json from their home
- *
- * @param array $params
- */
- public function updateLegacyId (array $params) {
- $oldId = 'amazon::' . $params['key'] . md5($params['secret']);
-
- // find by old id or bucket
- $stmt = \OC::$server->getDatabaseConnection()->prepare(
- 'SELECT `numeric_id`, `id` FROM `*PREFIX*storages` WHERE `id` IN (?, ?)'
- );
- $stmt->execute(array($oldId, $this->id));
- while ($row = $stmt->fetch()) {
- $storages[$row['id']] = $row['numeric_id'];
- }
-
- if (isset($storages[$this->id]) && isset($storages[$oldId])) {
- // if both ids exist, delete the old storage and corresponding filecache entries
- \OC\Files\Cache\Storage::remove($oldId);
- } else if (isset($storages[$oldId])) {
- // if only the old id exists do an update
- $stmt = \OC::$server->getDatabaseConnection()->prepare(
- 'UPDATE `*PREFIX*storages` SET `id` = ? WHERE `id` = ?'
- );
- $stmt->execute(array($this->id, $oldId));
- }
- // only the bucket based id may exist, do nothing
- }
-
- /**
- * Remove a file or folder
- *
- * @param string $path
- * @return bool
- */
- protected function remove($path) {
- // remember fileType to reduce http calls
- $fileType = $this->filetype($path);
- if ($fileType === 'dir') {
- return $this->rmdir($path);
- } else if ($fileType === 'file') {
- return $this->unlink($path);
- } else {
- return false;
- }
- }
-
- public function mkdir($path) {
- $path = $this->normalizePath($path);
-
- if ($this->is_dir($path)) {
- return false;
- }
-
- try {
- $this->getConnection()->putObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $path . '/',
- 'Body' => '',
- 'ContentType' => 'httpd/unix-directory'
- ));
- $this->testTimeout();
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
-
- return true;
- }
-
- public function file_exists($path) {
- return $this->filetype($path) !== false;
- }
-
-
- public function rmdir($path) {
- $path = $this->normalizePath($path);
-
- if ($this->isRoot($path)) {
- return $this->clearBucket();
- }
-
- if (!$this->file_exists($path)) {
- return false;
- }
-
- return $this->batchDelete($path);
- }
-
- protected function clearBucket() {
- try {
- $this->getConnection()->clearBucket($this->bucket);
- return true;
- // clearBucket() is not working with Ceph, so if it fails we try the slower approach
- } catch (\Exception $e) {
- return $this->batchDelete();
- }
- return false;
- }
-
- private function batchDelete ($path = null) {
- $params = array(
- 'Bucket' => $this->bucket
- );
- if ($path !== null) {
- $params['Prefix'] = $path . '/';
- }
- try {
- // Since there are no real directories on S3, we need
- // to delete all objects prefixed with the path.
- do {
- // instead of the iterator, manually loop over the list ...
- $objects = $this->getConnection()->listObjects($params);
- // ... so we can delete the files in batches
- $this->getConnection()->deleteObjects(array(
- 'Bucket' => $this->bucket,
- 'Objects' => $objects['Contents']
- ));
- $this->testTimeout();
- // we reached the end when the list is no longer truncated
- } while ($objects['IsTruncated']);
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
- return true;
- }
-
- public function opendir($path) {
- $path = $this->normalizePath($path);
-
- if ($this->isRoot($path)) {
- $path = '';
- } else {
- $path .= '/';
- }
-
- try {
- $files = array();
- $result = $this->getConnection()->getIterator('ListObjects', array(
- 'Bucket' => $this->bucket,
- 'Delimiter' => '/',
- 'Prefix' => $path
- ), array('return_prefixes' => true));
-
- foreach ($result as $object) {
- if (isset($object['Key']) && $object['Key'] === $path) {
- // it's the directory itself, skip
- continue;
- }
- $file = basename(
- isset($object['Key']) ? $object['Key'] : $object['Prefix']
- );
- $files[] = $file;
- }
-
- return IteratorDirectory::wrap($files);
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
- }
-
- public function stat($path) {
- $path = $this->normalizePath($path);
-
- try {
- $stat = array();
- if ($this->is_dir($path)) {
- //folders don't really exist
- $stat['size'] = -1; //unknown
- $stat['mtime'] = time() - $this->rescanDelay * 1000;
- } else {
- $result = $this->getConnection()->headObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $path
- ));
-
- $stat['size'] = $result['ContentLength'] ? $result['ContentLength'] : 0;
- if ($result['Metadata']['lastmodified']) {
- $stat['mtime'] = strtotime($result['Metadata']['lastmodified']);
- } else {
- $stat['mtime'] = strtotime($result['LastModified']);
- }
- }
- $stat['atime'] = time();
-
- return $stat;
- } catch(S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
- }
-
- public function filetype($path) {
- $path = $this->normalizePath($path);
-
- if ($this->isRoot($path)) {
- return 'dir';
- }
-
- try {
- if ($this->getConnection()->doesObjectExist($this->bucket, $path)) {
- return 'file';
- }
- if ($this->getConnection()->doesObjectExist($this->bucket, $path.'/')) {
- return 'dir';
- }
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
-
- return false;
- }
-
- public function unlink($path) {
- $path = $this->normalizePath($path);
-
- if ($this->is_dir($path)) {
- return $this->rmdir($path);
- }
-
- try {
- $this->getConnection()->deleteObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $path
- ));
- $this->testTimeout();
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
-
- return true;
- }
-
- public function fopen($path, $mode) {
- $path = $this->normalizePath($path);
-
- switch ($mode) {
- case 'r':
- case 'rb':
- $tmpFile = \OCP\Files::tmpFile();
- self::$tmpFiles[$tmpFile] = $path;
-
- try {
- $this->getConnection()->getObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $path,
- 'SaveAs' => $tmpFile
- ));
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
-
- return fopen($tmpFile, 'r');
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- if (strrpos($path, '.') !== false) {
- $ext = substr($path, strrpos($path, '.'));
- } else {
- $ext = '';
- }
- $tmpFile = \OCP\Files::tmpFile($ext);
- \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack'));
- if ($this->file_exists($path)) {
- $source = $this->fopen($path, 'r');
- file_put_contents($tmpFile, $source);
- }
- self::$tmpFiles[$tmpFile] = $path;
-
- return fopen('close://' . $tmpFile, $mode);
- }
- return false;
- }
-
- public function touch($path, $mtime = null) {
- $path = $this->normalizePath($path);
-
- $metadata = array();
- if (is_null($mtime)) {
- $mtime = time();
- }
- $metadata = [
- 'lastmodified' => gmdate(\Aws\Common\Enum\DateFormat::RFC1123, $mtime)
- ];
-
- $fileType = $this->filetype($path);
- try {
- if ($fileType !== false) {
- if ($fileType === 'dir' && ! $this->isRoot($path)) {
- $path .= '/';
- }
- $this->getConnection()->copyObject([
- 'Bucket' => $this->bucket,
- 'Key' => $this->cleanKey($path),
- 'Metadata' => $metadata,
- 'CopySource' => $this->bucket . '/' . $path,
- 'MetadataDirective' => 'REPLACE',
- ]);
- $this->testTimeout();
- } else {
- $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
- $this->getConnection()->putObject([
- 'Bucket' => $this->bucket,
- 'Key' => $this->cleanKey($path),
- 'Metadata' => $metadata,
- 'Body' => '',
- 'ContentType' => $mimeType,
- 'MetadataDirective' => 'REPLACE',
- ]);
- $this->testTimeout();
- }
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
-
- return true;
- }
-
- public function copy($path1, $path2) {
- $path1 = $this->normalizePath($path1);
- $path2 = $this->normalizePath($path2);
-
- if ($this->is_file($path1)) {
- try {
- $this->getConnection()->copyObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $this->cleanKey($path2),
- 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1)
- ));
- $this->testTimeout();
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
- } else {
- $this->remove($path2);
-
- try {
- $this->getConnection()->copyObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $path2 . '/',
- 'CopySource' => S3Client::encodeKey($this->bucket . '/' . $path1 . '/')
- ));
- $this->testTimeout();
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
-
- $dh = $this->opendir($path1);
- if (is_resource($dh)) {
- while (($file = readdir($dh)) !== false) {
- if (\OC\Files\Filesystem::isIgnoredDir($file)) {
- continue;
- }
-
- $source = $path1 . '/' . $file;
- $target = $path2 . '/' . $file;
- $this->copy($source, $target);
- }
- }
- }
-
- return true;
- }
-
- public function rename($path1, $path2) {
- $path1 = $this->normalizePath($path1);
- $path2 = $this->normalizePath($path2);
-
- if ($this->is_file($path1)) {
-
- if ($this->copy($path1, $path2) === false) {
- return false;
- }
-
- if ($this->unlink($path1) === false) {
- $this->unlink($path2);
- return false;
- }
- } else {
-
- if ($this->copy($path1, $path2) === false) {
- return false;
- }
-
- if ($this->rmdir($path1) === false) {
- $this->rmdir($path2);
- return false;
- }
- }
-
- return true;
- }
-
- public function test() {
- $test = $this->getConnection()->getBucketAcl(array(
- 'Bucket' => $this->bucket,
- ));
- if (isset($test) && !is_null($test->getPath('Owner/ID'))) {
- return true;
- }
- return false;
- }
-
- public function getId() {
- return $this->id;
- }
-
- /**
- * Returns the connection
- *
- * @return S3Client connected client
- * @throws \Exception if connection could not be made
- */
- public function getConnection() {
- if (!is_null($this->connection)) {
- return $this->connection;
- }
-
- $scheme = ($this->params['use_ssl'] === false) ? 'http' : 'https';
- $base_url = $scheme . '://' . $this->params['hostname'] . ':' . $this->params['port'] . '/';
-
- $this->connection = S3Client::factory(array(
- 'key' => $this->params['key'],
- 'secret' => $this->params['secret'],
- 'base_url' => $base_url,
- 'region' => $this->params['region'],
- S3Client::COMMAND_PARAMS => [
- 'PathStyle' => $this->params['use_path_style'],
- ],
- ));
-
- if (!$this->connection->isValidBucketName($this->bucket)) {
- throw new \Exception("The configured bucket name is invalid.");
- }
-
- if (!$this->connection->doesBucketExist($this->bucket)) {
- try {
- $this->connection->createBucket(array(
- 'Bucket' => $this->bucket
- ));
- $this->connection->waitUntilBucketExists(array(
- 'Bucket' => $this->bucket,
- 'waiter.interval' => 1,
- 'waiter.max_attempts' => 15
- ));
- $this->testTimeout();
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- throw new \Exception('Creation of bucket failed. '.$e->getMessage());
- }
- }
-
- return $this->connection;
- }
-
- public function writeBack($tmpFile) {
- if (!isset(self::$tmpFiles[$tmpFile])) {
- return false;
- }
-
- try {
- $this->getConnection()->putObject(array(
- 'Bucket' => $this->bucket,
- 'Key' => $this->cleanKey(self::$tmpFiles[$tmpFile]),
- 'SourceFile' => $tmpFile,
- 'ContentType' => \OC::$server->getMimeTypeDetector()->detect($tmpFile),
- 'ContentLength' => filesize($tmpFile)
- ));
- $this->testTimeout();
-
- unlink($tmpFile);
- } catch (S3Exception $e) {
- \OCP\Util::logException('files_external', $e);
- return false;
- }
- }
-
- /**
- * check if curl is installed
- */
- public static function checkDependencies() {
- return true;
- }
-
-}
diff --git a/apps/files_external/lib/api.php b/apps/files_external/lib/api.php
deleted file mode 100644
index 50a2f38c65b..00000000000
--- a/apps/files_external/lib/api.php
+++ /dev/null
@@ -1,87 +0,0 @@
-<?php
-/**
- * @author Jesús Macias <jmacias@solidgear.es>
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files\External;
-
-class Api {
-
- /**
- * Formats the given mount config to a mount entry.
- *
- * @param string $mountPoint mount point name, relative to the data dir
- * @param array $mountConfig mount config to format
- *
- * @return array entry
- */
- private static function formatMount($mountPoint, $mountConfig) {
- // strip "/$user/files" from mount point
- $mountPoint = explode('/', trim($mountPoint, '/'), 3);
- $mountPoint = $mountPoint[2];
-
- // split path from mount point
- $path = dirname($mountPoint);
- if ($path === '.') {
- $path = '';
- }
-
- $isSystemMount = !$mountConfig['personal'];
-
- $permissions = \OCP\Constants::PERMISSION_READ;
- // personal mounts can be deleted
- if (!$isSystemMount) {
- $permissions |= \OCP\Constants::PERMISSION_DELETE;
- }
-
- $entry = array(
- 'name' => basename($mountPoint),
- 'path' => $path,
- 'type' => 'dir',
- 'backend' => $mountConfig['backend'],
- 'scope' => ( $isSystemMount ? 'system' : 'personal' ),
- 'permissions' => $permissions,
- 'id' => $mountConfig['id'],
- 'class' => $mountConfig['class']
- );
- return $entry;
- }
-
- /**
- * Returns the mount points visible for this user.
- *
- * @param array $params
- * @return \OC_OCS_Result share information
- */
- public static function getUserMounts($params) {
- $entries = array();
- $user = \OC::$server->getUserSession()->getUser()->getUID();
-
- $mounts = \OC_Mount_Config::getAbsoluteMountPoints($user);
- foreach($mounts as $mountPoint => $mount) {
- $entries[] = self::formatMount($mountPoint, $mount);
- }
-
- return new \OC_OCS_Result($entries);
- }
-}
diff --git a/apps/files_external/lib/auth/amazons3/accesskey.php b/apps/files_external/lib/auth/amazons3/accesskey.php
deleted file mode 100644
index 296ed59a77a..00000000000
--- a/apps/files_external/lib/auth/amazons3/accesskey.php
+++ /dev/null
@@ -1,47 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\AmazonS3;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-
-/**
- * Amazon S3 access key authentication
- */
-class AccessKey extends AuthMechanism {
-
- const SCHEME_AMAZONS3_ACCESSKEY = 'amazons3_accesskey';
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('amazons3::accesskey')
- ->setScheme(self::SCHEME_AMAZONS3_ACCESSKEY)
- ->setText($l->t('Access key'))
- ->addParameters([
- (new DefinitionParameter('key', $l->t('Access key'))),
- (new DefinitionParameter('secret', $l->t('Secret key')))
- ->setType(DefinitionParameter::VALUE_PASSWORD),
- ]);
- }
-
-}
diff --git a/apps/files_external/lib/auth/builtin.php b/apps/files_external/lib/auth/builtin.php
deleted file mode 100644
index 8b43cb459cc..00000000000
--- a/apps/files_external/lib/auth/builtin.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_external\Lib\StorageConfig;
-
-/**
- * Builtin authentication mechanism, for legacy backends
- */
-class Builtin extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('builtin::builtin')
- ->setScheme(self::SCHEME_BUILTIN)
- ->setText($l->t('Builtin'))
- ;
- }
-
-}
diff --git a/apps/files_external/lib/auth/iuserprovided.php b/apps/files_external/lib/auth/iuserprovided.php
deleted file mode 100644
index 6852c804be5..00000000000
--- a/apps/files_external/lib/auth/iuserprovided.php
+++ /dev/null
@@ -1,36 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth;
-
-use OCP\IUser;
-
-/**
- * For auth mechanisms where the user needs to provide credentials
- */
-interface IUserProvided {
- /**
- * @param IUser $user the user for which to save the user provided options
- * @param int $mountId the mount id to save the options for
- * @param array $options the user provided options
- */
- public function saveBackendOptions(IUser $user, $mountId, array $options);
-}
diff --git a/apps/files_external/lib/auth/nullmechanism.php b/apps/files_external/lib/auth/nullmechanism.php
deleted file mode 100644
index 06083729e59..00000000000
--- a/apps/files_external/lib/auth/nullmechanism.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_external\Lib\StorageConfig;
-
-/**
- * Null authentication mechanism
- */
-class NullMechanism extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('null::null')
- ->setScheme(self::SCHEME_NULL)
- ->setText($l->t('None'))
- ;
- }
-
-}
diff --git a/apps/files_external/lib/auth/oauth1/oauth1.php b/apps/files_external/lib/auth/oauth1/oauth1.php
deleted file mode 100644
index dd83c9a6a69..00000000000
--- a/apps/files_external/lib/auth/oauth1/oauth1.php
+++ /dev/null
@@ -1,53 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\OAuth1;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-
-/**
- * OAuth1 authentication
- */
-class OAuth1 extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('oauth1::oauth1')
- ->setScheme(self::SCHEME_OAUTH1)
- ->setText($l->t('OAuth1'))
- ->addParameters([
- (new DefinitionParameter('configured', 'configured'))
- ->setType(DefinitionParameter::VALUE_HIDDEN),
- (new DefinitionParameter('app_key', $l->t('App key'))),
- (new DefinitionParameter('app_secret', $l->t('App secret')))
- ->setType(DefinitionParameter::VALUE_PASSWORD),
- (new DefinitionParameter('token', 'token'))
- ->setType(DefinitionParameter::VALUE_HIDDEN),
- (new DefinitionParameter('token_secret', 'token_secret'))
- ->setType(DefinitionParameter::VALUE_HIDDEN),
- ])
- ->setCustomJs('oauth1')
- ;
- }
-
-}
diff --git a/apps/files_external/lib/auth/oauth2/oauth2.php b/apps/files_external/lib/auth/oauth2/oauth2.php
deleted file mode 100644
index c89007b52ba..00000000000
--- a/apps/files_external/lib/auth/oauth2/oauth2.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\OAuth2;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-
-/**
- * OAuth2 authentication
- */
-class OAuth2 extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('oauth2::oauth2')
- ->setScheme(self::SCHEME_OAUTH2)
- ->setText($l->t('OAuth2'))
- ->addParameters([
- (new DefinitionParameter('configured', 'configured'))
- ->setType(DefinitionParameter::VALUE_HIDDEN),
- (new DefinitionParameter('client_id', $l->t('Client ID'))),
- (new DefinitionParameter('client_secret', $l->t('Client secret')))
- ->setType(DefinitionParameter::VALUE_PASSWORD),
- (new DefinitionParameter('token', 'token'))
- ->setType(DefinitionParameter::VALUE_HIDDEN),
- ])
- ->setCustomJs('oauth2')
- ;
- }
-
-}
diff --git a/apps/files_external/lib/auth/openstack/openstack.php b/apps/files_external/lib/auth/openstack/openstack.php
deleted file mode 100644
index 80bbb1299f7..00000000000
--- a/apps/files_external/lib/auth/openstack/openstack.php
+++ /dev/null
@@ -1,48 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\OpenStack;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-
-/**
- * OpenStack Keystone authentication
- */
-class OpenStack extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('openstack::openstack')
- ->setScheme(self::SCHEME_OPENSTACK)
- ->setText($l->t('OpenStack'))
- ->addParameters([
- (new DefinitionParameter('user', $l->t('Username'))),
- (new DefinitionParameter('password', $l->t('Password')))
- ->setType(DefinitionParameter::VALUE_PASSWORD),
- (new DefinitionParameter('tenant', $l->t('Tenant name'))),
- (new DefinitionParameter('url', $l->t('Identity endpoint URL'))),
- ])
- ;
- }
-
-}
diff --git a/apps/files_external/lib/auth/openstack/rackspace.php b/apps/files_external/lib/auth/openstack/rackspace.php
deleted file mode 100644
index c968321ca6c..00000000000
--- a/apps/files_external/lib/auth/openstack/rackspace.php
+++ /dev/null
@@ -1,46 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\OpenStack;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-
-/**
- * Rackspace authentication
- */
-class Rackspace extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('openstack::rackspace')
- ->setScheme(self::SCHEME_OPENSTACK)
- ->setText($l->t('Rackspace'))
- ->addParameters([
- (new DefinitionParameter('user', $l->t('Username'))),
- (new DefinitionParameter('key', $l->t('API key')))
- ->setType(DefinitionParameter::VALUE_PASSWORD),
- ])
- ;
- }
-
-}
diff --git a/apps/files_external/lib/auth/password/password.php b/apps/files_external/lib/auth/password/password.php
deleted file mode 100644
index 3b1942cc4a8..00000000000
--- a/apps/files_external/lib/auth/password/password.php
+++ /dev/null
@@ -1,45 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\Password;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-
-/**
- * Basic password authentication mechanism
- */
-class Password extends AuthMechanism {
-
- public function __construct(IL10N $l) {
- $this
- ->setIdentifier('password::password')
- ->setScheme(self::SCHEME_PASSWORD)
- ->setText($l->t('Username and password'))
- ->addParameters([
- (new DefinitionParameter('user', $l->t('Username'))),
- (new DefinitionParameter('password', $l->t('Password')))
- ->setType(DefinitionParameter::VALUE_PASSWORD),
- ]);
- }
-
-}
diff --git a/apps/files_external/lib/auth/password/sessioncredentials.php b/apps/files_external/lib/auth/password/sessioncredentials.php
deleted file mode 100644
index 429c549d80a..00000000000
--- a/apps/files_external/lib/auth/password/sessioncredentials.php
+++ /dev/null
@@ -1,86 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\Password;
-
-use \OCP\IUser;
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCP\ISession;
-use \OCP\Security\ICrypto;
-use \OCP\Files\Storage;
-use \OCA\Files_External\Lib\SessionStorageWrapper;
-use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
-
-/**
- * Username and password from login credentials, saved in session
- */
-class SessionCredentials extends AuthMechanism {
-
- /** @var ISession */
- protected $session;
-
- /** @var ICrypto */
- protected $crypto;
-
- public function __construct(IL10N $l, ISession $session, ICrypto $crypto) {
- $this->session = $session;
- $this->crypto = $crypto;
-
- $this
- ->setIdentifier('password::sessioncredentials')
- ->setScheme(self::SCHEME_PASSWORD)
- ->setText($l->t('Log-in credentials, save in session'))
- ->addParameters([
- ])
- ;
-
- \OCP\Util::connectHook('OC_User', 'post_login', $this, 'authenticate');
- }
-
- /**
- * Hook listener on post login
- *
- * @param array $params
- */
- public function authenticate(array $params) {
- $this->session->set('password::sessioncredentials/credentials', $this->crypto->encrypt(json_encode($params)));
- }
-
- public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
- $encrypted = $this->session->get('password::sessioncredentials/credentials');
- if (!isset($encrypted)) {
- throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
- }
-
- $credentials = json_decode($this->crypto->decrypt($encrypted), true);
- $storage->setBackendOption('user', $this->session->get('loginname'));
- $storage->setBackendOption('password', $credentials['password']);
- }
-
- public function wrapStorage(Storage $storage) {
- return new SessionStorageWrapper(['storage' => $storage]);
- }
-
-}
diff --git a/apps/files_external/lib/auth/publickey/rsa.php b/apps/files_external/lib/auth/publickey/rsa.php
deleted file mode 100644
index 9045f6818f9..00000000000
--- a/apps/files_external/lib/auth/publickey/rsa.php
+++ /dev/null
@@ -1,81 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Auth\PublicKey;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCP\IConfig;
-use OCP\IUser;
-use \phpseclib\Crypt\RSA as RSACrypt;
-
-/**
- * RSA public key authentication
- */
-class RSA extends AuthMechanism {
-
- const CREATE_KEY_BITS = 1024;
-
- /** @var IConfig */
- private $config;
-
- public function __construct(IL10N $l, IConfig $config) {
- $this->config = $config;
-
- $this
- ->setIdentifier('publickey::rsa')
- ->setScheme(self::SCHEME_PUBLICKEY)
- ->setText($l->t('RSA public key'))
- ->addParameters([
- (new DefinitionParameter('user', $l->t('Username'))),
- (new DefinitionParameter('public_key', $l->t('Public key'))),
- (new DefinitionParameter('private_key', 'private_key'))
- ->setType(DefinitionParameter::VALUE_HIDDEN),
- ])
- ->setCustomJs('public_key')
- ;
- }
-
- public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
- $auth = new RSACrypt();
- $auth->setPassword($this->config->getSystemValue('secret', ''));
- if (!$auth->loadKey($storage->getBackendOption('private_key'))) {
- throw new \RuntimeException('unable to load private key');
- }
- $storage->setBackendOption('public_key_auth', $auth);
- }
-
- /**
- * Generate a keypair
- *
- * @return array ['privatekey' => $privateKey, 'publickey' => $publicKey]
- */
- public function createKey() {
- $rsa = new RSACrypt();
- $rsa->setPublicKeyFormat(RSACrypt::PUBLIC_FORMAT_OPENSSH);
- $rsa->setPassword($this->config->getSystemValue('secret', ''));
-
- return $rsa->createKey(self::CREATE_KEY_BITS);
- }
-
-}
diff --git a/apps/files_external/lib/backend/amazons3.php b/apps/files_external/lib/backend/amazons3.php
deleted file mode 100644
index b2dedc10e4a..00000000000
--- a/apps/files_external/lib/backend/amazons3.php
+++ /dev/null
@@ -1,61 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-use \OCA\Files_External\Lib\Auth\AmazonS3\AccessKey;
-
-class AmazonS3 extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, AccessKey $legacyAuth) {
- $this
- ->setIdentifier('amazons3')
- ->addIdentifierAlias('\OC\Files\Storage\AmazonS3') // legacy compat
- ->setStorageClass('\OC\Files\Storage\AmazonS3')
- ->setText($l->t('Amazon S3'))
- ->addParameters([
- (new DefinitionParameter('bucket', $l->t('Bucket'))),
- (new DefinitionParameter('hostname', $l->t('Hostname')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('port', $l->t('Port')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('region', $l->t('Region')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('use_ssl', $l->t('Enable SSL')))
- ->setType(DefinitionParameter::VALUE_BOOLEAN),
- (new DefinitionParameter('use_path_style', $l->t('Enable Path Style')))
- ->setType(DefinitionParameter::VALUE_BOOLEAN),
- ])
- ->addAuthScheme(AccessKey::SCHEME_AMAZONS3_ACCESSKEY)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/dav.php b/apps/files_external/lib/backend/dav.php
deleted file mode 100644
index c6e9630be9e..00000000000
--- a/apps/files_external/lib/backend/dav.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-use \OCA\Files_External\Lib\Auth\Password\Password;
-
-class DAV extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, Password $legacyAuth) {
- $this
- ->setIdentifier('dav')
- ->addIdentifierAlias('\OC\Files\Storage\DAV') // legacy compat
- ->setStorageClass('\OC\Files\Storage\DAV')
- ->setText($l->t('WebDAV'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('URL'))),
- (new DefinitionParameter('root', $l->t('Remote subfolder')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('secure', $l->t('Secure https://')))
- ->setType(DefinitionParameter::VALUE_BOOLEAN),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/dropbox.php b/apps/files_external/lib/backend/dropbox.php
deleted file mode 100644
index 7a414731192..00000000000
--- a/apps/files_external/lib/backend/dropbox.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-use \OCA\Files_External\Lib\Auth\OAuth1\OAuth1;
-
-class Dropbox extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, OAuth1 $legacyAuth) {
- $this
- ->setIdentifier('dropbox')
- ->addIdentifierAlias('\OC\Files\Storage\Dropbox') // legacy compat
- ->setStorageClass('\OC\Files\Storage\Dropbox')
- ->setText($l->t('Dropbox'))
- ->addParameters([
- // all parameters handled in OAuth1 mechanism
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_OAUTH1)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/ftp.php b/apps/files_external/lib/backend/ftp.php
deleted file mode 100644
index b2b83a27405..00000000000
--- a/apps/files_external/lib/backend/ftp.php
+++ /dev/null
@@ -1,55 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-use \OCA\Files_External\Lib\Auth\Password\Password;
-
-class FTP extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, Password $legacyAuth) {
- $this
- ->setIdentifier('ftp')
- ->addIdentifierAlias('\OC\Files\Storage\FTP') // legacy compat
- ->setStorageClass('\OC\Files\Storage\FTP')
- ->setText($l->t('FTP'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('Host'))),
- (new DefinitionParameter('root', $l->t('Remote subfolder')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('secure', $l->t('Secure ftps://')))
- ->setType(DefinitionParameter::VALUE_BOOLEAN),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/google.php b/apps/files_external/lib/backend/google.php
deleted file mode 100644
index 93a8cd2177d..00000000000
--- a/apps/files_external/lib/backend/google.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-use \OCA\Files_External\Lib\Auth\OAuth2\OAuth2;
-
-class Google extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, OAuth2 $legacyAuth) {
- $this
- ->setIdentifier('googledrive')
- ->addIdentifierAlias('\OC\Files\Storage\Google') // legacy compat
- ->setStorageClass('\OC\Files\Storage\Google')
- ->setText($l->t('Google Drive'))
- ->addParameters([
- // all parameters handled in OAuth2 mechanism
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_OAUTH2)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/local.php b/apps/files_external/lib/backend/local.php
deleted file mode 100644
index 1db707e7247..00000000000
--- a/apps/files_external/lib/backend/local.php
+++ /dev/null
@@ -1,49 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\Auth\NullMechanism;
-
-class Local extends Backend {
-
- public function __construct(IL10N $l, NullMechanism $legacyAuth) {
- $this
- ->setIdentifier('local')
- ->addIdentifierAlias('\OC\Files\Storage\Local') // legacy compat
- ->setStorageClass('\OC\Files\Storage\Local')
- ->setText($l->t('Local'))
- ->addParameters([
- (new DefinitionParameter('datadir', $l->t('Location'))),
- ])
- ->setAllowedVisibility(BackendService::VISIBILITY_ADMIN)
- ->setPriority(BackendService::PRIORITY_DEFAULT + 50)
- ->addAuthScheme(AuthMechanism::SCHEME_NULL)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/owncloud.php b/apps/files_external/lib/backend/owncloud.php
deleted file mode 100644
index e7da328c5f1..00000000000
--- a/apps/files_external/lib/backend/owncloud.php
+++ /dev/null
@@ -1,52 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-
-use \OCA\Files_External\Lib\Auth\Password\Password;
-
-class OwnCloud extends Backend {
-
- public function __construct(IL10N $l, Password $legacyAuth) {
- $this
- ->setIdentifier('owncloud')
- ->addIdentifierAlias('\OC\Files\Storage\OwnCloud') // legacy compat
- ->setStorageClass('\OC\Files\Storage\OwnCloud')
- ->setText($l->t('ownCloud'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('URL'))),
- (new DefinitionParameter('root', $l->t('Remote subfolder')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('secure', $l->t('Secure https://')))
- ->setType(DefinitionParameter::VALUE_BOOLEAN),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/sftp.php b/apps/files_external/lib/backend/sftp.php
deleted file mode 100644
index 3e5ecb90131..00000000000
--- a/apps/files_external/lib/backend/sftp.php
+++ /dev/null
@@ -1,51 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-
-use \OCA\Files_External\Lib\Auth\Password\Password;
-
-class SFTP extends Backend {
-
- public function __construct(IL10N $l, Password $legacyAuth) {
- $this
- ->setIdentifier('sftp')
- ->addIdentifierAlias('\OC\Files\Storage\SFTP') // legacy compat
- ->setStorageClass('\OC\Files\Storage\SFTP')
- ->setText($l->t('SFTP'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('Host'))),
- (new DefinitionParameter('root', $l->t('Root')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
- ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/sftp_key.php b/apps/files_external/lib/backend/sftp_key.php
deleted file mode 100644
index 58dddedf784..00000000000
--- a/apps/files_external/lib/backend/sftp_key.php
+++ /dev/null
@@ -1,50 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\Auth\PublicKey\RSA;
-use \OCA\Files_External\Lib\Backend\SFTP;
-
-class SFTP_Key extends Backend {
-
- public function __construct(IL10N $l, RSA $legacyAuth, SFTP $sftpBackend) {
- $this
- ->setIdentifier('\OC\Files\Storage\SFTP_Key')
- ->setStorageClass('\OC\Files\Storage\SFTP')
- ->setText($l->t('SFTP with secret key login'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('Host'))),
- (new DefinitionParameter('root', $l->t('Remote subfolder')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY)
- ->setLegacyAuthMechanism($legacyAuth)
- ->deprecateTo($sftpBackend)
- ;
- }
-
-}
diff --git a/apps/files_external/lib/backend/smb.php b/apps/files_external/lib/backend/smb.php
deleted file mode 100644
index 9b71636936a..00000000000
--- a/apps/files_external/lib/backend/smb.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-use \OCA\Files_External\Lib\Auth\Password\Password;
-use OCP\IUser;
-
-class SMB extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, Password $legacyAuth) {
- $this
- ->setIdentifier('smb')
- ->addIdentifierAlias('\OC\Files\Storage\SMB') // legacy compat
- ->setStorageClass('\OC\Files\Storage\SMB')
- ->setText($l->t('SMB / CIFS'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('Host'))),
- (new DefinitionParameter('share', $l->t('Share'))),
- (new DefinitionParameter('root', $l->t('Remote subfolder')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('domain', $l->t('Domain')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
- ->setLegacyAuthMechanism($legacyAuth)
- ;
- }
-
- /**
- * @param StorageConfig $storage
- * @param IUser $user
- */
- public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
- $user = $storage->getBackendOption('user');
- if ($domain = $storage->getBackendOption('domain')) {
- $storage->setBackendOption('user', $domain.'\\'.$user);
- }
- }
-
-}
diff --git a/apps/files_external/lib/backend/smb_oc.php b/apps/files_external/lib/backend/smb_oc.php
deleted file mode 100644
index ba38754ce5a..00000000000
--- a/apps/files_external/lib/backend/smb_oc.php
+++ /dev/null
@@ -1,72 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\Auth\Password\SessionCredentials;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-use \OCA\Files_External\Lib\Backend\SMB;
-use OCP\IUser;
-
-/**
- * Deprecated SMB_OC class - use SMB with the password::sessioncredentials auth mechanism
- */
-class SMB_OC extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, SessionCredentials $legacyAuth, SMB $smbBackend) {
- $this
- ->setIdentifier('\OC\Files\Storage\SMB_OC')
- ->setStorageClass('\OC\Files\Storage\SMB')
- ->setText($l->t('SMB / CIFS using OC login'))
- ->addParameters([
- (new DefinitionParameter('host', $l->t('Host'))),
- (new DefinitionParameter('username_as_share', $l->t('Username as share')))
- ->setType(DefinitionParameter::VALUE_BOOLEAN),
- (new DefinitionParameter('share', $l->t('Share')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('root', $l->t('Remote subfolder')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- ])
- ->setPriority(BackendService::PRIORITY_DEFAULT - 10)
- ->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
- ->setLegacyAuthMechanism($legacyAuth)
- ->deprecateTo($smbBackend)
- ;
- }
-
- public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
- $username_as_share = ($storage->getBackendOption('username_as_share') === true);
-
- if ($username_as_share) {
- $share = '/' . $storage->getBackendOption('user');
- $storage->setBackendOption('share', $share);
- }
- }
-
-}
diff --git a/apps/files_external/lib/backend/swift.php b/apps/files_external/lib/backend/swift.php
deleted file mode 100644
index d6e4ac12f9a..00000000000
--- a/apps/files_external/lib/backend/swift.php
+++ /dev/null
@@ -1,62 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib\Backend;
-
-use \OCP\IL10N;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\Auth\AuthMechanism;
-use \OCA\Files_External\Service\BackendService;
-use \OCA\Files_External\Lib\Auth\OpenStack\OpenStack;
-use \OCA\Files_External\Lib\Auth\OpenStack\Rackspace;
-use \OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
-
-class Swift extends Backend {
-
- use LegacyDependencyCheckPolyfill;
-
- public function __construct(IL10N $l, OpenStack $openstackAuth, Rackspace $rackspaceAuth) {
- $this
- ->setIdentifier('swift')
- ->addIdentifierAlias('\OC\Files\Storage\Swift') // legacy compat
- ->setStorageClass('\OC\Files\Storage\Swift')
- ->setText($l->t('OpenStack Object Storage'))
- ->addParameters([
- (new DefinitionParameter('service_name', $l->t('Service name')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('region', $l->t('Region')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- (new DefinitionParameter('bucket', $l->t('Bucket'))),
- (new DefinitionParameter('timeout', $l->t('Request timeout (seconds)')))
- ->setFlag(DefinitionParameter::FLAG_OPTIONAL),
- ])
- ->addAuthScheme(AuthMechanism::SCHEME_OPENSTACK)
- ->setLegacyAuthMechanismCallback(function(array $params) use ($openstackAuth, $rackspaceAuth) {
- if (isset($params['options']['key']) && $params['options']['key']) {
- return $rackspaceAuth;
- }
- return $openstackAuth;
- })
- ;
- }
-
-}
diff --git a/apps/files_external/lib/config.php b/apps/files_external/lib/config.php
deleted file mode 100644
index 70f8550f39b..00000000000
--- a/apps/files_external/lib/config.php
+++ /dev/null
@@ -1,413 +0,0 @@
-<?php
-/**
- * @author Andreas Fischer <bantu@owncloud.com>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Björn Schießle <schiessle@owncloud.com>
- * @author Frank Karlitschek <frank@owncloud.org>
- * @author Jesús Macias <jmacias@solidgear.es>
- * @author Joas Schilling <nickvergessen@owncloud.com>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-use phpseclib\Crypt\AES;
-use \OCA\Files_External\Appinfo\Application;
-use \OCA\Files_External\Lib\Backend\LegacyBackend;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCA\Files_External\Lib\Backend\Backend;
-use \OCP\Files\StorageNotAvailableException;
-
-/**
- * Class to configure mount.json globally and for users
- */
-class OC_Mount_Config {
- // TODO: make this class non-static and give it a proper namespace
-
- const MOUNT_TYPE_GLOBAL = 'global';
- const MOUNT_TYPE_GROUP = 'group';
- const MOUNT_TYPE_USER = 'user';
- const MOUNT_TYPE_PERSONAL = 'personal';
-
- // whether to skip backend test (for unit tests, as this static class is not mockable)
- public static $skipTest = false;
-
- /** @var Application */
- public static $app;
-
- /**
- * @param string $class
- * @param array $definition
- * @return bool
- * @deprecated 8.2.0 use \OCA\Files_External\Service\BackendService::registerBackend()
- */
- public static function registerBackend($class, $definition) {
- $backendService = self::$app->getContainer()->query('OCA\Files_External\Service\BackendService');
- $auth = self::$app->getContainer()->query('OCA\Files_External\Lib\Auth\Builtin');
-
- $backendService->registerBackend(new LegacyBackend($class, $definition, $auth));
-
- return true;
- }
-
- /**
- * Returns the mount points for the given user.
- * The mount point is relative to the data directory.
- *
- * @param string $uid user
- * @return array of mount point string as key, mountpoint config as value
- *
- * @deprecated 8.2.0 use UserGlobalStoragesService::getStorages() and UserStoragesService::getStorages()
- */
- public static function getAbsoluteMountPoints($uid) {
- $mountPoints = array();
-
- $userGlobalStoragesService = self::$app->getContainer()->query('OCA\Files_External\Service\UserGlobalStoragesService');
- $userStoragesService = self::$app->getContainer()->query('OCA\Files_External\Service\UserStoragesService');
- $user = self::$app->getContainer()->query('OCP\IUserManager')->get($uid);
-
- $userGlobalStoragesService->setUser($user);
- $userStoragesService->setUser($user);
-
- foreach ($userGlobalStoragesService->getStorages() as $storage) {
- /** @var \OCA\Files_external\Lib\StorageConfig $storage */
- $mountPoint = '/'.$uid.'/files'.$storage->getMountPoint();
- $mountEntry = self::prepareMountPointEntry($storage, false);
- foreach ($mountEntry['options'] as &$option) {
- $option = self::setUserVars($uid, $option);
- }
- $mountPoints[$mountPoint] = $mountEntry;
- }
-
- foreach ($userStoragesService->getStorages() as $storage) {
- $mountPoint = '/'.$uid.'/files'.$storage->getMountPoint();
- $mountEntry = self::prepareMountPointEntry($storage, true);
- foreach ($mountEntry['options'] as &$option) {
- $option = self::setUserVars($uid, $option);
- }
- $mountPoints[$mountPoint] = $mountEntry;
- }
-
- $userGlobalStoragesService->resetUser();
- $userStoragesService->resetUser();
-
- return $mountPoints;
- }
-
- /**
- * Get the system mount points
- *
- * @return array
- *
- * @deprecated 8.2.0 use GlobalStoragesService::getStorages()
- */
- public static function getSystemMountPoints() {
- $mountPoints = [];
- $service = self::$app->getContainer()->query('OCA\Files_External\Service\GlobalStoragesService');
-
- foreach ($service->getStorages() as $storage) {
- $mountPoints[] = self::prepareMountPointEntry($storage, false);
- }
-
- return $mountPoints;
- }
-
- /**
- * Get the personal mount points of the current user
- *
- * @return array
- *
- * @deprecated 8.2.0 use UserStoragesService::getStorages()
- */
- public static function getPersonalMountPoints() {
- $mountPoints = [];
- $service = self::$app->getContainer()->query('OCA\Files_External\Service\UserStoragesService');
-
- foreach ($service->getStorages() as $storage) {
- $mountPoints[] = self::prepareMountPointEntry($storage, true);
- }
-
- return $mountPoints;
- }
-
- /**
- * Convert a StorageConfig to the legacy mountPoints array format
- * There's a lot of extra information in here, to satisfy all of the legacy functions
- *
- * @param StorageConfig $storage
- * @param bool $isPersonal
- * @return array
- */
- private static function prepareMountPointEntry(StorageConfig $storage, $isPersonal) {
- $mountEntry = [];
-
- $mountEntry['mountpoint'] = substr($storage->getMountPoint(), 1); // remove leading slash
- $mountEntry['class'] = $storage->getBackend()->getIdentifier();
- $mountEntry['backend'] = $storage->getBackend()->getText();
- $mountEntry['authMechanism'] = $storage->getAuthMechanism()->getIdentifier();
- $mountEntry['personal'] = $isPersonal;
- $mountEntry['options'] = self::decryptPasswords($storage->getBackendOptions());
- $mountEntry['mountOptions'] = $storage->getMountOptions();
- $mountEntry['priority'] = $storage->getPriority();
- $mountEntry['applicable'] = [
- 'groups' => $storage->getApplicableGroups(),
- 'users' => $storage->getApplicableUsers(),
- ];
- // if mountpoint is applicable to all users the old API expects ['all']
- if (empty($mountEntry['applicable']['groups']) && empty($mountEntry['applicable']['users'])) {
- $mountEntry['applicable']['users'] = ['all'];
- }
-
- $mountEntry['id'] = $storage->getId();
-
- return $mountEntry;
- }
-
- /**
- * fill in the correct values for $user
- *
- * @param string $user user value
- * @param string|array $input
- * @return string
- */
- public static function setUserVars($user, $input) {
- if (is_array($input)) {
- foreach ($input as &$value) {
- if (is_string($value)) {
- $value = str_replace('$user', $user, $value);
- }
- }
- } else {
- if (is_string($input)) {
- $input = str_replace('$user', $user, $input);
- }
- }
- return $input;
- }
-
- /**
- * Test connecting using the given backend configuration
- *
- * @param string $class backend class name
- * @param array $options backend configuration options
- * @param boolean $isPersonal
- * @return int see self::STATUS_*
- * @throws Exception
- */
- public static function getBackendStatus($class, $options, $isPersonal) {
- if (self::$skipTest) {
- return StorageNotAvailableException::STATUS_SUCCESS;
- }
- foreach ($options as &$option) {
- $option = self::setUserVars(OCP\User::getUser(), $option);
- }
- if (class_exists($class)) {
- try {
- /** @var \OC\Files\Storage\Common $storage */
- $storage = new $class($options);
-
- try {
- $result = $storage->test($isPersonal);
- $storage->setAvailability($result);
- if ($result) {
- return StorageNotAvailableException::STATUS_SUCCESS;
- }
- } catch (\Exception $e) {
- $storage->setAvailability(false);
- throw $e;
- }
- } catch (Exception $exception) {
- \OCP\Util::logException('files_external', $exception);
- throw $exception;
- }
- }
- return StorageNotAvailableException::STATUS_ERROR;
- }
-
- /**
- * Read the mount points in the config file into an array
- *
- * @param string|null $user If not null, personal for $user, otherwise system
- * @return array
- */
- public static function readData($user = null) {
- if (isset($user)) {
- $jsonFile = \OC::$server->getUserManager()->get($user)->getHome() . '/mount.json';
- } else {
- $config = \OC::$server->getConfig();
- $datadir = $config->getSystemValue('datadirectory', \OC::$SERVERROOT . '/data/');
- $jsonFile = $config->getSystemValue('mount_file', $datadir . '/mount.json');
- }
- if (is_file($jsonFile)) {
- $mountPoints = json_decode(file_get_contents($jsonFile), true);
- if (is_array($mountPoints)) {
- return $mountPoints;
- }
- }
- return array();
- }
-
- /**
- * Get backend dependency message
- * TODO: move into AppFramework along with templates
- *
- * @param Backend[] $backends
- * @return string
- */
- public static function dependencyMessage($backends) {
- $l = \OC::$server->getL10N('files_external');
- $message = '';
- $dependencyGroups = [];
-
- foreach ($backends as $backend) {
- foreach ($backend->checkDependencies() as $dependency) {
- if ($message = $dependency->getMessage()) {
- $message .= '<br />' . $l->t('<b>Note:</b> ') . $message;
- } else {
- $dependencyGroups[$dependency->getDependency()][] = $backend;
- }
- }
- }
-
- foreach ($dependencyGroups as $module => $dependants) {
- $backends = implode(', ', array_map(function($backend) {
- return '<i>' . $backend->getText() . '</i>';
- }, $dependants));
- $message .= '<br />' . OC_Mount_Config::getSingleDependencyMessage($l, $module, $backends);
- }
-
- return $message;
- }
-
- /**
- * Returns a dependency missing message
- *
- * @param \OCP\IL10N $l
- * @param string $module
- * @param string $backend
- * @return string
- */
- private static function getSingleDependencyMessage(\OCP\IL10N $l, $module, $backend) {
- switch (strtolower($module)) {
- case 'curl':
- return (string)$l->t('<b>Note:</b> The cURL support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', $backend);
- case 'ftp':
- return (string)$l->t('<b>Note:</b> The FTP support in PHP is not enabled or installed. Mounting of %s is not possible. Please ask your system administrator to install it.', $backend);
- default:
- return (string)$l->t('<b>Note:</b> "%s" is not installed. Mounting of %s is not possible. Please ask your system administrator to install it.', array($module, $backend));
- }
- }
-
- /**
- * Encrypt passwords in the given config options
- *
- * @param array $options mount options
- * @return array updated options
- */
- public static function encryptPasswords($options) {
- if (isset($options['password'])) {
- $options['password_encrypted'] = self::encryptPassword($options['password']);
- // do not unset the password, we want to keep the keys order
- // on load... because that's how the UI currently works
- $options['password'] = '';
- }
- return $options;
- }
-
- /**
- * Decrypt passwords in the given config options
- *
- * @param array $options mount options
- * @return array updated options
- */
- public static function decryptPasswords($options) {
- // note: legacy options might still have the unencrypted password in the "password" field
- if (isset($options['password_encrypted'])) {
- $options['password'] = self::decryptPassword($options['password_encrypted']);
- unset($options['password_encrypted']);
- }
- return $options;
- }
-
- /**
- * Encrypt a single password
- *
- * @param string $password plain text password
- * @return string encrypted password
- */
- private static function encryptPassword($password) {
- $cipher = self::getCipher();
- $iv = \OCP\Util::generateRandomBytes(16);
- $cipher->setIV($iv);
- return base64_encode($iv . $cipher->encrypt($password));
- }
-
- /**
- * Decrypts a single password
- *
- * @param string $encryptedPassword encrypted password
- * @return string plain text password
- */
- private static function decryptPassword($encryptedPassword) {
- $cipher = self::getCipher();
- $binaryPassword = base64_decode($encryptedPassword);
- $iv = substr($binaryPassword, 0, 16);
- $cipher->setIV($iv);
- $binaryPassword = substr($binaryPassword, 16);
- return $cipher->decrypt($binaryPassword);
- }
-
- /**
- * Returns the encryption cipher
- *
- * @return AES
- */
- private static function getCipher() {
- $cipher = new AES(AES::MODE_CBC);
- $cipher->setKey(\OC::$server->getConfig()->getSystemValue('passwordsalt', null));
- return $cipher;
- }
-
- /**
- * Computes a hash based on the given configuration.
- * This is mostly used to find out whether configurations
- * are the same.
- *
- * @param array $config
- * @return string
- */
- public static function makeConfigHash($config) {
- $data = json_encode(
- array(
- 'c' => $config['backend'],
- 'a' => $config['authMechanism'],
- 'm' => $config['mountpoint'],
- 'o' => $config['options'],
- 'p' => isset($config['priority']) ? $config['priority'] : -1,
- 'mo' => isset($config['mountOptions']) ? $config['mountOptions'] : [],
- )
- );
- return hash('md5', $data);
- }
-}
diff --git a/apps/files_external/lib/config/configadapter.php b/apps/files_external/lib/config/configadapter.php
deleted file mode 100644
index a19a111d3d9..00000000000
--- a/apps/files_external/lib/config/configadapter.php
+++ /dev/null
@@ -1,183 +0,0 @@
-<?php
-/**
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Config;
-
-use OC\Files\Storage\Wrapper\Availability;
-use OCA\Files_external\Migration\StorageMigrator;
-use OCP\Files\Storage;
-use OC\Files\Mount\MountPoint;
-use OCP\Files\Storage\IStorageFactory;
-use OCA\Files_External\Lib\PersonalMount;
-use OCP\Files\Config\IMountProvider;
-use OCP\IUser;
-use OCA\Files_external\Service\UserStoragesService;
-use OCA\Files_External\Service\UserGlobalStoragesService;
-use OCA\Files_External\Lib\StorageConfig;
-use OC\Files\Storage\FailedStorage;
-use OCP\Files\StorageNotAvailableException;
-
-/**
- * Make the old files_external config work with the new public mount config api
- */
-class ConfigAdapter implements IMountProvider {
-
- /** @var UserStoragesService */
- private $userStoragesService;
-
- /** @var UserGlobalStoragesService */
- private $userGlobalStoragesService;
- /** @var StorageMigrator */
- private $migrator;
-
- /**
- * @param UserStoragesService $userStoragesService
- * @param UserGlobalStoragesService $userGlobalStoragesService
- * @param StorageMigrator $migrator
- */
- public function __construct(
- UserStoragesService $userStoragesService,
- UserGlobalStoragesService $userGlobalStoragesService,
- StorageMigrator $migrator
- ) {
- $this->userStoragesService = $userStoragesService;
- $this->userGlobalStoragesService = $userGlobalStoragesService;
- $this->migrator = $migrator;
- }
-
- /**
- * Process storage ready for mounting
- *
- * @param StorageConfig $storage
- * @param IUser $user
- */
- private function prepareStorageConfig(StorageConfig &$storage, IUser $user) {
- foreach ($storage->getBackendOptions() as $option => $value) {
- $storage->setBackendOption($option, \OC_Mount_Config::setUserVars(
- $user->getUID(), $value
- ));
- }
-
- $objectStore = $storage->getBackendOption('objectstore');
- if ($objectStore) {
- $objectClass = $objectStore['class'];
- if (!is_subclass_of($objectClass, '\OCP\Files\ObjectStore\IObjectStore')) {
- throw new \InvalidArgumentException('Invalid object store');
- }
- $storage->setBackendOption('objectstore', new $objectClass($objectStore));
- }
-
- $storage->getAuthMechanism()->manipulateStorageConfig($storage, $user);
- $storage->getBackend()->manipulateStorageConfig($storage, $user);
- }
-
- /**
- * Construct the storage implementation
- *
- * @param StorageConfig $storageConfig
- * @return Storage
- */
- private function constructStorage(StorageConfig $storageConfig) {
- $class = $storageConfig->getBackend()->getStorageClass();
- $storage = new $class($storageConfig->getBackendOptions());
-
- // auth mechanism should fire first
- $storage = $storageConfig->getBackend()->wrapStorage($storage);
- $storage = $storageConfig->getAuthMechanism()->wrapStorage($storage);
-
- return $storage;
- }
-
- /**
- * Get all mountpoints applicable for the user
- *
- * @param \OCP\IUser $user
- * @param \OCP\Files\Storage\IStorageFactory $loader
- * @return \OCP\Files\Mount\IMountPoint[]
- */
- public function getMountsForUser(IUser $user, IStorageFactory $loader) {
- $this->migrator->migrateUser($user);
-
- $mounts = [];
-
- $this->userStoragesService->setUser($user);
- $this->userGlobalStoragesService->setUser($user);
-
- foreach ($this->userGlobalStoragesService->getUniqueStorages() as $storage) {
- try {
- $this->prepareStorageConfig($storage, $user);
- $impl = $this->constructStorage($storage);
- } catch (\Exception $e) {
- // propagate exception into filesystem
- $impl = new FailedStorage(['exception' => $e]);
- }
-
- try {
- $availability = $impl->getAvailability();
- if (!$availability['available'] && !Availability::shouldRecheck($availability)) {
- $impl = new FailedStorage([
- 'exception' => new StorageNotAvailableException('Storage with mount id ' . $storage->getId() . ' is not available')
- ]);
- }
- } catch (\Exception $e) {
- // propagate exception into filesystem
- $impl = new FailedStorage(['exception' => $e]);
- }
-
- $mount = new MountPoint(
- $impl,
- '/' . $user->getUID() . '/files' . $storage->getMountPoint(),
- null,
- $loader,
- $storage->getMountOptions()
- );
- $mounts[$storage->getMountPoint()] = $mount;
- }
-
- foreach ($this->userStoragesService->getStorages() as $storage) {
- try {
- $this->prepareStorageConfig($storage, $user);
- $impl = $this->constructStorage($storage);
- } catch (\Exception $e) {
- // propagate exception into filesystem
- $impl = new FailedStorage(['exception' => $e]);
- }
-
- $mount = new PersonalMount(
- $this->userStoragesService,
- $storage->getId(),
- $impl,
- '/' . $user->getUID() . '/files' . $storage->getMountPoint(),
- null,
- $loader,
- $storage->getMountOptions()
- );
- $mounts[$storage->getMountPoint()] = $mount;
- }
-
- $this->userStoragesService->resetUser();
- $this->userGlobalStoragesService->resetUser();
-
- return $mounts;
- }
-}
diff --git a/apps/files_external/lib/definitionparameter.php b/apps/files_external/lib/definitionparameter.php
deleted file mode 100644
index 16c07f4b8cc..00000000000
--- a/apps/files_external/lib/definitionparameter.php
+++ /dev/null
@@ -1,193 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-/**
- * Parameter for an external storage definition
- */
-class DefinitionParameter implements \JsonSerializable {
-
- /** Value constants */
- const VALUE_TEXT = 0;
- const VALUE_BOOLEAN = 1;
- const VALUE_PASSWORD = 2;
- const VALUE_HIDDEN = 3;
-
- /** Flag constants */
- const FLAG_NONE = 0;
- const FLAG_OPTIONAL = 1;
- const FLAG_USER_PROVIDED = 2;
-
- /** @var string name of parameter */
- private $name;
-
- /** @var string human-readable parameter text */
- private $text;
-
- /** @var int value type, see self::VALUE_* constants */
- private $type = self::VALUE_TEXT;
-
- /** @var int flags, see self::FLAG_* constants */
- private $flags = self::FLAG_NONE;
-
- /**
- * @param string $name
- * @param string $text
- */
- public function __construct($name, $text) {
- $this->name = $name;
- $this->text = $text;
- }
-
- /**
- * @return string
- */
- public function getName() {
- return $this->name;
- }
-
- /**
- * @return string
- */
- public function getText() {
- return $this->text;
- }
-
- /**
- * Get value type
- *
- * @return int
- */
- public function getType() {
- return $this->type;
- }
-
- /**
- * Set value type
- *
- * @param int $type
- * @return self
- */
- public function setType($type) {
- $this->type = $type;
- return $this;
- }
-
- /**
- * @return string
- */
- public function getTypeName() {
- switch ($this->type) {
- case self::VALUE_BOOLEAN:
- return 'boolean';
- case self::VALUE_TEXT:
- return 'text';
- case self::VALUE_PASSWORD:
- return 'password';
- default:
- return 'unknown';
- }
- }
-
- /**
- * @return int
- */
- public function getFlags() {
- return $this->flags;
- }
-
- /**
- * @param int $flags
- * @return self
- */
- public function setFlags($flags) {
- $this->flags = $flags;
- return $this;
- }
-
- /**
- * @param int $flag
- * @return self
- */
- public function setFlag($flag) {
- $this->flags |= $flag;
- return $this;
- }
-
- /**
- * @param int $flag
- * @return bool
- */
- public function isFlagSet($flag) {
- return (bool)($this->flags & $flag);
- }
-
- /**
- * Serialize into JSON for client-side JS
- *
- * @return string
- */
- public function jsonSerialize() {
- return [
- 'value' => $this->getText(),
- 'flags' => $this->getFlags(),
- 'type' => $this->getType()
- ];
- }
-
- public function isOptional() {
- return $this->isFlagSet(self::FLAG_OPTIONAL) || $this->isFlagSet(self::FLAG_USER_PROVIDED);
- }
-
- /**
- * Validate a parameter value against this
- * Convert type as necessary
- *
- * @param mixed $value Value to check
- * @return bool success
- */
- public function validateValue(&$value) {
- switch ($this->getType()) {
- case self::VALUE_BOOLEAN:
- if (!is_bool($value)) {
- switch ($value) {
- case 'true':
- $value = true;
- break;
- case 'false':
- $value = false;
- break;
- default:
- return false;
- }
- }
- break;
- default:
- if (!$value && !$this->isOptional()) {
- return false;
- }
- break;
- }
- return true;
- }
-}
diff --git a/apps/files_external/lib/dependencytrait.php b/apps/files_external/lib/dependencytrait.php
deleted file mode 100644
index eed3ba1b327..00000000000
--- a/apps/files_external/lib/dependencytrait.php
+++ /dev/null
@@ -1,41 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use \OCA\Files_External\Lib\MissingDependency;
-
-/**
- * Trait for objects that have dependencies for use
- */
-trait DependencyTrait {
-
- /**
- * Check if object is valid for use
- *
- * @return MissingDependency[] Unsatisfied dependencies
- */
- public function checkDependencies() {
- return []; // no dependencies by default
- }
-
-}
-
diff --git a/apps/files_external/lib/dropbox.php b/apps/files_external/lib/dropbox.php
deleted file mode 100644
index 8381ccbae59..00000000000
--- a/apps/files_external/lib/dropbox.php
+++ /dev/null
@@ -1,352 +0,0 @@
-<?php
-/**
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Sascha Schmidt <realriot@realriot.de>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-use GuzzleHttp\Exception\RequestException;
-use Icewind\Streams\IteratorDirectory;
-use Icewind\Streams\RetryWrapper;
-
-require_once __DIR__ . '/../3rdparty/Dropbox/autoload.php';
-
-class Dropbox extends \OC\Files\Storage\Common {
-
- private $dropbox;
- private $root;
- private $id;
- private $metaData = array();
- private $oauth;
-
- private static $tempFiles = array();
-
- public function __construct($params) {
- if (isset($params['configured']) && $params['configured'] == 'true'
- && isset($params['app_key'])
- && isset($params['app_secret'])
- && isset($params['token'])
- && isset($params['token_secret'])
- ) {
- $this->root = isset($params['root']) ? $params['root'] : '';
- $this->id = 'dropbox::'.$params['app_key'] . $params['token']. '/' . $this->root;
- $this->oauth = new \Dropbox_OAuth_Curl($params['app_key'], $params['app_secret']);
- $this->oauth->setToken($params['token'], $params['token_secret']);
- // note: Dropbox_API connection is lazy
- $this->dropbox = new \Dropbox_API($this->oauth, 'auto');
- } else {
- throw new \Exception('Creating \OC\Files\Storage\Dropbox storage failed');
- }
- }
-
- /**
- * @param string $path
- */
- private function deleteMetaData($path) {
- $path = ltrim($this->root.$path, '/');
- if (isset($this->metaData[$path])) {
- unset($this->metaData[$path]);
- return true;
- }
- return false;
- }
-
- private function setMetaData($path, $metaData) {
- $this->metaData[ltrim($path, '/')] = $metaData;
- }
-
- /**
- * Returns the path's metadata
- * @param string $path path for which to return the metadata
- * @param bool $list if true, also return the directory's contents
- * @return mixed directory contents if $list is true, file metadata if $list is
- * false, null if the file doesn't exist or "false" if the operation failed
- */
- private function getDropBoxMetaData($path, $list = false) {
- $path = ltrim($this->root.$path, '/');
- if ( ! $list && isset($this->metaData[$path])) {
- return $this->metaData[$path];
- } else {
- if ($list) {
- try {
- $response = $this->dropbox->getMetaData($path);
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- $contents = array();
- if ($response && isset($response['contents'])) {
- // Cache folder's contents
- foreach ($response['contents'] as $file) {
- if (!isset($file['is_deleted']) || !$file['is_deleted']) {
- $this->setMetaData($path.'/'.basename($file['path']), $file);
- $contents[] = $file;
- }
- }
- unset($response['contents']);
- }
- if (!isset($response['is_deleted']) || !$response['is_deleted']) {
- $this->setMetaData($path, $response);
- }
- // Return contents of folder only
- return $contents;
- } else {
- try {
- $requestPath = $path;
- if ($path === '.') {
- $requestPath = '';
- }
-
- $response = $this->dropbox->getMetaData($requestPath, 'false');
- if (!isset($response['is_deleted']) || !$response['is_deleted']) {
- $this->setMetaData($path, $response);
- return $response;
- }
- return null;
- } catch (\Exception $exception) {
- if ($exception instanceof \Dropbox_Exception_NotFound) {
- // don't log, might be a file_exist check
- return false;
- }
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- }
- }
- }
-
- public function getId(){
- return $this->id;
- }
-
- public function mkdir($path) {
- $path = $this->root.$path;
- try {
- $this->dropbox->createFolder($path);
- return true;
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- }
-
- public function rmdir($path) {
- return $this->unlink($path);
- }
-
- public function opendir($path) {
- $contents = $this->getDropBoxMetaData($path, true);
- if ($contents !== false) {
- $files = array();
- foreach ($contents as $file) {
- $files[] = basename($file['path']);
- }
- return IteratorDirectory::wrap($files);
- }
- return false;
- }
-
- public function stat($path) {
- $metaData = $this->getDropBoxMetaData($path);
- if ($metaData) {
- $stat['size'] = $metaData['bytes'];
- $stat['atime'] = time();
- $stat['mtime'] = (isset($metaData['modified'])) ? strtotime($metaData['modified']) : time();
- return $stat;
- }
- return false;
- }
-
- public function filetype($path) {
- if ($path == '' || $path == '/') {
- return 'dir';
- } else {
- $metaData = $this->getDropBoxMetaData($path);
- if ($metaData) {
- if ($metaData['is_dir'] == 'true') {
- return 'dir';
- } else {
- return 'file';
- }
- }
- }
- return false;
- }
-
- public function file_exists($path) {
- if ($path == '' || $path == '/') {
- return true;
- }
- if ($this->getDropBoxMetaData($path)) {
- return true;
- }
- return false;
- }
-
- public function unlink($path) {
- try {
- $this->dropbox->delete($this->root.$path);
- $this->deleteMetaData($path);
- return true;
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- }
-
- public function rename($path1, $path2) {
- try {
- // overwrite if target file exists and is not a directory
- $destMetaData = $this->getDropBoxMetaData($path2);
- if (isset($destMetaData) && $destMetaData !== false && !$destMetaData['is_dir']) {
- $this->unlink($path2);
- }
- $this->dropbox->move($this->root.$path1, $this->root.$path2);
- $this->deleteMetaData($path1);
- return true;
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- }
-
- public function copy($path1, $path2) {
- $path1 = $this->root.$path1;
- $path2 = $this->root.$path2;
- try {
- $this->dropbox->copy($path1, $path2);
- return true;
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- }
-
- public function fopen($path, $mode) {
- $path = $this->root.$path;
- switch ($mode) {
- case 'r':
- case 'rb':
- try {
- // slashes need to stay
- $encodedPath = str_replace('%2F', '/', rawurlencode(trim($path, '/')));
- $downloadUrl = 'https://api-content.dropbox.com/1/files/auto/' . $encodedPath;
- $headers = $this->oauth->getOAuthHeader($downloadUrl, [], 'GET');
-
- $client = \OC::$server->getHTTPClientService()->newClient();
- try {
- $response = $client->get($downloadUrl, [
- 'headers' => $headers,
- 'stream' => true,
- ]);
- } catch (RequestException $e) {
- if (!is_null($e->getResponse())) {
- if ($e->getResponse()->getStatusCode() === 404) {
- return false;
- } else {
- throw $e;
- }
- } else {
- throw $e;
- }
- }
-
- $handle = $response->getBody();
- return RetryWrapper::wrap($handle);
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- if (strrpos($path, '.') !== false) {
- $ext = substr($path, strrpos($path, '.'));
- } else {
- $ext = '';
- }
- $tmpFile = \OCP\Files::tmpFile($ext);
- \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack'));
- if ($this->file_exists($path)) {
- $source = $this->fopen($path, 'r');
- file_put_contents($tmpFile, $source);
- }
- self::$tempFiles[$tmpFile] = $path;
- return fopen('close://'.$tmpFile, $mode);
- }
- return false;
- }
-
- public function writeBack($tmpFile) {
- if (isset(self::$tempFiles[$tmpFile])) {
- $handle = fopen($tmpFile, 'r');
- try {
- $this->dropbox->putFile(self::$tempFiles[$tmpFile], $handle);
- unlink($tmpFile);
- $this->deleteMetaData(self::$tempFiles[$tmpFile]);
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- }
- }
- }
-
- public function free_space($path) {
- try {
- $info = $this->dropbox->getAccountInfo();
- return $info['quota_info']['quota'] - $info['quota_info']['normal'];
- } catch (\Exception $exception) {
- \OCP\Util::writeLog('files_external', $exception->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- }
-
- public function touch($path, $mtime = null) {
- if ($this->file_exists($path)) {
- return false;
- } else {
- $this->file_put_contents($path, '');
- }
- return true;
- }
-
- /**
- * check if curl is installed
- */
- public static function checkDependencies() {
- return true;
- }
-
-}
diff --git a/apps/files_external/lib/frontenddefinitiontrait.php b/apps/files_external/lib/frontenddefinitiontrait.php
deleted file mode 100644
index 9f2b7c40f7f..00000000000
--- a/apps/files_external/lib/frontenddefinitiontrait.php
+++ /dev/null
@@ -1,150 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use \OCA\Files_External\Lib\DefinitionParameter;
-use \OCA\Files_External\Lib\StorageConfig;
-
-/**
- * Trait for objects that have a frontend representation
- */
-trait FrontendDefinitionTrait {
-
- /** @var string human-readable mechanism name */
- private $text;
-
- /** @var DefinitionParameter[] parameters for mechanism */
- private $parameters = [];
-
- /** @var string|null custom JS */
- private $customJs = null;
-
- /**
- * @return string
- */
- public function getText() {
- return $this->text;
- }
-
- /**
- * @param string $text
- * @return self
- */
- public function setText($text) {
- $this->text = $text;
- return $this;
- }
-
- /**
- * @param FrontendDefinitionTrait $a
- * @param FrontendDefinitionTrait $b
- * @return int
- */
- public static function lexicalCompare(FrontendDefinitionTrait $a, FrontendDefinitionTrait $b) {
- return strcmp($a->getText(), $b->getText());
- }
-
- /**
- * @return DefinitionParameter[]
- */
- public function getParameters() {
- return $this->parameters;
- }
-
- /**
- * @param DefinitionParameter[] $parameters
- * @return self
- */
- public function addParameters(array $parameters) {
- foreach ($parameters as $parameter) {
- $this->addParameter($parameter);
- }
- return $this;
- }
-
- /**
- * @param DefinitionParameter $parameter
- * @return self
- */
- public function addParameter(DefinitionParameter $parameter) {
- $this->parameters[$parameter->getName()] = $parameter;
- return $this;
- }
-
- /**
- * @return string|null
- */
- public function getCustomJs() {
- return $this->customJs;
- }
-
- /**
- * @param string $custom
- * @return self
- */
- public function setCustomJs($custom) {
- $this->customJs = $custom;
- return $this;
- }
-
- /**
- * Serialize into JSON for client-side JS
- *
- * @return array
- */
- public function jsonSerializeDefinition() {
- $configuration = [];
- foreach ($this->getParameters() as $parameter) {
- $configuration[$parameter->getName()] = $parameter;
- }
-
- $data = [
- 'name' => $this->getText(),
- 'configuration' => $configuration,
- ];
- if (isset($this->customJs)) {
- $data['custom'] = $this->getCustomJs();
- }
- return $data;
- }
-
- /**
- * Check if parameters are satisfied in a StorageConfig
- *
- * @param StorageConfig $storage
- * @return bool
- */
- public function validateStorageDefinition(StorageConfig $storage) {
- foreach ($this->getParameters() as $name => $parameter) {
- $value = $storage->getBackendOption($name);
- if (!is_null($value) || !$parameter->isOptional()) {
- if (!$parameter->validateValue($value)) {
- return false;
- }
- $storage->setBackendOption($name, $value);
- }
- }
- return true;
- }
-
-}
diff --git a/apps/files_external/lib/ftp.php b/apps/files_external/lib/ftp.php
deleted file mode 100644
index 7249aeceb5d..00000000000
--- a/apps/files_external/lib/ftp.php
+++ /dev/null
@@ -1,155 +0,0 @@
-<?php
-/**
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Felix Moeller <mail@felixmoeller.de>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-use Icewind\Streams\RetryWrapper;
-
-class FTP extends \OC\Files\Storage\StreamWrapper{
- private $password;
- private $user;
- private $host;
- private $secure;
- private $root;
-
- private static $tempFiles=array();
-
- public function __construct($params) {
- if (isset($params['host']) && isset($params['user']) && isset($params['password'])) {
- $this->host=$params['host'];
- $this->user=$params['user'];
- $this->password=$params['password'];
- if (isset($params['secure'])) {
- $this->secure = $params['secure'];
- } else {
- $this->secure = false;
- }
- $this->root=isset($params['root'])?$params['root']:'/';
- if ( ! $this->root || $this->root[0]!='/') {
- $this->root='/'.$this->root;
- }
- if (substr($this->root, -1) !== '/') {
- $this->root .= '/';
- }
- } else {
- throw new \Exception('Creating \OC\Files\Storage\FTP storage failed');
- }
-
- }
-
- public function getId(){
- return 'ftp::' . $this->user . '@' . $this->host . '/' . $this->root;
- }
-
- /**
- * construct the ftp url
- * @param string $path
- * @return string
- */
- public function constructUrl($path) {
- $url='ftp';
- if ($this->secure) {
- $url.='s';
- }
- $url.='://'.urlencode($this->user).':'.urlencode($this->password).'@'.$this->host.$this->root.$path;
- return $url;
- }
-
- /**
- * Unlinks file or directory
- * @param string $path
- */
- public function unlink($path) {
- if ($this->is_dir($path)) {
- return $this->rmdir($path);
- }
- else {
- $url = $this->constructUrl($path);
- $result = unlink($url);
- clearstatcache(true, $url);
- return $result;
- }
- }
- public function fopen($path,$mode) {
- switch($mode) {
- case 'r':
- case 'rb':
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- //these are supported by the wrapper
- $context = stream_context_create(array('ftp' => array('overwrite' => true)));
- $handle = fopen($this->constructUrl($path), $mode, false, $context);
- return RetryWrapper::wrap($handle);
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- //emulate these
- if (strrpos($path, '.')!==false) {
- $ext=substr($path, strrpos($path, '.'));
- } else {
- $ext='';
- }
- $tmpFile=\OCP\Files::tmpFile($ext);
- \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack'));
- if ($this->file_exists($path)) {
- $this->getFile($path, $tmpFile);
- }
- self::$tempFiles[$tmpFile]=$path;
- return fopen('close://'.$tmpFile, $mode);
- }
- return false;
- }
-
- public function writeBack($tmpFile) {
- if (isset(self::$tempFiles[$tmpFile])) {
- $this->uploadFile($tmpFile, self::$tempFiles[$tmpFile]);
- unlink($tmpFile);
- }
- }
-
- /**
- * check if php-ftp is installed
- */
- public static function checkDependencies() {
- if (function_exists('ftp_login')) {
- return(true);
- } else {
- return array('ftp');
- }
- }
-
-}
diff --git a/apps/files_external/lib/google.php b/apps/files_external/lib/google.php
deleted file mode 100644
index 62d264dfeef..00000000000
--- a/apps/files_external/lib/google.php
+++ /dev/null
@@ -1,710 +0,0 @@
-<?php
-/**
- * @author Adam Williamson <awilliam@redhat.com>
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Christopher Schäpers <kondou@ts.unde.re>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-use GuzzleHttp\Exception\RequestException;
-use Icewind\Streams\IteratorDirectory;
-use Icewind\Streams\RetryWrapper;
-
-set_include_path(get_include_path().PATH_SEPARATOR.
- \OC_App::getAppPath('files_external').'/3rdparty/google-api-php-client/src');
-require_once 'Google/Client.php';
-require_once 'Google/Service/Drive.php';
-
-class Google extends \OC\Files\Storage\Common {
-
- private $client;
- private $id;
- private $service;
- private $driveFiles;
-
- private static $tempFiles = array();
-
- // Google Doc mimetypes
- const FOLDER = 'application/vnd.google-apps.folder';
- const DOCUMENT = 'application/vnd.google-apps.document';
- const SPREADSHEET = 'application/vnd.google-apps.spreadsheet';
- const DRAWING = 'application/vnd.google-apps.drawing';
- const PRESENTATION = 'application/vnd.google-apps.presentation';
-
- public function __construct($params) {
- if (isset($params['configured']) && $params['configured'] === 'true'
- && isset($params['client_id']) && isset($params['client_secret'])
- && isset($params['token'])
- ) {
- $this->client = new \Google_Client();
- $this->client->setClientId($params['client_id']);
- $this->client->setClientSecret($params['client_secret']);
- $this->client->setScopes(array('https://www.googleapis.com/auth/drive'));
- $this->client->setAccessToken($params['token']);
- // if curl isn't available we're likely to run into
- // https://github.com/google/google-api-php-client/issues/59
- // - disable gzip to avoid it.
- if (!function_exists('curl_version') || !function_exists('curl_exec')) {
- $this->client->setClassConfig("Google_Http_Request", "disable_gzip", true);
- }
- // note: API connection is lazy
- $this->service = new \Google_Service_Drive($this->client);
- $token = json_decode($params['token'], true);
- $this->id = 'google::'.substr($params['client_id'], 0, 30).$token['created'];
- } else {
- throw new \Exception('Creating \OC\Files\Storage\Google storage failed');
- }
- }
-
- public function getId() {
- return $this->id;
- }
-
- /**
- * Get the Google_Service_Drive_DriveFile object for the specified path.
- * Returns false on failure.
- * @param string $path
- * @return \Google_Service_Drive_DriveFile|false
- */
- private function getDriveFile($path) {
- // Remove leading and trailing slashes
- $path = trim($path, '/');
- if (isset($this->driveFiles[$path])) {
- return $this->driveFiles[$path];
- } else if ($path === '') {
- $root = $this->service->files->get('root');
- $this->driveFiles[$path] = $root;
- return $root;
- } else {
- // Google Drive SDK does not have methods for retrieving files by path
- // Instead we must find the id of the parent folder of the file
- $parentId = $this->getDriveFile('')->getId();
- $folderNames = explode('/', $path);
- $path = '';
- // Loop through each folder of this path to get to the file
- foreach ($folderNames as $name) {
- // Reconstruct path from beginning
- if ($path === '') {
- $path .= $name;
- } else {
- $path .= '/'.$name;
- }
- if (isset($this->driveFiles[$path])) {
- $parentId = $this->driveFiles[$path]->getId();
- } else {
- $q = "title='" . str_replace("'","\\'", $name) . "' and '" . str_replace("'","\\'", $parentId) . "' in parents and trashed = false";
- $result = $this->service->files->listFiles(array('q' => $q))->getItems();
- if (!empty($result)) {
- // Google Drive allows files with the same name, ownCloud doesn't
- if (count($result) > 1) {
- $this->onDuplicateFileDetected($path);
- return false;
- } else {
- $file = current($result);
- $this->driveFiles[$path] = $file;
- $parentId = $file->getId();
- }
- } else {
- // Google Docs have no extension in their title, so try without extension
- $pos = strrpos($path, '.');
- if ($pos !== false) {
- $pathWithoutExt = substr($path, 0, $pos);
- $file = $this->getDriveFile($pathWithoutExt);
- if ($file) {
- // Switch cached Google_Service_Drive_DriveFile to the correct index
- unset($this->driveFiles[$pathWithoutExt]);
- $this->driveFiles[$path] = $file;
- $parentId = $file->getId();
- } else {
- return false;
- }
- } else {
- return false;
- }
- }
- }
- }
- return $this->driveFiles[$path];
- }
- }
-
- /**
- * Set the Google_Service_Drive_DriveFile object in the cache
- * @param string $path
- * @param Google_Service_Drive_DriveFile|false $file
- */
- private function setDriveFile($path, $file) {
- $path = trim($path, '/');
- $this->driveFiles[$path] = $file;
- if ($file === false) {
- // Set all child paths as false
- $len = strlen($path);
- foreach ($this->driveFiles as $key => $file) {
- if (substr($key, 0, $len) === $path) {
- $this->driveFiles[$key] = false;
- }
- }
- }
- }
-
- /**
- * Write a log message to inform about duplicate file names
- * @param string $path
- */
- private function onDuplicateFileDetected($path) {
- $about = $this->service->about->get();
- $user = $about->getName();
- \OCP\Util::writeLog('files_external',
- 'Ignoring duplicate file name: '.$path.' on Google Drive for Google user: '.$user,
- \OCP\Util::INFO
- );
- }
-
- /**
- * Generate file extension for a Google Doc, choosing Open Document formats for download
- * @param string $mimetype
- * @return string
- */
- private function getGoogleDocExtension($mimetype) {
- if ($mimetype === self::DOCUMENT) {
- return 'odt';
- } else if ($mimetype === self::SPREADSHEET) {
- return 'ods';
- } else if ($mimetype === self::DRAWING) {
- return 'jpg';
- } else if ($mimetype === self::PRESENTATION) {
- // Download as .odp is not available
- return 'pdf';
- } else {
- return '';
- }
- }
-
- public function mkdir($path) {
- if (!$this->is_dir($path)) {
- $parentFolder = $this->getDriveFile(dirname($path));
- if ($parentFolder) {
- $folder = new \Google_Service_Drive_DriveFile();
- $folder->setTitle(basename($path));
- $folder->setMimeType(self::FOLDER);
- $parent = new \Google_Service_Drive_ParentReference();
- $parent->setId($parentFolder->getId());
- $folder->setParents(array($parent));
- $result = $this->service->files->insert($folder);
- if ($result) {
- $this->setDriveFile($path, $result);
- }
- return (bool)$result;
- }
- }
- return false;
- }
-
- public function rmdir($path) {
- if (!$this->isDeletable($path)) {
- return false;
- }
- if (trim($path, '/') === '') {
- $dir = $this->opendir($path);
- if(is_resource($dir)) {
- while (($file = readdir($dir)) !== false) {
- if (!\OC\Files\Filesystem::isIgnoredDir($file)) {
- if (!$this->unlink($path.'/'.$file)) {
- return false;
- }
- }
- }
- closedir($dir);
- }
- $this->driveFiles = array();
- return true;
- } else {
- return $this->unlink($path);
- }
- }
-
- public function opendir($path) {
- $folder = $this->getDriveFile($path);
- if ($folder) {
- $files = array();
- $duplicates = array();
- $pageToken = true;
- while ($pageToken) {
- $params = array();
- if ($pageToken !== true) {
- $params['pageToken'] = $pageToken;
- }
- $params['q'] = "'" . str_replace("'","\\'", $folder->getId()) . "' in parents and trashed = false";
- $children = $this->service->files->listFiles($params);
- foreach ($children->getItems() as $child) {
- $name = $child->getTitle();
- // Check if this is a Google Doc i.e. no extension in name
- $extension = $child->getFileExtension();
- if (empty($extension)
- && $child->getMimeType() !== self::FOLDER
- ) {
- $name .= '.'.$this->getGoogleDocExtension($child->getMimeType());
- }
- if ($path === '') {
- $filepath = $name;
- } else {
- $filepath = $path.'/'.$name;
- }
- // Google Drive allows files with the same name, ownCloud doesn't
- // Prevent opendir() from returning any duplicate files
- $key = array_search($name, $files);
- if ($key !== false || isset($duplicates[$filepath])) {
- if (!isset($duplicates[$filepath])) {
- $duplicates[$filepath] = true;
- $this->setDriveFile($filepath, false);
- unset($files[$key]);
- $this->onDuplicateFileDetected($filepath);
- }
- } else {
- // Cache the Google_Service_Drive_DriveFile for future use
- $this->setDriveFile($filepath, $child);
- $files[] = $name;
- }
- }
- $pageToken = $children->getNextPageToken();
- }
- return IteratorDirectory::wrap($files);
- } else {
- return false;
- }
- }
-
- public function stat($path) {
- $file = $this->getDriveFile($path);
- if ($file) {
- $stat = array();
- if ($this->filetype($path) === 'dir') {
- $stat['size'] = 0;
- } else {
- // Check if this is a Google Doc
- if ($this->getMimeType($path) !== $file->getMimeType()) {
- // Return unknown file size
- $stat['size'] = \OCP\Files\FileInfo::SPACE_UNKNOWN;
- } else {
- $stat['size'] = $file->getFileSize();
- }
- }
- $stat['atime'] = strtotime($file->getLastViewedByMeDate());
- $stat['mtime'] = strtotime($file->getModifiedDate());
- $stat['ctime'] = strtotime($file->getCreatedDate());
- return $stat;
- } else {
- return false;
- }
- }
-
- public function filetype($path) {
- if ($path === '') {
- return 'dir';
- } else {
- $file = $this->getDriveFile($path);
- if ($file) {
- if ($file->getMimeType() === self::FOLDER) {
- return 'dir';
- } else {
- return 'file';
- }
- } else {
- return false;
- }
- }
- }
-
- public function isUpdatable($path) {
- $file = $this->getDriveFile($path);
- if ($file) {
- return $file->getEditable();
- } else {
- return false;
- }
- }
-
- public function file_exists($path) {
- return (bool)$this->getDriveFile($path);
- }
-
- public function unlink($path) {
- $file = $this->getDriveFile($path);
- if ($file) {
- $result = $this->service->files->trash($file->getId());
- if ($result) {
- $this->setDriveFile($path, false);
- }
- return (bool)$result;
- } else {
- return false;
- }
- }
-
- public function rename($path1, $path2) {
- $file = $this->getDriveFile($path1);
- if ($file) {
- $newFile = $this->getDriveFile($path2);
- if (dirname($path1) === dirname($path2)) {
- if ($newFile) {
- // rename to the name of the target file, could be an office file without extension
- $file->setTitle($newFile->getTitle());
- } else {
- $file->setTitle(basename(($path2)));
- }
- } else {
- // Change file parent
- $parentFolder2 = $this->getDriveFile(dirname($path2));
- if ($parentFolder2) {
- $parent = new \Google_Service_Drive_ParentReference();
- $parent->setId($parentFolder2->getId());
- $file->setParents(array($parent));
- } else {
- return false;
- }
- }
- // We need to get the object for the existing file with the same
- // name (if there is one) before we do the patch. If oldfile
- // exists and is a directory we have to delete it before we
- // do the rename too.
- $oldfile = $this->getDriveFile($path2);
- if ($oldfile && $this->is_dir($path2)) {
- $this->rmdir($path2);
- $oldfile = false;
- }
- $result = $this->service->files->patch($file->getId(), $file);
- if ($result) {
- $this->setDriveFile($path1, false);
- $this->setDriveFile($path2, $result);
- if ($oldfile && $newFile) {
- // only delete if they have a different id (same id can happen for part files)
- if ($newFile->getId() !== $oldfile->getId()) {
- $this->service->files->delete($oldfile->getId());
- }
- }
- }
- return (bool)$result;
- } else {
- return false;
- }
- }
-
- public function fopen($path, $mode) {
- $pos = strrpos($path, '.');
- if ($pos !== false) {
- $ext = substr($path, $pos);
- } else {
- $ext = '';
- }
- switch ($mode) {
- case 'r':
- case 'rb':
- $file = $this->getDriveFile($path);
- if ($file) {
- $exportLinks = $file->getExportLinks();
- $mimetype = $this->getMimeType($path);
- $downloadUrl = null;
- if ($exportLinks && isset($exportLinks[$mimetype])) {
- $downloadUrl = $exportLinks[$mimetype];
- } else {
- $downloadUrl = $file->getDownloadUrl();
- }
- if (isset($downloadUrl)) {
- $request = new \Google_Http_Request($downloadUrl, 'GET', null, null);
- $httpRequest = $this->client->getAuth()->sign($request);
- // the library's service doesn't support streaming, so we use Guzzle instead
- $client = \OC::$server->getHTTPClientService()->newClient();
- try {
- $response = $client->get($downloadUrl, [
- 'headers' => $httpRequest->getRequestHeaders(),
- 'stream' => true,
- 'verify' => __DIR__ . '/../3rdparty/google-api-php-client/src/Google/IO/cacerts.pem',
- ]);
- } catch (RequestException $e) {
- if(!is_null($e->getResponse())) {
- if ($e->getResponse()->getStatusCode() === 404) {
- return false;
- } else {
- throw $e;
- }
- } else {
- throw $e;
- }
- }
-
- $handle = $response->getBody();
- return RetryWrapper::wrap($handle);
- }
- }
- return false;
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- $tmpFile = \OCP\Files::tmpFile($ext);
- \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack'));
- if ($this->file_exists($path)) {
- $source = $this->fopen($path, 'rb');
- file_put_contents($tmpFile, $source);
- }
- self::$tempFiles[$tmpFile] = $path;
- return fopen('close://'.$tmpFile, $mode);
- }
- }
-
- public function writeBack($tmpFile) {
- if (isset(self::$tempFiles[$tmpFile])) {
- $path = self::$tempFiles[$tmpFile];
- $parentFolder = $this->getDriveFile(dirname($path));
- if ($parentFolder) {
- $mimetype = \OC::$server->getMimeTypeDetector()->detect($tmpFile);
- $params = array(
- 'mimeType' => $mimetype,
- 'uploadType' => 'media'
- );
- $result = false;
-
- $chunkSizeBytes = 10 * 1024 * 1024;
-
- $useChunking = false;
- $size = filesize($tmpFile);
- if ($size > $chunkSizeBytes) {
- $useChunking = true;
- } else {
- $params['data'] = file_get_contents($tmpFile);
- }
-
- if ($this->file_exists($path)) {
- $file = $this->getDriveFile($path);
- $this->client->setDefer($useChunking);
- $request = $this->service->files->update($file->getId(), $file, $params);
- } else {
- $file = new \Google_Service_Drive_DriveFile();
- $file->setTitle(basename($path));
- $file->setMimeType($mimetype);
- $parent = new \Google_Service_Drive_ParentReference();
- $parent->setId($parentFolder->getId());
- $file->setParents(array($parent));
- $this->client->setDefer($useChunking);
- $request = $this->service->files->insert($file, $params);
- }
-
- if ($useChunking) {
- // Create a media file upload to represent our upload process.
- $media = new \Google_Http_MediaFileUpload(
- $this->client,
- $request,
- 'text/plain',
- null,
- true,
- $chunkSizeBytes
- );
- $media->setFileSize($size);
-
- // Upload the various chunks. $status will be false until the process is
- // complete.
- $status = false;
- $handle = fopen($tmpFile, 'rb');
- while (!$status && !feof($handle)) {
- $chunk = fread($handle, $chunkSizeBytes);
- $status = $media->nextChunk($chunk);
- }
-
- // The final value of $status will be the data from the API for the object
- // that has been uploaded.
- $result = false;
- if ($status !== false) {
- $result = $status;
- }
-
- fclose($handle);
- } else {
- $result = $request;
- }
-
- // Reset to the client to execute requests immediately in the future.
- $this->client->setDefer(false);
-
- if ($result) {
- $this->setDriveFile($path, $result);
- }
- }
- unlink($tmpFile);
- }
- }
-
- public function getMimeType($path) {
- $file = $this->getDriveFile($path);
- if ($file) {
- $mimetype = $file->getMimeType();
- // Convert Google Doc mimetypes, choosing Open Document formats for download
- if ($mimetype === self::FOLDER) {
- return 'httpd/unix-directory';
- } else if ($mimetype === self::DOCUMENT) {
- return 'application/vnd.oasis.opendocument.text';
- } else if ($mimetype === self::SPREADSHEET) {
- return 'application/x-vnd.oasis.opendocument.spreadsheet';
- } else if ($mimetype === self::DRAWING) {
- return 'image/jpeg';
- } else if ($mimetype === self::PRESENTATION) {
- // Download as .odp is not available
- return 'application/pdf';
- } else {
- // use extension-based detection, could be an encrypted file
- return parent::getMimeType($path);
- }
- } else {
- return false;
- }
- }
-
- public function free_space($path) {
- $about = $this->service->about->get();
- return $about->getQuotaBytesTotal() - $about->getQuotaBytesUsed();
- }
-
- public function touch($path, $mtime = null) {
- $file = $this->getDriveFile($path);
- $result = false;
- if ($file) {
- if (isset($mtime)) {
- // This is just RFC3339, but frustratingly, GDrive's API *requires*
- // the fractions portion be present, while no handy PHP constant
- // for RFC3339 or ISO8601 includes it. So we do it ourselves.
- $file->setModifiedDate(date('Y-m-d\TH:i:s.uP', $mtime));
- $result = $this->service->files->patch($file->getId(), $file, array(
- 'setModifiedDate' => true,
- ));
- } else {
- $result = $this->service->files->touch($file->getId());
- }
- } else {
- $parentFolder = $this->getDriveFile(dirname($path));
- if ($parentFolder) {
- $file = new \Google_Service_Drive_DriveFile();
- $file->setTitle(basename($path));
- $parent = new \Google_Service_Drive_ParentReference();
- $parent->setId($parentFolder->getId());
- $file->setParents(array($parent));
- $result = $this->service->files->insert($file);
- }
- }
- if ($result) {
- $this->setDriveFile($path, $result);
- }
- return (bool)$result;
- }
-
- public function test() {
- if ($this->free_space('')) {
- return true;
- }
- return false;
- }
-
- public function hasUpdated($path, $time) {
- $appConfig = \OC::$server->getAppConfig();
- if ($this->is_file($path)) {
- return parent::hasUpdated($path, $time);
- } else {
- // Google Drive doesn't change modified times of folders when files inside are updated
- // Instead we use the Changes API to see if folders have been updated, and it's a pain
- $folder = $this->getDriveFile($path);
- if ($folder) {
- $result = false;
- $folderId = $folder->getId();
- $startChangeId = $appConfig->getValue('files_external', $this->getId().'cId');
- $params = array(
- 'includeDeleted' => true,
- 'includeSubscribed' => true,
- );
- if (isset($startChangeId)) {
- $startChangeId = (int)$startChangeId;
- $largestChangeId = $startChangeId;
- $params['startChangeId'] = $startChangeId + 1;
- } else {
- $largestChangeId = 0;
- }
- $pageToken = true;
- while ($pageToken) {
- if ($pageToken !== true) {
- $params['pageToken'] = $pageToken;
- }
- $changes = $this->service->changes->listChanges($params);
- if ($largestChangeId === 0 || $largestChangeId === $startChangeId) {
- $largestChangeId = $changes->getLargestChangeId();
- }
- if (isset($startChangeId)) {
- // Check if a file in this folder has been updated
- // There is no way to filter by folder at the API level...
- foreach ($changes->getItems() as $change) {
- $file = $change->getFile();
- if ($file) {
- foreach ($file->getParents() as $parent) {
- if ($parent->getId() === $folderId) {
- $result = true;
- // Check if there are changes in different folders
- } else if ($change->getId() <= $largestChangeId) {
- // Decrement id so this change is fetched when called again
- $largestChangeId = $change->getId();
- $largestChangeId--;
- }
- }
- }
- }
- $pageToken = $changes->getNextPageToken();
- } else {
- // Assuming the initial scan just occurred and changes are negligible
- break;
- }
- }
- $appConfig->setValue('files_external', $this->getId().'cId', $largestChangeId);
- return $result;
- }
- }
- return false;
- }
-
- /**
- * check if curl is installed
- */
- public static function checkDependencies() {
- return true;
- }
-
-}
diff --git a/apps/files_external/lib/identifiertrait.php b/apps/files_external/lib/identifiertrait.php
deleted file mode 100644
index c49f4fcbc8d..00000000000
--- a/apps/files_external/lib/identifiertrait.php
+++ /dev/null
@@ -1,102 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-/**
- * Trait for objects requiring an identifier (and/or identifier aliases)
- * Also supports deprecation to a different object, linking the objects
- */
-trait IdentifierTrait {
-
- /** @var string */
- protected $identifier;
-
- /** @var string[] */
- protected $identifierAliases = [];
-
- /** @var IdentifierTrait */
- protected $deprecateTo = null;
-
- /**
- * @return string
- */
- public function getIdentifier() {
- return $this->identifier;
- }
-
- /**
- * @param string $identifier
- * @return self
- */
- public function setIdentifier($identifier) {
- $this->identifier = $identifier;
- $this->identifierAliases[] = $identifier;
- return $this;
- }
-
- /**
- * @return string[]
- */
- public function getIdentifierAliases() {
- return $this->identifierAliases;
- }
-
- /**
- * @param string $alias
- * @return self
- */
- public function addIdentifierAlias($alias) {
- $this->identifierAliases[] = $alias;
- return $this;
- }
-
- /**
- * @return object|null
- */
- public function getDeprecateTo() {
- return $this->deprecateTo;
- }
-
- /**
- * @param object $destinationObject
- * @return self
- */
- public function deprecateTo($destinationObject) {
- $this->deprecateTo = $destinationObject;
- return $this;
- }
-
- /**
- * @return array
- */
- public function jsonSerializeIdentifier() {
- $data = [
- 'identifier' => $this->identifier,
- 'identifierAliases' => $this->identifierAliases,
- ];
- if ($this->deprecateTo) {
- $data['deprecateTo'] = $this->deprecateTo->getIdentifier();
- }
- return $data;
- }
-
-}
diff --git a/apps/files_external/lib/insufficientdataformeaningfulanswerexception.php b/apps/files_external/lib/insufficientdataformeaningfulanswerexception.php
deleted file mode 100644
index 1906057eb67..00000000000
--- a/apps/files_external/lib/insufficientdataformeaningfulanswerexception.php
+++ /dev/null
@@ -1,42 +0,0 @@
-<?php
-/**
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use \OCP\Files\StorageNotAvailableException;
-
-/**
- * Authentication mechanism or backend has insufficient data
- */
-class InsufficientDataForMeaningfulAnswerException extends StorageNotAvailableException {
- /**
- * StorageNotAvailableException constructor.
- *
- * @param string $message
- * @param int $code
- * @param \Exception $previous
- * @since 6.0.0
- */
- public function __construct($message = '', $code = self::STATUS_INDETERMINATE, \Exception $previous = null) {
- parent::__construct($message, $code, $previous);
- }
-}
diff --git a/apps/files_external/lib/missingdependency.php b/apps/files_external/lib/missingdependency.php
deleted file mode 100644
index a4a20dd1128..00000000000
--- a/apps/files_external/lib/missingdependency.php
+++ /dev/null
@@ -1,64 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-/**
- * External storage backend dependency
- */
-class MissingDependency {
-
- /** @var string */
- private $dependency;
-
- /** @var string|null Custom message */
- private $message = null;
-
- /**
- * @param string $dependency
- */
- public function __construct($dependency) {
- $this->dependency = $dependency;
- }
-
- /**
- * @return string
- */
- public function getDependency() {
- return $this->dependency;
- }
-
- /**
- * @return string|null
- */
- public function getMessage() {
- return $this->message;
- }
-
- /**
- * @param string $message
- * @return self
- */
- public function setMessage($message) {
- $this->message = $message;
- return $this;
- }
-}
diff --git a/apps/files_external/lib/notfoundexception.php b/apps/files_external/lib/notfoundexception.php
deleted file mode 100644
index dd3dd1907a5..00000000000
--- a/apps/files_external/lib/notfoundexception.php
+++ /dev/null
@@ -1,28 +0,0 @@
-<?php
-/**
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_external;
-
-/**
- * Storage is not found
- */
-class NotFoundException extends \Exception {
-}
diff --git a/apps/files_external/lib/owncloud.php b/apps/files_external/lib/owncloud.php
deleted file mode 100644
index c4824e6bd14..00000000000
--- a/apps/files_external/lib/owncloud.php
+++ /dev/null
@@ -1,74 +0,0 @@
-<?php
-/**
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-/**
- * ownCloud backend for external storage based on DAV backend.
- *
- * The ownCloud URL consists of three parts:
- * http://%host/%context/remote.php/webdav/%root
- *
- */
-class OwnCloud extends \OC\Files\Storage\DAV{
- const OC_URL_SUFFIX = 'remote.php/webdav';
-
- public function __construct($params) {
- // extract context path from host if specified
- // (owncloud install path on host)
- $host = $params['host'];
- // strip protocol
- if (substr($host, 0, 8) == "https://") {
- $host = substr($host, 8);
- $params['secure'] = true;
- } else if (substr($host, 0, 7) == "http://") {
- $host = substr($host, 7);
- $params['secure'] = false;
- }
- $contextPath = '';
- $hostSlashPos = strpos($host, '/');
- if ($hostSlashPos !== false){
- $contextPath = substr($host, $hostSlashPos);
- $host = substr($host, 0, $hostSlashPos);
- }
-
- if (substr($contextPath, -1) !== '/'){
- $contextPath .= '/';
- }
-
- if (isset($params['root'])){
- $root = $params['root'];
- if (substr($root, 0, 1) !== '/'){
- $root = '/' . $root;
- }
- }
- else{
- $root = '/';
- }
-
- $params['host'] = $host;
- $params['root'] = $contextPath . self::OC_URL_SUFFIX . $root;
-
- parent::__construct($params);
- }
-}
diff --git a/apps/files_external/lib/personalmount.php b/apps/files_external/lib/personalmount.php
deleted file mode 100644
index c3b71fbef32..00000000000
--- a/apps/files_external/lib/personalmount.php
+++ /dev/null
@@ -1,88 +0,0 @@
-<?php
-/**
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use OC\Files\Mount\MountPoint;
-use OC\Files\Mount\MoveableMount;
-use OCA\Files_External\Service\UserStoragesService;
-
-/**
- * Person mount points can be moved by the user
- */
-class PersonalMount extends MountPoint implements MoveableMount {
- /** @var UserStoragesService */
- protected $storagesService;
-
- /** @var int */
- protected $numericStorageId;
-
- /**
- * @param UserStoragesService $storagesService
- * @param int $storageId
- * @param \OCP\Files\Storage $storage
- * @param string $mountpoint
- * @param array $arguments (optional) configuration for the storage backend
- * @param \OCP\Files\Storage\IStorageFactory $loader
- * @param array $mountOptions mount specific options
- */
- public function __construct(
- UserStoragesService $storagesService,
- $storageId,
- $storage,
- $mountpoint,
- $arguments = null,
- $loader = null,
- $mountOptions = null
- ) {
- parent::__construct($storage, $mountpoint, $arguments, $loader, $mountOptions);
- $this->storagesService = $storagesService;
- $this->numericStorageId = $storageId;
- }
-
- /**
- * Move the mount point to $target
- *
- * @param string $target the target mount point
- * @return bool
- */
- public function moveMount($target) {
- $storage = $this->storagesService->getStorage($this->numericStorageId);
- // remove "/$user/files" prefix
- $targetParts = explode('/', trim($target, '/'), 3);
- $storage->setMountPoint($targetParts[2]);
- $this->storagesService->updateStorage($storage);
- $this->setMountPoint($target);
- return true;
- }
-
- /**
- * Remove the mount points
- *
- * @return bool
- */
- public function removeMount() {
- $this->storagesService->removeStorage($this->numericStorageId);
- return true;
- }
-}
diff --git a/apps/files_external/lib/prioritytrait.php b/apps/files_external/lib/prioritytrait.php
deleted file mode 100644
index 9745015bef4..00000000000
--- a/apps/files_external/lib/prioritytrait.php
+++ /dev/null
@@ -1,60 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use \OCA\Files_External\Service\BackendService;
-
-/**
- * Trait to implement priority mechanics for a configuration class
- */
-trait PriorityTrait {
-
- /** @var int initial priority */
- protected $priority = BackendService::PRIORITY_DEFAULT;
-
- /**
- * @return int
- */
- public function getPriority() {
- return $this->priority;
- }
-
- /**
- * @param int $priority
- * @return self
- */
- public function setPriority($priority) {
- $this->priority = $priority;
- return $this;
- }
-
- /**
- * @param PriorityTrait $a
- * @param PriorityTrait $b
- * @return int
- */
- public static function priorityCompare(PriorityTrait $a, PriorityTrait $b) {
- return ($a->getPriority() - $b->getPriority());
- }
-
-}
-
diff --git a/apps/files_external/lib/sessionstoragewrapper.php b/apps/files_external/lib/sessionstoragewrapper.php
deleted file mode 100644
index c592cb87a34..00000000000
--- a/apps/files_external/lib/sessionstoragewrapper.php
+++ /dev/null
@@ -1,43 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use \OCP\Files\Storage;
-use \OC\Files\Storage\Wrapper\PermissionsMask;
-use \OCP\Constants;
-
-/**
- * Wrap Storage in PermissionsMask for session ephemeral use
- */
-class SessionStorageWrapper extends PermissionsMask {
-
- /**
- * @param array $arguments ['storage' => $storage]
- */
- public function __construct($arguments) {
- // disable sharing permission
- $arguments['mask'] = Constants::PERMISSION_ALL & ~Constants::PERMISSION_SHARE;
- parent::__construct($arguments);
- }
-
-}
-
diff --git a/apps/files_external/lib/sftp.php b/apps/files_external/lib/sftp.php
deleted file mode 100644
index a6984f3b4e0..00000000000
--- a/apps/files_external/lib/sftp.php
+++ /dev/null
@@ -1,467 +0,0 @@
-<?php
-/**
- * @author Andreas Fischer <bantu@owncloud.com>
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author hkjolhede <hkjolhede@gmail.com>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Lennart Rosam <lennart.rosam@medien-systempartner.de>
- * @author Lukas Reschke <lukas@owncloud.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Ross Nicoll <jrn@jrn.me.uk>
- * @author SA <stephen@mthosting.net>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-namespace OC\Files\Storage;
-use Icewind\Streams\IteratorDirectory;
-
-use Icewind\Streams\RetryWrapper;
-use phpseclib\Net\SFTP\Stream;
-
-/**
-* Uses phpseclib's Net\SFTP class and the Net\SFTP\Stream stream wrapper to
-* provide access to SFTP servers.
-*/
-class SFTP extends \OC\Files\Storage\Common {
- private $host;
- private $user;
- private $root;
- private $port = 22;
-
- private $auth;
-
- /**
- * @var SFTP
- */
- protected $client;
-
- /**
- * @param string $host protocol://server:port
- * @return array [$server, $port]
- */
- private function splitHost($host) {
- $input = $host;
- if (strpos($host, '://') === false) {
- // add a protocol to fix parse_url behavior with ipv6
- $host = 'http://' . $host;
- }
-
- $parsed = parse_url($host);
- if(is_array($parsed) && isset($parsed['port'])) {
- return [$parsed['host'], $parsed['port']];
- } else if (is_array($parsed)) {
- return [$parsed['host'], 22];
- } else {
- return [$input, 22];
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function __construct($params) {
- // Register sftp://
- Stream::register();
-
- $parsedHost = $this->splitHost($params['host']);
-
- $this->host = $parsedHost[0];
- $this->port = $parsedHost[1];
-
- if (!isset($params['user'])) {
- throw new \UnexpectedValueException('no authentication parameters specified');
- }
- $this->user = $params['user'];
-
- if (isset($params['public_key_auth'])) {
- $this->auth = $params['public_key_auth'];
- } elseif (isset($params['password'])) {
- $this->auth = $params['password'];
- } else {
- throw new \UnexpectedValueException('no authentication parameters specified');
- }
-
- $this->root
- = isset($params['root']) ? $this->cleanPath($params['root']) : '/';
-
- if ($this->root[0] != '/') {
- $this->root = '/' . $this->root;
- }
-
- if (substr($this->root, -1, 1) != '/') {
- $this->root .= '/';
- }
- }
-
- /**
- * Returns the connection.
- *
- * @return \phpseclib\Net\SFTP connected client instance
- * @throws \Exception when the connection failed
- */
- public function getConnection() {
- if (!is_null($this->client)) {
- return $this->client;
- }
-
- $hostKeys = $this->readHostKeys();
- $this->client = new \phpseclib\Net\SFTP($this->host, $this->port);
-
- // The SSH Host Key MUST be verified before login().
- $currentHostKey = $this->client->getServerPublicHostKey();
- if (array_key_exists($this->host, $hostKeys)) {
- if ($hostKeys[$this->host] != $currentHostKey) {
- throw new \Exception('Host public key does not match known key');
- }
- } else {
- $hostKeys[$this->host] = $currentHostKey;
- $this->writeHostKeys($hostKeys);
- }
-
- if (!$this->client->login($this->user, $this->auth)) {
- throw new \Exception('Login failed');
- }
- return $this->client;
- }
-
- /**
- * {@inheritdoc}
- */
- public function test() {
- if (
- !isset($this->host)
- || !isset($this->user)
- ) {
- return false;
- }
- return $this->getConnection()->nlist() !== false;
- }
-
- /**
- * {@inheritdoc}
- */
- public function getId(){
- $id = 'sftp::' . $this->user . '@' . $this->host;
- if ($this->port !== 22) {
- $id .= ':' . $this->port;
- }
- // note: this will double the root slash,
- // we should not change it to keep compatible with
- // old storage ids
- $id .= '/' . $this->root;
- return $id;
- }
-
- /**
- * @return string
- */
- public function getHost() {
- return $this->host;
- }
-
- /**
- * @return string
- */
- public function getRoot() {
- return $this->root;
- }
-
- /**
- * @return mixed
- */
- public function getUser() {
- return $this->user;
- }
-
- /**
- * @param string $path
- * @return string
- */
- private function absPath($path) {
- return $this->root . $this->cleanPath($path);
- }
-
- /**
- * @return string|false
- */
- private function hostKeysPath() {
- try {
- $storage_view = \OCP\Files::getStorage('files_external');
- if ($storage_view) {
- return \OC::$server->getConfig()->getSystemValue('datadirectory') .
- $storage_view->getAbsolutePath('') .
- 'ssh_hostKeys';
- }
- } catch (\Exception $e) {
- }
- return false;
- }
-
- /**
- * @param $keys
- * @return bool
- */
- protected function writeHostKeys($keys) {
- try {
- $keyPath = $this->hostKeysPath();
- if ($keyPath && file_exists($keyPath)) {
- $fp = fopen($keyPath, 'w');
- foreach ($keys as $host => $key) {
- fwrite($fp, $host . '::' . $key . "\n");
- }
- fclose($fp);
- return true;
- }
- } catch (\Exception $e) {
- }
- return false;
- }
-
- /**
- * @return array
- */
- protected function readHostKeys() {
- try {
- $keyPath = $this->hostKeysPath();
- if (file_exists($keyPath)) {
- $hosts = array();
- $keys = array();
- $lines = file($keyPath, FILE_IGNORE_NEW_LINES | FILE_SKIP_EMPTY_LINES);
- if ($lines) {
- foreach ($lines as $line) {
- $hostKeyArray = explode("::", $line, 2);
- if (count($hostKeyArray) == 2) {
- $hosts[] = $hostKeyArray[0];
- $keys[] = $hostKeyArray[1];
- }
- }
- return array_combine($hosts, $keys);
- }
- }
- } catch (\Exception $e) {
- }
- return array();
- }
-
- /**
- * {@inheritdoc}
- */
- public function mkdir($path) {
- try {
- return $this->getConnection()->mkdir($this->absPath($path));
- } catch (\Exception $e) {
- return false;
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function rmdir($path) {
- try {
- $result = $this->getConnection()->delete($this->absPath($path), true);
- // workaround: stray stat cache entry when deleting empty folders
- // see https://github.com/phpseclib/phpseclib/issues/706
- $this->getConnection()->clearStatCache();
- return $result;
- } catch (\Exception $e) {
- return false;
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function opendir($path) {
- try {
- $list = $this->getConnection()->nlist($this->absPath($path));
- if ($list === false) {
- return false;
- }
-
- $id = md5('sftp:' . $path);
- $dirStream = array();
- foreach($list as $file) {
- if ($file != '.' && $file != '..') {
- $dirStream[] = $file;
- }
- }
- return IteratorDirectory::wrap($dirStream);
- } catch(\Exception $e) {
- return false;
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function filetype($path) {
- try {
- $stat = $this->getConnection()->stat($this->absPath($path));
- if ($stat['type'] == NET_SFTP_TYPE_REGULAR) {
- return 'file';
- }
-
- if ($stat['type'] == NET_SFTP_TYPE_DIRECTORY) {
- return 'dir';
- }
- } catch (\Exception $e) {
-
- }
- return false;
- }
-
- /**
- * {@inheritdoc}
- */
- public function file_exists($path) {
- try {
- return $this->getConnection()->stat($this->absPath($path)) !== false;
- } catch (\Exception $e) {
- return false;
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function unlink($path) {
- try {
- return $this->getConnection()->delete($this->absPath($path), true);
- } catch (\Exception $e) {
- return false;
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function fopen($path, $mode) {
- try {
- $absPath = $this->absPath($path);
- switch($mode) {
- case 'r':
- case 'rb':
- if ( !$this->file_exists($path)) {
- return false;
- }
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- $context = stream_context_create(array('sftp' => array('session' => $this->getConnection())));
- $handle = fopen($this->constructUrl($path), $mode, false, $context);
- return RetryWrapper::wrap($handle);
- }
- } catch (\Exception $e) {
- }
- return false;
- }
-
- /**
- * {@inheritdoc}
- */
- public function touch($path, $mtime=null) {
- try {
- if (!is_null($mtime)) {
- return false;
- }
- if (!$this->file_exists($path)) {
- $this->getConnection()->put($this->absPath($path), '');
- } else {
- return false;
- }
- } catch (\Exception $e) {
- return false;
- }
- return true;
- }
-
- /**
- * @param string $path
- * @param string $target
- * @throws \Exception
- */
- public function getFile($path, $target) {
- $this->getConnection()->get($path, $target);
- }
-
- /**
- * @param string $path
- * @param string $target
- * @throws \Exception
- */
- public function uploadFile($path, $target) {
- $this->getConnection()->put($target, $path, NET_SFTP_LOCAL_FILE);
- }
-
- /**
- * {@inheritdoc}
- */
- public function rename($source, $target) {
- try {
- if (!$this->is_dir($target) && $this->file_exists($target)) {
- $this->unlink($target);
- }
- return $this->getConnection()->rename(
- $this->absPath($source),
- $this->absPath($target)
- );
- } catch (\Exception $e) {
- return false;
- }
- }
-
- /**
- * {@inheritdoc}
- */
- public function stat($path) {
- try {
- $stat = $this->getConnection()->stat($this->absPath($path));
-
- $mtime = $stat ? $stat['mtime'] : -1;
- $size = $stat ? $stat['size'] : 0;
-
- return array('mtime' => $mtime, 'size' => $size, 'ctime' => -1);
- } catch (\Exception $e) {
- return false;
- }
- }
-
- /**
- * @param string $path
- * @return string
- */
- public function constructUrl($path) {
- // Do not pass the password here. We want to use the Net_SFTP object
- // supplied via stream context or fail. We only supply username and
- // hostname because this might show up in logs (they are not used).
- $url = 'sftp://' . urlencode($this->user) . '@' . $this->host . ':' . $this->port . $this->root . $path;
- return $url;
- }
-}
diff --git a/apps/files_external/lib/smb.php b/apps/files_external/lib/smb.php
deleted file mode 100644
index 08c4b25a088..00000000000
--- a/apps/files_external/lib/smb.php
+++ /dev/null
@@ -1,396 +0,0 @@
-<?php
-/**
- * @author Arthur Schiwon <blizzz@owncloud.com>
- * @author Jesús Macias <jmacias@solidgear.es>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Michael Gapczynski <GapczynskiM@gmail.com>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-use Icewind\SMB\Exception\ConnectException;
-use Icewind\SMB\Exception\Exception;
-use Icewind\SMB\Exception\ForbiddenException;
-use Icewind\SMB\Exception\NotFoundException;
-use Icewind\SMB\NativeServer;
-use Icewind\SMB\Server;
-use Icewind\Streams\CallbackWrapper;
-use Icewind\Streams\IteratorDirectory;
-use OC\Cache\CappedMemoryCache;
-use OC\Files\Filesystem;
-use OCP\Files\StorageNotAvailableException;
-
-class SMB extends Common {
- /**
- * @var \Icewind\SMB\Server
- */
- protected $server;
-
- /**
- * @var \Icewind\SMB\Share
- */
- protected $share;
-
- /**
- * @var string
- */
- protected $root;
-
- /**
- * @var \Icewind\SMB\FileInfo[]
- */
- protected $statCache;
-
- public function __construct($params) {
- if (isset($params['host']) && isset($params['user']) && isset($params['password']) && isset($params['share'])) {
- if (Server::NativeAvailable()) {
- $this->server = new NativeServer($params['host'], $params['user'], $params['password']);
- } else {
- $this->server = new Server($params['host'], $params['user'], $params['password']);
- }
- $this->share = $this->server->getShare(trim($params['share'], '/'));
-
- $this->root = isset($params['root']) ? $params['root'] : '/';
- if (!$this->root || $this->root[0] != '/') {
- $this->root = '/' . $this->root;
- }
- if (substr($this->root, -1, 1) != '/') {
- $this->root .= '/';
- }
- } else {
- throw new \Exception('Invalid configuration');
- }
- $this->statCache = new CappedMemoryCache();
- }
-
- /**
- * @return string
- */
- public function getId() {
- // FIXME: double slash to keep compatible with the old storage ids,
- // failure to do so will lead to creation of a new storage id and
- // loss of shares from the storage
- return 'smb::' . $this->server->getUser() . '@' . $this->server->getHost() . '//' . $this->share->getName() . '/' . $this->root;
- }
-
- /**
- * @param string $path
- * @return string
- */
- protected function buildPath($path) {
- return Filesystem::normalizePath($this->root . '/' . $path);
- }
-
- /**
- * @param string $path
- * @return \Icewind\SMB\IFileInfo
- * @throws StorageNotAvailableException
- */
- protected function getFileInfo($path) {
- try {
- $path = $this->buildPath($path);
- if (!isset($this->statCache[$path])) {
- $this->statCache[$path] = $this->share->stat($path);
- }
- return $this->statCache[$path];
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- /**
- * @param string $path
- * @return \Icewind\SMB\IFileInfo[]
- * @throws StorageNotAvailableException
- */
- protected function getFolderContents($path) {
- try {
- $path = $this->buildPath($path);
- $files = $this->share->dir($path);
- foreach ($files as $file) {
- $this->statCache[$path . '/' . $file->getName()] = $file;
- }
- return $files;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- /**
- * @param \Icewind\SMB\IFileInfo $info
- * @return array
- */
- protected function formatInfo($info) {
- return array(
- 'size' => $info->getSize(),
- 'mtime' => $info->getMTime()
- );
- }
-
- /**
- * @param string $path
- * @return array
- */
- public function stat($path) {
- return $this->formatInfo($this->getFileInfo($path));
- }
-
- /**
- * @param string $path
- * @return bool
- */
- public function unlink($path) {
- try {
- if ($this->is_dir($path)) {
- return $this->rmdir($path);
- } else {
- $path = $this->buildPath($path);
- unset($this->statCache[$path]);
- $this->share->del($path);
- return true;
- }
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- /**
- * check if a file or folder has been updated since $time
- *
- * @param string $path
- * @param int $time
- * @return bool
- */
- public function hasUpdated($path, $time) {
- if (!$path and $this->root == '/') {
- // mtime doesn't work for shares, but giving the nature of the backend,
- // doing a full update is still just fast enough
- return true;
- } else {
- $actualTime = $this->filemtime($path);
- return $actualTime > $time;
- }
- }
-
- /**
- * @param string $path
- * @param string $mode
- * @return resource
- */
- public function fopen($path, $mode) {
- $fullPath = $this->buildPath($path);
- try {
- switch ($mode) {
- case 'r':
- case 'rb':
- if (!$this->file_exists($path)) {
- return false;
- }
- return $this->share->read($fullPath);
- case 'w':
- case 'wb':
- $source = $this->share->write($fullPath);
- return CallBackWrapper::wrap($source, null, null, function () use ($fullPath) {
- unset($this->statCache[$fullPath]);
- });
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- //emulate these
- if (strrpos($path, '.') !== false) {
- $ext = substr($path, strrpos($path, '.'));
- } else {
- $ext = '';
- }
- if ($this->file_exists($path)) {
- if (!$this->isUpdatable($path)) {
- return false;
- }
- $tmpFile = $this->getCachedFile($path);
- } else {
- if (!$this->isCreatable(dirname($path))) {
- return false;
- }
- $tmpFile = \OCP\Files::tmpFile($ext);
- }
- $source = fopen($tmpFile, $mode);
- $share = $this->share;
- return CallbackWrapper::wrap($source, null, null, function () use ($tmpFile, $fullPath, $share) {
- unset($this->statCache[$fullPath]);
- $share->put($tmpFile, $fullPath);
- unlink($tmpFile);
- });
- }
- return false;
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- public function rmdir($path) {
- try {
- $this->statCache = array();
- $content = $this->share->dir($this->buildPath($path));
- foreach ($content as $file) {
- if ($file->isDirectory()) {
- $this->rmdir($path . '/' . $file->getName());
- } else {
- $this->share->del($file->getPath());
- }
- }
- $this->share->rmdir($this->buildPath($path));
- return true;
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- public function touch($path, $time = null) {
- try {
- if (!$this->file_exists($path)) {
- $fh = $this->share->write($this->buildPath($path));
- fclose($fh);
- return true;
- }
- return false;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- public function opendir($path) {
- try {
- $files = $this->getFolderContents($path);
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- }
- $names = array_map(function ($info) {
- /** @var \Icewind\SMB\IFileInfo $info */
- return $info->getName();
- }, $files);
- return IteratorDirectory::wrap($names);
- }
-
- public function filetype($path) {
- try {
- return $this->getFileInfo($path)->isDirectory() ? 'dir' : 'file';
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- }
- }
-
- public function mkdir($path) {
- $path = $this->buildPath($path);
- try {
- $this->share->mkdir($path);
- return true;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- } catch (Exception $e) {
- return false;
- }
- }
-
- public function file_exists($path) {
- try {
- $this->getFileInfo($path);
- return true;
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- } catch (ConnectException $e) {
- throw new StorageNotAvailableException($e->getMessage(), $e->getCode(), $e);
- }
- }
-
- public function isReadable($path) {
- try {
- $info = $this->getFileInfo($path);
- return !$info->isHidden();
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- }
- }
-
- public function isUpdatable($path) {
- try {
- $info = $this->getFileInfo($path);
- return !$info->isHidden() && !$info->isReadOnly();
- } catch (NotFoundException $e) {
- return false;
- } catch (ForbiddenException $e) {
- return false;
- }
- }
-
- /**
- * check if smbclient is installed
- */
- public static function checkDependencies() {
- return (
- (bool)\OC_Helper::findBinaryPath('smbclient')
- || Server::NativeAvailable()
- ) ? true : ['smbclient'];
- }
-
- /**
- * Test a storage for availability
- *
- * @return bool
- */
- public function test() {
- try {
- return parent::test();
- } catch (Exception $e) {
- return false;
- }
- }
-}
diff --git a/apps/files_external/lib/storagemodifiertrait.php b/apps/files_external/lib/storagemodifiertrait.php
deleted file mode 100644
index 30c2108feec..00000000000
--- a/apps/files_external/lib/storagemodifiertrait.php
+++ /dev/null
@@ -1,69 +0,0 @@
-<?php
-/**
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OCA\Files_External\Lib;
-
-use \OCP\IUser;
-use \OCP\Files\Storage;
-use \OCA\Files_External\Lib\StorageConfig;
-use \OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
-use \OCP\Files\StorageNotAvailableException;
-
-/**
- * Trait for objects that can modify StorageConfigs and wrap Storages
- *
- * When a storage implementation is being prepared for use, the StorageConfig
- * is passed through manipulateStorageConfig() to update any parameters as
- * necessary. After the storage implementation has been constructed, it is
- * passed through wrapStorage(), potentially replacing the implementation with
- * a wrapped storage that changes its behaviour.
- *
- * Certain configuration options need to be set before the implementation is
- * constructed, while others are retrieved directly from the storage
- * implementation and so need a wrapper to be modified.
- */
-trait StorageModifierTrait {
-
- /**
- * Modify a StorageConfig parameters
- *
- * @param StorageConfig $storage
- * @param IUser $user User the storage is being used as
- * @throws InsufficientDataForMeaningfulAnswerException
- * @throws StorageNotAvailableException
- */
- public function manipulateStorageConfig(StorageConfig &$storage, IUser $user = null) {
- }
-
- /**
- * Wrap a Storage if necessary
- *
- * @param Storage $storage
- * @return Storage
- * @throws InsufficientDataForMeaningfulAnswerException
- * @throws StorageNotAvailableException
- */
- public function wrapStorage(Storage $storage) {
- return $storage;
- }
-
-}
-
diff --git a/apps/files_external/lib/streamwrapper.php b/apps/files_external/lib/streamwrapper.php
deleted file mode 100644
index efb51f32ba4..00000000000
--- a/apps/files_external/lib/streamwrapper.php
+++ /dev/null
@@ -1,128 +0,0 @@
-<?php
-/**
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-abstract class StreamWrapper extends Common {
-
- /**
- * @param string $path
- * @return string|null
- */
- abstract public function constructUrl($path);
-
- public function mkdir($path) {
- return mkdir($this->constructUrl($path));
- }
-
- public function rmdir($path) {
- if ($this->is_dir($path) && $this->isDeletable($path)) {
- $dh = $this->opendir($path);
- if (!is_resource($dh)) {
- return false;
- }
- while (($file = readdir($dh)) !== false) {
- if ($this->is_dir($path . '/' . $file)) {
- $this->rmdir($path . '/' . $file);
- } else {
- $this->unlink($path . '/' . $file);
- }
- }
- $url = $this->constructUrl($path);
- $success = rmdir($url);
- clearstatcache(false, $url);
- return $success;
- } else {
- return false;
- }
- }
-
- public function opendir($path) {
- return opendir($this->constructUrl($path));
- }
-
- public function filetype($path) {
- return @filetype($this->constructUrl($path));
- }
-
- public function file_exists($path) {
- return file_exists($this->constructUrl($path));
- }
-
- public function unlink($path) {
- $url = $this->constructUrl($path);
- $success = unlink($url);
- // normally unlink() is supposed to do this implicitly,
- // but doing it anyway just to be sure
- clearstatcache(false, $url);
- return $success;
- }
-
- public function fopen($path, $mode) {
- return fopen($this->constructUrl($path), $mode);
- }
-
- public function touch($path, $mtime = null) {
- if ($this->file_exists($path)) {
- if (is_null($mtime)) {
- $fh = $this->fopen($path, 'a');
- fwrite($fh, '');
- fclose($fh);
-
- return true;
- } else {
- return false; //not supported
- }
- } else {
- $this->file_put_contents($path, '');
- return true;
- }
- }
-
- /**
- * @param string $path
- * @param string $target
- */
- public function getFile($path, $target) {
- return copy($this->constructUrl($path), $target);
- }
-
- /**
- * @param string $target
- */
- public function uploadFile($path, $target) {
- return copy($path, $this->constructUrl($target));
- }
-
- public function rename($path1, $path2) {
- return rename($this->constructUrl($path1), $this->constructUrl($path2));
- }
-
- public function stat($path) {
- return stat($this->constructUrl($path));
- }
-
-}
diff --git a/apps/files_external/lib/swift.php b/apps/files_external/lib/swift.php
deleted file mode 100644
index 9282fe28669..00000000000
--- a/apps/files_external/lib/swift.php
+++ /dev/null
@@ -1,599 +0,0 @@
-<?php
-/**
- * @author Bart Visscher <bartv@thisnet.nl>
- * @author Benjamin Liles <benliles@arch.tamu.edu>
- * @author Christian Berendt <berendt@b1-systems.de>
- * @author Daniel Tosello <tosello.daniel@gmail.com>
- * @author Felix Moeller <mail@felixmoeller.de>
- * @author Jörn Friedrich Dreyer <jfd@butonic.de>
- * @author Martin Mattel <martin.mattel@diemattels.at>
- * @author Morris Jobke <hey@morrisjobke.de>
- * @author Philipp Kapfer <philipp.kapfer@gmx.at>
- * @author Robin Appelman <icewind@owncloud.com>
- * @author Robin McCorkell <robin@mccorkell.me.uk>
- * @author Thomas Müller <thomas.mueller@tmit.eu>
- * @author Tim Dettrick <t.dettrick@uq.edu.au>
- * @author Vincent Petry <pvince81@owncloud.com>
- *
- * @copyright Copyright (c) 2016, ownCloud, Inc.
- * @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/>
- *
- */
-
-namespace OC\Files\Storage;
-
-use Guzzle\Http\Url;
-use Guzzle\Http\Exception\ClientErrorResponseException;
-use Icewind\Streams\IteratorDirectory;
-use OpenCloud;
-use OpenCloud\Common\Exceptions;
-use OpenCloud\OpenStack;
-use OpenCloud\Rackspace;
-use OpenCloud\ObjectStore\Resource\DataObject;
-use OpenCloud\ObjectStore\Exception;
-
-class Swift extends \OC\Files\Storage\Common {
-
- /**
- * @var \OpenCloud\ObjectStore\Service
- */
- private $connection;
- /**
- * @var \OpenCloud\ObjectStore\Resource\Container
- */
- private $container;
- /**
- * @var \OpenCloud\OpenStack
- */
- private $anchor;
- /**
- * @var string
- */
- private $bucket;
- /**
- * Connection parameters
- *
- * @var array
- */
- private $params;
- /**
- * @var array
- */
- private static $tmpFiles = array();
-
- /**
- * @param string $path
- */
- private function normalizePath($path) {
- $path = trim($path, '/');
-
- if (!$path) {
- $path = '.';
- }
-
- $path = str_replace('#', '%23', $path);
-
- return $path;
- }
-
- const SUBCONTAINER_FILE = '.subcontainers';
-
- /**
- * translate directory path to container name
- *
- * @param string $path
- * @return string
- */
- private function getContainerName($path) {
- $path = trim(trim($this->root, '/') . "/" . $path, '/.');
- return str_replace('/', '\\', $path);
- }
-
- /**
- * @param string $path
- */
- private function doesObjectExist($path) {
- try {
- $this->getContainer()->getPartialObject($path);
- return true;
- } catch (ClientErrorResponseException $e) {
- // Expected response is "404 Not Found", so only log if it isn't
- if ($e->getResponse()->getStatusCode() !== 404) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- }
- return false;
- }
- }
-
- public function __construct($params) {
- if ((empty($params['key']) and empty($params['password']))
- or empty($params['user']) or empty($params['bucket'])
- or empty($params['region'])
- ) {
- throw new \Exception("API Key or password, Username, Bucket and Region have to be configured.");
- }
-
- $this->id = 'swift::' . $params['user'] . md5($params['bucket']);
-
- $bucketUrl = Url::factory($params['bucket']);
- if ($bucketUrl->isAbsolute()) {
- $this->bucket = end(($bucketUrl->getPathSegments()));
- $params['endpoint_url'] = $bucketUrl->addPath('..')->normalizePath();
- } else {
- $this->bucket = $params['bucket'];
- }
-
- if (empty($params['url'])) {
- $params['url'] = 'https://identity.api.rackspacecloud.com/v2.0/';
- }
-
- if (empty($params['service_name'])) {
- $params['service_name'] = 'cloudFiles';
- }
-
- $this->params = $params;
- }
-
- public function mkdir($path) {
- $path = $this->normalizePath($path);
-
- if ($this->is_dir($path)) {
- return false;
- }
-
- if ($path !== '.') {
- $path .= '/';
- }
-
- try {
- $customHeaders = array('content-type' => 'httpd/unix-directory');
- $metadataHeaders = DataObject::stockHeaders(array());
- $allHeaders = $customHeaders + $metadataHeaders;
- $this->getContainer()->uploadObject($path, '', $allHeaders);
- } catch (Exceptions\CreateUpdateError $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- return true;
- }
-
- public function file_exists($path) {
- $path = $this->normalizePath($path);
-
- if ($path !== '.' && $this->is_dir($path)) {
- $path .= '/';
- }
-
- return $this->doesObjectExist($path);
- }
-
- public function rmdir($path) {
- $path = $this->normalizePath($path);
-
- if (!$this->is_dir($path) || !$this->isDeletable($path)) {
- return false;
- }
-
- $dh = $this->opendir($path);
- while ($file = readdir($dh)) {
- if (\OC\Files\Filesystem::isIgnoredDir($file)) {
- continue;
- }
-
- if ($this->is_dir($path . '/' . $file)) {
- $this->rmdir($path . '/' . $file);
- } else {
- $this->unlink($path . '/' . $file);
- }
- }
-
- try {
- $this->getContainer()->dataObject()->setName($path . '/')->delete();
- } catch (Exceptions\DeleteError $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- return true;
- }
-
- public function opendir($path) {
- $path = $this->normalizePath($path);
-
- if ($path === '.') {
- $path = '';
- } else {
- $path .= '/';
- }
-
- $path = str_replace('%23', '#', $path); // the prefix is sent as a query param, so revert the encoding of #
-
- try {
- $files = array();
- /** @var OpenCloud\Common\Collection $objects */
- $objects = $this->getContainer()->objectList(array(
- 'prefix' => $path,
- 'delimiter' => '/'
- ));
-
- /** @var OpenCloud\ObjectStore\Resource\DataObject $object */
- foreach ($objects as $object) {
- $file = basename($object->getName());
- if ($file !== basename($path)) {
- $files[] = $file;
- }
- }
-
- return IteratorDirectory::wrap($files);
- } catch (\Exception $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- }
-
- public function stat($path) {
- $path = $this->normalizePath($path);
-
- if ($path === '.') {
- $path = '';
- } else if ($this->is_dir($path)) {
- $path .= '/';
- }
-
- try {
- /** @var DataObject $object */
- $object = $this->getContainer()->getPartialObject($path);
- } catch (ClientErrorResponseException $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- $dateTime = \DateTime::createFromFormat(\DateTime::RFC1123, $object->getLastModified());
- if ($dateTime !== false) {
- $mtime = $dateTime->getTimestamp();
- } else {
- $mtime = null;
- }
- $objectMetadata = $object->getMetadata();
- $metaTimestamp = $objectMetadata->getProperty('timestamp');
- if (isset($metaTimestamp)) {
- $mtime = $metaTimestamp;
- }
-
- if (!empty($mtime)) {
- $mtime = floor($mtime);
- }
-
- $stat = array();
- $stat['size'] = (int)$object->getContentLength();
- $stat['mtime'] = $mtime;
- $stat['atime'] = time();
- return $stat;
- }
-
- public function filetype($path) {
- $path = $this->normalizePath($path);
-
- if ($path !== '.' && $this->doesObjectExist($path)) {
- return 'file';
- }
-
- if ($path !== '.') {
- $path .= '/';
- }
-
- if ($this->doesObjectExist($path)) {
- return 'dir';
- }
- }
-
- public function unlink($path) {
- $path = $this->normalizePath($path);
-
- if ($this->is_dir($path)) {
- return $this->rmdir($path);
- }
-
- try {
- $this->getContainer()->dataObject()->setName($path)->delete();
- } catch (ClientErrorResponseException $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- return true;
- }
-
- public function fopen($path, $mode) {
- $path = $this->normalizePath($path);
-
- switch ($mode) {
- case 'r':
- case 'rb':
- try {
- $c = $this->getContainer();
- $streamFactory = new \Guzzle\Stream\PhpStreamRequestFactory();
- $streamInterface = $streamFactory->fromRequest(
- $c->getClient()
- ->get($c->getUrl($path)));
- $streamInterface->rewind();
- $stream = $streamInterface->getStream();
- stream_context_set_option($stream, 'swift','content', $streamInterface);
- if(!strrpos($streamInterface
- ->getMetaData('wrapper_data')[0], '404 Not Found')) {
- return $stream;
- }
- return false;
- } catch (\Guzzle\Http\Exception\BadResponseException $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
- case 'w':
- case 'wb':
- case 'a':
- case 'ab':
- case 'r+':
- case 'w+':
- case 'wb+':
- case 'a+':
- case 'x':
- case 'x+':
- case 'c':
- case 'c+':
- if (strrpos($path, '.') !== false) {
- $ext = substr($path, strrpos($path, '.'));
- } else {
- $ext = '';
- }
- $tmpFile = \OCP\Files::tmpFile($ext);
- \OC\Files\Stream\Close::registerCallback($tmpFile, array($this, 'writeBack'));
- // Fetch existing file if required
- if ($mode[0] !== 'w' && $this->file_exists($path)) {
- if ($mode[0] === 'x') {
- // File cannot already exist
- return false;
- }
- $source = $this->fopen($path, 'r');
- file_put_contents($tmpFile, $source);
- // Seek to end if required
- if ($mode[0] === 'a') {
- fseek($tmpFile, 0, SEEK_END);
- }
- }
- self::$tmpFiles[$tmpFile] = $path;
-
- return fopen('close://' . $tmpFile, $mode);
- }
- }
-
- public function touch($path, $mtime = null) {
- $path = $this->normalizePath($path);
- if (is_null($mtime)) {
- $mtime = time();
- }
- $metadata = array('timestamp' => $mtime);
- if ($this->file_exists($path)) {
- if ($this->is_dir($path) && $path != '.') {
- $path .= '/';
- }
-
- $object = $this->getContainer()->getPartialObject($path);
- $object->saveMetadata($metadata);
- return true;
- } else {
- $mimeType = \OC::$server->getMimeTypeDetector()->detectPath($path);
- $customHeaders = array('content-type' => $mimeType);
- $metadataHeaders = DataObject::stockHeaders($metadata);
- $allHeaders = $customHeaders + $metadataHeaders;
- $this->getContainer()->uploadObject($path, '', $allHeaders);
- return true;
- }
- }
-
- public function copy($path1, $path2) {
- $path1 = $this->normalizePath($path1);
- $path2 = $this->normalizePath($path2);
-
- $fileType = $this->filetype($path1);
- if ($fileType === 'file') {
-
- // make way
- $this->unlink($path2);
-
- try {
- $source = $this->getContainer()->getPartialObject($path1);
- $source->copy($this->bucket . '/' . $path2);
- } catch (ClientErrorResponseException $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- } else if ($fileType === 'dir') {
-
- // make way
- $this->unlink($path2);
-
- try {
- $source = $this->getContainer()->getPartialObject($path1 . '/');
- $source->copy($this->bucket . '/' . $path2 . '/');
- } catch (ClientErrorResponseException $e) {
- \OCP\Util::writeLog('files_external', $e->getMessage(), \OCP\Util::ERROR);
- return false;
- }
-
- $dh = $this->opendir($path1);
- while ($file = readdir($dh)) {
- if (\OC\Files\Filesystem::isIgnoredDir($file)) {
- continue;
- }
-
- $source = $path1 . '/' . $file;
- $target = $path2 . '/' . $file;
- $this->copy($source, $target);
- }
-
- } else {
- //file does not exist
- return false;
- }
-
- return true;
- }
-
- public function rename($path1, $path2) {
- $path1 = $this->normalizePath($path1);
- $path2 = $this->normalizePath($path2);
-
- $fileType = $this->filetype($path1);
-
- if ($fileType === 'dir' || $fileType === 'file') {
-
- // make way
- $this->unlink($path2);
-
- // copy
- if ($this->copy($path1, $path2) === false) {
- return false;
- }
-
- // cleanup
- if ($this->unlink($path1) === false) {
- $this->unlink($path2);
- return false;
- }
-
- return true;
- }
-
- return false;
- }
-
- public function getId() {
- return $this->id;
- }
-
- /**
- * Returns the connection
- *
- * @return OpenCloud\ObjectStore\Service connected client
- * @throws \Exception if connection could not be made
- */
- public function getConnection() {
- if (!is_null($this->connection)) {
- return $this->connection;
- }
-
- $settings = array(
- 'username' => $this->params['user'],
- );
-
- if (!empty($this->params['password'])) {
- $settings['password'] = $this->params['password'];
- } else if (!empty($this->params['key'])) {
- $settings['apiKey'] = $this->params['key'];
- }
-
- if (!empty($this->params['tenant'])) {
- $settings['tenantName'] = $this->params['tenant'];
- }
-
- if (!empty($this->params['timeout'])) {
- $settings['timeout'] = $this->params['timeout'];
- }
-
- if (isset($settings['apiKey'])) {
- $this->anchor = new Rackspace($this->params['url'], $settings);
- } else {
- $this->anchor = new OpenStack($this->params['url'], $settings);
- }
-
- $connection = $this->anchor->objectStoreService($this->params['service_name'], $this->params['region']);
-
- if (!empty($this->params['endpoint_url'])) {
- $endpoint = $connection->getEndpoint();
- $endpoint->setPublicUrl($this->params['endpoint_url']);
- $endpoint->setPrivateUrl($this->params['endpoint_url']);
- $connection->setEndpoint($endpoint);
- }
-
- $this->connection = $connection;
-
- return $this->connection;
- }
-
- /**
- * Returns the initialized object store container.
- *
- * @return OpenCloud\ObjectStore\Resource\Container
- */
- public function getContainer() {
- if (!is_null($this->container)) {
- return $this->container;
- }
-
- try {
- $this->container = $this->getConnection()->getContainer($this->bucket);
- } catch (ClientErrorResponseException $e) {
- $this->container = $this->getConnection()->createContainer($this->bucket);
- }
-
- if (!$this->file_exists('.')) {
- $this->mkdir('.');
- }
-
- return $this->container;
- }
-
- public function writeBack($tmpFile) {
- if (!isset(self::$tmpFiles[$tmpFile])) {
- return false;
- }
- $fileData = fopen($tmpFile, 'r');
- $this->getContainer()->uploadObject(self::$tmpFiles[$tmpFile], $fileData);
- unlink($tmpFile);
- }
-
- public function hasUpdated($path, $time) {
- if ($this->is_file($path)) {
- return parent::hasUpdated($path, $time);
- }
- $path = $this->normalizePath($path);
- $dh = $this->opendir($path);
- $content = array();
- while (($file = readdir($dh)) !== false) {
- $content[] = $file;
- }
- if ($path === '.') {
- $path = '';
- }
- $cachedContent = $this->getCache()->getFolderContents($path);
- $cachedNames = array_map(function ($content) {
- return $content['name'];
- }, $cachedContent);
- sort($cachedNames);
- sort($content);
- return $cachedNames != $content;
- }
-
- /**
- * check if curl is installed
- */
- public static function checkDependencies() {
- return true;
- }
-
-}