feat(files_external): Migrate settings to Vue

Template parameters are migrated to initial state, common state between admin and user settings is shared in the CommonSettingsTrait.
The template is cleaned and replaced with only a stub for the Vue mount.
Code only used for the frontend of the settings is moved from the MountConfig to the CommonSettingsTrait (the missing dependency messages).

On the frontend a wrapper view is created that currently holds the global credentials settings and the external storages settings.
- The global credentials sections is now a stand-alone sections - fully implemented.
- The external storages section holds the table + user config + warnings on missing dependencies

The legacy UI is temporarly renamed but will be removed in a following commit.

Signed-off-by: Ferdinand Thiessen <opensource@fthiessen.de>
This commit is contained in:
Ferdinand Thiessen 2024-03-18 00:48:10 +01:00 committed by nextcloud-command
parent d4674060dc
commit e1133ec926
25 changed files with 2324 additions and 2143 deletions

View file

@ -1,4 +0,0 @@
/*!
* 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 */

View file

@ -1 +0,0 @@
{"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"}

View file

@ -1,2 +0,0 @@
SPDX-FileCopyrightText: 2018 Nextcloud GmbH and Nextcloud contributors
SPDX-License-Identifier: AGPL-3.0-or-later

View file

@ -1,194 +0,0 @@
/*!
* 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);
}

View file

@ -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: 604 B

After

Width:  |  Height:  |  Size: 605 B

File diff suppressed because it is too large Load diff

View file

@ -1,47 +0,0 @@
(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});
})();

View file

@ -1,8 +0,0 @@
<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>

View file

@ -18,10 +18,8 @@ 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;
@ -110,52 +108,6 @@ 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
*

View file

@ -7,11 +7,13 @@
namespace OCA\Files_External\Settings;
use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
use OCA\Files_External\MountConfig;
use OCA\Files_External\Lib\Backend\Backend;
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 {
@ -22,6 +24,8 @@ class Admin implements ISettings {
private GlobalStoragesService $globalStoragesService,
private BackendService $backendService,
private GlobalAuth $globalAuth,
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
) {
}
@ -29,20 +33,29 @@ class Admin implements ISettings {
* @return TemplateResponse
*/
public function getForm() {
$parameters = [
'encryptionEnabled' => $this->encryptionManager->isEnabled(),
'visibilityType' => BackendService::VISIBILITY_ADMIN,
'storages' => $this->globalStoragesService->getStorages(),
'backends' => $this->backendService->getAvailableBackends(),
'authMechanisms' => $this->backendService->getAuthMechanisms(),
'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()),
// Shared settings (user & admin)
$this->setInitialState(BackendService::VISIBILITY_ADMIN);
// Admin specific
$backends = $this->backendService->getAvailableBackends();
$allowedBackends = array_filter($backends, fn (Backend $backend) => $backend->isVisibleFor(BackendService::VISIBILITY_PERSONAL));
$this->initialState->provideInitialState('user-mounting', [
'allowUserMounting' => $this->backendService->isUserMountingAllowed(),
'globalCredentials' => $this->globalAuth->getAuth(''),
'globalCredentialsUid' => '',
];
'allowedBackends' => array_values(array_map(fn (Backend $backend) => $backend->getIdentifier(), $allowedBackends)),
'backends' => array_values(
array_map(
fn (Backend $backend) => [
'id' => $backend->getIdentifier(),
'displayName' => $backend->getText(),
'deprecated' => $backend->getDeprecateTo()?->getIdentifier(),
],
$backends,
),
),
]);
$this->loadScriptsAndStyles();
return new TemplateResponse('files_external', 'settings', $parameters, '');
return new TemplateResponse('files_external', 'settings', renderAs: '');
}
/**

View file

@ -3,34 +3,58 @@
declare(strict_types=1);
/**
* @copyright Copyright (c) 2024 Ferdinand Thiessen <opensource@fthiessen.de>
*
* @author Ferdinand Thiessen <opensource@fthiessen.de>
*
* @license AGPL-3.0-or-later
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as
* published by the Free Software Foundation, either version 3 of the
* License, or (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*
* SPDX-FileCopyrightText: 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\IURLGenerator;
use OCP\Util;
trait CommonSettingsTrait {
protected BackendService $backendService;
private BackendService $backendService;
private IInitialState $initialState;
private IURLGenerator $urlGenerator;
private GlobalAuth $globalAuth;
private ?string $userId = null;
/**
* Set the initial state for the user / admin settings
*
* @param int $visibilityType The visibility type used to determine which options to show (admin vs user settings)
*/
protected function setInitialState(int $visibilityType) {
$allowUserMounting = $this->backendService->isUserMountingAllowed();
$isAdmin = $visibilityType === 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,
]);
$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 ?? ''),
),
);
}
/**
* Load the frontend script including the custom backend dependencies
@ -52,4 +76,36 @@ trait CommonSettingsTrait {
}
}
}
/**
* 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->backendService->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,
];
}
}

View file

@ -7,46 +7,32 @@
namespace OCA\Files_External\Settings;
use OCA\Files_External\Lib\Auth\Password\GlobalAuth;
use OCA\Files_External\MountConfig;
use OCA\Files_External\Service\BackendService;
use OCA\Files_External\Service\UserGlobalStoragesService;
use OCP\AppFramework\Http\TemplateResponse;
use OCP\Encryption\IManager;
use OCP\IUserSession;
use OCP\AppFramework\Services\IInitialState;
use OCP\IURLGenerator;
use OCP\Settings\ISettings;
class Personal implements ISettings {
use CommonSettingsTrait;
public function __construct(
private IManager $encryptionManager,
private UserGlobalStoragesService $userGlobalStoragesService,
?string $userId,
private BackendService $backendService,
private GlobalAuth $globalAuth,
private IUserSession $userSession,
private IInitialState $initialState,
private IURLGenerator $urlGenerator,
) {
$this->userId = $userId;
}
/**
* @return TemplateResponse
*/
public function getForm() {
$uid = $this->userSession->getUser()->getUID();
$parameters = [
'encryptionEnabled' => $this->encryptionManager->isEnabled(),
'visibilityType' => BackendService::VISIBILITY_PERSONAL,
'storages' => $this->userGlobalStoragesService->getStorages(),
'backends' => $this->backendService->getAvailableBackends(),
'authMechanisms' => $this->backendService->getAuthMechanisms(),
'dependencies' => MountConfig::dependencyMessage($this->backendService->getBackends()),
'allowUserMounting' => $this->backendService->isUserMountingAllowed(),
'globalCredentials' => $this->globalAuth->getAuth($uid),
'globalCredentialsUid' => $uid,
];
$this->setInitialState(BackendService::VISIBILITY_PERSONAL);
$this->loadScriptsAndStyles();
return new TemplateResponse('files_external', 'settings', $parameters, '');
return new TemplateResponse('files_external', 'settings', renderAs: '');
}
/**

View file

@ -0,0 +1,40 @@
<template>
<!-- The ID is just for backwards compatibility -->
<table id="externalStorage">
<thead>
<tr>
<th>
<span class="hidden-visually" v-text="t('files_external', 'Folder name')" />
</th>
<th>{{ t('files_external', 'Folder name') }}</th>
<th>{{ t('files_external', 'External storage') }}</th>
<th>{{ t('files_external', 'Authentication') }}</th>
<th>{{ t('files_external', 'Configuration') }}</th>
<th v-if="isAdmin">
{{ t('files_external', 'Available for') }}
</th>
</tr>
</thead>
<tbody>
<ExternalStorageRow />
</tbody>
</table>
</template>
<script lang="ts">
import { loadState } from '@nextcloud/initial-state'
import { defineComponent } from 'vue'
const { isAdmin } = loadState<{ isAdmin: boolean }>('files_external', 'settings')
export default defineComponent({
name: 'ExternalStorageTable',
setup() {
// Non reactive props
return {
isAdmin,
}
},
})
</script>

View file

@ -0,0 +1,120 @@
<!--
- SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
- SPDX-License-Identifier: AGPL-3.0-or-later
-->
<script setup lang="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[]
backends: {
id: string
displayName: string
deprecated?: string
}[]
}>('files_external', 'user-mounting')
const availableBackends = userMounting.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>
<template v-for="backend of availableBackends">
<NcCheckboxRadioSwitch
v-if="!backend.deprecated"
:key="backend.id"
v-model="allowedBackends"
:value="backend.id"
name="allowUserMountingBackends[]">
{{ backend.displayName }}
</NcCheckboxRadioSwitch>
<input
v-else-if="backend.id in allowedBackends"
:key="`${backend.id}-deprecated`"
:data-deprecate-to="backend.deprecated"
:value="backend.id"
name="allowUserMountingBackends[]"
type="hidden">
</template>
</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>

View file

@ -0,0 +1,10 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
import { createApp } from 'vue'
import FilesExternalApp from './views/FilesExternalSettings.vue'
const app = createApp(FilesExternalApp)
app.mount('#files-external')

File diff suppressed because it is too large Load diff

View file

@ -0,0 +1,57 @@
/*!
* 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 { generateUrl } from '@nextcloud/router'
import { defineStore } from 'pinia'
export const useStorages = defineStore('files_external--storages', {
state() {
return {
globalStorages: [] as IStorage[],
userStorages: [] as IStorage[],
}
},
getters: {
allStorages(state) {
return [...state.globalStorages, state.userStorages]
},
},
actions: {
async loadGlobalStorages() {
const url = 'apps/files_external/globalstorages'
const { data } = await axios.get<IStorage[]>(generateUrl(url))
this.globalStorages = data
},
},
/* result = Object.values(result);
var onCompletion = jQuery.Deferred();
var $rows = $();
result.forEach(function(storageParams) {
storageParams.mountPoint = (storageParams.mountPoint === '/')? '/' : storageParams.mountPoint.substr(1); // trim leading slash
var storageConfig = new self._storageConfigClass();
_.extend(storageConfig, storageParams);
var $tr = self.newStorage(storageConfig, onCompletion, true);
// don't recheck config automatically when there are a large number of storages
if (result.length < 20) {
self.recheckStorageConfig($tr);
} else {
self.updateStatus($tr, StorageConfig.Status.INDETERMINATE, t('files_external', 'Automatic status checking is disabled due to the large number of configured storages, click to check status'));
}
$rows = $rows.add($tr);
});
initApplicableUsersMultiselect($rows.find('.applicableUsers'), this._userListLimit);
self.$el.find('tr#addMountPoint').before($rows);
onCompletion.resolve();
onLoaded2.resolve();
}
}, */
})

