Skip to content

Commit f1706df

Browse files
authored
Merge pull request #46859 from nextcloud/fix-status-check-and-saving-of-external-storages
2 parents ab511e8 + fa0862c commit f1706df

File tree

12 files changed

+214
-16
lines changed

12 files changed

+214
-16
lines changed

apps/files_external/css/settings.css

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/files_external/css/settings.css.map

Lines changed: 1 addition & 1 deletion
Some generated files are not rendered by default. Learn more about customizing how changed files appear on GitHub.

apps/files_external/css/settings.scss

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -28,6 +28,8 @@
2828
/* overwrite conflicting core styles */
2929
display: table-cell;
3030
vertical-align: middle;
31+
/* ensure width does not change even if internal span is not displayed */
32+
width: 43px;
3133
}
3234

3335
#externalStorage td.status > span {

apps/files_external/js/settings.js

Lines changed: 29 additions & 11 deletions
Original file line numberDiff line numberDiff line change
@@ -766,6 +766,8 @@ MountConfigListView.prototype = _.extend({
766766
storageConfig.backend = $target.val();
767767
$tr.find('.mountPoint input').val('');
768768

769+
$tr.find('.selectBackend').prop('selectedIndex', 0)
770+
769771
var onCompletion = jQuery.Deferred();
770772
$tr = this.newStorage(storageConfig, onCompletion);
771773
$tr.find('.applicableToAllUsers').prop('checked', false).trigger('change');
@@ -874,7 +876,7 @@ MountConfigListView.prototype = _.extend({
874876
$tr.find('.applicable,.mountOptionsToggle').empty();
875877
$tr.find('.save').empty();
876878
if (backend.invalid) {
877-
this.updateStatus($tr, false, 'Unknown backend: ' + backend.name);
879+
this.updateStatus($tr, false, t('files_external', 'Unknown backend: {backendName}', {backendName: backend.name}));
878880
}
879881
return $tr;
880882
}
@@ -977,9 +979,10 @@ MountConfigListView.prototype = _.extend({
977979
data: {'testOnly' : true},
978980
contentType: 'application/json',
979981
success: function(result) {
982+
result = Object.values(result);
980983
var onCompletion = jQuery.Deferred();
981984
var $rows = $();
982-
Object.values(result).forEach(function(storageParams) {
985+
result.forEach(function(storageParams) {
983986
var storageConfig;
984987
var isUserGlobal = storageParams.type === 'system' && self._isPersonal;
985988
storageParams.mountPoint = storageParams.mountPoint.substr(1); // trim leading slash
@@ -1008,6 +1011,13 @@ MountConfigListView.prototype = _.extend({
10081011
// userglobal storages do not expose configuration data
10091012
$tr.find('.configuration').text(t('files_external', 'Admin defined'));
10101013
}
1014+
1015+
// don't recheck config automatically when there are a large number of storages
1016+
if (result.length < 20) {
1017+
self.recheckStorageConfig($tr);
1018+
} else {
1019+
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'));
1020+
}
10111021
$rows = $rows.add($tr);
10121022
});
10131023
initApplicableUsersMultiselect(self.$el.find('.applicableUsers'), this._userListLimit);
@@ -1226,8 +1236,9 @@ MountConfigListView.prototype = _.extend({
12261236
success: function () {
12271237
$tr.remove();
12281238
},
1229-
error: function () {
1230-
self.updateStatus($tr, StorageConfig.Status.ERROR);
1239+
error: function (result) {
1240+
const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined;
1241+
self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage);
12311242
}
12321243
});
12331244
}
@@ -1254,19 +1265,20 @@ MountConfigListView.prototype = _.extend({
12541265
if (concurrentTimer === undefined
12551266
|| $tr.data('save-timer') === concurrentTimer
12561267
) {
1257-
self.updateStatus($tr, result.status);
1268+
self.updateStatus($tr, result.status, result.statusMessage);
12581269
$tr.data('id', result.id);
12591270

12601271
if (_.isFunction(callback)) {
12611272
callback(storage);
12621273
}
12631274
}
12641275
},
1265-
error: function() {
1276+
error: function(result) {
12661277
if (concurrentTimer === undefined
12671278
|| $tr.data('save-timer') === concurrentTimer
12681279
) {
1269-
self.updateStatus($tr, StorageConfig.Status.ERROR);
1280+
const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined;
1281+
self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage);
12701282
}
12711283
}
12721284
});
@@ -1290,8 +1302,9 @@ MountConfigListView.prototype = _.extend({
12901302
success: function(result) {
12911303
self.updateStatus($tr, result.status, result.statusMessage);
12921304
},
1293-
error: function() {
1294-
self.updateStatus($tr, StorageConfig.Status.ERROR);
1305+
error: function(result) {
1306+
const statusMessage = (result && result.responseJSON) ? result.responseJSON.message : undefined;
1307+
self.updateStatus($tr, StorageConfig.Status.ERROR, statusMessage);
12951308
}
12961309
});
12971310
},
@@ -1308,6 +1321,7 @@ MountConfigListView.prototype = _.extend({
13081321
switch (status) {
13091322
case null:
13101323
// remove status
1324+
$statusSpan.hide();
13111325
break;
13121326
case StorageConfig.Status.IN_PROGRESS:
13131327
$statusSpan.attr('class', 'icon-loading-small');
@@ -1321,9 +1335,13 @@ MountConfigListView.prototype = _.extend({
13211335
default:
13221336
$statusSpan.attr('class', 'error icon-error-white');
13231337
}
1324-
if (typeof message === 'string') {
1325-
$statusSpan.attr('title', message);
1338+
if (status !== null) {
1339+
$statusSpan.show();
1340+
}
1341+
if (typeof message !== 'string') {
1342+
message = t('files_external', 'Click to recheck the configuration');
13261343
}
1344+
$statusSpan.attr('title', message);
13271345
},
13281346

13291347
/**

apps/files_external/lib/Lib/Auth/Password/UserGlobalAuth.php

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -9,6 +9,7 @@
99
namespace OCA\Files_External\Lib\Auth\Password;
1010

1111
use OCA\Files_External\Lib\Auth\AuthMechanism;
12+
use OCA\Files_External\Lib\DefinitionParameter;
1213
use OCA\Files_External\Lib\InsufficientDataForMeaningfulAnswerException;
1314
use OCA\Files_External\Lib\StorageConfig;
1415
use OCA\Files_External\Service\BackendService;
@@ -41,6 +42,12 @@ public function saveBackendOptions(IUser $user, $id, $backendOptions) {
4142
if (!isset($backendOptions['user']) && !isset($backendOptions['password'])) {
4243
return;
4344
}
45+
46+
if ($backendOptions['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) {
47+
$oldCredentials = $this->credentialsManager->retrieve($user->getUID(), self::CREDENTIALS_IDENTIFIER);
48+
$backendOptions['password'] = $oldCredentials['password'];
49+
}
50+
4451
// make sure we're not setting any unexpected keys
4552
$credentials = [
4653
'user' => $backendOptions['user'],

apps/files_external/lib/Lib/Auth/Password/UserProvided.php

Lines changed: 5 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -47,6 +47,11 @@ private function getCredentialsIdentifier($storageId) {
4747
}
4848

4949
public function saveBackendOptions(IUser $user, $mountId, array $options) {
50+
if ($options['password'] === DefinitionParameter::UNMODIFIED_PLACEHOLDER) {
51+
$oldCredentials = $this->credentialsManager->retrieve($user->getUID(), $this->getCredentialsIdentifier($mountId));
52+
$options['password'] = $oldCredentials['password'];
53+
}
54+
5055
$this->credentialsManager->store($user->getUID(), $this->getCredentialsIdentifier($mountId), [
5156
'user' => $options['user'], // explicitly copy the fields we want instead of just passing the entire $options array
5257
'password' => $options['password'] // this way we prevent users from being able to modify any other field

apps/files_external/templates/settings.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -146,7 +146,7 @@ function writeParameterInput($parameter, $options, $classes = []) {
146146
<?php endif; ?>
147147
>
148148
<td class="status">
149-
<span data-placement="right" title="<?php p($l->t('Click to recheck the configuration')); ?>"></span>
149+
<span data-placement="right" title="<?php p($l->t('Click to recheck the configuration')); ?>" style="display: none;"></span>
150150
</td>
151151
<td class="mountPoint"><input type="text" name="mountPoint" value=""
152152
placeholder="<?php p($l->t('Folder name')); ?>">

build/integration/features/bootstrap/BasicStructure.php

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -327,7 +327,7 @@ public function sendingAToWithRequesttoken($method, $url, $body = null) {
327327
$fd = $body->getRowsHash();
328328
$options['form_params'] = $fd;
329329
} elseif ($body) {
330-
$options = array_merge($options, $body);
330+
$options = array_merge_recursive($options, $body);
331331
}
332332

333333
$client = new Client();
Lines changed: 103 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,103 @@
1+
<?php
2+
/**
3+
* SPDX-FileCopyrightText: 2024 Nextcloud GmbH and Nextcloud contributors
4+
* SPDX-License-Identifier: AGPL-3.0-or-later
5+
*/
6+
use Behat\Gherkin\Node\TableNode;
7+
use PHPUnit\Framework\Assert;
8+
9+
require __DIR__ . '/../../vendor/autoload.php';
10+
11+
trait ExternalStorage {
12+
private array $storageIds = [];
13+
14+
private array $lastExternalStorageData;
15+
16+
/**
17+
* @AfterScenario
18+
**/
19+
public function deleteCreatedStorages(): void {
20+
foreach ($this->storageIds as $storageId) {
21+
$this->deleteStorage($storageId);
22+
}
23+
$this->storageIds = [];
24+
}
25+
26+
private function deleteStorage(string $storageId): void {
27+
// Based on "runOcc" from CommandLine trait
28+
$args = ['files_external:delete', '--yes', $storageId];
29+
$args = array_map(function ($arg) {
30+
return escapeshellarg($arg);
31+
}, $args);
32+
$args[] = '--no-ansi --no-warnings';
33+
$args = implode(' ', $args);
34+
35+
$descriptor = [
36+
0 => ['pipe', 'r'],
37+
1 => ['pipe', 'w'],
38+
2 => ['pipe', 'w'],
39+
];
40+
$process = proc_open('php console.php ' . $args, $descriptor, $pipes, $ocPath = '../..');
41+
$lastStdOut = stream_get_contents($pipes[1]);
42+
proc_close($process);
43+
}
44+
45+
/**
46+
* @When logged in user creates external global storage
47+
*
48+
* @param TableNode $fields
49+
*/
50+
public function loggedInUserCreatesExternalGlobalStorage(TableNode $fields): void {
51+
$this->sendJsonWithRequestToken('POST', '/index.php/apps/files_external/globalstorages', $fields);
52+
$this->theHTTPStatusCodeShouldBe('201');
53+
54+
$this->lastExternalStorageData = json_decode($this->response->getBody(), $asAssociativeArray = true);
55+
56+
$this->storageIds[] = $this->lastExternalStorageData['id'];
57+
}
58+
59+
/**
60+
* @When logged in user updates last external userglobal storage
61+
*
62+
* @param TableNode $fields
63+
*/
64+
public function loggedInUserUpdatesLastExternalUserglobalStorage(TableNode $fields): void {
65+
$this->sendJsonWithRequestToken('PUT', '/index.php/apps/files_external/userglobalstorages/' . $this->lastExternalStorageData['id'], $fields);
66+
$this->theHTTPStatusCodeShouldBe('200');
67+
68+
$this->lastExternalStorageData = json_decode($this->response->getBody(), $asAssociativeArray = true);
69+
}
70+
71+
/**
72+
* @Then fields of last external storage match with
73+
*
74+
* @param TableNode $fields
75+
*/
76+
public function fieldsOfLastExternalStorageMatchWith(TableNode $fields): void {
77+
foreach ($fields->getRowsHash() as $expectedField => $expectedValue) {
78+
if (!array_key_exists($expectedField, $this->lastExternalStorageData)) {
79+
Assert::fail("$expectedField was not found in response");
80+
}
81+
82+
Assert::assertEquals($expectedValue, $this->lastExternalStorageData[$expectedField], "Field '$expectedField' does not match ({$this->lastExternalStorageData[$expectedField]})");
83+
}
84+
}
85+
86+
private function sendJsonWithRequestToken(string $method, string $url, TableNode $fields): void {
87+
$isFirstField = true;
88+
$fieldsAsJsonString = '{';
89+
foreach ($fields->getRowsHash() as $key => $value) {
90+
$fieldsAsJsonString .= ($isFirstField ? '' : ',') . '"' . $key . '":' . $value;
91+
$isFirstField = false;
92+
}
93+
$fieldsAsJsonString .= '}';
94+
95+
$body = [
96+
'headers' => [
97+
'Content-Type' => 'application/json',
98+
],
99+
'body' => $fieldsAsJsonString,
100+
];
101+
$this->sendingAToWithRequesttoken($method, $url, $body);
102+
}
103+
}

build/integration/features/bootstrap/FeatureContext.php

Lines changed: 1 addition & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -15,6 +15,7 @@
1515
*/
1616
class FeatureContext implements Context, SnippetAcceptingContext {
1717
use ContactsMenu;
18+
use ExternalStorage;
1819
use Search;
1920
use WebDav;
2021
use Trashbin;

0 commit comments

Comments
 (0)