Compare commits
2 Commits
| Author | SHA1 | Date | |
|---|---|---|---|
| 0de4631714 | |||
| ff6d2fc353 |
@@ -1,29 +0,0 @@
|
||||
# SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
# SPDX-License-Identifier: AGPL-3.0-only
|
||||
# Files removed at build time
|
||||
|
||||
# Global exclude
|
||||
.editorconfig
|
||||
.git
|
||||
.git-blame-ignore-revs
|
||||
.gitattributes
|
||||
.github
|
||||
.gitignore
|
||||
.gitmodules
|
||||
.idea
|
||||
.l10nignore
|
||||
.nextcloudignore
|
||||
.noopenapi
|
||||
.tx
|
||||
cypress
|
||||
tests
|
||||
|
||||
# Server specific
|
||||
/.devcontainer
|
||||
/__mocks__
|
||||
/__tests__
|
||||
/autotest*.sh
|
||||
/build
|
||||
/config/config.php
|
||||
/contribute
|
||||
/data
|
||||
+1
-1
Submodule 3rdparty updated: a21a6aebfc...08d9ba0139
@@ -10,11 +10,13 @@ declare(strict_types=1);
|
||||
namespace OCA\CloudFederationAPI\Controller;
|
||||
|
||||
use JsonException;
|
||||
use NCU\Security\Signature\Exceptions\IncomingRequestException;
|
||||
use OCP\AppFramework\Controller;
|
||||
use OCP\AppFramework\Http;
|
||||
use OCP\AppFramework\Http\Attribute\BruteForceProtection;
|
||||
use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\Attribute\PublicPage;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\Http\JSONResponse;
|
||||
use OCP\AppFramework\Http\Response;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
@@ -22,7 +24,6 @@ use OCP\IRequest;
|
||||
use OCP\OCM\Events\OCMEndpointRequestEvent;
|
||||
use OCP\OCM\Exceptions\OCMArgumentException;
|
||||
use OCP\OCM\IOCMDiscoveryService;
|
||||
use OCP\Security\Signature\Exceptions\IncomingRequestException;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
class OCMRequestController extends Controller {
|
||||
@@ -82,6 +83,6 @@ class OCMRequestController extends Controller {
|
||||
);
|
||||
$this->eventDispatcher->dispatchTyped($event);
|
||||
|
||||
return $event->getResponse() ?? new Response(Http::STATUS_NOT_FOUND);
|
||||
return $event->getResponse() ?? new DataResponse('', Http::STATUS_NOT_FOUND);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -7,6 +7,12 @@
|
||||
|
||||
namespace OCA\CloudFederationAPI\Controller;
|
||||
|
||||
use NCU\Federation\ISignedCloudFederationProvider;
|
||||
use NCU\Security\Signature\Exceptions\IdentityNotFoundException;
|
||||
use NCU\Security\Signature\Exceptions\IncomingRequestException;
|
||||
use NCU\Security\Signature\Exceptions\SignatoryNotFoundException;
|
||||
use NCU\Security\Signature\IIncomingSignedRequest;
|
||||
use NCU\Security\Signature\ISignatureManager;
|
||||
use OC\OCM\OCMSignatoryManager;
|
||||
use OCA\CloudFederationAPI\Config;
|
||||
use OCA\CloudFederationAPI\Db\FederatedInviteMapper;
|
||||
@@ -31,18 +37,12 @@ use OCP\Federation\Exceptions\ProviderDoesNotExistsException;
|
||||
use OCP\Federation\ICloudFederationFactory;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Federation\ISignedCloudFederationProvider;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IRequest;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserManager;
|
||||
use OCP\OCM\IOCMDiscoveryService;
|
||||
use OCP\Security\Signature\Exceptions\IdentityNotFoundException;
|
||||
use OCP\Security\Signature\Exceptions\IncomingRequestException;
|
||||
use OCP\Security\Signature\Exceptions\SignatoryNotFoundException;
|
||||
use OCP\Security\Signature\IIncomingSignedRequest;
|
||||
use OCP\Security\Signature\ISignatureManager;
|
||||
use OCP\Share\Exceptions\ShareNotFound;
|
||||
use OCP\Util;
|
||||
use Psr\Log\LoggerInterface;
|
||||
|
||||
@@ -9,6 +9,7 @@ declare(strict_types=1);
|
||||
|
||||
namespace OCA\CloudFederationApi\Tests;
|
||||
|
||||
use NCU\Security\Signature\ISignatureManager;
|
||||
use OCA\CloudFederationAPI\Config;
|
||||
use OCA\CloudFederationAPI\Controller\RequestHandlerController;
|
||||
use OCA\CloudFederationAPI\Db\FederatedInvite;
|
||||
@@ -28,7 +29,6 @@ use OCP\IURLGenerator;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use OCP\OCM\IOCMDiscoveryService;
|
||||
use OCP\Security\Signature\ISignatureManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Psr\Log\LoggerInterface;
|
||||
use Test\TestCase;
|
||||
|
||||
@@ -1,10 +1,8 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
|
||||
import moment from '@nextcloud/moment'
|
||||
import { createPinia, PiniaVuePlugin } from 'pinia'
|
||||
import Vue, { type ComponentPublicInstance } from 'vue'
|
||||
@@ -21,7 +19,7 @@ let ActivityTabPluginInstance
|
||||
*/
|
||||
export function registerCommentsPlugins() {
|
||||
window.OCA.Activity.registerSidebarAction({
|
||||
mount: async (el: HTMLElement, { node, reload }: { node: INode, reload: () => void }) => {
|
||||
mount: async (el, { fileInfo, reload }) => {
|
||||
const pinia = createPinia()
|
||||
|
||||
if (!ActivityTabPluginView) {
|
||||
@@ -34,10 +32,10 @@ export function registerCommentsPlugins() {
|
||||
pinia,
|
||||
propsData: {
|
||||
reloadCallback: reload,
|
||||
resourceId: node.fileid,
|
||||
resourceId: fileInfo.id,
|
||||
},
|
||||
})
|
||||
logger.info('Comments plugin mounted in Activity sidebar action', { node })
|
||||
logger.info('Comments plugin mounted in Activity sidebar action', { fileInfo })
|
||||
},
|
||||
unmount: () => {
|
||||
// destroy previous instance if available
|
||||
@@ -47,15 +45,9 @@ export function registerCommentsPlugins() {
|
||||
},
|
||||
})
|
||||
|
||||
window.OCA.Activity.registerSidebarEntries(async ({ node, limit, offset }: { node: INode, limit?: number, offset?: number }) => {
|
||||
const { data: comments } = await getComments(
|
||||
{ resourceType: 'files', resourceId: node.fileid },
|
||||
{
|
||||
limit,
|
||||
offset: offset ?? 0,
|
||||
},
|
||||
)
|
||||
logger.debug('Loaded comments', { node, comments })
|
||||
window.OCA.Activity.registerSidebarEntries(async ({ fileInfo, limit, offset }) => {
|
||||
const { data: comments } = await getComments({ resourceType: 'files', resourceId: fileInfo.id }, { limit, offset })
|
||||
logger.debug('Loaded comments', { fileInfo, comments })
|
||||
const { default: CommentView } = await import('./views/ActivityCommentEntry.vue')
|
||||
// @ts-expect-error Types are broken for Vue2
|
||||
const CommentsViewObject = Vue.extend(CommentView)
|
||||
@@ -70,7 +62,7 @@ export function registerCommentsPlugins() {
|
||||
el: element,
|
||||
propsData: {
|
||||
comment,
|
||||
resourceId: node.fileid,
|
||||
resourceId: fileInfo.id,
|
||||
reloadCallback: reload,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -18,7 +18,6 @@ use OCP\AppFramework\Http\Attribute\NoCSRFRequired;
|
||||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\Dashboard\IAPIWidget;
|
||||
use OCP\Dashboard\IAPIWidgetV2;
|
||||
use OCP\Dashboard\IButtonWidget;
|
||||
@@ -31,6 +30,7 @@ use OCP\Dashboard\Model\WidgetButton;
|
||||
use OCP\Dashboard\Model\WidgetItem;
|
||||
|
||||
use OCP\Dashboard\Model\WidgetOptions;
|
||||
use OCP\IConfig;
|
||||
use OCP\IRequest;
|
||||
|
||||
/**
|
||||
@@ -45,7 +45,7 @@ class DashboardApiController extends OCSController {
|
||||
IRequest $request,
|
||||
private IManager $dashboardManager,
|
||||
private IAppConfig $appConfig,
|
||||
private IUserConfig $userConfig,
|
||||
private IConfig $config,
|
||||
private ?string $userId,
|
||||
private DashboardService $service,
|
||||
) {
|
||||
@@ -59,7 +59,7 @@ class DashboardApiController extends OCSController {
|
||||
private function getShownWidgets(array $widgetIds): array {
|
||||
if (empty($widgetIds)) {
|
||||
$systemDefault = $this->appConfig->getAppValueString('layout', 'recommendations,spreed,mail,calendar');
|
||||
$widgetIds = explode(',', $this->userConfig->getValueString($this->userId, 'dashboard', 'layout', $systemDefault));
|
||||
$widgetIds = explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', $systemDefault));
|
||||
}
|
||||
|
||||
return array_filter(
|
||||
@@ -202,7 +202,7 @@ class DashboardApiController extends OCSController {
|
||||
#[NoAdminRequired]
|
||||
#[ApiRoute(verb: 'POST', url: '/api/v3/layout')]
|
||||
public function updateLayout(array $layout): DataResponse {
|
||||
$this->userConfig->setValueString($this->userId, 'dashboard', 'layout', implode(',', $layout));
|
||||
$this->config->setUserValue($this->userId, 'dashboard', 'layout', implode(',', $layout));
|
||||
return new DataResponse(['layout' => $layout]);
|
||||
}
|
||||
|
||||
@@ -230,7 +230,7 @@ class DashboardApiController extends OCSController {
|
||||
#[NoAdminRequired]
|
||||
#[ApiRoute(verb: 'POST', url: '/api/v3/statuses')]
|
||||
public function updateStatuses(array $statuses): DataResponse {
|
||||
$this->userConfig->setValueString($this->userId, 'dashboard', 'statuses', implode(',', $statuses));
|
||||
$this->config->setUserValue($this->userId, 'dashboard', 'statuses', implode(',', $statuses));
|
||||
return new DataResponse(['statuses' => $statuses]);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -17,10 +17,10 @@ use OCP\AppFramework\Http\Attribute\OpenAPI;
|
||||
use OCP\AppFramework\Http\FeaturePolicy;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\Dashboard\IIconWidget;
|
||||
use OCP\Dashboard\IManager;
|
||||
use OCP\Dashboard\IWidget;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\IConfig;
|
||||
use OCP\IL10N;
|
||||
use OCP\IRequest;
|
||||
@@ -33,9 +33,9 @@ class DashboardController extends Controller {
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
private IInitialState $initialState,
|
||||
private IEventDispatcher $eventDispatcher,
|
||||
private IManager $dashboardManager,
|
||||
private IConfig $config,
|
||||
private IUserConfig $userConfig,
|
||||
private IL10N $l10n,
|
||||
private ?string $userId,
|
||||
private DashboardService $service,
|
||||
@@ -67,9 +67,9 @@ class DashboardController extends Controller {
|
||||
$this->initialState->provideInitialState('statuses', $this->service->getStatuses());
|
||||
$this->initialState->provideInitialState('layout', $this->service->getLayout());
|
||||
$this->initialState->provideInitialState('appStoreEnabled', $this->config->getSystemValueBool('appstoreenabled', true));
|
||||
$this->initialState->provideInitialState('firstRun', $this->userConfig->getValueBool($this->userId, 'dashboard', 'firstRun', true));
|
||||
$this->initialState->provideInitialState('firstRun', $this->config->getUserValue($this->userId, 'dashboard', 'firstRun', '1') === '1');
|
||||
$this->initialState->provideInitialState('birthdate', $this->service->getBirthdate());
|
||||
$this->userConfig->setValueBool($this->userId, 'dashboard', 'firstRun', false);
|
||||
$this->config->setUserValue($this->userId, 'dashboard', 'firstRun', '0');
|
||||
|
||||
$response = new TemplateResponse('dashboard', 'index', [
|
||||
'id-app-content' => '#app-dashboard',
|
||||
|
||||
@@ -12,12 +12,12 @@ use JsonException;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\Accounts\PropertyDoesNotExistException;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUserManager;
|
||||
|
||||
class DashboardService {
|
||||
public function __construct(
|
||||
private IUserConfig $userConfig,
|
||||
private IConfig $config,
|
||||
private IAppConfig $appConfig,
|
||||
private ?string $userId,
|
||||
private IUserManager $userManager,
|
||||
@@ -31,24 +31,21 @@ class DashboardService {
|
||||
*/
|
||||
public function getLayout(): array {
|
||||
$systemDefault = $this->appConfig->getAppValueString('layout', 'recommendations,spreed,mail,calendar');
|
||||
return array_values(array_filter(
|
||||
explode(',', $this->userConfig->getValueString($this->userId, 'dashboard', 'layout', $systemDefault)),
|
||||
fn (string $value) => $value !== '')
|
||||
);
|
||||
return array_values(array_filter(explode(',', $this->config->getUserValue($this->userId, 'dashboard', 'layout', $systemDefault)), fn (string $value) => $value !== ''));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<string>
|
||||
*/
|
||||
public function getStatuses(): array {
|
||||
$configStatuses = $this->userConfig->getValueString($this->userId, 'dashboard', 'statuses');
|
||||
public function getStatuses() {
|
||||
$configStatuses = $this->config->getUserValue($this->userId, 'dashboard', 'statuses', '');
|
||||
try {
|
||||
// Parse the old format
|
||||
/** @var array<string, bool> $statuses */
|
||||
$statuses = json_decode($configStatuses, true, 512, JSON_THROW_ON_ERROR);
|
||||
// We avoid getting an empty array as it will not produce an object in UI's JS
|
||||
return array_keys(array_filter($statuses, static fn (bool $value) => $value));
|
||||
} catch (JsonException) {
|
||||
} catch (JsonException $e) {
|
||||
return array_values(array_filter(explode(',', $configStatuses), fn (string $value) => $value !== ''));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -13,7 +13,7 @@ use OC\Accounts\Account;
|
||||
use OCA\Dashboard\Service\DashboardService;
|
||||
use OCP\Accounts\IAccountManager;
|
||||
use OCP\AppFramework\Services\IAppConfig;
|
||||
use OCP\Config\IUserConfig;
|
||||
use OCP\IConfig;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
@@ -21,7 +21,7 @@ use Test\TestCase;
|
||||
|
||||
class DashboardServiceTest extends TestCase {
|
||||
|
||||
private IUserConfig&MockObject $userConfig;
|
||||
private IConfig&MockObject $config;
|
||||
private IAppConfig&MockObject $appConfig;
|
||||
private IUserManager&MockObject $userManager;
|
||||
private IAccountManager&MockObject $accountManager;
|
||||
@@ -30,13 +30,13 @@ class DashboardServiceTest extends TestCase {
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->userConfig = $this->createMock(IUserConfig::class);
|
||||
$this->config = $this->createMock(IConfig::class);
|
||||
$this->appConfig = $this->createMock(IAppConfig::class);
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->accountManager = $this->createMock(IAccountManager::class);
|
||||
|
||||
$this->service = new DashboardService(
|
||||
$this->userConfig,
|
||||
$this->config,
|
||||
$this->appConfig,
|
||||
'alice',
|
||||
$this->userManager,
|
||||
@@ -90,7 +90,7 @@ class DashboardServiceTest extends TestCase {
|
||||
|
||||
public function testGetBirthdateNoUserId(): void {
|
||||
$service = new DashboardService(
|
||||
$this->userConfig,
|
||||
$this->config,
|
||||
$this->appConfig,
|
||||
null,
|
||||
$this->userManager,
|
||||
|
||||
@@ -150,13 +150,12 @@ class Server extends \Sabre\DAV\Server {
|
||||
string $pluginName,
|
||||
string $eventName,
|
||||
): callable {
|
||||
$connection = \OCP\Server::get(Connection::class);
|
||||
return function (PropFind $propFind, INode $node) use (
|
||||
$callBack,
|
||||
$pluginName,
|
||||
$eventName,
|
||||
$connection,
|
||||
): bool {
|
||||
$connection = \OCP\Server::get(Connection::class);
|
||||
$queriesBefore = $connection->getStats()['executed'];
|
||||
$result = $callBack($propFind, $node);
|
||||
$queriesAfter = $connection->getStats()['executed'];
|
||||
|
||||
@@ -123,7 +123,7 @@ class TagsPlugin extends \Sabre\DAV\ServerPlugin {
|
||||
$isFav = false;
|
||||
$tags = $this->getTags($fileId);
|
||||
if ($tags) {
|
||||
$favPos = array_search(self::TAG_FAVORITE, $tags, true);
|
||||
$favPos = array_search(self::TAG_FAVORITE, $tags);
|
||||
if ($favPos !== false) {
|
||||
$isFav = true;
|
||||
unset($tags[$favPos]);
|
||||
|
||||
@@ -18,9 +18,7 @@ class ExternalCalendarTest extends TestCase {
|
||||
parent::setUp();
|
||||
|
||||
$this->abstractExternalCalendar
|
||||
= $this->getMockBuilder(ExternalCalendar::class)
|
||||
->setConstructorArgs(['example-app-id', 'calendar-uri-in-backend'])
|
||||
->getMock();
|
||||
= $this->getMockForAbstractClass(ExternalCalendar::class, ['example-app-id', 'calendar-uri-in-backend']);
|
||||
}
|
||||
|
||||
public function testGetName():void {
|
||||
|
||||
@@ -103,10 +103,10 @@ class EncryptAllTest extends TestCase {
|
||||
$this->user2 = $this->createMock(IUser::class);
|
||||
$this->user2->method('getUID')->willReturn('user2');
|
||||
|
||||
$this->userManager->expects($this->any())->method('getSeenUsers')->willReturnCallback(function () {
|
||||
$this->userManager->expects($this->any())->method('getSeenUsers')->will($this->returnCallback(function () {
|
||||
yield $this->user1;
|
||||
yield $this->user2;
|
||||
});
|
||||
}));
|
||||
$this->secureRandom = $this->getMockBuilder(ISecureRandom::class)->disableOriginalConstructor()->getMock();
|
||||
$this->secureRandom->expects($this->any())->method('generate')->willReturn('12345678');
|
||||
|
||||
|
||||
@@ -6,6 +6,7 @@
|
||||
*/
|
||||
namespace OCA\FederatedFileSharing\OCM;
|
||||
|
||||
use NCU\Federation\ISignedCloudFederationProvider;
|
||||
use OC\AppFramework\Http;
|
||||
use OC\Files\Filesystem;
|
||||
use OC\Files\SetupManager;
|
||||
@@ -29,7 +30,6 @@ use OCP\Federation\ICloudFederationFactory;
|
||||
use OCP\Federation\ICloudFederationProviderManager;
|
||||
use OCP\Federation\ICloudFederationShare;
|
||||
use OCP\Federation\ICloudIdManager;
|
||||
use OCP\Federation\ISignedCloudFederationProvider;
|
||||
use OCP\Files\IFilenameValidator;
|
||||
use OCP\Files\NotFoundException;
|
||||
use OCP\HintException;
|
||||
|
||||
@@ -134,11 +134,11 @@ async function copyCloudId(): Promise<void> {
|
||||
</a>
|
||||
</p>
|
||||
|
||||
<div>
|
||||
<p>{{ t('federatedfilesharing', 'HTML Code:') }}</p>
|
||||
<p>
|
||||
{{ t('federatedfilesharing', 'HTML Code:') }}
|
||||
<br>
|
||||
<pre><code>{{ htmlCode }}</code></pre>
|
||||
</div>
|
||||
<pre>{{ htmlCode }}</pre>
|
||||
</p>
|
||||
</template>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
@@ -320,7 +320,6 @@ OC.L10N.register(
|
||||
"Open online" : "Megnyitás online",
|
||||
"Rename" : "Átnevezés",
|
||||
"Details" : "Részletek",
|
||||
"Unfavorite" : "Nem kedvenc",
|
||||
"View in folder" : "Megtekintés mappában",
|
||||
"Today" : "Ma",
|
||||
"Last 7 days" : "Előző 7 nap",
|
||||
|
||||
@@ -318,7 +318,6 @@
|
||||
"Open online" : "Megnyitás online",
|
||||
"Rename" : "Átnevezés",
|
||||
"Details" : "Részletek",
|
||||
"Unfavorite" : "Nem kedvenc",
|
||||
"View in folder" : "Megtekintés mappában",
|
||||
"Today" : "Ma",
|
||||
"Last 7 days" : "Előző 7 nap",
|
||||
|
||||
@@ -201,7 +201,6 @@ OC.L10N.register(
|
||||
"Upload some content or sync with your devices!" : "파일을 업로드하거나 장치와 동기화하십시오!",
|
||||
"Go back" : "뒤로 가기",
|
||||
"Views" : "보기",
|
||||
"Loading …" : "로딩 중 …",
|
||||
"Your files" : "내 파일",
|
||||
"Open in files" : "파일에서 열기",
|
||||
"File cannot be accessed" : "파일에 접근할 수 없음",
|
||||
|
||||
@@ -199,7 +199,6 @@
|
||||
"Upload some content or sync with your devices!" : "파일을 업로드하거나 장치와 동기화하십시오!",
|
||||
"Go back" : "뒤로 가기",
|
||||
"Views" : "보기",
|
||||
"Loading …" : "로딩 중 …",
|
||||
"Your files" : "내 파일",
|
||||
"Open in files" : "파일에서 열기",
|
||||
"File cannot be accessed" : "파일에 접근할 수 없음",
|
||||
|
||||
@@ -9,6 +9,7 @@ import type { RootDirectory } from './DropServiceUtils.ts'
|
||||
|
||||
import { showError, showInfo, showSuccess, showWarning } from '@nextcloud/dialogs'
|
||||
import { NodeStatus } from '@nextcloud/files'
|
||||
import { getRootPath } from '@nextcloud/files/dav'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { join } from '@nextcloud/paths'
|
||||
import { getUploader, hasConflict } from '@nextcloud/upload'
|
||||
@@ -124,13 +125,14 @@ export async function onDropExternalFiles(root: RootDirectory, destination: IFol
|
||||
// If the file is a directory, we need to create it first
|
||||
// then browse its tree and upload its contents.
|
||||
if (file instanceof Directory) {
|
||||
const absolutePath = join(getRootPath(), destination.path, relativePath)
|
||||
try {
|
||||
logger.debug('Processing directory', { relativePath })
|
||||
await createDirectoryIfNotExists(relativePath)
|
||||
await createDirectoryIfNotExists(absolutePath)
|
||||
await uploadDirectoryContents(file, relativePath)
|
||||
} catch (error) {
|
||||
showError(t('files', 'Unable to create the directory {directory}', { directory: file.name }))
|
||||
logger.error('Unable to create the directory', { error, relativePath, directory: file })
|
||||
logger.error('', { error, absolutePath, directory: file })
|
||||
}
|
||||
continue
|
||||
}
|
||||
|
||||
@@ -8,9 +8,8 @@ import type { FileStat, ResponseDataDetailed } from 'webdav'
|
||||
|
||||
import { showInfo, showWarning } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { defaultRemoteURL, defaultRootPath, getClient, getDefaultPropfind, resultToNode } from '@nextcloud/files/dav'
|
||||
import { getClient, getDefaultPropfind, resultToNode } from '@nextcloud/files/dav'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { join } from '@nextcloud/paths'
|
||||
import { openConflictPicker } from '@nextcloud/upload'
|
||||
import logger from '../logger.ts'
|
||||
|
||||
@@ -133,17 +132,18 @@ function readDirectory(directory: FileSystemDirectoryEntry): Promise<FileSystemE
|
||||
}
|
||||
|
||||
/**
|
||||
* @param path - The path relative to the dav root
|
||||
* Create a directory if it does not exist
|
||||
*
|
||||
* @param absolutePath - the absolute path of the directory to create
|
||||
*/
|
||||
export async function createDirectoryIfNotExists(path: string) {
|
||||
const davUrl = join(defaultRemoteURL, defaultRootPath)
|
||||
const davClient = getClient(davUrl)
|
||||
const dirExists = await davClient.exists(path)
|
||||
export async function createDirectoryIfNotExists(absolutePath: string) {
|
||||
const davClient = getClient()
|
||||
const dirExists = await davClient.exists(absolutePath)
|
||||
if (!dirExists) {
|
||||
logger.debug('Directory does not exist, creating it', { path })
|
||||
await davClient.createDirectory(path, { recursive: true })
|
||||
const stat = await davClient.stat(path, { details: true, data: getDefaultPropfind() }) as ResponseDataDetailed<FileStat>
|
||||
emit('files:node:created', resultToNode(stat.data, defaultRootPath, davUrl))
|
||||
logger.debug('Directory does not exist, creating it', { absolutePath })
|
||||
await davClient.createDirectory(absolutePath, { recursive: true })
|
||||
const stat = await davClient.stat(absolutePath, { details: true, data: getDefaultPropfind() }) as ResponseDataDetailed<FileStat>
|
||||
emit('files:node:created', resultToNode(stat.data))
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -18,6 +18,11 @@ return [
|
||||
'url' => '/ajax/applicable',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'Ajax#oauth2Callback',
|
||||
'url' => '/ajax/oauth2.php',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'Ajax#getSshKeys',
|
||||
'url' => '/ajax/public_key.php',
|
||||
|
||||
@@ -37,16 +37,13 @@ return array(
|
||||
'OCA\\Files_External\\Controller\\StoragesController' => $baseDir . '/../lib/Controller/StoragesController.php',
|
||||
'OCA\\Files_External\\Controller\\UserGlobalStoragesController' => $baseDir . '/../lib/Controller/UserGlobalStoragesController.php',
|
||||
'OCA\\Files_External\\Controller\\UserStoragesController' => $baseDir . '/../lib/Controller/UserStoragesController.php',
|
||||
'OCA\\Files_External\\Event\\StorageCreatedEvent' => $baseDir . '/../lib/Event/StorageCreatedEvent.php',
|
||||
'OCA\\Files_External\\Event\\StorageDeletedEvent' => $baseDir . '/../lib/Event/StorageDeletedEvent.php',
|
||||
'OCA\\Files_External\\Event\\StorageUpdatedEvent' => $baseDir . '/../lib/Event/StorageUpdatedEvent.php',
|
||||
'OCA\\Files_External\\Lib\\ApplicableHelper' => $baseDir . '/../lib/Lib/ApplicableHelper.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\AmazonS3\\AccessKey' => $baseDir . '/../lib/Lib/Auth/AmazonS3/AccessKey.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\AuthMechanism' => $baseDir . '/../lib/Lib/Auth/AuthMechanism.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\Builtin' => $baseDir . '/../lib/Lib/Auth/Builtin.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\IUserProvided' => $baseDir . '/../lib/Lib/Auth/IUserProvided.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\InvalidAuth' => $baseDir . '/../lib/Lib/Auth/InvalidAuth.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\NullMechanism' => $baseDir . '/../lib/Lib/Auth/NullMechanism.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OAuth2\\OAuth2' => $baseDir . '/../lib/Lib/Auth/OAuth2/OAuth2.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV2' => $baseDir . '/../lib/Lib/Auth/OpenStack/OpenStackV2.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV3' => $baseDir . '/../lib/Lib/Auth/OpenStack/OpenStackV3.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OpenStack\\Rackspace' => $baseDir . '/../lib/Lib/Auth/OpenStack/Rackspace.php',
|
||||
@@ -120,13 +117,11 @@ return array(
|
||||
'OCA\\Files_External\\Service\\GlobalStoragesService' => $baseDir . '/../lib/Service/GlobalStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\ImportLegacyStoragesService' => $baseDir . '/../lib/Service/ImportLegacyStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\LegacyStoragesService' => $baseDir . '/../lib/Service/LegacyStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\MountCacheService' => $baseDir . '/../lib/Service/MountCacheService.php',
|
||||
'OCA\\Files_External\\Service\\StoragesService' => $baseDir . '/../lib/Service/StoragesService.php',
|
||||
'OCA\\Files_External\\Service\\UserGlobalStoragesService' => $baseDir . '/../lib/Service/UserGlobalStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\UserStoragesService' => $baseDir . '/../lib/Service/UserStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\UserTrait' => $baseDir . '/../lib/Service/UserTrait.php',
|
||||
'OCA\\Files_External\\Settings\\Admin' => $baseDir . '/../lib/Settings/Admin.php',
|
||||
'OCA\\Files_External\\Settings\\CommonSettingsTrait' => $baseDir . '/../lib/Settings/CommonSettingsTrait.php',
|
||||
'OCA\\Files_External\\Settings\\Personal' => $baseDir . '/../lib/Settings/Personal.php',
|
||||
'OCA\\Files_External\\Settings\\PersonalSection' => $baseDir . '/../lib/Settings/PersonalSection.php',
|
||||
'OCA\\Files_External\\Settings\\Section' => $baseDir . '/../lib/Settings/Section.php',
|
||||
|
||||
@@ -52,16 +52,13 @@ class ComposerStaticInitFiles_External
|
||||
'OCA\\Files_External\\Controller\\StoragesController' => __DIR__ . '/..' . '/../lib/Controller/StoragesController.php',
|
||||
'OCA\\Files_External\\Controller\\UserGlobalStoragesController' => __DIR__ . '/..' . '/../lib/Controller/UserGlobalStoragesController.php',
|
||||
'OCA\\Files_External\\Controller\\UserStoragesController' => __DIR__ . '/..' . '/../lib/Controller/UserStoragesController.php',
|
||||
'OCA\\Files_External\\Event\\StorageCreatedEvent' => __DIR__ . '/..' . '/../lib/Event/StorageCreatedEvent.php',
|
||||
'OCA\\Files_External\\Event\\StorageDeletedEvent' => __DIR__ . '/..' . '/../lib/Event/StorageDeletedEvent.php',
|
||||
'OCA\\Files_External\\Event\\StorageUpdatedEvent' => __DIR__ . '/..' . '/../lib/Event/StorageUpdatedEvent.php',
|
||||
'OCA\\Files_External\\Lib\\ApplicableHelper' => __DIR__ . '/..' . '/../lib/Lib/ApplicableHelper.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\AmazonS3\\AccessKey' => __DIR__ . '/..' . '/../lib/Lib/Auth/AmazonS3/AccessKey.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\AuthMechanism' => __DIR__ . '/..' . '/../lib/Lib/Auth/AuthMechanism.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\Builtin' => __DIR__ . '/..' . '/../lib/Lib/Auth/Builtin.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\IUserProvided' => __DIR__ . '/..' . '/../lib/Lib/Auth/IUserProvided.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\InvalidAuth' => __DIR__ . '/..' . '/../lib/Lib/Auth/InvalidAuth.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\NullMechanism' => __DIR__ . '/..' . '/../lib/Lib/Auth/NullMechanism.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OAuth2\\OAuth2' => __DIR__ . '/..' . '/../lib/Lib/Auth/OAuth2/OAuth2.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV2' => __DIR__ . '/..' . '/../lib/Lib/Auth/OpenStack/OpenStackV2.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OpenStack\\OpenStackV3' => __DIR__ . '/..' . '/../lib/Lib/Auth/OpenStack/OpenStackV3.php',
|
||||
'OCA\\Files_External\\Lib\\Auth\\OpenStack\\Rackspace' => __DIR__ . '/..' . '/../lib/Lib/Auth/OpenStack/Rackspace.php',
|
||||
@@ -135,13 +132,11 @@ class ComposerStaticInitFiles_External
|
||||
'OCA\\Files_External\\Service\\GlobalStoragesService' => __DIR__ . '/..' . '/../lib/Service/GlobalStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\ImportLegacyStoragesService' => __DIR__ . '/..' . '/../lib/Service/ImportLegacyStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\LegacyStoragesService' => __DIR__ . '/..' . '/../lib/Service/LegacyStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\MountCacheService' => __DIR__ . '/..' . '/../lib/Service/MountCacheService.php',
|
||||
'OCA\\Files_External\\Service\\StoragesService' => __DIR__ . '/..' . '/../lib/Service/StoragesService.php',
|
||||
'OCA\\Files_External\\Service\\UserGlobalStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserGlobalStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\UserStoragesService' => __DIR__ . '/..' . '/../lib/Service/UserStoragesService.php',
|
||||
'OCA\\Files_External\\Service\\UserTrait' => __DIR__ . '/..' . '/../lib/Service/UserTrait.php',
|
||||
'OCA\\Files_External\\Settings\\Admin' => __DIR__ . '/..' . '/../lib/Settings/Admin.php',
|
||||
'OCA\\Files_External\\Settings\\CommonSettingsTrait' => __DIR__ . '/..' . '/../lib/Settings/CommonSettingsTrait.php',
|
||||
'OCA\\Files_External\\Settings\\Personal' => __DIR__ . '/..' . '/../lib/Settings/Personal.php',
|
||||
'OCA\\Files_External\\Settings\\PersonalSection' => __DIR__ . '/..' . '/../lib/Settings/PersonalSection.php',
|
||||
'OCA\\Files_External\\Settings\\Section' => __DIR__ . '/..' . '/../lib/Settings/Section.php',
|
||||
|
||||
@@ -0,0 +1,4 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/#files_external{margin-bottom:0px}#externalStorage{margin:15px 0 20px 0}#externalStorage tr.externalStorageLoading>td{text-align:center}#externalStorage td{height:50px}#externalStorage td.mountOptionsToggle,#externalStorage td.remove,#externalStorage td.save{position:relative;padding:0 !important;width:44px}#externalStorage td.mountOptionsToggle [class^=icon-],#externalStorage td.mountOptionsToggle [class*=" icon-"],#externalStorage td.remove [class^=icon-],#externalStorage td.remove [class*=" icon-"],#externalStorage td.save [class^=icon-],#externalStorage td.save [class*=" icon-"]{width:44px;height:44px;margin:3px;opacity:.5;padding:14px;vertical-align:text-bottom;cursor:pointer}#externalStorage td.mountOptionsToggle [class^=icon-]:hover,#externalStorage td.mountOptionsToggle [class*=" icon-"]:hover,#externalStorage td.remove [class^=icon-]:hover,#externalStorage td.remove [class*=" icon-"]:hover,#externalStorage td.save [class^=icon-]:hover,#externalStorage td.save [class*=" icon-"]:hover{opacity:1}#externalStorage td.mountPoint,#externalStorage td.backend,#externalStorage td.authentication,#externalStorage td.configuration{min-width:160px;width:15%}#externalStorage td.status{display:table-cell;vertical-align:middle;width:43px}#externalStorage td.status>span{display:inline-block;height:28px;width:28px;vertical-align:text-bottom;border-radius:50%;cursor:pointer}#externalStorage td>input:not(.applicableToAllUsers),#externalStorage td>select{width:100%}#externalStorage td>img{padding-top:7px;opacity:.5}#externalStorage td>img:hover{cursor:pointer;opacity:1}#externalStorage .popovermenu li>.menuitem{width:fit-content !important}#addMountPoint>td{border:none}#addMountPoint>td.applicable{visibility:hidden}#addMountPoint>td.hidden{visibility:hidden}#selectBackend{margin-inline-start:-10px;width:150px}#externalStorage td.configuration,#externalStorage td.backend{white-space:normal}#externalStorage td.configuration>*{white-space:nowrap}#externalStorage td.configuration input.added{margin-inline-end:6px}#externalStorage label>input[type=checkbox]{margin-inline-end:3px}#externalStorage td.configuration label{width:100%;display:inline-flex;align-items:center}#externalStorage td.configuration input.disabled-success{background-color:rgba(134,255,110,.9)}#externalStorage td.applicable label{display:inline-flex;align-items:center}#externalStorage td.applicable div.chzn-container{position:relative;top:3px}#externalStorage .select2-container.applicableUsers{width:100% !important}#userMountingBackends{padding-inline-start:25px}.files-external-select2 .select2-results .select2-result-label{height:32px;padding:3px}.files-external-select2 .select2-results .select2-result-label>span{display:block;position:relative}.files-external-select2 .select2-results .select2-result-label .avatardiv{display:inline-block}.files-external-select2 .select2-results .select2-result-label .avatardiv+span{position:absolute;top:5px;margin-inline-start:10px}.files-external-select2 .select2-results .select2-result-label .avatardiv[data-type=group]+span{vertical-align:top;top:6px;position:absolute;max-width:80%;inset-inline-start:30px;text-overflow:ellipsis;white-space:nowrap;overflow:hidden}#externalStorage .select2-container .select2-search-choice{display:flex}#externalStorage .select2-container .select2-search-choice .select2-search-choice-close{display:block;inset-inline-start:auto;position:relative;width:20px}#externalStorage .mountOptionsToggle .dropdown{width:auto}.nav-icon-external-storage{background-image:var(--icon-external-dark)}.global_credentials__personal{margin:10px auto;text-align:center;width:min(400px,100vw)}/*# sourceMappingURL=settings.css.map */
|
||||
@@ -0,0 +1 @@
|
||||
{"version":3,"sourceRoot":"","sources":["settings.scss"],"names":[],"mappings":"AAAA;AAAA;AAAA;AAAA,GAIA,gBACC,kBAGD,iBACC,qBAEA,8CACC,kBAGD,oBACC,YAEA,2FAGC,kBACA,qBACA,WACA,yRAEC,WACA,YACA,WACA,WACA,aACA,2BACA,eACA,6TACC,UAKH,gIAIC,gBACA,UAGD,2BAEC,mBACA,sBAEA,WAEA,gCACC,qBACA,YACA,WACA,2BACA,kBACA,eAIF,gFACC,WAGD,wBACC,gBACA,WAEA,8BACC,eACA,UAKH,2CACC,6BAIF,8BAEA,+CAEA,2CAEA,eACC,0BACA,YAGD,8DAEC,mBAGD,oCACC,mBAGD,8CACC,sBAGD,4CACC,sBAGD,wCACC,WACA,oBACA,mBAGD,yDACC,sCAGD,qCACC,oBACA,mBAGD,kDACC,kBACA,QAGD,oDACC,sBAGD,sBACC,0BAGD,+DACC,YACA,YAGD,oEACC,cACA,kBAGD,0EACC,qBAGD,+EACC,kBACA,QACA,yBAGD,gGACC,mBACA,QACA,kBACA,cACA,wBACA,uBACA,mBACA,gBAGD,2DACC,aACA,wFACC,cACA,wBACA,kBACA,WAIF,+CACC,WAGD,2BACC,2CAGD,8BACI,iBACA,kBACA","file":"settings.css"}
|
||||
@@ -0,0 +1,2 @@
|
||||
SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
@@ -0,0 +1,194 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
#files_external {
|
||||
margin-bottom: 0px;
|
||||
}
|
||||
|
||||
#externalStorage {
|
||||
margin: 15px 0 20px 0;
|
||||
|
||||
tr.externalStorageLoading > td {
|
||||
text-align: center;
|
||||
}
|
||||
|
||||
td {
|
||||
height: 50px;
|
||||
|
||||
&.mountOptionsToggle,
|
||||
&.remove,
|
||||
&.save {
|
||||
position: relative;
|
||||
padding: 0 !important;
|
||||
width: 44px;
|
||||
[class^='icon-'],
|
||||
[class*=' icon-'] {
|
||||
width: 44px;
|
||||
height: 44px;
|
||||
margin: 3px;
|
||||
opacity: 0.5;
|
||||
padding: 14px;
|
||||
vertical-align: text-bottom;
|
||||
cursor: pointer;
|
||||
&:hover {
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
&.mountPoint,
|
||||
&.backend,
|
||||
&.authentication,
|
||||
&.configuration {
|
||||
min-width: 160px;
|
||||
width: 15%;
|
||||
}
|
||||
|
||||
&.status {
|
||||
/* overwrite conflicting core styles */
|
||||
display: table-cell;
|
||||
vertical-align: middle;
|
||||
/* ensure width does not change even if internal span is not displayed */
|
||||
width: 43px;
|
||||
|
||||
> span {
|
||||
display: inline-block;
|
||||
height: 28px;
|
||||
width: 28px;
|
||||
vertical-align: text-bottom;
|
||||
border-radius: 50%;
|
||||
cursor: pointer;
|
||||
}
|
||||
}
|
||||
|
||||
> input:not(.applicableToAllUsers), & > select {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
> img {
|
||||
padding-top: 7px;
|
||||
opacity: 0.5;
|
||||
|
||||
&:hover {
|
||||
cursor:pointer;
|
||||
opacity: 1;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
.popovermenu li > .menuitem {
|
||||
width: fit-content !important;
|
||||
}
|
||||
}
|
||||
|
||||
#addMountPoint>td { border:none; }
|
||||
|
||||
#addMountPoint>td.applicable { visibility:hidden; }
|
||||
|
||||
#addMountPoint>td.hidden { visibility:hidden; }
|
||||
|
||||
#selectBackend {
|
||||
margin-inline-start: -10px;
|
||||
width: 150px;
|
||||
}
|
||||
|
||||
#externalStorage td.configuration,
|
||||
#externalStorage td.backend {
|
||||
white-space: normal;
|
||||
}
|
||||
|
||||
#externalStorage td.configuration > * {
|
||||
white-space: nowrap;
|
||||
}
|
||||
|
||||
#externalStorage td.configuration input.added {
|
||||
margin-inline-end: 6px;
|
||||
}
|
||||
|
||||
#externalStorage label > input[type="checkbox"] {
|
||||
margin-inline-end: 3px;
|
||||
}
|
||||
|
||||
#externalStorage td.configuration label {
|
||||
width: 100%;
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#externalStorage td.configuration input.disabled-success {
|
||||
background-color: rgba(134, 255, 110, 0.9);
|
||||
}
|
||||
|
||||
#externalStorage td.applicable label {
|
||||
display: inline-flex;
|
||||
align-items: center;
|
||||
}
|
||||
|
||||
#externalStorage td.applicable div.chzn-container {
|
||||
position: relative;
|
||||
top: 3px;
|
||||
}
|
||||
|
||||
#externalStorage .select2-container.applicableUsers {
|
||||
width: 100% !important;
|
||||
}
|
||||
|
||||
#userMountingBackends {
|
||||
padding-inline-start: 25px;
|
||||
}
|
||||
|
||||
.files-external-select2 .select2-results .select2-result-label {
|
||||
height: 32px;
|
||||
padding: 3px;
|
||||
}
|
||||
|
||||
.files-external-select2 .select2-results .select2-result-label > span {
|
||||
display: block;
|
||||
position: relative;
|
||||
}
|
||||
|
||||
.files-external-select2 .select2-results .select2-result-label .avatardiv {
|
||||
display:inline-block;
|
||||
}
|
||||
|
||||
.files-external-select2 .select2-results .select2-result-label .avatardiv + span {
|
||||
position: absolute;
|
||||
top: 5px;
|
||||
margin-inline-start: 10px;
|
||||
}
|
||||
|
||||
.files-external-select2 .select2-results .select2-result-label .avatardiv[data-type="group"] + span {
|
||||
vertical-align: top;
|
||||
top: 6px;
|
||||
position: absolute;
|
||||
max-width: 80%;
|
||||
inset-inline-start: 30px;
|
||||
text-overflow: ellipsis;
|
||||
white-space: nowrap;
|
||||
overflow: hidden;
|
||||
}
|
||||
|
||||
#externalStorage .select2-container .select2-search-choice {
|
||||
display: flex;
|
||||
.select2-search-choice-close {
|
||||
display: block;
|
||||
inset-inline-start: auto;
|
||||
position: relative;
|
||||
width: 20px;
|
||||
}
|
||||
}
|
||||
|
||||
#externalStorage .mountOptionsToggle .dropdown {
|
||||
width: auto;
|
||||
}
|
||||
|
||||
.nav-icon-external-storage {
|
||||
background-image: var(--icon-external-dark);
|
||||
}
|
||||
|
||||
.global_credentials__personal {
|
||||
margin: 10px auto;
|
||||
text-align: center;
|
||||
width: min(400px, 100vw);
|
||||
}
|
||||
@@ -1 +1 @@
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M456-432h156.48q24.96 0 42.24-17.39Q672-466.77 672-491.89q0-25.11-17.42-42.61T612-552h-1q-4.83-30.72-27.99-51.36Q559.85-624 528-624q-26 0-45.98 12.96-19.99 12.96-30.46 35.04-28.54 1.92-48.05 22.56Q384-532.8 384-504q0 28.8 21 50.4 21 21.6 51 21.6ZM120-144q-29.7 0-50.85-21.15Q48-186.3 48-216v-504h72v504h633v72H120Zm144-144q-29.7 0-50.85-21.15Q192-330.3 192-360v-432q0-29.7 21.15-50.85Q234.3-864 264-864h168l96 96h264q30-1 51 20.44 21 21.45 21 51.56v336q0 29.7-21.15 50.85Q821.7-288 792-288H264Z"/></svg>
|
||||
<svg xmlns="http://www.w3.org/2000/svg" height="20px" viewBox="0 -960 960 960" width="20px"><path d="M456-432h156.48q24.96 0 42.24-17.39Q672-466.77 672-491.89q0-25.11-17.42-42.61T612-552h-1q-4.83-30.72-27.99-51.36Q559.85-624 528-624q-26 0-45.98 12.96-19.99 12.96-30.46 35.04-28.54 1.92-48.05 22.56Q384-532.8 384-504q0 28.8 21 50.4 21 21.6 51 21.6ZM120-144q-29.7 0-50.85-21.15Q48-186.3 48-216v-504h72v504h633v72H120Zm144-144q-29.7 0-50.85-21.15Q192-330.3 192-360v-432q0-29.7 21.15-50.85Q234.3-864 264-864h168l96 96h264q30-1 51 20.44 21 21.45 21 51.56v336q0 29.7-21.15 50.85Q821.7-288 792-288H264Z"/></svg>
|
||||
|
Before Width: | Height: | Size: 605 B After Width: | Height: | Size: 604 B |
@@ -0,0 +1,108 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2018-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
/**
|
||||
*
|
||||
* @param $tr
|
||||
*/
|
||||
function displayGranted($tr) {
|
||||
$tr.find('.configuration input.auth-param').attr('disabled', 'disabled').addClass('disabled-success')
|
||||
}
|
||||
|
||||
OCA.Files_External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) {
|
||||
if (authMechanism === 'oauth2::oauth2') {
|
||||
const config = $tr.find('.configuration')
|
||||
config.append($(document.createElement('input'))
|
||||
.addClass('button auth-param')
|
||||
.attr('type', 'button')
|
||||
.attr('value', t('files_external', 'Grant access'))
|
||||
.attr('name', 'oauth2_grant'))
|
||||
|
||||
onCompletion.then(function() {
|
||||
const configured = $tr.find('[data-parameter="configured"]')
|
||||
if ($(configured).val() == 'true') {
|
||||
displayGranted($tr)
|
||||
} else {
|
||||
const client_id = $tr.find('.configuration [data-parameter="client_id"]').val()
|
||||
const client_secret = $tr.find('.configuration [data-parameter="client_secret"]')
|
||||
.val()
|
||||
if (client_id != '' && client_secret != '') {
|
||||
const params = {}
|
||||
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) {
|
||||
params[key] = value
|
||||
})
|
||||
if (params.code !== undefined) {
|
||||
const token = $tr.find('.configuration [data-parameter="token"]')
|
||||
const statusSpan = $tr.find('.status span')
|
||||
statusSpan.removeClass()
|
||||
statusSpan.addClass('waiting')
|
||||
$.post(
|
||||
OC.filePath('files_external', 'ajax', 'oauth2.php'),
|
||||
{
|
||||
step: 2,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect: location.protocol + '//' + location.host + location.pathname,
|
||||
code: params.code,
|
||||
},
|
||||
function(result) {
|
||||
if (result && result.status == 'success') {
|
||||
$(token).val(result.data.token)
|
||||
$(configured).val('true')
|
||||
OCA.Files_External.Settings.mountConfig.saveStorageConfig($tr, function(status) {
|
||||
if (status) {
|
||||
displayGranted($tr)
|
||||
}
|
||||
})
|
||||
} else {
|
||||
OC.dialogs.alert(
|
||||
result.data.message,
|
||||
t('files_external', 'Error configuring OAuth2'),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
|
||||
$('#externalStorage').on('click', '[name="oauth2_grant"]', function(event) {
|
||||
event.preventDefault()
|
||||
const tr = $(this).parent().parent()
|
||||
const configured = $(this).parent().find('[data-parameter="configured"]')
|
||||
const client_id = $(this).parent().find('[data-parameter="client_id"]').val()
|
||||
const client_secret = $(this).parent().find('[data-parameter="client_secret"]').val()
|
||||
if (client_id != '' && client_secret != '') {
|
||||
const token = $(this).parent().find('[data-parameter="token"]')
|
||||
$.post(
|
||||
OC.filePath('files_external', 'ajax', 'oauth2.php'),
|
||||
{
|
||||
step: 1,
|
||||
client_id,
|
||||
client_secret,
|
||||
redirect: location.protocol + '//' + location.host + location.pathname,
|
||||
},
|
||||
function(result) {
|
||||
if (result && result.status == 'success') {
|
||||
$(configured).val('false')
|
||||
$(token).val('false')
|
||||
OCA.Files_External.Settings.mountConfig.saveStorageConfig(tr, function(status) {
|
||||
window.location = result.data.url
|
||||
})
|
||||
} else {
|
||||
OC.dialogs.alert(
|
||||
result.data.message,
|
||||
t('files_external', 'Error configuring OAuth2'),
|
||||
)
|
||||
}
|
||||
},
|
||||
)
|
||||
}
|
||||
})
|
||||
})
|
||||
@@ -0,0 +1,76 @@
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2017-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2015 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
OCA.Files_External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) {
|
||||
if (scheme === 'publickey' && authMechanism === 'publickey::rsa') {
|
||||
const config = $tr.find('.configuration')
|
||||
if ($(config).find('[name="public_key_generate"]').length === 0) {
|
||||
setupTableRow($tr, config)
|
||||
onCompletion.then(function() {
|
||||
// If there's no private key, build one
|
||||
if (0 === $(config).find('[data-parameter="private_key"]').val().length) {
|
||||
generateKeys($tr)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
$('#externalStorage').on('click', '[name="public_key_generate"]', function(event) {
|
||||
event.preventDefault()
|
||||
const tr = $(this).parent().parent()
|
||||
generateKeys(tr)
|
||||
})
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tr
|
||||
* @param config
|
||||
*/
|
||||
function setupTableRow(tr, config) {
|
||||
const selectList = document.createElement('select')
|
||||
selectList.id = 'keyLength'
|
||||
|
||||
const options = [1024, 2048, 4096]
|
||||
for (let i = 0; i < options.length; i++) {
|
||||
const option = document.createElement('option')
|
||||
option.value = options[i]
|
||||
option.text = options[i]
|
||||
selectList.appendChild(option)
|
||||
}
|
||||
|
||||
$(config).append(selectList)
|
||||
|
||||
$(config).append($(document.createElement('input'))
|
||||
.addClass('button auth-param')
|
||||
.attr('type', 'button')
|
||||
.attr('value', t('files_external', 'Generate keys'))
|
||||
.attr('name', 'public_key_generate'))
|
||||
}
|
||||
|
||||
/**
|
||||
*
|
||||
* @param tr
|
||||
*/
|
||||
function generateKeys(tr) {
|
||||
const config = $(tr).find('.configuration')
|
||||
const keyLength = config.find('#keyLength').val()
|
||||
|
||||
$.post(OC.filePath('files_external', 'ajax', 'public_key.php'), {
|
||||
keyLength,
|
||||
}, function(result) {
|
||||
if (result && result.status === 'success') {
|
||||
$(config).find('[data-parameter="public_key"]').val(result.data.public_key).keyup()
|
||||
$(config).find('[data-parameter="private_key"]').val(result.data.private_key)
|
||||
OCA.Files_External.Settings.mountConfig.saveStorageConfig(tr, function() {
|
||||
// Nothing to do
|
||||
})
|
||||
} else {
|
||||
OC.dialogs.alert(result.data.message, t('files_external', 'Error generating key pair'))
|
||||
}
|
||||
})
|
||||
}
|
||||
})
|
||||
@@ -0,0 +1,47 @@
|
||||
(function() {
|
||||
var template = Handlebars.template, templates = OCA.Files_External.Templates = OCA.Files_External.Templates || {};
|
||||
templates['credentialsDialog'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
|
||||
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
|
||||
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
|
||||
return parent[propertyName];
|
||||
}
|
||||
return undefined
|
||||
};
|
||||
|
||||
return "<div id=\"files_external_div_form\"><div>\n <div>"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"credentials_text") || (depth0 != null ? lookupProperty(depth0,"credentials_text") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"credentials_text","hash":{},"data":data,"loc":{"start":{"line":2,"column":6},"end":{"line":2,"column":26}}}) : helper)))
|
||||
+ "</div>\n <form>\n <input type=\"text\" name=\"username\" placeholder=\""
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"placeholder_username") || (depth0 != null ? lookupProperty(depth0,"placeholder_username") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"placeholder_username","hash":{},"data":data,"loc":{"start":{"line":4,"column":51},"end":{"line":4,"column":75}}}) : helper)))
|
||||
+ "\"/>\n <input type=\"password\" name=\"password\" placeholder=\""
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"placeholder_password") || (depth0 != null ? lookupProperty(depth0,"placeholder_password") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"placeholder_password","hash":{},"data":data,"loc":{"start":{"line":5,"column":55},"end":{"line":5,"column":79}}}) : helper)))
|
||||
+ "\"/>\n </form>\n </div>\n</div>\n";
|
||||
},"useData":true});
|
||||
templates['mountOptionsDropDown'] = template({"compiler":[8,">= 4.3.0"],"main":function(container,depth0,helpers,partials,data) {
|
||||
var helper, alias1=depth0 != null ? depth0 : (container.nullContext || {}), alias2=container.hooks.helperMissing, alias3="function", alias4=container.escapeExpression, lookupProperty = container.lookupProperty || function(parent, propertyName) {
|
||||
if (Object.prototype.hasOwnProperty.call(parent, propertyName)) {
|
||||
return parent[propertyName];
|
||||
}
|
||||
return undefined
|
||||
};
|
||||
|
||||
return "<div class=\"popovermenu open\">\n <ul>\n <li class=\"optionRow\">\n <span class=\"menuitem\">\n <input id=\"mountOptionsEncrypt\" class=\"checkbox\" name=\"encrypt\" type=\"checkbox\" value=\"true\" checked=\"checked\"/>\n <label for=\"mountOptionsEncrypt\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsEncryptLabel") || (depth0 != null ? lookupProperty(depth0,"mountOptionsEncryptLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsEncryptLabel","hash":{},"data":data,"loc":{"start":{"line":6,"column":37},"end":{"line":6,"column":65}}}) : helper)))
|
||||
+ "</label>\n </span>\n </li>\n <li class=\"optionRow\">\n <span class=\"menuitem\">\n <input id=\"mountOptionsPreviews\" class=\"checkbox\" name=\"previews\" type=\"checkbox\" value=\"true\" checked=\"checked\"/>\n <label for=\"mountOptionsPreviews\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsPreviewsLabel") || (depth0 != null ? lookupProperty(depth0,"mountOptionsPreviewsLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsPreviewsLabel","hash":{},"data":data,"loc":{"start":{"line":12,"column":38},"end":{"line":12,"column":67}}}) : helper)))
|
||||
+ "</label>\n </span>\n </li>\n <li class=\"optionRow\">\n <span class=\"menuitem\">\n <input id=\"mountOptionsSharing\" class=\"checkbox\" name=\"enable_sharing\" type=\"checkbox\" value=\"true\"/>\n <label for=\"mountOptionsSharing\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsSharingLabel") || (depth0 != null ? lookupProperty(depth0,"mountOptionsSharingLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsSharingLabel","hash":{},"data":data,"loc":{"start":{"line":18,"column":37},"end":{"line":18,"column":65}}}) : helper)))
|
||||
+ "</label>\n </span>\n </li>\n <li class=\"optionRow\">\n <span class=\"menuitem icon-search\">\n <label for=\"mountOptionsFilesystemCheck\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsFilesystemCheckLabel") || (depth0 != null ? lookupProperty(depth0,"mountOptionsFilesystemCheckLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsFilesystemCheckLabel","hash":{},"data":data,"loc":{"start":{"line":23,"column":45},"end":{"line":23,"column":81}}}) : helper)))
|
||||
+ "</label>\n <select id=\"mountOptionsFilesystemCheck\" name=\"filesystem_check_changes\" data-type=\"int\">\n <option value=\"0\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsFilesystemCheckOnce") || (depth0 != null ? lookupProperty(depth0,"mountOptionsFilesystemCheckOnce") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsFilesystemCheckOnce","hash":{},"data":data,"loc":{"start":{"line":25,"column":23},"end":{"line":25,"column":58}}}) : helper)))
|
||||
+ "</option>\n <option value=\"1\" selected=\"selected\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsFilesystemCheckDA") || (depth0 != null ? lookupProperty(depth0,"mountOptionsFilesystemCheckDA") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsFilesystemCheckDA","hash":{},"data":data,"loc":{"start":{"line":26,"column":43},"end":{"line":26,"column":76}}}) : helper)))
|
||||
+ "</option>\n </select>\n </span>\n </li>\n <li class=\"optionRow\">\n <span class=\"menuitem\">\n <input id=\"mountOptionsEncoding\" class=\"checkbox\" name=\"encoding_compatibility\" type=\"checkbox\" value=\"true\"/>\n <label for=\"mountOptionsEncoding\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsEncodingLabel") || (depth0 != null ? lookupProperty(depth0,"mountOptionsEncodingLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsEncodingLabel","hash":{},"data":data,"loc":{"start":{"line":33,"column":38},"end":{"line":33,"column":67}}}) : helper)))
|
||||
+ "</label>\n </span>\n </li>\n <li class=\"optionRow\">\n <span class=\"menuitem\">\n <input id=\"mountOptionsReadOnly\" class=\"checkbox\" name=\"readonly\" type=\"checkbox\" value=\"true\"/>\n <label for=\"mountOptionsReadOnly\">"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"mountOptionsReadOnlyLabel") || (depth0 != null ? lookupProperty(depth0,"mountOptionsReadOnlyLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"mountOptionsReadOnlyLabel","hash":{},"data":data,"loc":{"start":{"line":39,"column":38},"end":{"line":39,"column":67}}}) : helper)))
|
||||
+ "</label>\n </span>\n </li>\n <li class=\"optionRow persistent\">\n <a href=\"#\" class=\"menuitem remove icon-delete\">\n <span>"
|
||||
+ alias4(((helper = (helper = lookupProperty(helpers,"deleteLabel") || (depth0 != null ? lookupProperty(depth0,"deleteLabel") : depth0)) != null ? helper : alias2),(typeof helper === alias3 ? helper.call(alias1,{"name":"deleteLabel","hash":{},"data":data,"loc":{"start":{"line":44,"column":10},"end":{"line":44,"column":25}}}) : helper)))
|
||||
+ "</span>\n </a>\n </li>\n </ul>\n</div>\n";
|
||||
},"useData":true});
|
||||
})();
|
||||
@@ -0,0 +1,8 @@
|
||||
<div id="files_external_div_form"><div>
|
||||
<div>{{credentials_text}}</div>
|
||||
<form>
|
||||
<input type="text" name="username" placeholder="{{placeholder_username}}"/>
|
||||
<input type="password" name="password" placeholder="{{placeholder_password}}"/>
|
||||
</form>
|
||||
</div>
|
||||
</div>
|
||||
@@ -0,0 +1,48 @@
|
||||
<div class="popovermenu open">
|
||||
<ul>
|
||||
<li class="optionRow">
|
||||
<span class="menuitem">
|
||||
<input id="mountOptionsEncrypt" class="checkbox" name="encrypt" type="checkbox" value="true" checked="checked"/>
|
||||
<label for="mountOptionsEncrypt">{{mountOptionsEncryptLabel}}</label>
|
||||
</span>
|
||||
</li>
|
||||
<li class="optionRow">
|
||||
<span class="menuitem">
|
||||
<input id="mountOptionsPreviews" class="checkbox" name="previews" type="checkbox" value="true" checked="checked"/>
|
||||
<label for="mountOptionsPreviews">{{mountOptionsPreviewsLabel}}</label>
|
||||
</span>
|
||||
</li>
|
||||
<li class="optionRow">
|
||||
<span class="menuitem">
|
||||
<input id="mountOptionsSharing" class="checkbox" name="enable_sharing" type="checkbox" value="true"/>
|
||||
<label for="mountOptionsSharing">{{mountOptionsSharingLabel}}</label>
|
||||
</span>
|
||||
</li>
|
||||
<li class="optionRow">
|
||||
<span class="menuitem icon-search">
|
||||
<label for="mountOptionsFilesystemCheck">{{mountOptionsFilesystemCheckLabel}}</label>
|
||||
<select id="mountOptionsFilesystemCheck" name="filesystem_check_changes" data-type="int">
|
||||
<option value="0">{{mountOptionsFilesystemCheckOnce}}</option>
|
||||
<option value="1" selected="selected">{{mountOptionsFilesystemCheckDA}}</option>
|
||||
</select>
|
||||
</span>
|
||||
</li>
|
||||
<li class="optionRow">
|
||||
<span class="menuitem">
|
||||
<input id="mountOptionsEncoding" class="checkbox" name="encoding_compatibility" type="checkbox" value="true"/>
|
||||
<label for="mountOptionsEncoding">{{mountOptionsEncodingLabel}}</label>
|
||||
</span>
|
||||
</li>
|
||||
<li class="optionRow">
|
||||
<span class="menuitem">
|
||||
<input id="mountOptionsReadOnly" class="checkbox" name="readonly" type="checkbox" value="true"/>
|
||||
<label for="mountOptionsReadOnly">{{mountOptionsReadOnlyLabel}}</label>
|
||||
</span>
|
||||
</li>
|
||||
<li class="optionRow persistent">
|
||||
<a href="#" class="menuitem remove icon-delete">
|
||||
<span>{{deleteLabel}}</span>
|
||||
</a>
|
||||
</li>
|
||||
</ul>
|
||||
</div>
|
||||
@@ -11,12 +11,10 @@ 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\Event\StorageCreatedEvent;
|
||||
use OCA\Files_External\Event\StorageDeletedEvent;
|
||||
use OCA\Files_External\Event\StorageUpdatedEvent;
|
||||
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;
|
||||
@@ -44,22 +42,19 @@ 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\StorePasswordListener;
|
||||
use OCA\Files_External\Listener\UserDeletedListener;
|
||||
use OCA\Files_External\Service\BackendService;
|
||||
use OCA\Files_External\Service\MountCacheService;
|
||||
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\BeforeGroupDeletedEvent;
|
||||
use OCP\Group\Events\GroupDeletedEvent;
|
||||
use OCP\Group\Events\UserAddedEvent;
|
||||
use OCP\Group\Events\UserRemovedEvent;
|
||||
use OCP\User\Events\PostLoginEvent;
|
||||
use OCP\User\Events\UserCreatedEvent;
|
||||
use OCP\User\Events\PasswordUpdatedEvent;
|
||||
use OCP\User\Events\UserDeletedEvent;
|
||||
use OCP\User\Events\UserLoggedInEvent;
|
||||
|
||||
/**
|
||||
* @package OCA\Files_External\AppInfo
|
||||
@@ -80,15 +75,8 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
|
||||
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
|
||||
$context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class);
|
||||
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
|
||||
$context->registerEventListener(StorageCreatedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(StorageDeletedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(StorageUpdatedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(BeforeGroupDeletedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(UserCreatedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(UserAddedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(UserRemovedEvent::class, MountCacheService::class);
|
||||
$context->registerEventListener(PostLoginEvent::class, MountCacheService::class);
|
||||
|
||||
$context->registerEventListener(UserLoggedInEvent::class, StorePasswordListener::class);
|
||||
$context->registerEventListener(PasswordUpdatedEvent::class, StorePasswordListener::class);
|
||||
$context->registerConfigLexicon(ConfigLexicon::class);
|
||||
}
|
||||
|
||||
@@ -148,6 +136,9 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
|
||||
$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),
|
||||
|
||||
@@ -17,7 +17,6 @@ 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\IAuthoritativeMountProvider;
|
||||
use OCP\Files\Config\IMountProvider;
|
||||
use OCP\Files\Mount\IMountPoint;
|
||||
use OCP\Files\ObjectStore\IObjectStore;
|
||||
@@ -33,7 +32,7 @@ use Psr\Log\LoggerInterface;
|
||||
/**
|
||||
* Make the old files_external config work with the new public mount config api
|
||||
*/
|
||||
class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
|
||||
class ConfigAdapter implements IMountProvider {
|
||||
public function __construct(
|
||||
private UserStoragesService $userStoragesService,
|
||||
private UserGlobalStoragesService $userGlobalStoragesService,
|
||||
@@ -74,11 +73,6 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
|
||||
$storage->getBackend()->manipulateStorageConfig($storage, $user);
|
||||
}
|
||||
|
||||
public function constructStorageForUser(IUser $user, StorageConfig $storage) {
|
||||
$this->prepareStorageConfig($storage, $user);
|
||||
return $this->constructStorage($storage);
|
||||
}
|
||||
|
||||
/**
|
||||
* Construct the storage implementation
|
||||
*
|
||||
@@ -111,7 +105,8 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
|
||||
|
||||
$storages = array_map(function (StorageConfig $storageConfig) use ($user) {
|
||||
try {
|
||||
return $this->constructStorageForUser($user, $storageConfig);
|
||||
$this->prepareStorageConfig($storageConfig, $user);
|
||||
return $this->constructStorage($storageConfig);
|
||||
} catch (\Exception $e) {
|
||||
// propagate exception into filesystem
|
||||
return new FailedStorage(['exception' => $e]);
|
||||
@@ -128,7 +123,7 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
|
||||
$availability = $storage->getAvailability();
|
||||
if (!$availability['available'] && !Availability::shouldRecheck($availability)) {
|
||||
$storage = new FailedStorage([
|
||||
'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available'),
|
||||
'exception' => new StorageNotAvailableException('Storage with mount id ' . $storageConfig->getId() . ' is not available')
|
||||
]);
|
||||
}
|
||||
} catch (\Exception $e) {
|
||||
@@ -153,7 +148,7 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
|
||||
null,
|
||||
$loader,
|
||||
$storageConfig->getMountOptions(),
|
||||
$storageConfig->getId(),
|
||||
$storageConfig->getId()
|
||||
);
|
||||
} else {
|
||||
return new SystemMountPoint(
|
||||
@@ -163,7 +158,7 @@ class ConfigAdapter implements IMountProvider, IAuthoritativeMountProvider {
|
||||
null,
|
||||
$loader,
|
||||
$storageConfig->getMountOptions(),
|
||||
$storageConfig->getId(),
|
||||
$storageConfig->getId()
|
||||
);
|
||||
}
|
||||
}, $storageConfigs, $availableStorages);
|
||||
|
||||
@@ -43,10 +43,8 @@ class UserContext {
|
||||
}
|
||||
try {
|
||||
$shareToken = $this->request->getParam('token');
|
||||
if ($shareToken !== null) {
|
||||
$share = $this->shareManager->getShareByToken($shareToken);
|
||||
return $share->getShareOwner();
|
||||
}
|
||||
$share = $this->shareManager->getShareByToken($shareToken);
|
||||
return $share->getShareOwner();
|
||||
} catch (ShareNotFound $e) {
|
||||
}
|
||||
|
||||
|
||||
@@ -42,6 +42,15 @@ class AjaxController extends Controller {
|
||||
parent::__construct($appName, $request);
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* Legacy endpoint for oauth2 callback
|
||||
*/
|
||||
#[NoAdminRequired()]
|
||||
public function oauth2Callback(): JSONResponse {
|
||||
return new JSONResponse(['status' => 'success']);
|
||||
}
|
||||
|
||||
/**
|
||||
* Returns a list of users and groups that match the given pattern.
|
||||
* Used for user and group picker in the admin settings.
|
||||
|
||||
@@ -73,9 +73,9 @@ class GlobalStoragesController extends StoragesController {
|
||||
*/
|
||||
#[PasswordConfirmationRequired(strict: true)]
|
||||
public function create(
|
||||
string $mountPoint,
|
||||
string $backend,
|
||||
string $authMechanism,
|
||||
$mountPoint,
|
||||
$backend,
|
||||
$authMechanism,
|
||||
$backendOptions,
|
||||
$mountOptions,
|
||||
$applicableUsers,
|
||||
@@ -138,10 +138,10 @@ class GlobalStoragesController extends StoragesController {
|
||||
*/
|
||||
#[PasswordConfirmationRequired(strict: true)]
|
||||
public function update(
|
||||
int $id,
|
||||
string $mountPoint,
|
||||
string $backend,
|
||||
string $authMechanism,
|
||||
$id,
|
||||
$mountPoint,
|
||||
$backend,
|
||||
$authMechanism,
|
||||
$backendOptions,
|
||||
$mountOptions,
|
||||
$applicableUsers,
|
||||
|
||||
@@ -67,9 +67,9 @@ abstract class StoragesController extends Controller {
|
||||
* @return StorageConfig|DataResponse
|
||||
*/
|
||||
protected function createStorage(
|
||||
string $mountPoint,
|
||||
string $backend,
|
||||
string $authMechanism,
|
||||
$mountPoint,
|
||||
$backend,
|
||||
$authMechanism,
|
||||
$backendOptions,
|
||||
$mountOptions = null,
|
||||
$applicableUsers = null,
|
||||
@@ -236,7 +236,7 @@ abstract class StoragesController extends Controller {
|
||||
} catch (StorageNotAvailableException $e) {
|
||||
$storage->setStatus(
|
||||
(int)$e->getCode(),
|
||||
$e->getMessage(),
|
||||
$this->l10n->t('%s', [$e->getMessage()])
|
||||
);
|
||||
} catch (\Exception $e) {
|
||||
// FIXME: convert storage exceptions to StorageNotAvailableException
|
||||
|
||||
@@ -138,7 +138,7 @@ class UserGlobalStoragesController extends StoragesController {
|
||||
#[NoAdminRequired]
|
||||
#[PasswordConfirmationRequired(strict: true)]
|
||||
public function update(
|
||||
int $id,
|
||||
$id,
|
||||
$backendOptions,
|
||||
) {
|
||||
try {
|
||||
|
||||
@@ -103,9 +103,9 @@ class UserStoragesController extends StoragesController {
|
||||
#[NoAdminRequired]
|
||||
#[PasswordConfirmationRequired(strict: true)]
|
||||
public function create(
|
||||
string $mountPoint,
|
||||
string $backend,
|
||||
string $authMechanism,
|
||||
$mountPoint,
|
||||
$backend,
|
||||
$authMechanism,
|
||||
$backendOptions,
|
||||
$mountOptions,
|
||||
) {
|
||||
@@ -137,7 +137,6 @@ class UserStoragesController extends StoragesController {
|
||||
$newStorage = $this->service->addStorage($newStorage);
|
||||
$this->updateStorageStatus($newStorage);
|
||||
|
||||
$newStorage->setType(StorageConfig::MOUNT_TYPE_PERSONAL);
|
||||
return new DataResponse(
|
||||
$newStorage->jsonSerialize(true),
|
||||
Http::STATUS_CREATED
|
||||
@@ -159,10 +158,10 @@ class UserStoragesController extends StoragesController {
|
||||
#[NoAdminRequired]
|
||||
#[PasswordConfirmationRequired(strict: true)]
|
||||
public function update(
|
||||
int $id,
|
||||
string $mountPoint,
|
||||
string $backend,
|
||||
string $authMechanism,
|
||||
$id,
|
||||
$mountPoint,
|
||||
$backend,
|
||||
$authMechanism,
|
||||
$backendOptions,
|
||||
$mountOptions,
|
||||
) {
|
||||
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Event;
|
||||
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCP\EventDispatcher\Event;
|
||||
|
||||
class StorageCreatedEvent extends Event {
|
||||
public function __construct(
|
||||
private readonly StorageConfig $newConfig,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getNewConfig(): StorageConfig {
|
||||
return $this->newConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,24 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Event;
|
||||
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCP\EventDispatcher\Event;
|
||||
|
||||
class StorageDeletedEvent extends Event {
|
||||
public function __construct(
|
||||
private readonly StorageConfig $oldConfig,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getOldConfig(): StorageConfig {
|
||||
return $this->oldConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,29 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Event;
|
||||
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCP\EventDispatcher\Event;
|
||||
|
||||
class StorageUpdatedEvent extends Event {
|
||||
public function __construct(
|
||||
private readonly StorageConfig $oldConfig,
|
||||
private readonly StorageConfig $newConfig,
|
||||
) {
|
||||
parent::__construct();
|
||||
}
|
||||
|
||||
public function getOldConfig(): StorageConfig {
|
||||
return $this->oldConfig;
|
||||
}
|
||||
|
||||
public function getNewConfig(): StorageConfig {
|
||||
return $this->newConfig;
|
||||
}
|
||||
}
|
||||
@@ -1,114 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Lib;
|
||||
|
||||
use OC\User\LazyUser;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
|
||||
class ApplicableHelper {
|
||||
public function __construct(
|
||||
private readonly IUserManager $userManager,
|
||||
private readonly IGroupManager $groupManager,
|
||||
) {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get all users that have access to a storage
|
||||
*
|
||||
* @return \Iterator<string, IUser>
|
||||
*/
|
||||
public function getUsersForStorage(StorageConfig $storage): \Iterator {
|
||||
$yielded = [];
|
||||
if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) {
|
||||
yield from $this->userManager->getSeenUsers();
|
||||
}
|
||||
foreach ($storage->getApplicableUsers() as $userId) {
|
||||
$yielded[$userId] = true;
|
||||
yield $userId => new LazyUser($userId, $this->userManager);
|
||||
}
|
||||
foreach ($storage->getApplicableGroups() as $groupId) {
|
||||
$group = $this->groupManager->get($groupId);
|
||||
if ($group !== null) {
|
||||
foreach ($group->getUsers() as $user) {
|
||||
if (!isset($yielded[$user->getUID()])) {
|
||||
$yielded[$user->getUID()] = true;
|
||||
yield $user->getUID() => $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function isApplicableForUser(StorageConfig $storage, IUser $user): bool {
|
||||
if (count($storage->getApplicableUsers()) + count($storage->getApplicableGroups()) === 0) {
|
||||
return true;
|
||||
}
|
||||
if (in_array($user->getUID(), $storage->getApplicableUsers())) {
|
||||
return true;
|
||||
}
|
||||
$groupIds = $this->groupManager->getUserGroupIds($user);
|
||||
foreach ($groupIds as $groupId) {
|
||||
if (in_array($groupId, $storage->getApplicableGroups())) {
|
||||
return true;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
}
|
||||
|
||||
/**
|
||||
* Return all users that are applicable for storage $a, but not for $b
|
||||
*
|
||||
* @return \Iterator<IUser>
|
||||
*/
|
||||
public function diffApplicable(StorageConfig $a, StorageConfig $b): \Iterator {
|
||||
$aIsAll = count($a->getApplicableUsers()) + count($a->getApplicableGroups()) === 0;
|
||||
$bIsAll = count($b->getApplicableUsers()) + count($b->getApplicableGroups()) === 0;
|
||||
if ($bIsAll) {
|
||||
return;
|
||||
}
|
||||
|
||||
if ($aIsAll) {
|
||||
foreach ($this->getUsersForStorage($a) as $user) {
|
||||
if (!$this->isApplicableForUser($b, $user)) {
|
||||
yield $user;
|
||||
}
|
||||
}
|
||||
} else {
|
||||
$yielded = [];
|
||||
foreach ($a->getApplicableGroups() as $groupId) {
|
||||
if (!in_array($groupId, $b->getApplicableGroups())) {
|
||||
$group = $this->groupManager->get($groupId);
|
||||
if ($group) {
|
||||
foreach ($group->getUsers() as $user) {
|
||||
if (!$this->isApplicableForUser($b, $user)) {
|
||||
if (!isset($yielded[$user->getUID()])) {
|
||||
$yielded[$user->getUID()] = true;
|
||||
yield $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
foreach ($a->getApplicableUsers() as $userId) {
|
||||
if (!in_array($userId, $b->getApplicableUsers())) {
|
||||
$user = $this->userManager->get($userId);
|
||||
if ($user && !$this->isApplicableForUser($b, $user)) {
|
||||
if (!isset($yielded[$user->getUID()])) {
|
||||
$yielded[$user->getUID()] = true;
|
||||
yield $user;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -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')
|
||||
;
|
||||
}
|
||||
}
|
||||
@@ -35,7 +35,7 @@ class RSA extends AuthMechanism {
|
||||
->setType(DefinitionParameter::VALUE_PASSWORD)
|
||||
->setFlag(DefinitionParameter::FLAG_HIDDEN),
|
||||
])
|
||||
->addCustomJs('auth_rsa')
|
||||
->addCustomJs('public_key')
|
||||
;
|
||||
}
|
||||
|
||||
|
||||
@@ -28,7 +28,6 @@ class SFTP extends Backend {
|
||||
])
|
||||
->addAuthScheme(AuthMechanism::SCHEME_PASSWORD)
|
||||
->addAuthScheme(AuthMechanism::SCHEME_PUBLICKEY)
|
||||
->addAuthScheme(AuthMechanism::SCHEME_OAUTH2)
|
||||
->setLegacyAuthMechanism($legacyAuth)
|
||||
;
|
||||
}
|
||||
|
||||
@@ -12,7 +12,6 @@ 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\ResponseDefinitions;
|
||||
use OCP\IUser;
|
||||
|
||||
/**
|
||||
* External storage configuration
|
||||
@@ -436,13 +435,4 @@ class StorageConfig implements \JsonSerializable {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
public function getMountPointForUser(IUser $user): string {
|
||||
return '/' . $user->getUID() . '/files/' . trim($this->mountPoint, '/') . '/';
|
||||
}
|
||||
|
||||
public function __clone() {
|
||||
$this->backend = clone $this->backend;
|
||||
$this->authMechanism = clone $this->authMechanism;
|
||||
}
|
||||
}
|
||||
|
||||
@@ -18,8 +18,10 @@ 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;
|
||||
|
||||
@@ -108,6 +110,52 @@ class MountConfig {
|
||||
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
|
||||
*
|
||||
|
||||
@@ -263,7 +263,7 @@ class BackendService {
|
||||
* @param Backend $backend
|
||||
* @return bool
|
||||
*/
|
||||
public function isAllowedUserBackend(Backend $backend): bool {
|
||||
protected function isAllowedUserBackend(Backend $backend): bool {
|
||||
return ($this->isUserMountingAllowed() && array_intersect($backend->getIdentifierAliases(), $this->userMountingBackends));
|
||||
}
|
||||
|
||||
|
||||
@@ -15,9 +15,6 @@ use OCP\Security\ICrypto;
|
||||
|
||||
/**
|
||||
* Stores the mount config in the database
|
||||
*
|
||||
* @psalm-type ApplicableConfig = array{type: int, value: string}
|
||||
* @psalm-type StorageConfigData = array{type: int, priority: int, applicable: list<ApplicableConfig>, config: array, options: array, ...<string, mixed>}
|
||||
*/
|
||||
class DBConfigService {
|
||||
public const MOUNT_TYPE_ADMIN = 1;
|
||||
@@ -83,39 +80,6 @@ class DBConfigService {
|
||||
return $this->getMountsFromQuery($query);
|
||||
}
|
||||
|
||||
/**
|
||||
* @param list<string> $groupIds
|
||||
* @return list<StorageConfigData>
|
||||
*/
|
||||
public function getMountsForGroups(array $groupIds): array {
|
||||
$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()->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);
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<StorageConfigData>
|
||||
*/
|
||||
public function getGlobalMounts(): array {
|
||||
$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()->andX( // global mounts
|
||||
$builder->expr()->eq('a.type', $builder->createNamedParameter(self::APPLICABLE_TYPE_GLOBAL, IQueryBuilder::PARAM_INT)),
|
||||
$builder->expr()->isNull('a.value'),
|
||||
), );
|
||||
|
||||
return $this->getMountsFromQuery($query);
|
||||
}
|
||||
|
||||
public function modifyMountsOnUserDelete(string $uid): void {
|
||||
$this->modifyMountsOnDelete($uid, self::APPLICABLE_TYPE_USER);
|
||||
}
|
||||
@@ -412,10 +376,7 @@ class DBConfigService {
|
||||
$query->executeStatement();
|
||||
}
|
||||
|
||||
/**
|
||||
* @return list<StorageConfigData>
|
||||
*/
|
||||
private function getMountsFromQuery(IQueryBuilder $query): array {
|
||||
private function getMountsFromQuery(IQueryBuilder $query) {
|
||||
$result = $query->executeQuery();
|
||||
$mounts = $result->fetchAllAssociative();
|
||||
$uniqueMounts = [];
|
||||
@@ -452,9 +413,9 @@ class DBConfigService {
|
||||
* @param string $table
|
||||
* @param string[] $fields
|
||||
* @param int[] $mountIds
|
||||
* @return array<int, list<array>> [$mountId => [['field1' => $value1, ...], ...], ...]
|
||||
* @return array [$mountId => [['field1' => $value1, ...], ...], ...]
|
||||
*/
|
||||
private function selectForMounts(string $table, array $fields, array $mountIds): array {
|
||||
private function selectForMounts($table, array $fields, array $mountIds) {
|
||||
if (count($mountIds) === 0) {
|
||||
return [];
|
||||
}
|
||||
@@ -486,9 +447,9 @@ class DBConfigService {
|
||||
|
||||
/**
|
||||
* @param int[] $mountIds
|
||||
* @return array<int, list<ApplicableConfig>> [$id => [['type' => $type, 'value' => $value], ...], ...]
|
||||
* @return array [$id => [['type' => $type, 'value' => $value], ...], ...]
|
||||
*/
|
||||
public function getApplicableForMounts(array $mountIds): array {
|
||||
public function getApplicableForMounts($mountIds) {
|
||||
return $this->selectForMounts('external_applicable', ['type', 'value'], $mountIds);
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,8 @@
|
||||
namespace OCA\Files_External\Service;
|
||||
|
||||
use OC\Files\Filesystem;
|
||||
use OCA\Files_External\Event\StorageCreatedEvent;
|
||||
use OCA\Files_External\Event\StorageDeletedEvent;
|
||||
use OCA\Files_External\Event\StorageUpdatedEvent;
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCA\Files_External\MountConfig;
|
||||
use OCP\IGroup;
|
||||
|
||||
/**
|
||||
* Service class to manage global external storage
|
||||
@@ -66,13 +62,9 @@ class GlobalStoragesService extends StoragesService {
|
||||
protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) {
|
||||
// if mount point changed, it's like a deletion + creation
|
||||
if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) {
|
||||
$this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage));
|
||||
$this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage));
|
||||
$this->triggerHooks($oldStorage, Filesystem::signal_delete_mount);
|
||||
$this->triggerHooks($newStorage, Filesystem::signal_create_mount);
|
||||
return;
|
||||
} else {
|
||||
$this->eventDispatcher->dispatchTyped(new StorageUpdatedEvent($oldStorage, $newStorage));
|
||||
}
|
||||
|
||||
$userAdditions = array_diff($newStorage->getApplicableUsers(), $oldStorage->getApplicableUsers());
|
||||
@@ -170,31 +162,4 @@ class GlobalStoragesService extends StoragesService {
|
||||
|
||||
return array_combine($keys, $configs);
|
||||
}
|
||||
|
||||
/**
|
||||
* Gets all storages for the group, not including any global storages
|
||||
* @return StorageConfig[]
|
||||
*/
|
||||
public function getAllStoragesForGroup(IGroup $group): array {
|
||||
$mounts = $this->dbConfig->getMountsForGroups([$group->getGID()]);
|
||||
$configs = array_map($this->getStorageConfigFromDBMount(...), $mounts);
|
||||
$configs = array_filter($configs, static fn (?StorageConfig $config): bool => $config instanceof StorageConfig);
|
||||
$keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs);
|
||||
|
||||
$storages = array_combine($keys, $configs);
|
||||
return array_filter($storages, $this->validateStorage(...));
|
||||
}
|
||||
|
||||
/**
|
||||
* @return StorageConfig[]
|
||||
*/
|
||||
public function getAllGlobalStorages(): array {
|
||||
$mounts = $this->dbConfig->getGlobalMounts();
|
||||
|
||||
$configs = array_map($this->getStorageConfigFromDBMount(...), $mounts);
|
||||
$configs = array_filter($configs, static fn (?StorageConfig $config): bool => $config instanceof StorageConfig);
|
||||
$keys = array_map(static fn (StorageConfig $config) => $config->getId(), $configs);
|
||||
$storages = array_combine($keys, $configs);
|
||||
return array_filter($storages, $this->validateStorage(...));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,205 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2025 Robin Appelman <robin@icewind.nl>
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Service;
|
||||
|
||||
use OC\Files\Cache\CacheEntry;
|
||||
use OC\Files\Storage\FailedStorage;
|
||||
use OCA\Files_External\Config\ConfigAdapter;
|
||||
use OCA\Files_External\Event\StorageCreatedEvent;
|
||||
use OCA\Files_External\Event\StorageDeletedEvent;
|
||||
use OCA\Files_External\Event\StorageUpdatedEvent;
|
||||
use OCA\Files_External\Lib\ApplicableHelper;
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCP\Cache\CappedMemoryCache;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\Files\Cache\ICacheEntry;
|
||||
use OCP\Files\Config\IUserMountCache;
|
||||
use OCP\Group\Events\BeforeGroupDeletedEvent;
|
||||
use OCP\Group\Events\UserAddedEvent;
|
||||
use OCP\Group\Events\UserRemovedEvent;
|
||||
use OCP\IGroup;
|
||||
use OCP\IUser;
|
||||
use OCP\User\Events\PostLoginEvent;
|
||||
use OCP\User\Events\UserCreatedEvent;
|
||||
|
||||
/**
|
||||
* Listens to config events and update the mounts for the applicable users
|
||||
*
|
||||
* @template-implements IEventListener<StorageCreatedEvent|StorageDeletedEvent|StorageUpdatedEvent|BeforeGroupDeletedEvent|UserCreatedEvent|UserAddedEvent|UserRemovedEvent|PostLoginEvent|Event>
|
||||
*/
|
||||
class MountCacheService implements IEventListener {
|
||||
private CappedMemoryCache $storageRootCache;
|
||||
|
||||
public function __construct(
|
||||
private readonly IUserMountCache $userMountCache,
|
||||
private readonly ConfigAdapter $configAdapter,
|
||||
private readonly GlobalStoragesService $storagesService,
|
||||
private readonly ApplicableHelper $applicableHelper,
|
||||
) {
|
||||
$this->storageRootCache = new CappedMemoryCache();
|
||||
}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if ($event instanceof StorageCreatedEvent) {
|
||||
$this->handleAddedStorage($event->getNewConfig());
|
||||
}
|
||||
if ($event instanceof StorageDeletedEvent) {
|
||||
$this->handleDeletedStorage($event->getOldConfig());
|
||||
}
|
||||
if ($event instanceof StorageUpdatedEvent) {
|
||||
$this->handleUpdatedStorage($event->getOldConfig(), $event->getNewConfig());
|
||||
}
|
||||
if ($event instanceof UserAddedEvent) {
|
||||
$this->handleUserAdded($event->getGroup(), $event->getUser());
|
||||
}
|
||||
if ($event instanceof UserRemovedEvent) {
|
||||
$this->handleUserRemoved($event->getGroup(), $event->getUser());
|
||||
}
|
||||
if ($event instanceof BeforeGroupDeletedEvent) {
|
||||
$this->handleGroupDeleted($event->getGroup());
|
||||
}
|
||||
if ($event instanceof UserCreatedEvent) {
|
||||
$this->handleUserCreated($event->getUser());
|
||||
}
|
||||
if ($event instanceof PostLoginEvent) {
|
||||
$this->onLogin($event->getUser());
|
||||
}
|
||||
}
|
||||
|
||||
public function handleDeletedStorage(StorageConfig $storage): void {
|
||||
foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) {
|
||||
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
|
||||
}
|
||||
}
|
||||
|
||||
public function handleAddedStorage(StorageConfig $storage): void {
|
||||
foreach ($this->applicableHelper->getUsersForStorage($storage) as $user) {
|
||||
$this->registerForUser($user, $storage);
|
||||
}
|
||||
}
|
||||
|
||||
public function handleUpdatedStorage(StorageConfig $oldStorage, StorageConfig $newStorage): void {
|
||||
foreach ($this->applicableHelper->diffApplicable($oldStorage, $newStorage) as $user) {
|
||||
$this->userMountCache->removeMount($oldStorage->getMountPointForUser($user));
|
||||
}
|
||||
foreach ($this->applicableHelper->diffApplicable($newStorage, $oldStorage) as $user) {
|
||||
$this->registerForUser($user, $newStorage);
|
||||
}
|
||||
}
|
||||
|
||||
private function getCacheEntryForRoot(IUser $user, StorageConfig $storage): ICacheEntry {
|
||||
try {
|
||||
$userStorage = $this->configAdapter->constructStorageForUser($user, clone $storage);
|
||||
} catch (\Exception $e) {
|
||||
$userStorage = new FailedStorage(['exception' => $e]);
|
||||
}
|
||||
|
||||
$cachedEntry = $this->storageRootCache->get($userStorage->getId());
|
||||
if ($cachedEntry !== null) {
|
||||
return $cachedEntry;
|
||||
}
|
||||
|
||||
$cache = $userStorage->getCache();
|
||||
$entry = $cache->get('');
|
||||
if ($entry && $entry->getId() !== -1) {
|
||||
$this->storageRootCache->set($userStorage->getId(), $entry);
|
||||
return $entry;
|
||||
}
|
||||
|
||||
// create a "fake" root entry so we have a fileid so we don't have to interact with the remote service
|
||||
// this will be scanned on first access
|
||||
$data = [
|
||||
'path' => '',
|
||||
'path_hash' => md5(''),
|
||||
'size' => 0,
|
||||
'unencrypted_size' => 0,
|
||||
'mtime' => 0,
|
||||
'mimetype' => ICacheEntry::DIRECTORY_MIMETYPE,
|
||||
'parent' => -1,
|
||||
'name' => '',
|
||||
'storage_mtime' => 0,
|
||||
'permissions' => 31,
|
||||
'storage' => $cache->getNumericStorageId(),
|
||||
'etag' => '',
|
||||
'encrypted' => 0,
|
||||
'checksum' => '',
|
||||
];
|
||||
if ($cache->getNumericStorageId() !== -1) {
|
||||
$data['fileid'] = $cache->insert('', $data);
|
||||
} else {
|
||||
$data['fileid'] = -1;
|
||||
}
|
||||
|
||||
$entry = new CacheEntry($data);
|
||||
$this->storageRootCache->set($userStorage->getId(), $entry);
|
||||
return $entry;
|
||||
}
|
||||
|
||||
private function registerForUser(IUser $user, StorageConfig $storage): void {
|
||||
$this->userMountCache->addMount(
|
||||
$user,
|
||||
$storage->getMountPointForUser($user),
|
||||
$this->getCacheEntryForRoot($user, $storage),
|
||||
ConfigAdapter::class,
|
||||
$storage->getId(),
|
||||
);
|
||||
}
|
||||
|
||||
private function handleUserRemoved(IGroup $group, IUser $user): void {
|
||||
$storages = $this->storagesService->getAllStoragesForGroup($group);
|
||||
foreach ($storages as $storage) {
|
||||
if (!$this->applicableHelper->isApplicableForUser($storage, $user)) {
|
||||
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function handleUserAdded(IGroup $group, IUser $user): void {
|
||||
$storages = $this->storagesService->getAllStoragesForGroup($group);
|
||||
foreach ($storages as $storage) {
|
||||
$this->registerForUser($user, $storage);
|
||||
}
|
||||
}
|
||||
|
||||
private function handleGroupDeleted(IGroup $group): void {
|
||||
$storages = $this->storagesService->getAllStoragesForGroup($group);
|
||||
foreach ($storages as $storage) {
|
||||
$this->removeGroupFromStorage($storage, $group);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Remove mounts from users in a group, if they don't have access to the storage trough other means
|
||||
*/
|
||||
private function removeGroupFromStorage(StorageConfig $storage, IGroup $group): void {
|
||||
foreach ($group->searchUsers('') as $user) {
|
||||
if (!$this->applicableHelper->isApplicableForUser($storage, $user)) {
|
||||
$this->userMountCache->removeMount($storage->getMountPointForUser($user));
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
private function handleUserCreated(IUser $user): void {
|
||||
$storages = $this->storagesService->getAllGlobalStorages();
|
||||
foreach ($storages as $storage) {
|
||||
$this->registerForUser($user, $storage);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Since storage config can rely on login credentials, we might need to update the config
|
||||
*/
|
||||
private function onLogin(IUser $user): void {
|
||||
$storages = $this->storagesService->getAllGlobalStorages();
|
||||
foreach ($storages as $storage) {
|
||||
$this->registerForUser($user, $storage);
|
||||
}
|
||||
}
|
||||
}
|
||||
@@ -12,8 +12,6 @@ use OC\Files\Filesystem;
|
||||
use OCA\Files\AppInfo\Application as FilesApplication;
|
||||
use OCA\Files\ConfigLexicon;
|
||||
use OCA\Files_External\AppInfo\Application;
|
||||
use OCA\Files_External\Event\StorageCreatedEvent;
|
||||
use OCA\Files_External\Event\StorageDeletedEvent;
|
||||
use OCA\Files_External\Lib\Auth\AuthMechanism;
|
||||
use OCA\Files_External\Lib\Auth\InvalidAuth;
|
||||
use OCA\Files_External\Lib\Backend\Backend;
|
||||
@@ -22,6 +20,7 @@ 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\IAppConfig;
|
||||
@@ -37,11 +36,13 @@ 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 IAppConfig $appConfig,
|
||||
) {
|
||||
@@ -243,7 +244,6 @@ abstract class StoragesService {
|
||||
// add new storage
|
||||
$allStorages[$configId] = $newStorage;
|
||||
|
||||
$this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage));
|
||||
$this->triggerHooks($newStorage, Filesystem::signal_create_mount);
|
||||
|
||||
$newStorage->setStatus(StorageNotAvailableException::STATUS_SUCCESS);
|
||||
@@ -424,6 +424,15 @@ abstract class StoragesService {
|
||||
|
||||
$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);
|
||||
}
|
||||
}
|
||||
|
||||
$this->updateOverwriteHomeFolders();
|
||||
|
||||
return $this->getStorage($id);
|
||||
@@ -446,7 +455,6 @@ abstract class StoragesService {
|
||||
$this->dbConfig->removeMount($id);
|
||||
|
||||
$deletedStorage = $this->getStorageConfigFromDBMount($existingMount);
|
||||
$this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($deletedStorage));
|
||||
$this->triggerHooks($deletedStorage, Filesystem::signal_delete_mount);
|
||||
|
||||
// delete oc_storages entries and oc_filecache
|
||||
|
||||
@@ -9,6 +9,7 @@ namespace OCA\Files_External\Service;
|
||||
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCP\EventDispatcher\IEventDispatcher;
|
||||
use OCP\Files\Config\IUserMountCache;
|
||||
use OCP\IAppConfig;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
@@ -26,10 +27,11 @@ class UserGlobalStoragesService extends GlobalStoragesService {
|
||||
DBConfigService $dbConfig,
|
||||
IUserSession $userSession,
|
||||
protected IGroupManager $groupManager,
|
||||
IUserMountCache $userMountCache,
|
||||
IEventDispatcher $eventDispatcher,
|
||||
IAppConfig $appConfig,
|
||||
) {
|
||||
parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig);
|
||||
parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig);
|
||||
$this->userSession = $userSession;
|
||||
}
|
||||
|
||||
|
||||
@@ -8,12 +8,11 @@
|
||||
namespace OCA\Files_External\Service;
|
||||
|
||||
use OC\Files\Filesystem;
|
||||
use OCA\Files_External\Event\StorageCreatedEvent;
|
||||
use OCA\Files_External\Event\StorageDeletedEvent;
|
||||
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\IAppConfig;
|
||||
use OCP\IUserSession;
|
||||
|
||||
@@ -31,11 +30,12 @@ class UserStoragesService extends StoragesService {
|
||||
BackendService $backendService,
|
||||
DBConfigService $dbConfig,
|
||||
IUserSession $userSession,
|
||||
IUserMountCache $userMountCache,
|
||||
IEventDispatcher $eventDispatcher,
|
||||
IAppConfig $appConfig,
|
||||
) {
|
||||
$this->userSession = $userSession;
|
||||
parent::__construct($backendService, $dbConfig, $eventDispatcher, $appConfig);
|
||||
parent::__construct($backendService, $dbConfig, $userMountCache, $eventDispatcher, $appConfig);
|
||||
}
|
||||
|
||||
protected function readDBConfig() {
|
||||
@@ -72,8 +72,6 @@ class UserStoragesService extends StoragesService {
|
||||
protected function triggerChangeHooks(StorageConfig $oldStorage, StorageConfig $newStorage) {
|
||||
// if mount point changed, it's like a deletion + creation
|
||||
if ($oldStorage->getMountPoint() !== $newStorage->getMountPoint()) {
|
||||
$this->eventDispatcher->dispatchTyped(new StorageDeletedEvent($oldStorage));
|
||||
$this->eventDispatcher->dispatchTyped(new StorageCreatedEvent($newStorage));
|
||||
$this->triggerHooks($oldStorage, Filesystem::signal_delete_mount);
|
||||
$this->triggerHooks($newStorage, Filesystem::signal_create_mount);
|
||||
}
|
||||
|
||||
@@ -7,45 +7,40 @@
|
||||
namespace OCA\Files_External\Settings;
|
||||
|
||||
use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
|
||||
use OCA\Files_External\Lib\Backend\Backend;
|
||||
use OCA\Files_External\MountConfig;
|
||||
use OCA\Files_External\Service\BackendService;
|
||||
use OCA\Files_External\Service\GlobalStoragesService;
|
||||
use OCP\AppFramework\Http\TemplateResponse;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Encryption\IManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
class Admin implements ISettings {
|
||||
use CommonSettingsTrait;
|
||||
|
||||
public function __construct(
|
||||
private IManager $encryptionManager,
|
||||
private GlobalStoragesService $globalStoragesService,
|
||||
private BackendService $backendService,
|
||||
private GlobalAuth $globalAuth,
|
||||
private IInitialState $initialState,
|
||||
private IURLGenerator $urlGenerator,
|
||||
) {
|
||||
$this->visibility = BackendService::VISIBILITY_ADMIN;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm() {
|
||||
$this->setInitialState();
|
||||
|
||||
// Admin specific
|
||||
$backends = $this->backendService->getAvailableBackends();
|
||||
$allowedBackends = array_filter($backends, fn (Backend $backend) => $backend->isVisibleFor(BackendService::VISIBILITY_PERSONAL));
|
||||
$this->initialState->provideInitialState('user-mounting', [
|
||||
$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(),
|
||||
'allowedBackends' => array_values(array_map(fn (Backend $backend) => $backend->getIdentifier(), $allowedBackends)),
|
||||
]);
|
||||
'globalCredentials' => $this->globalAuth->getAuth(''),
|
||||
'globalCredentialsUid' => '',
|
||||
];
|
||||
|
||||
$this->loadScriptsAndStyles();
|
||||
return new TemplateResponse('files_external', 'settings', renderAs: '');
|
||||
return new TemplateResponse('files_external', 'settings', $parameters, '');
|
||||
}
|
||||
|
||||
/**
|
||||
|
||||
@@ -1,141 +0,0 @@
|
||||
<?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\Settings;
|
||||
|
||||
use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
|
||||
use OCA\Files_External\Lib\Backend\Backend;
|
||||
use OCA\Files_External\Service\BackendService;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\Encryption\IManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\Util;
|
||||
|
||||
trait CommonSettingsTrait {
|
||||
private BackendService $backendService;
|
||||
private IManager $encryptionManager;
|
||||
private IInitialState $initialState;
|
||||
private IURLGenerator $urlGenerator;
|
||||
private GlobalAuth $globalAuth;
|
||||
|
||||
private int $visibility;
|
||||
private ?string $userId = null;
|
||||
/** @var Backend[]|null */
|
||||
private ?array $backends = null;
|
||||
|
||||
/**
|
||||
* Set the initial state for the user / admin settings
|
||||
*/
|
||||
protected function setInitialState() {
|
||||
$allowUserMounting = $this->backendService->isUserMountingAllowed();
|
||||
$isAdmin = $this->visibility === BackendService::VISIBILITY_ADMIN;
|
||||
$canCreateMounts = $isAdmin || $allowUserMounting;
|
||||
|
||||
$this->initialState->provideInitialState('settings', [
|
||||
/** Link to external files documentation */
|
||||
'docUrl' => $this->urlGenerator->linkToDocs('admin-external-storage'),
|
||||
/** List of backend dependency or missing module issues to be shown on the fronend */
|
||||
'dependencyIssues' => $canCreateMounts ? $this->dependencyMessage() : null,
|
||||
/** Is this the admin settings or just user settings */
|
||||
'isAdmin' => $isAdmin,
|
||||
'hasEncryption' => $this->encryptionManager->isEnabled(),
|
||||
]);
|
||||
|
||||
$this->initialState->provideInitialState(
|
||||
'global-credentials',
|
||||
array_merge(
|
||||
/** User ID of the credentials - empty string for global admin defined */
|
||||
['uid' => $this->userId ?? '' ],
|
||||
/** username and password configured */
|
||||
$this->globalAuth->getAuth($this->userId ?? ''),
|
||||
),
|
||||
);
|
||||
|
||||
$this->initialState->provideInitialState(
|
||||
'allowedBackends',
|
||||
array_map(fn (Backend $backend) => $backend->getIdentifier(), $this->getAvailableBackends()),
|
||||
);
|
||||
$this->initialState->provideInitialState(
|
||||
'backends',
|
||||
array_values($this->backendService->getAvailableBackends()),
|
||||
);
|
||||
$this->initialState->provideInitialState(
|
||||
'authMechanisms',
|
||||
array_values($this->backendService->getAuthMechanisms()),
|
||||
);
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the frontend script including the custom backend dependencies
|
||||
*/
|
||||
protected function loadScriptsAndStyles() {
|
||||
Util::addStyle('files_external', 'init_settings');
|
||||
Util::addInitScript('files_external', 'init_settings');
|
||||
|
||||
Util::addScript('files_external', 'settings');
|
||||
Util::addStyle('files_external', 'settings');
|
||||
|
||||
// load custom JS
|
||||
foreach ($this->backendService->getAvailableBackends() as $backend) {
|
||||
foreach ($backend->getCustomJs() as $script) {
|
||||
Util::addStyle('files_external', $script);
|
||||
Util::addScript('files_external', $script);
|
||||
}
|
||||
}
|
||||
|
||||
foreach ($this->backendService->getAuthMechanisms() as $authMechanism) {
|
||||
foreach ($authMechanism->getCustomJs() as $script) {
|
||||
Util::addStyle('files_external', $script);
|
||||
Util::addScript('files_external', $script);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Get backend dependency error messages
|
||||
* @return array{messages: string[], modules: array<string,string[]>}
|
||||
*/
|
||||
private function dependencyMessage(): array {
|
||||
$messages = [];
|
||||
$dependencyGroups = [];
|
||||
|
||||
// Try all backends and check their dependencies
|
||||
foreach ($this->getAvailableBackends() as $backend) {
|
||||
foreach ($backend->checkDependencies() as $dependency) {
|
||||
$dependencyMessage = $dependency->getMessage();
|
||||
if ($dependencyMessage !== null) {
|
||||
// There is a custom message so we use that
|
||||
$messages[] = $dependencyMessage;
|
||||
} else {
|
||||
// No custom message so just add the dependency and add the backend to the list of dependants
|
||||
$dependencyGroups[$dependency->getDependency()][] = $backend;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
$backendDisplayName = fn (Backend $backend) => $backend->getText();
|
||||
|
||||
// Create a mapping [ 'dependency' => ['backendName1', ... ]]
|
||||
$missingModules = array_map(fn (array $dependants) => array_map($backendDisplayName, $dependants), $dependencyGroups);
|
||||
return [
|
||||
'messages' => $messages,
|
||||
'modules' => $missingModules,
|
||||
];
|
||||
}
|
||||
|
||||
private function getAvailableBackends(): array {
|
||||
if ($this->backends === null) {
|
||||
$backends = $this->backendService->getAvailableBackends();
|
||||
if ($this->visibility === BackendService::VISIBILITY_PERSONAL) {
|
||||
$backends = array_filter($backends, $this->backendService->isAllowedUserBackend(...));
|
||||
}
|
||||
$this->backends = array_values($backends);
|
||||
}
|
||||
return $this->backends;
|
||||
}
|
||||
}
|
||||
@@ -7,42 +7,50 @@
|
||||
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\AppFramework\Services\IInitialState;
|
||||
use OCP\Encryption\IManager;
|
||||
use OCP\IURLGenerator;
|
||||
use OCP\IUserSession;
|
||||
use OCP\Settings\ISettings;
|
||||
|
||||
class Personal implements ISettings {
|
||||
use CommonSettingsTrait;
|
||||
|
||||
public function __construct(
|
||||
?string $userId,
|
||||
private IManager $encryptionManager,
|
||||
private UserGlobalStoragesService $userGlobalStoragesService,
|
||||
private BackendService $backendService,
|
||||
private GlobalAuth $globalAuth,
|
||||
private IInitialState $initialState,
|
||||
private IURLGenerator $urlGenerator,
|
||||
private IManager $encryptionManager,
|
||||
private IUserSession $userSession,
|
||||
) {
|
||||
$this->userId = $userId;
|
||||
$this->visibility = BackendService::VISIBILITY_PERSONAL;
|
||||
}
|
||||
|
||||
/**
|
||||
* @return TemplateResponse
|
||||
*/
|
||||
public function getForm() {
|
||||
$this->setInitialState();
|
||||
$this->loadScriptsAndStyles();
|
||||
return new TemplateResponse('files_external', 'settings', renderAs: '');
|
||||
$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() {
|
||||
if (!$this->backendService->isUserMountingAllowed()) {
|
||||
return null;
|
||||
}
|
||||
|
||||
return 'externalstorages';
|
||||
}
|
||||
|
||||
|
||||
@@ -1,14 +1,14 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { IStorage } from '../types.ts'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import { DefaultType, File, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test } from 'vitest'
|
||||
import { StorageStatus } from '../types.ts'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
||||
import { action } from './enterCredentialsAction.ts'
|
||||
|
||||
const view = {
|
||||
@@ -31,8 +31,8 @@ describe('Enter credentials action conditions tests', () => {
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: StorageStatus.Success,
|
||||
} as IStorage,
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -72,8 +72,8 @@ describe('Enter credentials action enabled tests', () => {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
status: StorageStatus.Success,
|
||||
} as IStorage,
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -87,9 +87,9 @@ describe('Enter credentials action enabled tests', () => {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
status: StorageStatus.IncompleteConf,
|
||||
status: STORAGE_STATUS.INCOMPLETE_CONF,
|
||||
userProvided: true,
|
||||
} as IStorage,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -103,9 +103,9 @@ describe('Enter credentials action enabled tests', () => {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
status: StorageStatus.IncompleteConf,
|
||||
status: STORAGE_STATUS.INCOMPLETE_CONF,
|
||||
authMechanism: 'password::global::user',
|
||||
} as IStorage,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -119,7 +119,7 @@ describe('Enter credentials action enabled tests', () => {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
} as IStorage,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
|
||||
@@ -1,30 +1,33 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { AxiosResponse } from '@nextcloud/axios'
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { IStorage } from '../types.ts'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import LoginSvg from '@mdi/svg/svg/login.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { DefaultType, FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { spawnDialog } from '@nextcloud/vue/functions/dialog'
|
||||
import { defineAsyncComponent } from 'vue'
|
||||
import { StorageStatus } from '../types.ts'
|
||||
import { isMissingAuthConfig } from '../utils/credentialsUtils.ts'
|
||||
import Vue, { defineAsyncComponent } from 'vue'
|
||||
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
||||
import { isNodeExternalStorage } from '../utils/externalStorageUtils.ts'
|
||||
|
||||
// Add password confirmation interceptors as
|
||||
// the backend requires the user to confirm their password
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
type CredentialResponse = {
|
||||
login?: string
|
||||
password?: string
|
||||
}
|
||||
|
||||
/**
|
||||
* Set credentials for external storage
|
||||
*
|
||||
@@ -32,7 +35,7 @@ addPasswordConfirmationInterceptors(axios)
|
||||
* @param login The username
|
||||
* @param password The password
|
||||
*/
|
||||
async function setCredentials(node: INode, login: string, password: string): Promise<null | true> {
|
||||
async function setCredentials(node: Node, login: string, password: string): Promise<null | true> {
|
||||
const configResponse = await axios.request({
|
||||
method: 'PUT',
|
||||
url: generateUrl('apps/files_external/userglobalstorages/{id}', { id: node.attributes.id }),
|
||||
@@ -40,10 +43,10 @@ async function setCredentials(node: INode, login: string, password: string): Pro
|
||||
data: {
|
||||
backendOptions: { user: login, password },
|
||||
},
|
||||
}) as AxiosResponse<IStorage>
|
||||
}) as AxiosResponse<StorageConfig>
|
||||
|
||||
const config = configResponse.data
|
||||
if (config.status !== StorageStatus.Success) {
|
||||
if (config.status !== STORAGE_STATUS.SUCCESS) {
|
||||
showError(t('files_external', 'Unable to update this external storage config. {statusMessage}', {
|
||||
statusMessage: config?.statusMessage || '',
|
||||
}))
|
||||
@@ -52,9 +55,7 @@ async function setCredentials(node: INode, login: string, password: string): Pro
|
||||
|
||||
// Success update config attribute
|
||||
showSuccess(t('files_external', 'New configuration successfully saved'))
|
||||
node.attributes.config = config
|
||||
emit('files:node:updated', node)
|
||||
|
||||
Vue.set(node.attributes, 'config', config)
|
||||
return true
|
||||
}
|
||||
|
||||
@@ -76,7 +77,7 @@ export const action = new FileAction({
|
||||
return false
|
||||
}
|
||||
|
||||
const config = (node.attributes?.config || {}) as IStorage
|
||||
const config = (node.attributes?.config || {}) as StorageConfig
|
||||
if (isMissingAuthConfig(config)) {
|
||||
return true
|
||||
}
|
||||
@@ -85,10 +86,17 @@ export const action = new FileAction({
|
||||
},
|
||||
|
||||
async exec({ nodes }) {
|
||||
const { login, password } = await spawnDialog(defineAsyncComponent(() => import('../views/CredentialsDialog.vue'))) ?? {}
|
||||
const { login, password } = await new Promise<CredentialResponse>((resolve) => spawnDialog(
|
||||
defineAsyncComponent(() => import('../views/CredentialsDialog.vue')),
|
||||
{},
|
||||
(args) => {
|
||||
resolve(args as CredentialResponse)
|
||||
},
|
||||
))
|
||||
|
||||
if (login && password) {
|
||||
try {
|
||||
await setCredentials(nodes[0]!, login, password)
|
||||
await setCredentials(nodes[0], login, password)
|
||||
showSuccess(t('files_external', 'Credentials successfully set'))
|
||||
} catch (error) {
|
||||
showError(t('files_external', 'Error while setting credentials: {error}', {
|
||||
|
||||
@@ -1,19 +1,18 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { AxiosError } from '@nextcloud/axios'
|
||||
import type { IStorage } from '../types.ts'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
|
||||
import { showWarning } from '@nextcloud/dialogs'
|
||||
import { emit } from '@nextcloud/event-bus'
|
||||
import { FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import Vue from 'vue'
|
||||
import { getStatus } from '../services/externalStorage.ts'
|
||||
import { StorageStatus } from '../types.ts'
|
||||
import { isMissingAuthConfig } from '../utils/credentialsUtils.ts'
|
||||
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
||||
import { isNodeExternalStorage } from '../utils/externalStorageUtils.ts'
|
||||
|
||||
import '../css/fileEntryStatus.scss'
|
||||
@@ -32,8 +31,7 @@ export const action = new FileAction({
|
||||
* Use this function to check the storage availability
|
||||
* We then update the node attributes directly.
|
||||
*
|
||||
* @param context - The action context
|
||||
* @param context.nodes - The node to render inline
|
||||
* @param node The node to render inline
|
||||
*/
|
||||
async renderInline({ nodes }) {
|
||||
if (nodes.length !== 1 || !nodes[0]) {
|
||||
@@ -45,44 +43,44 @@ export const action = new FileAction({
|
||||
span.className = 'files-list__row-status'
|
||||
span.innerHTML = t('files_external', 'Checking storage …')
|
||||
|
||||
let config: IStorage | undefined
|
||||
try {
|
||||
const { data } = await getStatus(node.attributes.id, node.attributes.scope === 'system')
|
||||
config = data
|
||||
node.attributes.config = config
|
||||
emit('files:node:updated', node)
|
||||
let config = null as unknown as StorageConfig
|
||||
getStatus(node.attributes.id, node.attributes.scope === 'system')
|
||||
.then((response) => {
|
||||
config = response.data
|
||||
Vue.set(node.attributes, 'config', config)
|
||||
|
||||
if (config.status !== StorageStatus.Success) {
|
||||
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
|
||||
}
|
||||
if (config.status !== STORAGE_STATUS.SUCCESS) {
|
||||
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
|
||||
}
|
||||
|
||||
span.remove()
|
||||
} catch (error) {
|
||||
// If axios failed or if something else prevented
|
||||
// us from getting the config
|
||||
if ((error as AxiosError).response && !config) {
|
||||
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
|
||||
basename: node.basename,
|
||||
}))
|
||||
}
|
||||
span.remove()
|
||||
})
|
||||
.catch((error) => {
|
||||
// If axios failed or if something else prevented
|
||||
// us from getting the config
|
||||
if ((error as AxiosError).response && !config) {
|
||||
showWarning(t('files_external', 'We were unable to check the external storage {basename}', {
|
||||
basename: node.basename,
|
||||
}))
|
||||
}
|
||||
|
||||
// Reset inline status
|
||||
span.innerHTML = ''
|
||||
// Reset inline status
|
||||
span.innerHTML = ''
|
||||
|
||||
// Checking if we really have an error
|
||||
const isWarning = !config ? false : isMissingAuthConfig(config)
|
||||
const overlay = document.createElement('span')
|
||||
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
|
||||
// Checking if we really have an error
|
||||
const isWarning = !config ? false : isMissingAuthConfig(config)
|
||||
const overlay = document.createElement('span')
|
||||
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
|
||||
|
||||
// Only show an icon for errors, warning like missing credentials
|
||||
// have a dedicated inline action button
|
||||
if (!isWarning) {
|
||||
span.innerHTML = AlertSvg
|
||||
span.title = (error as Error).message
|
||||
}
|
||||
// Only show an icon for errors, warning like missing credentials
|
||||
// have a dedicated inline action button
|
||||
if (!isWarning) {
|
||||
span.innerHTML = AlertSvg
|
||||
span.title = (error as Error).message
|
||||
}
|
||||
|
||||
span.prepend(overlay)
|
||||
}
|
||||
span.prepend(overlay)
|
||||
})
|
||||
|
||||
return span
|
||||
},
|
||||
|
||||
@@ -1,19 +1,16 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { View } from '@nextcloud/files'
|
||||
import type { IStorage } from '../types.ts'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import * as dialogs from '@nextcloud/dialogs'
|
||||
import { DefaultType, FileAction, Folder, Permission } from '@nextcloud/files'
|
||||
import { describe, expect, test, vi } from 'vitest'
|
||||
import { StorageStatus } from '../types.ts'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
||||
import { action } from './openInFilesAction.ts'
|
||||
|
||||
vi.mock('@nextcloud/dialogs', { spy: true })
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
@@ -34,8 +31,8 @@ describe('Open in files action conditions tests', () => {
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: StorageStatus.Success,
|
||||
} as IStorage,
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -67,8 +64,8 @@ describe('Open in files action conditions tests', () => {
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: StorageStatus.Error,
|
||||
} as IStorage,
|
||||
status: STORAGE_STATUS.ERROR,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
expect(action.displayName({
|
||||
@@ -105,7 +102,6 @@ describe('Open in files action enabled tests', () => {
|
||||
describe('Open in files action execute tests', () => {
|
||||
test('Open in files', async () => {
|
||||
const goToRouteMock = vi.fn()
|
||||
// @ts-expect-error - mocking for tests
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const storage = new Folder({
|
||||
@@ -116,8 +112,8 @@ describe('Open in files action execute tests', () => {
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: StorageStatus.Success,
|
||||
} as IStorage,
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -134,8 +130,8 @@ describe('Open in files action execute tests', () => {
|
||||
})
|
||||
|
||||
test('Open in files broken storage', async () => {
|
||||
// @ts-expect-error - spy added by vitest
|
||||
dialogs.showConfirmation.mockImplementationOnce(() => Promise.resolve(true))
|
||||
const confirmMock = vi.fn()
|
||||
window.OC = { dialogs: { confirm: confirmMock } }
|
||||
|
||||
const storage = new Folder({
|
||||
id: 1,
|
||||
@@ -145,8 +141,8 @@ describe('Open in files action execute tests', () => {
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: StorageStatus.Error,
|
||||
} as IStorage,
|
||||
status: STORAGE_STATUS.ERROR,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
@@ -158,6 +154,6 @@ describe('Open in files action execute tests', () => {
|
||||
})
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(dialogs.showConfirmation).toHaveBeenCalledOnce()
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
|
||||
@@ -1,22 +1,20 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IStorage } from '../types.ts'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import { showConfirmation } from '@nextcloud/dialogs'
|
||||
import { DefaultType, FileAction } from '@nextcloud/files'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { StorageStatus } from '../types.ts'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'open-in-files-external-storage',
|
||||
displayName: ({ nodes }) => {
|
||||
const config = nodes?.[0]?.attributes?.config as IStorage || { status: StorageStatus.Indeterminate }
|
||||
if (config.status !== StorageStatus.Success) {
|
||||
const config = nodes?.[0]?.attributes?.config as StorageConfig || { status: STORAGE_STATUS.INDETERMINATE }
|
||||
if (config.status !== STORAGE_STATUS.SUCCESS) {
|
||||
return t('files_external', 'Examine this faulty external storage configuration')
|
||||
}
|
||||
return t('files', 'Open in Files')
|
||||
@@ -26,18 +24,18 @@ export const action = new FileAction({
|
||||
enabled: ({ view }) => view.id === 'extstoragemounts',
|
||||
|
||||
async exec({ nodes }) {
|
||||
const config = nodes[0]?.attributes?.config as IStorage
|
||||
if (config?.status !== StorageStatus.Success) {
|
||||
const redirect = await showConfirmation({
|
||||
name: t('files_external', 'External mount error'),
|
||||
text: t('files_external', 'There was an error with this external storage. Do you want to review this mount point config in the settings page?'),
|
||||
labelConfirm: t('files_external', 'Open settings'),
|
||||
labelReject: t('files_external', 'Ignore'),
|
||||
})
|
||||
if (redirect === true) {
|
||||
const scope = getCurrentUser()?.isAdmin ? 'admin' : 'user'
|
||||
window.location.href = generateUrl(`/settings/${scope}/externalstorages`)
|
||||
}
|
||||
const config = nodes[0]?.attributes?.config as StorageConfig
|
||||
if (config?.status !== STORAGE_STATUS.SUCCESS) {
|
||||
window.OC.dialogs.confirm(
|
||||
t('files_external', 'There was an error with this external storage. Do you want to review this mount point config in the settings page?'),
|
||||
t('files_external', 'External mount error'),
|
||||
(redirect) => {
|
||||
if (redirect === true) {
|
||||
const scope = getCurrentUser()?.isAdmin ? 'admin' : 'user'
|
||||
window.location.href = generateUrl(`/settings/${scope}/externalstorages`)
|
||||
}
|
||||
},
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
|
||||
@@ -1,20 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IAuthMechanism } from './types.ts'
|
||||
|
||||
import { defineAsyncComponent, defineCustomElement } from 'vue'
|
||||
|
||||
const AuthMechanismRsa = defineAsyncComponent(() => import('./views/AuthMechanismRsa.vue'))
|
||||
const AuthMechanismRsaComponent = defineCustomElement(AuthMechanismRsa, { shadowRoot: false })
|
||||
customElements.define('files_external-auth-mechanism-rsa', AuthMechanismRsaComponent)
|
||||
|
||||
window.OCA.FilesExternal.AuthMechanism!.registerHandler({
|
||||
id: 'rsa',
|
||||
tagName: 'files_external-auth-mechanism-rsa',
|
||||
enabled(authMechanism: IAuthMechanism) {
|
||||
return authMechanism.scheme === 'publickey' && authMechanism.identifier === 'publickey::rsa'
|
||||
},
|
||||
})
|
||||
-149
@@ -1,149 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script lang="ts">
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
|
||||
const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings')
|
||||
const allowedBackendIds = loadState<string[]>('files_external', 'allowedBackends')
|
||||
const backends = loadState<IBackend[]>('files_external', 'backends')
|
||||
.filter((b) => allowedBackendIds.includes(b.identifier))
|
||||
|
||||
const allAuthMechanisms = loadState<IAuthMechanism[]>('files_external', 'authMechanisms')
|
||||
</script>
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAuthMechanism, IBackend, IStorage } from '../../types.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, ref, toRaw, watch, watchEffect } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import ApplicableEntities from './ApplicableEntities.vue'
|
||||
import AuthMechanismConfiguration from './AuthMechanismConfiguration.vue'
|
||||
import BackendConfiguration from './BackendConfiguration.vue'
|
||||
import MountOptions from './MountOptions.vue'
|
||||
|
||||
const open = defineModel<boolean>('open', { default: true })
|
||||
|
||||
const {
|
||||
storage = { backendOptions: {}, mountOptions: {}, type: isAdmin ? 'system' : 'personal' },
|
||||
} = defineProps<{
|
||||
storage?: Partial<IStorage> & { backendOptions: IStorage['backendOptions'] }
|
||||
}>()
|
||||
|
||||
defineEmits<{
|
||||
close: [storage?: Partial<IStorage>]
|
||||
}>()
|
||||
|
||||
const internalStorage = ref(structuredClone(toRaw(storage)))
|
||||
watchEffect(() => {
|
||||
if (open.value) {
|
||||
internalStorage.value = structuredClone(toRaw(storage))
|
||||
}
|
||||
})
|
||||
|
||||
const backend = computed({
|
||||
get() {
|
||||
return backends.find((b) => b.identifier === internalStorage.value.backend)
|
||||
},
|
||||
set(value?: IBackend) {
|
||||
internalStorage.value.backend = value?.identifier
|
||||
},
|
||||
})
|
||||
|
||||
const authMechanisms = computed(() => allAuthMechanisms
|
||||
.filter(({ scheme }) => backend.value?.authSchemes[scheme]))
|
||||
const authMechanism = computed({
|
||||
get() {
|
||||
return authMechanisms.value.find((a) => a.identifier === internalStorage.value.authMechanism)
|
||||
},
|
||||
set(value?: IAuthMechanism) {
|
||||
internalStorage.value.authMechanism = value?.identifier
|
||||
},
|
||||
})
|
||||
|
||||
// auto set the auth mechanism if there's only one available
|
||||
watch(authMechanisms, () => {
|
||||
if (authMechanisms.value.length === 1) {
|
||||
internalStorage.value.authMechanism = authMechanisms.value[0]!.identifier
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcDialog
|
||||
v-model:open="open"
|
||||
is-form
|
||||
:content-classes="$style.externalStorageDialog"
|
||||
:name="internalStorage.id ? t('files_external', 'Edit storage') : t('files_external', 'Add storage')"
|
||||
@submit="$emit('close', internalStorage)"
|
||||
@update:open="$event || $emit('close')">
|
||||
<NcTextField
|
||||
v-model="internalStorage.mountPoint"
|
||||
:label="t('files_external', 'Folder name')"
|
||||
required />
|
||||
|
||||
<MountOptions v-model="internalStorage.mountOptions" />
|
||||
|
||||
<ApplicableEntities
|
||||
v-if="isAdmin"
|
||||
v-model:groups="internalStorage.applicableGroups"
|
||||
v-model:users="internalStorage.applicableUsers" />
|
||||
|
||||
<NcSelect
|
||||
v-model="backend"
|
||||
:options="backends"
|
||||
:disabled="!!(internalStorage.id && internalStorage.backend)"
|
||||
:input-label="t('files_external', 'External storage')"
|
||||
label="name"
|
||||
required />
|
||||
|
||||
<NcSelect
|
||||
v-model="authMechanism"
|
||||
:options="authMechanisms"
|
||||
:disabled="!internalStorage.backend || authMechanisms.length <= 1 || !!(internalStorage.id && internalStorage.authMechanism)"
|
||||
:input-label="t('files_external', 'Authentication')"
|
||||
label="name"
|
||||
required />
|
||||
|
||||
<BackendConfiguration
|
||||
v-if="backend"
|
||||
v-model="internalStorage.backendOptions"
|
||||
:class="$style.externalStorageDialog__configuration"
|
||||
:configuration="backend.configuration" />
|
||||
|
||||
<AuthMechanismConfiguration
|
||||
v-if="authMechanism"
|
||||
v-model="internalStorage.backendOptions"
|
||||
:class="$style.externalStorageDialog__configuration"
|
||||
:auth-mechanism="authMechanism" />
|
||||
|
||||
<template #actions>
|
||||
<NcButton v-if="storage.id" @click="$emit('close')">
|
||||
{{ t('files_external', 'Cancel') }}
|
||||
</NcButton>
|
||||
|
||||
<NcButton variant="primary" type="submit">
|
||||
{{ storage.id ? t('files_external', 'Edit') : t('files_external', 'Create') }}
|
||||
</NcButton>
|
||||
</template>
|
||||
</NcDialog>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.externalStorageDialog {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-grid-baseline);
|
||||
min-height: calc(14 * var(--default-clickable-area)) !important;
|
||||
}
|
||||
|
||||
.externalStorageDialog__configuration {
|
||||
margin-block: 0.5rem;
|
||||
}
|
||||
</style>
|
||||
@@ -1,67 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from '@nextcloud/axios'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { useDebounceFn } from '@vueuse/core'
|
||||
import { computed, ref } from 'vue'
|
||||
import NcSelectUsers from '@nextcloud/vue/components/NcSelectUsers'
|
||||
import { mapGroupToUserData, useGroups, useUsers } from '../../composables/useEntities.ts'
|
||||
|
||||
type IUserData = InstanceType<typeof NcSelectUsers>['$props']['options'][number]
|
||||
|
||||
const groups = defineModel<string[]>('groups', { default: () => [] })
|
||||
const users = defineModel<string[]>('users', { default: () => [] })
|
||||
|
||||
const entities = ref<IUserData[]>([])
|
||||
const selectedUsers = useUsers(users)
|
||||
const selectedGroups = useGroups(groups)
|
||||
|
||||
const model = computed({
|
||||
get() {
|
||||
return [...selectedGroups.value, ...selectedUsers.value]
|
||||
},
|
||||
set(value: IUserData[]) {
|
||||
users.value = value.filter((u) => u.user).map((u) => u.user!)
|
||||
groups.value = value.filter((g) => g.isNoUser).map((g) => g.id)
|
||||
},
|
||||
})
|
||||
|
||||
const debouncedSearch = useDebounceFn(onSearch, 500)
|
||||
|
||||
/**
|
||||
* Handle searching for users and groups
|
||||
*
|
||||
* @param pattern - The pattern to search
|
||||
*/
|
||||
async function onSearch(pattern: string) {
|
||||
const { data } = await axios.get<{ groups: Record<string, string>, users: Record<string, string> }>(
|
||||
generateUrl('apps/files_external/ajax/applicable'),
|
||||
{ params: { pattern, limit: 20 } },
|
||||
)
|
||||
|
||||
const newEntries = [
|
||||
...entities.value.map((e) => [e.id, e]),
|
||||
...Object.entries(data.groups)
|
||||
.map(([id, displayName]) => [id, { ...mapGroupToUserData(id), displayName }]),
|
||||
...Object.entries(data.users)
|
||||
.map(([id, displayName]) => [`user:${id}`, { id: `user:${id}`, user: id, displayName }]),
|
||||
] as [string, IUserData][]
|
||||
|
||||
entities.value = [...new Map(newEntries).values()]
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcSelectUsers
|
||||
v-model="model"
|
||||
keep-open
|
||||
multiple
|
||||
:options="entities"
|
||||
:input-label="t('files_external', 'Restrict to')"
|
||||
@search="debouncedSearch" />
|
||||
</template>
|
||||
-111
@@ -1,111 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAuthMechanism } from '../../types.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { NcLoadingIcon } from '@nextcloud/vue'
|
||||
import { computed, ref, watch, watchEffect } from 'vue'
|
||||
import ConfigurationEntry from './ConfigurationEntry.vue'
|
||||
import { ConfigurationFlag, ConfigurationType } from '../../types.ts'
|
||||
|
||||
const modelValue = defineModel<Record<string, string | boolean>>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
authMechanism: IAuthMechanism
|
||||
}>()
|
||||
|
||||
const configuration = computed(() => {
|
||||
if (!props.authMechanism.configuration) {
|
||||
return undefined
|
||||
}
|
||||
|
||||
const entries = Object.entries(props.authMechanism.configuration)
|
||||
.filter(([, option]) => !(option.flags & ConfigurationFlag.UserProvided))
|
||||
return Object.fromEntries(entries) as typeof props.authMechanism.configuration
|
||||
})
|
||||
|
||||
const customComponent = computed(() => window.OCA.FilesExternal.AuthMechanism!.getHandler(props.authMechanism))
|
||||
const hasConfiguration = computed(() => {
|
||||
if (!configuration.value) {
|
||||
return false
|
||||
}
|
||||
for (const option of Object.values(configuration.value)) {
|
||||
if ((option.flags & ConfigurationFlag.Hidden) || (option.flags & ConfigurationFlag.UserProvided)) {
|
||||
continue
|
||||
}
|
||||
// a real config option
|
||||
return true
|
||||
}
|
||||
return false
|
||||
})
|
||||
|
||||
const isLoadingCustomComponent = ref(false)
|
||||
watchEffect(async () => {
|
||||
if (customComponent.value) {
|
||||
isLoadingCustomComponent.value = true
|
||||
await window.customElements.whenDefined(customComponent.value.tagName)
|
||||
isLoadingCustomComponent.value = false
|
||||
}
|
||||
})
|
||||
|
||||
watch(configuration, () => {
|
||||
for (const key in configuration.value) {
|
||||
if (!(key in modelValue.value)) {
|
||||
modelValue.value[key] = configuration.value[key]?.type === ConfigurationType.Boolean
|
||||
? false
|
||||
: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Update the model value when the custom component emits an update event.
|
||||
*
|
||||
* @param event - The custom event
|
||||
*/
|
||||
function onUpdateModelValue(event: CustomEvent) {
|
||||
const config = [event.detail].flat()[0]
|
||||
modelValue.value = { ...modelValue.value, ...config }
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset v-if="hasConfiguration" :class="$style.authMechanismConfiguration">
|
||||
<legend>
|
||||
{{ t('files_external', 'Authentication') }}
|
||||
</legend>
|
||||
|
||||
<template v-if="customComponent">
|
||||
<NcLoadingIcon v-if="isLoadingCustomComponent" />
|
||||
<!-- eslint-disable vue/attribute-hyphenation,vue/v-on-event-hyphenation -- for custom elements the casing is fixed! -->
|
||||
<component
|
||||
:is="customComponent.tagName"
|
||||
v-else
|
||||
:modelValue.prop="modelValue"
|
||||
:authMechanism.prop="authMechanism"
|
||||
@update:modelValue="onUpdateModelValue" />
|
||||
</template>
|
||||
|
||||
<template v-else>
|
||||
<ConfigurationEntry
|
||||
v-for="(configOption, configKey) in configuration"
|
||||
v-show="!(configOption.flags & ConfigurationFlag.Hidden)"
|
||||
:key="configOption.value"
|
||||
v-model="modelValue[configKey]!"
|
||||
:config-key
|
||||
:config-option />
|
||||
</template>
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.authMechanismConfiguration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-grid-baseline);
|
||||
}
|
||||
</style>
|
||||
@@ -1,53 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IConfigurationOption } from '../../types.ts'
|
||||
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { watch } from 'vue'
|
||||
import ConfigurationEntry from './ConfigurationEntry.vue'
|
||||
import { ConfigurationFlag, ConfigurationType } from '../../types.ts'
|
||||
|
||||
const modelValue = defineModel<Record<string, string | boolean>>({ required: true })
|
||||
|
||||
const props = defineProps<{
|
||||
configuration: Record<string, IConfigurationOption>
|
||||
}>()
|
||||
|
||||
watch(() => props.configuration, () => {
|
||||
for (const key in props.configuration) {
|
||||
if (!(key in modelValue.value)) {
|
||||
modelValue.value[key] = props.configuration[key]?.type === ConfigurationType.Boolean
|
||||
? false
|
||||
: ''
|
||||
}
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<fieldset :class="$style.backendConfiguration">
|
||||
<legend>
|
||||
{{ t('files_external', 'Storage configuration') }}
|
||||
</legend>
|
||||
|
||||
<ConfigurationEntry
|
||||
v-for="configOption, configKey in configuration"
|
||||
v-show="!(configOption.flags & ConfigurationFlag.Hidden)"
|
||||
:key="configOption.value"
|
||||
v-model="modelValue[configKey]!"
|
||||
:config-key="configKey"
|
||||
:config-option="configOption" />
|
||||
</fieldset>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.backendConfiguration {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-grid-baseline);
|
||||
}
|
||||
</style>
|
||||
@@ -1,38 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IConfigurationOption } from '../../types.ts'
|
||||
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import { ConfigurationFlag, ConfigurationType } from '../../types.ts'
|
||||
|
||||
const value = defineModel<string | boolean>('modelValue', { default: '' })
|
||||
|
||||
defineProps<{
|
||||
configKey: string
|
||||
configOption: IConfigurationOption
|
||||
}>()
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<component
|
||||
:is="configOption.type === ConfigurationType.Password ? NcPasswordField : NcTextField"
|
||||
v-if="configOption.type !== ConfigurationType.Boolean"
|
||||
v-model="value"
|
||||
:name="configKey"
|
||||
:required="!(configOption.flags & ConfigurationFlag.Optional)"
|
||||
:label="configOption.value"
|
||||
:title="configOption.tooltip" />
|
||||
<NcCheckboxRadioSwitch
|
||||
v-else
|
||||
v-model="value"
|
||||
type="switch"
|
||||
:title="configOption.tooltip">
|
||||
{{ configOption.value }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</template>
|
||||
@@ -1,123 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IMountOptions } from '../../types.ts'
|
||||
|
||||
import { mdiChevronDown, mdiChevronRight } from '@mdi/js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed, ref, useId, watchEffect } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import { MountOptionsCheckFilesystem } from '../../types.ts'
|
||||
|
||||
const mountOptions = defineModel<Partial<IMountOptions>>({ required: true })
|
||||
watchEffect(() => {
|
||||
if (Object.keys(mountOptions.value).length === 0) {
|
||||
mountOptions.value.encrypt = true
|
||||
mountOptions.value.previews = true
|
||||
mountOptions.value.enable_sharing = false
|
||||
mountOptions.value.filesystem_check_changes = MountOptionsCheckFilesystem.OncePerRequest
|
||||
mountOptions.value.encoding_compatibility = false
|
||||
mountOptions.value.readonly = false
|
||||
}
|
||||
})
|
||||
|
||||
const { hasEncryption } = loadState<{ hasEncryption: boolean }>('files_external', 'settings')
|
||||
|
||||
const idButton = useId()
|
||||
const idFieldset = useId()
|
||||
|
||||
const isExpanded = ref(false)
|
||||
|
||||
const checkFilesystemOptions = [
|
||||
{
|
||||
label: t('files_external', 'Never'),
|
||||
value: MountOptionsCheckFilesystem.Never,
|
||||
},
|
||||
{
|
||||
label: t('files_external', 'Once every direct access'),
|
||||
value: MountOptionsCheckFilesystem.OncePerRequest,
|
||||
},
|
||||
{
|
||||
label: t('files_external', 'Always'),
|
||||
value: MountOptionsCheckFilesystem.Always,
|
||||
},
|
||||
]
|
||||
const checkFilesystem = computed({
|
||||
get() {
|
||||
return checkFilesystemOptions.find((option) => option.value === mountOptions.value.filesystem_check_changes)
|
||||
},
|
||||
set(value) {
|
||||
mountOptions.value.filesystem_check_changes = value?.value ?? MountOptionsCheckFilesystem.OncePerRequest
|
||||
},
|
||||
})
|
||||
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div :class="$style.mountOptions">
|
||||
<NcButton
|
||||
:id="idButton"
|
||||
:aria-controls="idFieldset"
|
||||
:aria-expanded="isExpanded"
|
||||
variant="tertiary-no-background"
|
||||
@click="isExpanded = !isExpanded">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper directional :path="isExpanded ? mdiChevronDown : mdiChevronRight" />
|
||||
</template>
|
||||
{{ t('files_external', 'Mount options') }}
|
||||
</NcButton>
|
||||
|
||||
<fieldset
|
||||
v-show="isExpanded"
|
||||
:id="idFieldset"
|
||||
:class="$style.mountOptions__fieldset"
|
||||
:aria-labelledby="idButton">
|
||||
<NcSelect
|
||||
v-model="checkFilesystem"
|
||||
:input-label="t('files_external', 'Check filesystem changes')"
|
||||
:options="checkFilesystemOptions" />
|
||||
|
||||
<NcCheckboxRadioSwitch v-model="modelValue.readonly" type="switch">
|
||||
{{ t('files_external', 'Read only') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch v-model="modelValue.previews" type="switch">
|
||||
{{ t('files_external', 'Enable previews') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch v-model="modelValue.enable_sharing" type="switch">
|
||||
{{ t('files_external', 'Enable sharing') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch v-if="hasEncryption" v-model="modelValue.encrypt" type="switch">
|
||||
{{ t('files_external', 'Enable encryption') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
<NcCheckboxRadioSwitch v-model="modelValue.encoding_compatibility" type="switch">
|
||||
{{ t('files_external', 'Compatibility with Mac NFD encoding (slow)') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</fieldset>
|
||||
</div>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.mountOptions {
|
||||
background-color: hsl(from var(--color-primary-element-light) h s calc(l * 1.045));
|
||||
border-radius: var(--border-radius-element);
|
||||
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-grid-baseline);
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.mountOptions__fieldset {
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
gap: var(--default-grid-baseline);
|
||||
padding-inline: calc(2 * var(--default-grid-baseline)) var(--default-grid-baseline);
|
||||
}
|
||||
</style>
|
||||
@@ -1,99 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { computed } from 'vue'
|
||||
import ExternalStorageTableRow from './ExternalStorageTableRow.vue'
|
||||
import { useStorages } from '../store/storages.ts'
|
||||
|
||||
const store = useStorages()
|
||||
const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings')
|
||||
const storages = computed(() => {
|
||||
if (isAdmin) {
|
||||
return store.globalStorages
|
||||
} else {
|
||||
return [
|
||||
...store.userStorages,
|
||||
...store.globalStorages,
|
||||
]
|
||||
}
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<table :class="$style.storageTable" :aria-label="t('files_external', 'External storages')">
|
||||
<thead :class="$style.storageTable__header">
|
||||
<tr>
|
||||
<th :class="$style.storageTable__headerStatus">
|
||||
<span class="hidden-visually">
|
||||
{{ t('files_external', 'Status') }}
|
||||
</span>
|
||||
</th>
|
||||
<th :class="$style.storageTable__headerFolder">
|
||||
{{ t('files_external', 'Folder name') }}
|
||||
</th>
|
||||
<th :class="$style.storageTable__headerBackend">
|
||||
{{ t('files_external', 'External storage') }}
|
||||
</th>
|
||||
<th :class="$style.storageTable__headerAuthentication">
|
||||
{{ t('files_external', 'Authentication') }}
|
||||
</th>
|
||||
<th v-if="isAdmin">
|
||||
{{ t('files_external', 'Restricted to') }}
|
||||
</th>
|
||||
<th :class="$style.storageTable__headerActions">
|
||||
<span class="hidden-visually">
|
||||
{{ t('files_external', 'Actions') }}
|
||||
</span>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<ExternalStorageTableRow
|
||||
v-for="storage in storages"
|
||||
:key="storage.id"
|
||||
:is-admin
|
||||
:storage="storage" />
|
||||
</tbody>
|
||||
</table>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.storageTable {
|
||||
width: 100%;
|
||||
}
|
||||
|
||||
.storageTable td,th {
|
||||
padding-block: calc(var(--default-grid-baseline) / 2);
|
||||
padding-inline: var(--default-grid-baseline);
|
||||
}
|
||||
|
||||
.storageTable__header {
|
||||
color: var(--color-text-maxcontrast);
|
||||
min-height: var(--default-clickable-area);
|
||||
}
|
||||
|
||||
.storageTable__headerStatus {
|
||||
width: calc(var(--default-clickable-area) + 2 * var(--default-grid-baseline));
|
||||
}
|
||||
|
||||
.storageTable__headerFolder {
|
||||
width: 25%;
|
||||
}
|
||||
|
||||
.storageTable__headerBackend {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.storageTable__headerFAuthentication {
|
||||
width: 20%;
|
||||
}
|
||||
|
||||
.storageTable__headerActions {
|
||||
width: calc(2 * var(--default-clickable-area) + 3 * var(--default-grid-baseline));
|
||||
}
|
||||
</style>
|
||||
@@ -1,182 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IBackend, IStorage } from '../types.ts'
|
||||
|
||||
import { mdiAccountGroupOutline, mdiInformationOutline, mdiPencilOutline, mdiTrashCanOutline } from '@mdi/js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { NcChip, NcLoadingIcon, NcUserBubble, spawnDialog } from '@nextcloud/vue'
|
||||
import { computed, ref } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import AddExternalStorageDialog from './AddExternalStorageDialog/AddExternalStorageDialog.vue'
|
||||
import { useUsers } from '../composables/useEntities.ts'
|
||||
import { useStorages } from '../store/storages.ts'
|
||||
import { StorageStatus, StorageStatusIcons, StorageStatusMessage } from '../types.ts'
|
||||
|
||||
const props = defineProps<{
|
||||
storage: IStorage
|
||||
isAdmin: boolean
|
||||
}>()
|
||||
|
||||
const store = useStorages()
|
||||
|
||||
const backends = loadState<IBackend[]>('files_external', 'backends')
|
||||
const backendName = computed(() => backends.find((b) => b.identifier === props.storage.backend)!.name)
|
||||
|
||||
const authMechanisms = loadState<IBackend[]>('files_external', 'authMechanisms')
|
||||
const authMechanismName = computed(() => authMechanisms.find((a) => a.identifier === props.storage.authMechanism)!.name)
|
||||
|
||||
const checkingStatus = ref(false)
|
||||
const status = computed(() => {
|
||||
if (checkingStatus.value) {
|
||||
return {
|
||||
icon: 'loading',
|
||||
label: t('files_external', 'Checking …'),
|
||||
}
|
||||
}
|
||||
|
||||
const status = props.storage.status ?? StorageStatus.Indeterminate
|
||||
const label = props.storage.statusMessage || StorageStatusMessage[status]
|
||||
const icon = StorageStatusIcons[status]
|
||||
|
||||
const isWarning = status === StorageStatus.NetworkError || status === StorageStatus.Timeout
|
||||
const isError = !isWarning && status !== StorageStatus.Success && status !== StorageStatus.Indeterminate
|
||||
|
||||
return { icon, label, isWarning, isError }
|
||||
})
|
||||
|
||||
const users = useUsers(() => props.storage.applicableUsers || [])
|
||||
|
||||
/**
|
||||
* Handle deletion of the external storage mount point
|
||||
*/
|
||||
async function onDelete() {
|
||||
await store.deleteStorage(props.storage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Handle editing of the external storage mount point
|
||||
*/
|
||||
async function onEdit() {
|
||||
const storage = await spawnDialog(AddExternalStorageDialog, {
|
||||
storage: props.storage,
|
||||
})
|
||||
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
await store.updateStorage(storage as IStorage)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload the status of the external storage mount point
|
||||
*/
|
||||
async function reloadStatus() {
|
||||
checkingStatus.value = true
|
||||
try {
|
||||
await store.reloadStorage(props.storage)
|
||||
} finally {
|
||||
checkingStatus.value = false
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<tr :class="$style.storageTableRow">
|
||||
<td>
|
||||
<span class="hidden-visually">{{ status.label }}</span>
|
||||
<NcButton
|
||||
:aria-label="t('files_external', 'Recheck status')"
|
||||
:title="status.label"
|
||||
variant="tertiary-no-background"
|
||||
@click="reloadStatus">
|
||||
<template #icon>
|
||||
<NcLoadingIcon v-if="status.icon === 'loading'" />
|
||||
<NcIconSvgWrapper
|
||||
v-else
|
||||
:class="{
|
||||
[$style.storageTableRow__status_error]: status.isError,
|
||||
[$style.storageTableRow__status_warning]: status.isWarning,
|
||||
}"
|
||||
:path="status.icon" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</td>
|
||||
<td>{{ storage.mountPoint }}</td>
|
||||
<td>{{ backendName }}</td>
|
||||
<td>{{ authMechanismName }}</td>
|
||||
<td v-if="isAdmin">
|
||||
<div :class="$style.storageTableRow__cellApplicable">
|
||||
<NcChip
|
||||
v-for="group of storage.applicableGroups"
|
||||
:key="group"
|
||||
:icon-path="mdiAccountGroupOutline"
|
||||
no-close
|
||||
:text="group" />
|
||||
<NcUserBubble
|
||||
v-for="user of users"
|
||||
:key="user.user"
|
||||
:display-name="user.displayName"
|
||||
:size="24"
|
||||
:user="user.user" />
|
||||
</div>
|
||||
</td>
|
||||
<td>
|
||||
<div v-if="isAdmin || storage.type === 'personal'" :class="$style.storageTableRow__cellActions">
|
||||
<NcButton
|
||||
:aria-label="t('files_external', 'Edit')"
|
||||
:title="t('files_external', 'Edit')"
|
||||
@click="onEdit">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiPencilOutline" />
|
||||
</template>
|
||||
</NcButton>
|
||||
<NcButton
|
||||
:aria-label="t('files_external', 'Delete')"
|
||||
:title="t('files_external', 'Delete')"
|
||||
variant="error"
|
||||
@click="onDelete">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiTrashCanOutline" />
|
||||
</template>
|
||||
</NcButton>
|
||||
</div>
|
||||
<NcIconSvgWrapper
|
||||
v-else
|
||||
inline
|
||||
:path="mdiInformationOutline"
|
||||
:name="t('files_external', 'System provided storage')"
|
||||
:title="t('files_external', 'System provided storage')" />
|
||||
</td>
|
||||
</tr>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.storageTableRow__cellActions {
|
||||
display: flex;
|
||||
gap: var(--default-grid-baseline);
|
||||
}
|
||||
|
||||
.storageTableRow__cellApplicable {
|
||||
display: flex;
|
||||
flex-wrap: wrap;
|
||||
gap: var(--default-grid-baseline);
|
||||
align-items: center;
|
||||
|
||||
max-height: calc(48px + 2 * var(--default-grid-baseline));
|
||||
overflow: scroll;
|
||||
}
|
||||
|
||||
.storageTableRow__status_warning {
|
||||
color: var(--color-element-warning);
|
||||
}
|
||||
|
||||
.storageTableRow__status_error {
|
||||
color: var(--color-element-error);
|
||||
}
|
||||
</style>
|
||||
@@ -1,108 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { IBackend } from '../types.ts'
|
||||
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { ref, watch } from 'vue'
|
||||
import NcCheckboxRadioSwitch from '@nextcloud/vue/components/NcCheckboxRadioSwitch'
|
||||
|
||||
const userMounting = loadState<{
|
||||
allowUserMounting: boolean
|
||||
allowedBackends: string[]
|
||||
}>('files_external', 'user-mounting')
|
||||
|
||||
const availableBackends = loadState<IBackend[]>('files_external', 'backends')
|
||||
const allowUserMounting = ref(userMounting.allowUserMounting)
|
||||
const allowedBackends = ref<string[]>(userMounting.allowedBackends)
|
||||
|
||||
/**
|
||||
* When changing the enabled state of the user-mounting settings then also change this on the server
|
||||
*/
|
||||
watch(allowUserMounting, () => {
|
||||
const backupValue = !allowUserMounting.value
|
||||
window.OCP.AppConfig.setValue(
|
||||
'files_external',
|
||||
'allow_user_mounting',
|
||||
allowUserMounting.value ? 'yes' : 'no',
|
||||
{
|
||||
success: () => showSuccess(t('files_external', 'Saved')),
|
||||
error: () => {
|
||||
allowUserMounting.value = backupValue
|
||||
showError(t('files_external', 'Error while saving'))
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
|
||||
/**
|
||||
* Save list of allowed backends on the server
|
||||
*
|
||||
* @param newValue - The new changed value
|
||||
* @param oldValue - The old value for resetting on failure
|
||||
*/
|
||||
watch(allowedBackends, (newValue, oldValue) => {
|
||||
// save to server
|
||||
window.OCP.AppConfig.setValue(
|
||||
'files_external',
|
||||
'user_mounting_backends',
|
||||
newValue.join(','),
|
||||
{
|
||||
success: () => showSuccess(t('files_external', 'Saved allowed backends')),
|
||||
error: () => {
|
||||
showError(t('files_external', 'Failed to save allowed backends'))
|
||||
allowedBackends.value = oldValue
|
||||
},
|
||||
},
|
||||
)
|
||||
})
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<form>
|
||||
<h3 :class="$style.userMountSettings__heading">
|
||||
{{ t('files_external', 'Advanced options for external storage mounts') }}
|
||||
</h3>
|
||||
|
||||
<NcCheckboxRadioSwitch v-model="allowUserMounting" type="switch">
|
||||
{{ t('files_external', 'Allow people to mount external storage') }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
|
||||
<fieldset v-show="allowUserMounting" :class="$style.userMountSettings__backends">
|
||||
<legend>
|
||||
{{ t('files_external', 'External storage backends people are allowed to mount') }}
|
||||
</legend>
|
||||
<NcCheckboxRadioSwitch
|
||||
v-for="backend of availableBackends"
|
||||
:key="backend.identifier"
|
||||
v-model="allowedBackends"
|
||||
:value="backend.identifier"
|
||||
name="allowUserMountingBackends[]">
|
||||
{{ backend.name }}
|
||||
</NcCheckboxRadioSwitch>
|
||||
</fieldset>
|
||||
</form>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.userMountSettings__heading {
|
||||
font-weight: bold;
|
||||
font-size: 1.2rem;
|
||||
|
||||
margin-block-start: var(--default-clickable-area);
|
||||
}
|
||||
|
||||
.userMountSettings__backends {
|
||||
--padding: calc((var(--default-clickable-area) - 20px) / 2 + var(--default-grid-baseline));
|
||||
margin-block-start: var(--padding);
|
||||
margin-inline-start: var(--padding);
|
||||
|
||||
legend {
|
||||
font-weight: bold;
|
||||
}
|
||||
}
|
||||
</style>
|
||||
@@ -1,63 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { MaybeRefOrGetter } from 'vue'
|
||||
|
||||
import svgAccountGroupOutline from '@mdi/svg/svg/account-group-outline.svg?raw'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { computed, reactive, toValue, watchEffect } from 'vue'
|
||||
|
||||
const displayNames = reactive(new Map<string, string>())
|
||||
|
||||
/**
|
||||
* Fetch and provide user display names for given UIDs
|
||||
*
|
||||
* @param uids - The user ids to fetch display names for
|
||||
*/
|
||||
export function useUsers(uids: MaybeRefOrGetter<string[]>) {
|
||||
const users = computed(() => toValue(uids).map((uid) => ({
|
||||
id: `user:${uid}`,
|
||||
user: uid,
|
||||
displayName: displayNames.get(uid) || uid,
|
||||
})))
|
||||
|
||||
watchEffect(async () => {
|
||||
const missingUsers = toValue(uids).filter((uid) => !displayNames.has(uid))
|
||||
if (missingUsers.length > 0) {
|
||||
const { data } = await axios.post(generateUrl('/displaynames'), {
|
||||
users: missingUsers,
|
||||
})
|
||||
for (const [uid, displayName] of Object.entries(data.users)) {
|
||||
displayNames.set(uid, displayName as string)
|
||||
}
|
||||
}
|
||||
})
|
||||
|
||||
return users
|
||||
}
|
||||
|
||||
/**
|
||||
* Map group ids to IUserData objects
|
||||
*
|
||||
* @param gids - The group ids to create entities for
|
||||
*/
|
||||
export function useGroups(gids: MaybeRefOrGetter<string[]>) {
|
||||
return computed(() => toValue(gids).map(mapGroupToUserData))
|
||||
}
|
||||
|
||||
/**
|
||||
* Map a group id to an IUserData object
|
||||
*
|
||||
* @param gid - The group id to map
|
||||
*/
|
||||
export function mapGroupToUserData(gid: string) {
|
||||
return {
|
||||
id: gid,
|
||||
isNoUser: true,
|
||||
displayName: gid,
|
||||
iconSvg: svgAccountGroupOutline,
|
||||
}
|
||||
}
|
||||
Vendored
-18
@@ -1,18 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { AuthMechanism } from './services/AuthMachanism.ts'
|
||||
|
||||
declare global {
|
||||
interface Window {
|
||||
OCA: {
|
||||
FilesExternal: {
|
||||
AuthMechanism?: AuthMechanism
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export {}
|
||||
@@ -1,9 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { AuthMechanism } from './services/AuthMachanism.ts'
|
||||
|
||||
window.OCA.FilesExternal ??= {}
|
||||
window.OCA.FilesExternal.AuthMechanism = new AuthMechanism()
|
||||
@@ -1,10 +1,9 @@
|
||||
/*
|
||||
import FolderNetworkSvg from '@mdi/svg/svg/folder-network-outline.svg?raw'
|
||||
import { Column, getNavigation, registerFileAction, View } from '@nextcloud/files'
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import FolderNetworkSvg from '@mdi/svg/svg/folder-network-outline.svg?raw'
|
||||
import { Column, getNavigation, registerFileAction, View } from '@nextcloud/files'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { action as enterCredentialsAction } from './actions/enterCredentialsAction.ts'
|
||||
@@ -1,61 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IAuthMechanism } from '../types.ts'
|
||||
|
||||
import logger from '../utils/logger.ts'
|
||||
|
||||
interface IAuthConfigHandler {
|
||||
/**
|
||||
* Unique identifier for the auth mechanism handler
|
||||
*/
|
||||
id: string
|
||||
|
||||
/**
|
||||
* Tag name used to register the web component.
|
||||
*
|
||||
* The registered web component must have the following props:
|
||||
* - `authMechanism`: The auth mechanism configuration object {see IAuthMechanism}
|
||||
* - `modelValue`: The current configuration values as an object
|
||||
*
|
||||
* The web component must emit the following events:
|
||||
* - `update:modelValue`: Emitted when the configuration values change, with the new values as detail
|
||||
*
|
||||
* @see https://developer.mozilla.org/en-US/docs/Web/Web_Components/Using_custom_elements
|
||||
*/
|
||||
tagName: string
|
||||
|
||||
/**
|
||||
* Check if the auth mechanism is enabled
|
||||
*
|
||||
* @param authMechanism - The auth mechanism selected
|
||||
*/
|
||||
enabled(authMechanism: IAuthMechanism): boolean
|
||||
}
|
||||
|
||||
export class AuthMechanism {
|
||||
#registeredAuthMechanisms = new Map<string, IAuthConfigHandler>()
|
||||
|
||||
/**
|
||||
* Register a custom auth mechanism handler
|
||||
*
|
||||
* @param authMechanism - The auth mechanism to register
|
||||
*/
|
||||
registerHandler(authMechanism: IAuthConfigHandler) {
|
||||
if (this.#registeredAuthMechanisms.has(authMechanism.id)) {
|
||||
logger.warn(`Auth mechanism handler with id '${authMechanism.id}' is already registered`)
|
||||
}
|
||||
this.#registeredAuthMechanisms.set(authMechanism.id, authMechanism)
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the handler for a given auth mechanism
|
||||
*
|
||||
* @param authMechanism - The auth mechanism to get the handler for
|
||||
*/
|
||||
getHandler(authMechanism: IAuthMechanism): IAuthConfigHandler | undefined {
|
||||
return this.#registeredAuthMechanisms.values().find((handler) => handler.enabled(authMechanism))
|
||||
}
|
||||
}
|
||||
@@ -6,16 +6,32 @@
|
||||
import type { AxiosResponse } from '@nextcloud/axios'
|
||||
import type { ContentsWithRoot } from '@nextcloud/files'
|
||||
import type { OCSResponse } from '@nextcloud/typings/ocs'
|
||||
import type { IStorage } from '../types.ts'
|
||||
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { generateOcsUrl, generateRemoteUrl, generateUrl } from '@nextcloud/router'
|
||||
import { StorageStatus } from '../types.ts'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils.ts'
|
||||
|
||||
export const rootPath = `/files/${getCurrentUser()?.uid}`
|
||||
|
||||
export type StorageConfig = {
|
||||
applicableUsers?: string[]
|
||||
applicableGroups?: string[]
|
||||
authMechanism: string
|
||||
backend: string
|
||||
backendOptions: Record<string, string>
|
||||
can_edit: boolean
|
||||
id: number
|
||||
mountOptions?: Record<string, string>
|
||||
mountPoint: string
|
||||
priority: number
|
||||
status: number
|
||||
statusMessage: string
|
||||
type: 'system' | 'user'
|
||||
userProvided: boolean
|
||||
}
|
||||
|
||||
/**
|
||||
* https://github.com/nextcloud/server/blob/ac2bc2384efe3c15ff987b87a7432bc60d545c67/apps/files_external/lib/Controller/ApiController.php#L71-L97
|
||||
*/
|
||||
@@ -28,13 +44,12 @@ export type MountEntry = {
|
||||
permissions: number
|
||||
id: number
|
||||
class: string
|
||||
config: IStorage
|
||||
config: StorageConfig
|
||||
}
|
||||
|
||||
/**
|
||||
* Convert an OCS api result (mount entry) to a Folder instance
|
||||
*
|
||||
* @param ocsEntry - The OCS mount entry
|
||||
* @param ocsEntry
|
||||
*/
|
||||
function entryToFolder(ocsEntry: MountEntry): Folder {
|
||||
const path = (ocsEntry.path + '/' + ocsEntry.name).replace(/^\//gm, '')
|
||||
@@ -43,7 +58,7 @@ function entryToFolder(ocsEntry: MountEntry): Folder {
|
||||
source: generateRemoteUrl('dav' + rootPath + '/' + path),
|
||||
root: rootPath,
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
permissions: ocsEntry.config.status !== StorageStatus.Success
|
||||
permissions: ocsEntry.config.status !== STORAGE_STATUS.SUCCESS
|
||||
? Permission.NONE
|
||||
: ocsEntry?.permissions || Permission.READ,
|
||||
attributes: {
|
||||
@@ -54,7 +69,7 @@ function entryToFolder(ocsEntry: MountEntry): Folder {
|
||||
}
|
||||
|
||||
/**
|
||||
* Fetch the contents of external storage mounts
|
||||
*
|
||||
*/
|
||||
export async function getContents(): Promise<ContentsWithRoot> {
|
||||
const response = await axios.get(generateOcsUrl('apps/files_external/api/v1/mounts')) as AxiosResponse<OCSResponse<MountEntry[]>>
|
||||
@@ -73,12 +88,11 @@ export async function getContents(): Promise<ContentsWithRoot> {
|
||||
}
|
||||
|
||||
/**
|
||||
* Get the status of an external storage mount
|
||||
*
|
||||
* @param id - The storage ID
|
||||
* @param global - Whether the storage is global or user specific
|
||||
* @param id
|
||||
* @param global
|
||||
*/
|
||||
export function getStatus(id: number, global = true) {
|
||||
const type = global ? 'userglobalstorages' : 'userstorages'
|
||||
return axios.get(generateUrl(`apps/files_external/${type}/${id}?testOnly=false`)) as Promise<AxiosResponse<IStorage>>
|
||||
return axios.get(generateUrl(`apps/files_external/${type}/${id}?testOnly=false`)) as Promise<AxiosResponse<StorageConfig>>
|
||||
}
|
||||
|
||||
@@ -1,14 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { createPinia } from 'pinia'
|
||||
import { createApp } from 'vue'
|
||||
import FilesExternalApp from './views/FilesExternalSettings.vue'
|
||||
|
||||
const pinia = createPinia()
|
||||
const app = createApp(FilesExternalApp)
|
||||
app.config.idPrefix = 'files-external'
|
||||
app.use(pinia)
|
||||
app.mount('#files-external')
|
||||
File diff suppressed because it is too large
Load Diff
@@ -1,152 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IStorage } from '../types.d.ts'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { defineStore } from 'pinia'
|
||||
import { ref, toRaw } from 'vue'
|
||||
|
||||
const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings')
|
||||
|
||||
export const useStorages = defineStore('files_external--storages', () => {
|
||||
const globalStorages = ref<IStorage[]>([])
|
||||
const userStorages = ref<IStorage[]>([])
|
||||
|
||||
/**
|
||||
* Create a new global storage
|
||||
*
|
||||
* @param storage - The storage to create
|
||||
*/
|
||||
async function createGlobalStorage(storage: Partial<IStorage>) {
|
||||
const url = generateUrl('apps/files_external/globalstorages')
|
||||
const { data } = await axios.post<IStorage>(
|
||||
url,
|
||||
toRaw(storage),
|
||||
{ confirmPassword: PwdConfirmationMode.Strict },
|
||||
)
|
||||
globalStorages.value.push(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Create a new global storage
|
||||
*
|
||||
* @param storage - The storage to create
|
||||
*/
|
||||
async function createUserStorage(storage: Partial<IStorage>) {
|
||||
const url = generateUrl('apps/files_external/userstorages')
|
||||
const { data } = await axios.post<IStorage>(
|
||||
url,
|
||||
toRaw(storage),
|
||||
{ confirmPassword: PwdConfirmationMode.Strict },
|
||||
)
|
||||
userStorages.value.push(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Delete a storage
|
||||
*
|
||||
* @param storage - The storage to delete
|
||||
*/
|
||||
async function deleteStorage(storage: IStorage) {
|
||||
await axios.delete(getUrl(storage), {
|
||||
confirmPassword: PwdConfirmationMode.Strict,
|
||||
})
|
||||
|
||||
if (storage.type === 'personal') {
|
||||
userStorages.value = userStorages.value.filter((s) => s.id !== storage.id)
|
||||
} else {
|
||||
globalStorages.value = globalStorages.value.filter((s) => s.id !== storage.id)
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* Update an existing storage
|
||||
*
|
||||
* @param storage - The storage to update
|
||||
*/
|
||||
async function updateStorage(storage: IStorage) {
|
||||
const { data } = await axios.put(
|
||||
getUrl(storage),
|
||||
toRaw(storage),
|
||||
{ confirmPassword: PwdConfirmationMode.Strict },
|
||||
)
|
||||
|
||||
overrideStorage(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Reload a storage from the server
|
||||
*
|
||||
* @param storage - The storage to reload
|
||||
*/
|
||||
async function reloadStorage(storage: IStorage) {
|
||||
const { data } = await axios.get(getUrl(storage))
|
||||
overrideStorage(data)
|
||||
}
|
||||
|
||||
// initialize the store
|
||||
initialize()
|
||||
|
||||
return {
|
||||
globalStorages,
|
||||
userStorages,
|
||||
|
||||
createGlobalStorage,
|
||||
createUserStorage,
|
||||
deleteStorage,
|
||||
reloadStorage,
|
||||
updateStorage,
|
||||
}
|
||||
|
||||
/**
|
||||
* @param type - The type of storages to load
|
||||
*/
|
||||
async function loadStorages(type: string) {
|
||||
const url = `apps/files_external/${type}`
|
||||
const { data } = await axios.get<Record<number, IStorage>>(generateUrl(url))
|
||||
return Object.values(data)
|
||||
}
|
||||
|
||||
/**
|
||||
* Load the storages based on the user role
|
||||
*/
|
||||
async function initialize() {
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
if (isAdmin) {
|
||||
globalStorages.value = await loadStorages('globalstorages')
|
||||
} else {
|
||||
userStorages.value = await loadStorages('userstorages')
|
||||
globalStorages.value = await loadStorages('userglobalstorages')
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* @param storage - The storage to get the URL for
|
||||
*/
|
||||
function getUrl(storage: IStorage) {
|
||||
const type = storage.type === 'personal' ? 'userstorages' : 'globalstorages'
|
||||
return generateUrl(`apps/files_external/${type}/${storage.id}`)
|
||||
}
|
||||
|
||||
/**
|
||||
* Override a storage in the store
|
||||
*
|
||||
* @param storage - The storage save
|
||||
*/
|
||||
function overrideStorage(storage: IStorage) {
|
||||
if (storage.type === 'personal') {
|
||||
const index = userStorages.value.findIndex((s) => s.id === storage.id)
|
||||
userStorages.value.splice(index, 1, storage)
|
||||
} else {
|
||||
const index = globalStorages.value.findIndex((s) => s.id === storage.id)
|
||||
globalStorages.value.splice(index, 1, storage)
|
||||
}
|
||||
}
|
||||
})
|
||||
@@ -1,152 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { mdiCheckNetworkOutline, mdiCloseNetworkOutline, mdiHelpNetworkOutline, mdiNetworkOffOutline, mdiNetworkOutline } from '@mdi/js'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
|
||||
export const Visibility = Object.freeze({
|
||||
None: 0,
|
||||
Personal: 1,
|
||||
Admin: 2,
|
||||
Default: 3,
|
||||
})
|
||||
|
||||
export const ConfigurationType = Object.freeze({
|
||||
String: 0,
|
||||
Boolean: 1,
|
||||
Password: 2,
|
||||
})
|
||||
|
||||
export const ConfigurationFlag = Object.freeze({
|
||||
None: 0,
|
||||
Optional: 1,
|
||||
UserProvided: 2,
|
||||
Hidden: 4,
|
||||
})
|
||||
|
||||
export const StorageStatus = Object.freeze({
|
||||
Success: 0,
|
||||
Error: 1,
|
||||
Indeterminate: 2,
|
||||
IncompleteConf: 3,
|
||||
Unauthorized: 4,
|
||||
Timeout: 5,
|
||||
NetworkError: 6,
|
||||
})
|
||||
|
||||
export const MountOptionsCheckFilesystem = Object.freeze({
|
||||
/**
|
||||
* Never check the underlying filesystem for updates
|
||||
*/
|
||||
Never: 0,
|
||||
|
||||
/**
|
||||
* check the underlying filesystem for updates once every request for each file
|
||||
*/
|
||||
OncePerRequest: 1,
|
||||
|
||||
/**
|
||||
* Always check the underlying filesystem for updates
|
||||
*/
|
||||
Always: 2,
|
||||
})
|
||||
|
||||
export const StorageStatusIcons = Object.freeze({
|
||||
[StorageStatus.Success]: mdiCheckNetworkOutline,
|
||||
[StorageStatus.Error]: mdiCloseNetworkOutline,
|
||||
[StorageStatus.Indeterminate]: mdiNetworkOutline,
|
||||
[StorageStatus.IncompleteConf]: mdiHelpNetworkOutline,
|
||||
[StorageStatus.Unauthorized]: mdiCloseNetworkOutline,
|
||||
[StorageStatus.Timeout]: mdiNetworkOffOutline,
|
||||
[StorageStatus.NetworkError]: mdiNetworkOffOutline,
|
||||
})
|
||||
|
||||
export const StorageStatusMessage = Object.freeze({
|
||||
[StorageStatus.Success]: t('files_external', 'Connected'),
|
||||
[StorageStatus.Error]: t('files_external', 'Error'),
|
||||
[StorageStatus.Indeterminate]: t('files_external', 'Indeterminate'),
|
||||
[StorageStatus.IncompleteConf]: t('files_external', 'Incomplete configuration'),
|
||||
[StorageStatus.Unauthorized]: t('files_external', 'Unauthorized'),
|
||||
[StorageStatus.Timeout]: t('files_external', 'Timeout'),
|
||||
[StorageStatus.NetworkError]: t('files_external', 'Network error'),
|
||||
})
|
||||
|
||||
export interface IConfigurationOption {
|
||||
/**
|
||||
* Bitmask of ConfigurationFlag
|
||||
*
|
||||
* @see ConfigurationFlag
|
||||
*/
|
||||
flags: number
|
||||
/**
|
||||
* Type of the configuration option
|
||||
*
|
||||
* @see ConfigurationType
|
||||
*/
|
||||
type: typeof ConfigurationType[keyof typeof ConfigurationType]
|
||||
/**
|
||||
* Visible name of the configuration option
|
||||
*/
|
||||
value: string
|
||||
/**
|
||||
* Optional tooltip for the configuration option
|
||||
*/
|
||||
tooltip?: string
|
||||
}
|
||||
|
||||
export interface IAuthMechanism {
|
||||
name: string
|
||||
identifier: string
|
||||
identifierAliases: string[]
|
||||
scheme: string
|
||||
/**
|
||||
* The visibility of this auth mechanism
|
||||
*
|
||||
* @see Visibility
|
||||
*/
|
||||
visibility: number
|
||||
configuration: Record<string, IConfigurationOption>
|
||||
}
|
||||
|
||||
export interface IBackend {
|
||||
name: string
|
||||
identifier: string
|
||||
identifierAliases: string[]
|
||||
authSchemes: Record<string, boolean>
|
||||
priority: number
|
||||
configuration: Record<string, IConfigurationOption>
|
||||
}
|
||||
|
||||
export interface IMountOptions {
|
||||
encrypt: boolean
|
||||
previews: boolean
|
||||
enable_sharing: boolean
|
||||
/**
|
||||
* @see MountOptionsCheckFilesystem
|
||||
*/
|
||||
filesystem_check_changes: typeof MountOptionsCheckFilesystem[keyof typeof MountOptionsCheckFilesystem]
|
||||
encoding_compatibility: boolean
|
||||
readonly: boolean
|
||||
}
|
||||
|
||||
export interface IStorage {
|
||||
id?: number
|
||||
|
||||
mountPoint: string
|
||||
backend: string
|
||||
authMechanism: string
|
||||
backendOptions: Record<string, string | boolean>
|
||||
priority?: number
|
||||
applicableUsers?: string[]
|
||||
applicableGroups?: string[]
|
||||
mountOptions?: Record<string, unknown>
|
||||
/**
|
||||
* @see StorageStatus
|
||||
*/
|
||||
status?: typeof StorageStatus[keyof typeof StorageStatus]
|
||||
statusMessage?: string
|
||||
userProvided: boolean
|
||||
type: 'personal' | 'system'
|
||||
}
|
||||
@@ -1,20 +1,27 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { IStorage } from '../types.ts'
|
||||
import type { StorageConfig } from '../services/externalStorage.ts'
|
||||
|
||||
import { StorageStatus } from '../types.ts'
|
||||
// @see https://github.com/nextcloud/server/blob/ac2bc2384efe3c15ff987b87a7432bc60d545c67/lib/public/Files/StorageNotAvailableException.php#L41
|
||||
export enum STORAGE_STATUS {
|
||||
SUCCESS = 0,
|
||||
ERROR = 1,
|
||||
INDETERMINATE = 2,
|
||||
INCOMPLETE_CONF = 3,
|
||||
UNAUTHORIZED = 4,
|
||||
TIMEOUT = 5,
|
||||
NETWORK_ERROR = 6,
|
||||
}
|
||||
|
||||
/**
|
||||
* Check if the given storage configuration is missing authentication configuration
|
||||
*
|
||||
* @param config - The storage configuration to check
|
||||
* @param config
|
||||
*/
|
||||
export function isMissingAuthConfig(config: IStorage) {
|
||||
export function isMissingAuthConfig(config: StorageConfig) {
|
||||
// If we don't know the status, assume it is ok
|
||||
if (config.status === undefined || config.status === StorageStatus.Success) {
|
||||
if (!config.status || config.status === STORAGE_STATUS.SUCCESS) {
|
||||
return false
|
||||
}
|
||||
|
||||
|
||||
@@ -1,19 +1,17 @@
|
||||
/*!
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2023 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import type { INode } from '@nextcloud/files'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { MountEntry } from '../services/externalStorage.ts'
|
||||
|
||||
import { FileType } from '@nextcloud/files'
|
||||
|
||||
/**
|
||||
* Check if the given node represents an external storage mount
|
||||
*
|
||||
* @param node - The node to check
|
||||
* @param node
|
||||
*/
|
||||
export function isNodeExternalStorage(node: INode) {
|
||||
export function isNodeExternalStorage(node: Node) {
|
||||
// Not a folder, not a storage
|
||||
if (node.type === FileType.File) {
|
||||
return false
|
||||
|
||||
@@ -1,10 +0,0 @@
|
||||
/*!
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
import { getLoggerBuilder } from '@nextcloud/logger'
|
||||
|
||||
export default getLoggerBuilder()
|
||||
.setApp('files_external')
|
||||
.build()
|
||||
@@ -1,77 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import type { IAuthMechanism } from '../types.ts'
|
||||
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError } from '@nextcloud/dialogs'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { ref, watch } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcSelect from '@nextcloud/vue/components/NcSelect'
|
||||
import ConfigurationEntry from '../components/AddExternalStorageDialog/ConfigurationEntry.vue'
|
||||
import { ConfigurationFlag } from '../types.ts'
|
||||
import logger from '../utils/logger.ts'
|
||||
|
||||
const modelValue = defineModel<Record<string, string | boolean>>({ required: true })
|
||||
|
||||
defineProps<{
|
||||
authMechanism: IAuthMechanism
|
||||
}>()
|
||||
|
||||
const keySize = ref<number>()
|
||||
watch(keySize, () => {
|
||||
if (keySize.value) {
|
||||
modelValue.value.private_key = ''
|
||||
modelValue.value.public_key = ''
|
||||
}
|
||||
})
|
||||
|
||||
/**
|
||||
* Generates a new RSA key pair and fills the corresponding configuration entries.
|
||||
*/
|
||||
async function generateKeys() {
|
||||
try {
|
||||
// fallback to server-side key generation
|
||||
const { data } = await axios.post(generateUrl('/apps/files_external/ajax/public_key.php'), {
|
||||
keyLength: keySize.value,
|
||||
})
|
||||
|
||||
modelValue.value.private_key = data.data.private_key
|
||||
modelValue.value.public_key = data.data.public_key
|
||||
} catch (error) {
|
||||
logger.error('Error generating RSA key pair', { error })
|
||||
showError(t('files_external', 'Error generating key pair'))
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<div>
|
||||
<ConfigurationEntry
|
||||
v-for="configOption, configKey in authMechanism.configuration"
|
||||
v-show="!(configOption.flags & ConfigurationFlag.Hidden)"
|
||||
:key="configOption.value"
|
||||
v-model="modelValue[configKey]!"
|
||||
:config-key="configKey"
|
||||
:config-option="configOption" />
|
||||
|
||||
<NcSelect
|
||||
v-model="keySize"
|
||||
:clearable="false"
|
||||
:input-label="t('files_external', 'Key size')"
|
||||
:options="[1024, 2048, 4096]"
|
||||
required />
|
||||
|
||||
<NcButton
|
||||
:disabled="!keySize"
|
||||
wide
|
||||
@click="generateKeys">
|
||||
{{ t('files_external', 'Generate keys') }}
|
||||
</NcButton>
|
||||
</div>
|
||||
</template>
|
||||
@@ -3,28 +3,6 @@
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { ref } from 'vue'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
|
||||
defineEmits<{
|
||||
close: [payload?: { login: string, password: string }]
|
||||
}>()
|
||||
|
||||
const login = ref('')
|
||||
const password = ref('')
|
||||
|
||||
const dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{
|
||||
label: t('files_external', 'Confirm'),
|
||||
type: 'submit',
|
||||
variant: 'primary',
|
||||
}]
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcDialog
|
||||
:buttons="dialogButtons"
|
||||
@@ -44,8 +22,8 @@ const dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{
|
||||
|
||||
<!-- Login -->
|
||||
<NcTextField
|
||||
ref="login"
|
||||
v-model="login"
|
||||
autofocus
|
||||
class="external-storage-auth__login"
|
||||
data-cy-external-storage-auth-dialog-login
|
||||
:label="t('files_external', 'Login')"
|
||||
@@ -56,6 +34,7 @@ const dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{
|
||||
|
||||
<!-- Password -->
|
||||
<NcPasswordField
|
||||
ref="password"
|
||||
v-model="password"
|
||||
class="external-storage-auth__password"
|
||||
data-cy-external-storage-auth-dialog-password
|
||||
@@ -65,3 +44,46 @@ const dialogButtons: InstanceType<typeof NcDialog>['buttons'] = [{
|
||||
required />
|
||||
</NcDialog>
|
||||
</template>
|
||||
|
||||
<script lang="ts">
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { defineComponent } from 'vue'
|
||||
import NcDialog from '@nextcloud/vue/components/NcDialog'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
|
||||
export default defineComponent({
|
||||
name: 'CredentialsDialog',
|
||||
|
||||
components: {
|
||||
NcDialog,
|
||||
NcNoteCard,
|
||||
NcTextField,
|
||||
NcPasswordField,
|
||||
},
|
||||
|
||||
setup() {
|
||||
return {
|
||||
t,
|
||||
}
|
||||
},
|
||||
|
||||
data() {
|
||||
return {
|
||||
login: '',
|
||||
password: '',
|
||||
}
|
||||
},
|
||||
|
||||
computed: {
|
||||
dialogButtons() {
|
||||
return [{
|
||||
label: t('files_external', 'Confirm'),
|
||||
type: 'submit',
|
||||
variant: 'primary',
|
||||
}]
|
||||
},
|
||||
},
|
||||
})
|
||||
</script>
|
||||
|
||||
@@ -1,154 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import type { IStorage } from '../types.ts'
|
||||
|
||||
import { mdiPlus } from '@mdi/js'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { n, t } from '@nextcloud/l10n'
|
||||
import { ref } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcEmptyContent from '@nextcloud/vue/components/NcEmptyContent'
|
||||
import NcIconSvgWrapper from '@nextcloud/vue/components/NcIconSvgWrapper'
|
||||
import NcNoteCard from '@nextcloud/vue/components/NcNoteCard'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import AddExternalStorageDialog from '../components/AddExternalStorageDialog/AddExternalStorageDialog.vue'
|
||||
import ExternalStorageTable from '../components/ExternalStorageTable.vue'
|
||||
import UserMountSettings from '../components/UserMountSettings.vue'
|
||||
import filesExternalSvg from '../../img/app-dark.svg?raw'
|
||||
import { useStorages } from '../store/storages.ts'
|
||||
import logger from '../utils/logger.ts'
|
||||
|
||||
const settings = loadState('files_external', 'settings', {
|
||||
docUrl: '',
|
||||
dependencyIssues: {
|
||||
messages: null as string[] | null,
|
||||
modules: null as Record<string, string[]> | null,
|
||||
},
|
||||
isAdmin: false,
|
||||
})
|
||||
|
||||
const store = useStorages()
|
||||
|
||||
/** List of dependency issue messages */
|
||||
const dependencyIssues = settings.dependencyIssues?.messages ?? []
|
||||
/** Map of missing modules -> list of dependant backends */
|
||||
const missingModules = settings.dependencyIssues?.modules ?? {}
|
||||
|
||||
const showDialog = ref(false)
|
||||
const newStorage = ref<Partial<IStorage>>()
|
||||
|
||||
/**
|
||||
* Add a new external storage
|
||||
*
|
||||
* @param storage - The storage to add
|
||||
*/
|
||||
async function addStorage(storage?: Partial<IStorage>) {
|
||||
showDialog.value = false
|
||||
if (!storage) {
|
||||
return
|
||||
}
|
||||
|
||||
try {
|
||||
if (settings.isAdmin) {
|
||||
await store.createGlobalStorage(storage)
|
||||
} else {
|
||||
await store.createUserStorage(storage)
|
||||
}
|
||||
newStorage.value = undefined
|
||||
} catch (error) {
|
||||
logger.error('Failed to add external storage', { error })
|
||||
showDialog.value = true
|
||||
}
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcSettingsSection
|
||||
:doc-url="settings.docUrl"
|
||||
:name="t('files_external', 'External storage')"
|
||||
:description="
|
||||
t('files_external', 'External storage enables you to mount external storage services and devices as secondary Nextcloud storage devices.')
|
||||
+ (settings.isAdmin
|
||||
? ' ' + t('files_external', 'You may also allow people to mount their own external storage services.')
|
||||
: ''
|
||||
)">
|
||||
<!-- Dependency error messages -->
|
||||
<NcNoteCard
|
||||
v-for="message, index of dependencyIssues"
|
||||
:key="index"
|
||||
type="error">
|
||||
{{ message }}
|
||||
</NcNoteCard>
|
||||
|
||||
<!-- Missing modules for backends -->
|
||||
<NcNoteCard
|
||||
v-for="(dependants, module) in missingModules"
|
||||
:key="module"
|
||||
type="warning">
|
||||
<p>
|
||||
<template v-if="module === 'curl'">
|
||||
{{ t('files_external', 'The cURL support in PHP is not enabled or installed.') }}
|
||||
</template>
|
||||
<template v-else-if="module === 'ftp'">
|
||||
{{ t('files_external', 'The FTP support in PHP is not enabled or installed.') }}
|
||||
</template>
|
||||
<template v-else>
|
||||
{{ t('files_external', '{module} is not installed.', { module }) }}
|
||||
</template>
|
||||
{{ n(
|
||||
'files_external',
|
||||
'Please ask your system administrator to install it as otherwise mounting the following backend is not possible:',
|
||||
'Please ask your system administrator to install it as otherwise mounting the following backends is not possible:',
|
||||
dependants.length,
|
||||
) }}
|
||||
</p>
|
||||
<ul :class="$style.externalStoragesSection__dependantList" :aria-label="t('files_external', 'Dependant backends')">
|
||||
<li v-for="backend of dependants" :key="backend">
|
||||
{{ backend }}
|
||||
</li>
|
||||
</ul>
|
||||
</NcNoteCard>
|
||||
|
||||
<!-- For user settings if the user has no permission or for user and admin settings if no storage was configured -->
|
||||
<NcEmptyContent
|
||||
v-if="false"
|
||||
:description="t('files_external', 'No external storage configured or you do not have the permission to configure them')">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :svg="filesExternalSvg" :size="64" />
|
||||
</template>
|
||||
</NcEmptyContent>
|
||||
|
||||
<ExternalStorageTable />
|
||||
|
||||
<NcButton
|
||||
:class="$style.externalStoragesSection__newStorageButton"
|
||||
variant="primary"
|
||||
@click="showDialog = !showDialog">
|
||||
<template #icon>
|
||||
<NcIconSvgWrapper :path="mdiPlus" />
|
||||
</template>
|
||||
{{ t('files_external', 'Add external storage') }}
|
||||
</NcButton>
|
||||
|
||||
<AddExternalStorageDialog
|
||||
v-model="newStorage"
|
||||
v-model:open="showDialog"
|
||||
@close="addStorage" />
|
||||
|
||||
<UserMountSettings v-if="settings.isAdmin" />
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.externalStoragesSection__dependantList {
|
||||
list-style: disc !important;
|
||||
margin-inline-start: calc(var(--default-clickable-area) / 2);
|
||||
}
|
||||
|
||||
.externalStoragesSection__newStorageButton {
|
||||
margin-top: var(--default-clickable-area);
|
||||
}
|
||||
</style>
|
||||
@@ -1,13 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
<script setup lang="ts">
|
||||
import ExternalStoragesSection from './ExternalStoragesSection.vue'
|
||||
import GlobalCredentialsSection from './GlobalCredentialsSection.vue'
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<ExternalStoragesSection />
|
||||
<GlobalCredentialsSection />
|
||||
</template>
|
||||
@@ -1,103 +0,0 @@
|
||||
<!--
|
||||
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
- SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
-->
|
||||
|
||||
<script setup lang="ts">
|
||||
import axios from '@nextcloud/axios'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import { t } from '@nextcloud/l10n'
|
||||
import { addPasswordConfirmationInterceptors, PwdConfirmationMode } from '@nextcloud/password-confirmation'
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { ref } from 'vue'
|
||||
import NcButton from '@nextcloud/vue/components/NcButton'
|
||||
import NcPasswordField from '@nextcloud/vue/components/NcPasswordField'
|
||||
import NcSettingsSection from '@nextcloud/vue/components/NcSettingsSection'
|
||||
import NcTextField from '@nextcloud/vue/components/NcTextField'
|
||||
import logger from '../utils/logger.ts'
|
||||
|
||||
const globalCredentials = loadState<{
|
||||
uid: string
|
||||
user: string
|
||||
password: string
|
||||
}>('files_external', 'global-credentials')
|
||||
|
||||
const loading = ref(false)
|
||||
const username = ref(globalCredentials.user)
|
||||
const password = ref(globalCredentials.password)
|
||||
|
||||
addPasswordConfirmationInterceptors(axios)
|
||||
|
||||
/**
|
||||
* Submit the global credentials form
|
||||
*/
|
||||
async function onSubmit() {
|
||||
try {
|
||||
loading.value = true
|
||||
const { data } = await axios.post<boolean>(generateUrl('apps/files_external/globalcredentials'), {
|
||||
// This is the UID of the user to save the credentials (admins can set that also for other users)
|
||||
uid: globalCredentials.uid,
|
||||
user: username.value,
|
||||
password: password.value,
|
||||
}, { confirmPassword: PwdConfirmationMode.Strict })
|
||||
if (data) {
|
||||
showSuccess(t('files_external', 'Global credentials saved'))
|
||||
return
|
||||
}
|
||||
} catch (e) {
|
||||
logger.error(e as Error)
|
||||
// Error is handled below
|
||||
} finally {
|
||||
loading.value = false
|
||||
}
|
||||
// result was false so show an error
|
||||
showError(t('files_external', 'Could not save global credentials'))
|
||||
username.value = globalCredentials.user
|
||||
password.value = globalCredentials.password
|
||||
}
|
||||
</script>
|
||||
|
||||
<template>
|
||||
<NcSettingsSection
|
||||
:name="t('files_external', 'Global credentials')"
|
||||
:description="t('files_external', 'Global credentials can be used to authenticate with multiple external storages that have the same credentials.')">
|
||||
<form
|
||||
id="global_credentials"
|
||||
:class="$style.globalCredentialsSectionForm"
|
||||
autocomplete="false"
|
||||
@submit.prevent="onSubmit">
|
||||
<NcTextField
|
||||
v-model="username"
|
||||
name="username"
|
||||
autocomplete="false"
|
||||
:label="t('files_external', 'Login')" />
|
||||
<NcPasswordField
|
||||
v-model="password"
|
||||
name="password"
|
||||
autocomplete="false"
|
||||
:label="t('files_external', 'Password')" />
|
||||
<NcButton
|
||||
:class="$style.globalCredentialsSectionForm__submit"
|
||||
:disabled="loading"
|
||||
variant="primary"
|
||||
type="submit">
|
||||
{{ loading ? t('files_external', 'Saving …') : t('files_external', 'Save') }}
|
||||
</NcButton>
|
||||
</form>
|
||||
</NcSettingsSection>
|
||||
</template>
|
||||
|
||||
<style module>
|
||||
.globalCredentialsSectionForm {
|
||||
max-width: 400px;
|
||||
display: flex;
|
||||
flex-direction: column;
|
||||
align-items: end;
|
||||
gap: 15px;
|
||||
}
|
||||
|
||||
.globalCredentialsSectionForm__submit {
|
||||
min-width: max(40%, 44px);
|
||||
}
|
||||
</style>
|
||||
@@ -1,7 +1,186 @@
|
||||
<?php
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc.
|
||||
* SPDX-License-Identifier: AGPL-3.0-only
|
||||
*/
|
||||
use OCA\Files_External\Lib\Auth\AuthMechanism;
|
||||
use OCA\Files_External\Lib\Backend\Backend;
|
||||
use OCA\Files_External\Service\BackendService;
|
||||
|
||||
/** @var array $_ */
|
||||
|
||||
$canCreateMounts = $_['visibilityType'] === BackendService::VISIBILITY_ADMIN || $_['allowUserMounting'];
|
||||
|
||||
$l->t('Enable encryption');
|
||||
$l->t('Enable previews');
|
||||
$l->t('Enable sharing');
|
||||
$l->t('Check for changes');
|
||||
$l->t('Never');
|
||||
$l->t('Once every direct access');
|
||||
$l->t('Read only');
|
||||
|
||||
\OCP\Util::addScript('files_external', 'settings');
|
||||
\OCP\Util::addScript('files_external', 'templates');
|
||||
style('files_external', 'settings');
|
||||
|
||||
// load custom JS
|
||||
foreach ($_['backends'] as $backend) {
|
||||
/** @var Backend $backend */
|
||||
$scripts = $backend->getCustomJs();
|
||||
foreach ($scripts as $script) {
|
||||
script('files_external', $script);
|
||||
}
|
||||
}
|
||||
foreach ($_['authMechanisms'] as $authMechanism) {
|
||||
/** @var AuthMechanism $authMechanism */
|
||||
$scripts = $authMechanism->getCustomJs();
|
||||
foreach ($scripts as $script) {
|
||||
script('files_external', $script);
|
||||
}
|
||||
}
|
||||
|
||||
?>
|
||||
<div id="files-external"></div>
|
||||
|
||||
<div class="emptyfilelist emptycontent hidden">
|
||||
<div class="icon-external"></div>
|
||||
<h2><?php p($l->t('No external storage configured or you don\'t have the permission to configure them')); ?></h2>
|
||||
</div>
|
||||
|
||||
<?php
|
||||
$canCreateNewLocalStorage = \OCP\Server::get(\OCP\IConfig::class)->getSystemValue('files_external_allow_create_new_local', true);
|
||||
?>
|
||||
<form data-can-create="<?php echo $canCreateMounts?'true':'false' ?>" data-can-create-local="<?php echo $canCreateNewLocalStorage?'true':'false' ?>" id="files_external" class="section" data-encryption-enabled="<?php echo $_['encryptionEnabled']?'true': 'false'; ?>">
|
||||
<h2 class="inlineblock" data-anchor-name="external-storage"><?php p($l->t('External storage')); ?></h2>
|
||||
<a target="_blank" rel="noreferrer" class="icon-info" title="<?php p($l->t('Open documentation'));?>" href="<?php p(link_to_docs('admin-external-storage')); ?>"></a>
|
||||
<p class="settings-hint"><?php p($l->t('External storage enables you to mount external storage services and devices as secondary Nextcloud storage devices. You may also allow people to mount their own external storage services.')); ?></p>
|
||||
<?php if (isset($_['dependencies']) && ($_['dependencies'] !== '') && $canCreateMounts) {
|
||||
print_unescaped('' . $_['dependencies'] . '');
|
||||
} ?>
|
||||
<table id="externalStorage" class="grid" data-admin='<?php print_unescaped(json_encode($_['visibilityType'] === BackendService::VISIBILITY_ADMIN)); ?>'>
|
||||
<thead>
|
||||
<tr>
|
||||
<th></th>
|
||||
<th><?php p($l->t('Folder name')); ?></th>
|
||||
<th><?php p($l->t('External storage')); ?></th>
|
||||
<th><?php p($l->t('Authentication')); ?></th>
|
||||
<th><?php p($l->t('Configuration')); ?></th>
|
||||
<?php if ($_['visibilityType'] === BackendService::VISIBILITY_ADMIN) {
|
||||
print_unescaped('<th>' . $l->t('Available for') . '</th>');
|
||||
} ?>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
<th> </th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
<tr class="externalStorageLoading">
|
||||
<td colspan="8">
|
||||
<span id="externalStorageLoading" class="icon icon-loading"></span>
|
||||
</td>
|
||||
</tr>
|
||||
<tr id="addMountPoint"
|
||||
<?php if (!$canCreateMounts): ?>
|
||||
style="display: none;"
|
||||
<?php endif; ?>
|
||||
>
|
||||
<td class="status">
|
||||
<span data-placement="right" title="<?php p($l->t('Click to recheck the configuration')); ?>" style="display: none;"></span>
|
||||
</td>
|
||||
<td class="mountPoint"><input type="text" name="mountPoint" value=""
|
||||
placeholder="<?php p($l->t('Folder name')); ?>">
|
||||
</td>
|
||||
<td class="backend">
|
||||
<select id="selectBackend" class="selectBackend" data-configurations='<?php p(json_encode($_['backends'])); ?>'>
|
||||
<option value="" disabled selected
|
||||
style="display:none;">
|
||||
<?php p($l->t('Add storage')); ?>
|
||||
</option>
|
||||
<?php
|
||||
$sortedBackends = array_filter($_['backends'], function ($backend) use ($_) {
|
||||
return $backend->isVisibleFor($_['visibilityType']);
|
||||
});
|
||||
uasort($sortedBackends, function ($a, $b) {
|
||||
return strcasecmp($a->getText(), $b->getText());
|
||||
});
|
||||
?>
|
||||
<?php foreach ($sortedBackends as $backend): ?>
|
||||
<?php if ($backend->getDeprecateTo() || (!$canCreateNewLocalStorage && $backend->getIdentifier() == 'local')) {
|
||||
continue;
|
||||
} // ignore deprecated backends?>
|
||||
<option value="<?php p($backend->getIdentifier()); ?>"><?php p($backend->getText()); ?></option>
|
||||
<?php endforeach; ?>
|
||||
</select>
|
||||
</td>
|
||||
<td class="authentication" data-mechanisms='<?php p(json_encode($_['authMechanisms'])); ?>'></td>
|
||||
<td class="configuration"></td>
|
||||
<?php if ($_['visibilityType'] === BackendService::VISIBILITY_ADMIN): ?>
|
||||
<td class="applicable" align="right">
|
||||
<label><input type="checkbox" class="applicableToAllUsers" checked="" /><?php p($l->t('All people')); ?></label>
|
||||
<div class="applicableUsersContainer">
|
||||
<input type="hidden" class="applicableUsers" style="width:20em;" value="" />
|
||||
</div>
|
||||
</td>
|
||||
<?php endif; ?>
|
||||
<td class="mountOptionsToggle hidden">
|
||||
<button type="button" class="icon-more" aria-expanded="false" title="<?php p($l->t('Advanced settings')); ?>"></button>
|
||||
<input type="hidden" class="mountOptions" value="" />
|
||||
</td>
|
||||
<td class="save hidden">
|
||||
<button type="button" class="icon-checkmark" title="<?php p($l->t('Save')); ?>"></button>
|
||||
</td>
|
||||
</tr>
|
||||
</tbody>
|
||||
</table>
|
||||
|
||||
<?php if ($_['visibilityType'] === BackendService::VISIBILITY_ADMIN): ?>
|
||||
<input type="checkbox" name="allowUserMounting" id="allowUserMounting" class="checkbox"
|
||||
value="1" <?php if ($_['allowUserMounting']) {
|
||||
print_unescaped(' checked="checked"');
|
||||
} ?> />
|
||||
<label for="allowUserMounting"><?php p($l->t('Allow people to mount external storage')); ?></label> <span id="userMountingMsg" class="msg"></span>
|
||||
|
||||
<p id="userMountingBackends"<?php if (!$_['allowUserMounting']): ?> class="hidden"<?php endif; ?>>
|
||||
<?php
|
||||
$userBackends = array_filter($_['backends'], function ($backend) {
|
||||
return $backend->isAllowedVisibleFor(BackendService::VISIBILITY_PERSONAL);
|
||||
});
|
||||
?>
|
||||
<?php $i = 0;
|
||||
foreach ($userBackends as $backend): ?>
|
||||
<?php if ($deprecateTo = $backend->getDeprecateTo()): ?>
|
||||
<input type="hidden" id="allowUserMountingBackends<?php p($i); ?>" name="allowUserMountingBackends[]" value="<?php p($backend->getIdentifier()); ?>" data-deprecate-to="<?php p($deprecateTo->getIdentifier()); ?>" />
|
||||
<?php else: ?>
|
||||
<input type="checkbox" id="allowUserMountingBackends<?php p($i); ?>" class="checkbox" name="allowUserMountingBackends[]" value="<?php p($backend->getIdentifier()); ?>" <?php if ($backend->isVisibleFor(BackendService::VISIBILITY_PERSONAL)) {
|
||||
print_unescaped(' checked="checked"');
|
||||
} ?> />
|
||||
<label for="allowUserMountingBackends<?php p($i); ?>"><?php p($backend->getText()); ?></label> <br />
|
||||
<?php endif; ?>
|
||||
<?php $i++; ?>
|
||||
<?php endforeach; ?>
|
||||
</p>
|
||||
<?php endif; ?>
|
||||
</form>
|
||||
|
||||
<div class="followupsection">
|
||||
<form autocomplete="false" action="#"
|
||||
id="global_credentials" method="post"
|
||||
class="<?php if (isset($_['visibilityType']) && $_['visibilityType'] === BackendService::VISIBILITY_PERSONAL) {
|
||||
print_unescaped('global_credentials__personal');
|
||||
} ?>">
|
||||
<h2><?php p($l->t('Global credentials')); ?></h2>
|
||||
<p class="settings-hint"><?php p($l->t('Global credentials can be used to authenticate with multiple external storages that have the same credentials.')); ?></p>
|
||||
<input type="text" name="username"
|
||||
autocomplete="false"
|
||||
value="<?php p($_['globalCredentials']['user']); ?>"
|
||||
placeholder="<?php p($l->t('Login')) ?>"/>
|
||||
<input type="password" name="password"
|
||||
autocomplete="false"
|
||||
value="<?php p($_['globalCredentials']['password']); ?>"
|
||||
placeholder="<?php p($l->t('Password')) ?>"/>
|
||||
<input type="hidden" name="uid"
|
||||
value="<?php p($_['globalCredentialsUid']); ?>"/>
|
||||
<input type="submit" value="<?php p($l->t('Save')) ?>"/>
|
||||
</form>
|
||||
</div>
|
||||
|
||||
@@ -1,170 +0,0 @@
|
||||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
|
||||
* SPDX-License-Identifier: AGPL-3.0-or-later
|
||||
*/
|
||||
|
||||
namespace OCA\Files_External\Tests;
|
||||
|
||||
use OCA\Files_External\Lib\ApplicableHelper;
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
use OCP\IGroup;
|
||||
use OCP\IGroupManager;
|
||||
use OCP\IUser;
|
||||
use OCP\IUserManager;
|
||||
use PHPUnit\Framework\Attributes\DataProvider;
|
||||
use PHPUnit\Framework\MockObject\MockObject;
|
||||
use Test\TestCase;
|
||||
|
||||
class ApplicableHelperTest extends TestCase {
|
||||
private IUserManager|MockObject $userManager;
|
||||
private IGroupManager|MockObject $groupManager;
|
||||
|
||||
/** @var list<string> */
|
||||
private array $users = [];
|
||||
/** @var array<string, list<string>> */
|
||||
private array $groups = [];
|
||||
|
||||
private ApplicableHelper $applicableHelper;
|
||||
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
|
||||
$this->userManager = $this->createMock(IUserManager::class);
|
||||
$this->groupManager = $this->createMock(IGroupManager::class);
|
||||
|
||||
$this->userManager->method('get')
|
||||
->willReturnCallback(function (string $id) {
|
||||
$user = $this->createMock(IUser::class);
|
||||
$user->method('getUID')->willReturn($id);
|
||||
return $user;
|
||||
});
|
||||
$this->userManager->method('getSeenUsers')
|
||||
->willReturnCallback(fn () => new \ArrayIterator(array_map($this->userManager->get(...), $this->users)));
|
||||
$this->groupManager->method('get')
|
||||
->willReturnCallback(function (string $id) {
|
||||
$group = $this->createMock(IGroup::class);
|
||||
$group->method('getGID')->willReturn($id);
|
||||
$group->method('getUsers')
|
||||
->willReturn(array_map($this->userManager->get(...), $this->groups[$id] ?: []));
|
||||
return $group;
|
||||
});
|
||||
$this->groupManager->method('getUserGroupIds')
|
||||
->willReturnCallback(function (IUser $user) {
|
||||
$groups = [];
|
||||
foreach ($this->groups as $group => $users) {
|
||||
if (in_array($user->getUID(), $users)) {
|
||||
$groups[] = $group;
|
||||
}
|
||||
}
|
||||
return $groups;
|
||||
});
|
||||
|
||||
$this->applicableHelper = new ApplicableHelper($this->userManager, $this->groupManager);
|
||||
|
||||
$this->users = ['user1', 'user2', 'user3', 'user4'];
|
||||
$this->groups = [
|
||||
'group1' => ['user1', 'user2'],
|
||||
'group2' => ['user3'],
|
||||
];
|
||||
}
|
||||
|
||||
public static function usersForStorageProvider(): array {
|
||||
return [
|
||||
[[], [], ['user1', 'user2', 'user3', 'user4']],
|
||||
[['user1'], [], ['user1']],
|
||||
[['user1', 'user3'], [], ['user1', 'user3']],
|
||||
[['user1'], ['group1'], ['user1', 'user2']],
|
||||
[['user1'], ['group2'], ['user1', 'user3']],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('usersForStorageProvider')]
|
||||
public function testGetUsersForStorage(array $applicableUsers, array $applicableGroups, array $expected) {
|
||||
$storage = $this->createMock(StorageConfig::class);
|
||||
$storage->method('getApplicableUsers')
|
||||
->willReturn($applicableUsers);
|
||||
$storage->method('getApplicableGroups')
|
||||
->willReturn($applicableGroups);
|
||||
|
||||
$result = iterator_to_array($this->applicableHelper->getUsersForStorage($storage));
|
||||
$result = array_map(fn (IUser $user) => $user->getUID(), $result);
|
||||
sort($result);
|
||||
sort($expected);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
|
||||
public static function applicableProvider(): array {
|
||||
return [
|
||||
[[], [], 'user1', true],
|
||||
[['user1'], [], 'user1', true],
|
||||
[['user1'], [], 'user2', false],
|
||||
[['user1', 'user3'], [], 'user1', true],
|
||||
[['user1', 'user3'], [], 'user2', false],
|
||||
[['user1'], ['group1'], 'user1', true],
|
||||
[['user1'], ['group1'], 'user2', true],
|
||||
[['user1'], ['group1'], 'user3', false],
|
||||
[['user1'], ['group1'], 'user4', false],
|
||||
[['user1'], ['group2'], 'user1', true],
|
||||
[['user1'], ['group2'], 'user2', false],
|
||||
[['user1'], ['group2'], 'user3', true],
|
||||
[['user1'], ['group1'], 'user4', false],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('applicableProvider')]
|
||||
public function testIsApplicable(array $applicableUsers, array $applicableGroups, string $user, bool $expected) {
|
||||
$storage = $this->createMock(StorageConfig::class);
|
||||
$storage->method('getApplicableUsers')
|
||||
->willReturn($applicableUsers);
|
||||
$storage->method('getApplicableGroups')
|
||||
->willReturn($applicableGroups);
|
||||
|
||||
$this->assertEquals($expected, $this->applicableHelper->isApplicableForUser($storage, $this->userManager->get($user)));
|
||||
}
|
||||
|
||||
public static function diffProvider(): array {
|
||||
return [
|
||||
[[], [], [], [], []], // both all
|
||||
[['user1'], [], [], [], []], // all added
|
||||
[[], [], ['user1'], [], ['user2', 'user3', 'user4']], // all removed
|
||||
[[], [], [], ['group1'], ['user3', 'user4']], // all removed
|
||||
[[], [], ['user3'], ['group1'], ['user4']], // all removed
|
||||
[['user1'], [], ['user1'], [], []],
|
||||
[['user1'], [], ['user1', 'user2'], [], []],
|
||||
[['user1'], [], ['user2'], [], ['user1']],
|
||||
[['user1'], [], [], ['group1'], []],
|
||||
[['user1'], [], [], ['group2'], ['user1']],
|
||||
[[], ['group1'], [], ['group2'], ['user1', 'user2']],
|
||||
[[], ['group1'], ['user1'], [], ['user2']],
|
||||
[['user1'], ['group1'], ['user1'], [], ['user2']],
|
||||
[['user1'], ['group1'], [], ['group1'], []],
|
||||
[['user1'], ['group1'], [], ['group2'], ['user1', 'user2']],
|
||||
[['user1'], ['group1'], ['user1'], ['group2'], ['user2']],
|
||||
];
|
||||
}
|
||||
|
||||
#[DataProvider('diffProvider')]
|
||||
public function testDiff(array $applicableUsersA, array $applicableGroupsA, array $applicableUsersB, array $applicableGroupsB, array $expected) {
|
||||
$storageA = $this->createMock(StorageConfig::class);
|
||||
$storageA->method('getApplicableUsers')
|
||||
->willReturn($applicableUsersA);
|
||||
$storageA->method('getApplicableGroups')
|
||||
->willReturn($applicableGroupsA);
|
||||
|
||||
$storageB = $this->createMock(StorageConfig::class);
|
||||
$storageB->method('getApplicableUsers')
|
||||
->willReturn($applicableUsersB);
|
||||
$storageB->method('getApplicableGroups')
|
||||
->willReturn($applicableGroupsB);
|
||||
|
||||
$result = iterator_to_array($this->applicableHelper->diffApplicable($storageA, $storageB));
|
||||
$result = array_map(fn (IUser $user) => $user->getUID(), $result);
|
||||
sort($result);
|
||||
sort($expected);
|
||||
$this->assertEquals($expected, $result);
|
||||
}
|
||||
}
|
||||
@@ -11,10 +11,6 @@ use OCA\Files_External\Lib\DefinitionParameter;
|
||||
use OCA\Files_External\Lib\FrontendDefinitionTrait;
|
||||
use OCA\Files_External\Lib\StorageConfig;
|
||||
|
||||
class MockFrontendDefinitionTraitClass {
|
||||
use FrontendDefinitionTrait;
|
||||
}
|
||||
|
||||
class FrontendDefinitionTraitTest extends \Test\TestCase {
|
||||
public function testJsonSerialization(): void {
|
||||
$param = $this->getMockBuilder(DefinitionParameter::class)
|
||||
@@ -22,7 +18,7 @@ class FrontendDefinitionTraitTest extends \Test\TestCase {
|
||||
->getMock();
|
||||
$param->method('getName')->willReturn('foo');
|
||||
|
||||
$trait = new MockFrontendDefinitionTraitClass();
|
||||
$trait = $this->getMockForTrait(FrontendDefinitionTrait::class);
|
||||
$trait->setText('test');
|
||||
$trait->addParameters([$param]);
|
||||
$trait->addCustomJs('foo/bar.js');
|
||||
@@ -71,7 +67,7 @@ class FrontendDefinitionTraitTest extends \Test\TestCase {
|
||||
$storageConfig->expects($this->any())
|
||||
->method('setBackendOption');
|
||||
|
||||
$trait = new MockFrontendDefinitionTraitClass();
|
||||
$trait = $this->getMockForTrait(FrontendDefinitionTrait::class);
|
||||
$trait->setText('test');
|
||||
$trait->addParameters($backendParams);
|
||||
|
||||
@@ -102,7 +98,7 @@ class FrontendDefinitionTraitTest extends \Test\TestCase {
|
||||
->method('setBackendOption')
|
||||
->with('param', 'foobar');
|
||||
|
||||
$trait = new MockFrontendDefinitionTraitClass();
|
||||
$trait = $this->getMockForTrait(FrontendDefinitionTrait::class);
|
||||
$trait->setText('test');
|
||||
$trait->addParameter($param);
|
||||
|
||||
|
||||
@@ -11,14 +11,6 @@ namespace OCA\Files_External\Tests;
|
||||
use OCA\Files_External\Lib\LegacyDependencyCheckPolyfill;
|
||||
use OCA\Files_External\Lib\MissingDependency;
|
||||
|
||||
class MockLegacyDependencyCheckPolyfillClass {
|
||||
use LegacyDependencyCheckPolyfill;
|
||||
|
||||
public function getStorageClass(): string {
|
||||
return LegacyDependencyCheckPolyfillTest::class;
|
||||
}
|
||||
}
|
||||
|
||||
class LegacyDependencyCheckPolyfillTest extends \Test\TestCase {
|
||||
|
||||
/**
|
||||
@@ -32,7 +24,10 @@ class LegacyDependencyCheckPolyfillTest extends \Test\TestCase {
|
||||
}
|
||||
|
||||
public function testCheckDependencies(): void {
|
||||
$trait = new MockLegacyDependencyCheckPolyfillClass();
|
||||
$trait = $this->getMockForTrait(LegacyDependencyCheckPolyfill::class);
|
||||
$trait->expects($this->once())
|
||||
->method('getStorageClass')
|
||||
->willReturn(self::class);
|
||||
|
||||
$dependencies = $trait->checkDependencies();
|
||||
$this->assertCount(2, $dependencies);
|
||||
|
||||
@@ -17,7 +17,7 @@ use OCA\Files_External\Service\GlobalStoragesService;
|
||||
class GlobalStoragesServiceTest extends StoragesServiceTestCase {
|
||||
protected function setUp(): void {
|
||||
parent::setUp();
|
||||
$this->service = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->eventDispatcher, $this->appConfig);
|
||||
$this->service = new GlobalStoragesService($this->backendService, $this->dbConfig, $this->mountCache, $this->eventDispatcher, $this->appConfig);
|
||||
}
|
||||
|
||||
protected function tearDown(): void {
|
||||
|
||||
Some files were not shown because too many files have changed in this diff Show More
Reference in New Issue
Block a user