21
apps/files_external/src/types.d.ts vendored Normal file
View file

@ -0,0 +1,21 @@
/*!
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
export interface IStorage {
id?: number
mountPoint: string
backend: string
authMechanism: string
backendOptions: Record<string, unknown>
priority?: number
applicableUsers?: string[]
applicableGroups?: string[]
mountOptions?: Record<string, unknown>
status?: number
statusMessage?: string
userProvided: bool
type: 'personal' | 'system'
}

View file

@ -0,0 +1,3 @@
import { getLoggerBuilder } from '@nextcloud/logger'
export default getLoggerBuilder().setApp('files_external').build()

View file

@ -0,0 +1,88 @@
<!--
- 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 { n, t } from '@nextcloud/l10n'
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 UserMountSettings from '../components/UserMountSettings.vue'
import filesExternalSvg from '../../img/app-dark.svg?raw'
const settings = loadState('files_external', 'settings', {
docUrl: '',
dependencyIssues: {
messages: null as string[] | null,
modules: null as Record<string, string[]> | null,
},
isAdmin: false,
})
/** List of dependency issue messages */
const dependencyIssues = settings.dependencyIssues?.messages ?? []
/** Map of missing modules -> list of dependant backends */
const missingModules = settings.dependencyIssues?.modules ?? {}
</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. 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="files-external__dependant-list" :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 :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>
<UserMountSettings v-if="settings.isAdmin" />
</NcSettingsSection>
</template>
<style scoped lang="scss">
.files-external__dependant-list {
list-style: disc;
margin-inline-start: 22px;
}
</style>

View file

@ -0,0 +1,13 @@
<!--
- 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>

View file

@ -0,0 +1,103 @@
<!--
- 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>

View file

@ -0,0 +1,240 @@
<?php
use OCA\Files_External\Lib\Auth\AuthMechanism;
use OCA\Files_External\Lib\Backend\Backend;
use OCA\Files_External\Lib\DefinitionParameter;
use OCA\Files_External\Service\BackendService;
/** @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');
script('files_external', [
'settings',
'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);
}
}
function writeParameterInput($parameter, $options, $classes = []) {
$value = '';
if (isset($options[$parameter->getName()])) {
$value = $options[$parameter->getName()];
}
$placeholder = $parameter->getText();
$is_optional = $parameter->isFlagSet(DefinitionParameter::FLAG_OPTIONAL);
switch ($parameter->getType()) {
case DefinitionParameter::VALUE_PASSWORD: ?>
<?php if ($is_optional) {
$classes[] = 'optional';
} ?>
<input type="password"
<?php if (!empty($classes)): ?> class="<?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
value="<?php p($value); ?>"
placeholder="<?php p($placeholder); ?>"
/>
<?php
break;
case DefinitionParameter::VALUE_BOOLEAN: ?>
<?php $checkboxId = uniqid('checkbox_'); ?>
<div>
<label>
<input type="checkbox"
id="<?php p($checkboxId); ?>"
<?php if (!empty($classes)): ?> class="checkbox <?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
<?php if ($value === true): ?> checked="checked"<?php endif; ?>
/>
<?php p($placeholder); ?>
</label>
</div>
<?php
break;
case DefinitionParameter::VALUE_HIDDEN: ?>
<input type="hidden"
<?php if (!empty($classes)): ?> class="<?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
value="<?php p($value); ?>"
/>
<?php
break;
default: ?>
<?php if ($is_optional) {
$classes[] = 'optional';
} ?>
<input type="text"
<?php if (!empty($classes)): ?> class="<?php p(implode(' ', $classes)); ?>"<?php endif; ?>
data-parameter="<?php p($parameter->getName()); ?>"
value="<?php p($value); ?>"
placeholder="<?php p($placeholder); ?>"
/>
<?php
}
}
?>
<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 = \OC::$server->getConfig()->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']) and ($_['dependencies'] !== '') and $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>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</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')); ?>"></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>

View file

@ -1,186 +1,7 @@
<?php
/**
* SPDX-FileCopyrightText: 2016-2024 Nextcloud GmbH and Nextcloud contributors
* SPDX-FileCopyrightText: 2012-2016 ownCloud, Inc.
* SPDX-License-Identifier: AGPL-3.0-only
* SPDX-FileCopyrightText: 2026 Nextcloud GmbH and Nextcloud contributors
* SPDX-License-Identifier: AGPL-3.0-or-later
*/
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 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>&nbsp;</th>
<th>&nbsp;</th>
<th>&nbsp;</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>
<div id="files-external"></div>

View file

@ -22,7 +22,7 @@ const modules = {
},
files_external: {
init: resolve(import.meta.dirname, 'apps/files_external/src', 'init.ts'),
settings: resolve(import.meta.dirname, 'apps/files_external/src', 'settings.js'),
settings: resolve(import.meta.dirname, 'apps/files_external/src', 'settings-main.ts'),
},
files_reminders: {
init: resolve(import.meta.dirname, 'apps/files_reminders/src', 'files-init.ts'),