mirror of
https://github.com/nextcloud/server.git
synced 2026-05-28 04:32:30 -04:00
feat(files_external): migrate to vue
Signed-off-by: John Molakvoæ <skjnldsv@protonmail.com>
This commit is contained in:
parent
385f987a28
commit
38480fda3c
56 changed files with 1171 additions and 1475 deletions
|
|
@ -23,7 +23,7 @@
|
|||
<span />
|
||||
</template>
|
||||
|
||||
<script>
|
||||
<script lang="ts">
|
||||
/**
|
||||
* This component is used to render custom
|
||||
* elements provided by an API. Vue doesn't allow
|
||||
|
|
@ -46,20 +46,29 @@ export default {
|
|||
required: true,
|
||||
},
|
||||
},
|
||||
computed: {
|
||||
element() {
|
||||
return this.render(this.source, this.currentView)
|
||||
},
|
||||
},
|
||||
watch: {
|
||||
element() {
|
||||
this.$el.replaceWith(this.element)
|
||||
this.$el = this.element
|
||||
source() {
|
||||
this.updateRootElement()
|
||||
},
|
||||
currentView() {
|
||||
this.updateRootElement()
|
||||
},
|
||||
},
|
||||
mounted() {
|
||||
this.$el.replaceWith(this.element)
|
||||
this.$el = this.element
|
||||
this.updateRootElement()
|
||||
},
|
||||
methods: {
|
||||
async updateRootElement() {
|
||||
const span = document.createElement('span') as HTMLSpanElement
|
||||
this.$el.replaceWith(span)
|
||||
this.$el = span
|
||||
|
||||
const element = await this.render(this.source, this.currentView)
|
||||
if (element) {
|
||||
this.$el.replaceWith(element)
|
||||
this.$el = element
|
||||
}
|
||||
},
|
||||
},
|
||||
}
|
||||
</script>
|
||||
|
|
|
|||
|
|
@ -91,8 +91,12 @@
|
|||
|
||||
<!-- Actions -->
|
||||
<td v-show="!isRenamingSmallScreen" :class="`files-list__row-actions-${uniqueId}`" class="files-list__row-actions">
|
||||
<!-- Inline actions -->
|
||||
<!-- TODO: implement CustomElementRender -->
|
||||
<!-- Render actions -->
|
||||
<CustomElementRender v-for="action in enabledRenderActions"
|
||||
:key="action.id"
|
||||
:current-view="currentView"
|
||||
:render="action.renderInline"
|
||||
:source="source" />
|
||||
|
||||
<!-- Menu actions -->
|
||||
<NcActions v-if="active"
|
||||
|
|
@ -301,15 +305,16 @@ export default Vue.extend({
|
|||
return formatFileSize(size, true)
|
||||
},
|
||||
sizeOpacity() {
|
||||
const size = parseInt(this.source.size, 10) || 0
|
||||
if (!size || size < 0) {
|
||||
return 1
|
||||
}
|
||||
|
||||
// Whatever theme is active, the contrast will pass WCAG AA
|
||||
// with color main text over main background and an opacity of 0.7
|
||||
const minOpacity = 0.7
|
||||
const maxOpacitySize = 10 * 1024 * 1024
|
||||
|
||||
const size = parseInt(this.source.size, 10) || 0
|
||||
if (!size || size < 0) {
|
||||
return minOpacity
|
||||
}
|
||||
|
||||
return minOpacity + (1 - minOpacity) * Math.pow((this.source.size / maxOpacitySize), 2)
|
||||
},
|
||||
|
||||
|
|
@ -396,9 +401,17 @@ export default Vue.extend({
|
|||
return this.enabledActions.filter(action => action?.inline?.(this.source, this.currentView))
|
||||
},
|
||||
|
||||
// Enabled action that are displayed inline with a custom render function
|
||||
enabledRenderActions() {
|
||||
if (!this.active) {
|
||||
return []
|
||||
}
|
||||
return this.enabledActions.filter(action => typeof action.renderInline === 'function')
|
||||
},
|
||||
|
||||
// Default actions
|
||||
enabledDefaultActions() {
|
||||
return this.enabledActions.filter(action => !!action.default)
|
||||
return this.enabledActions.filter(action => !!action?.default)
|
||||
},
|
||||
|
||||
// Actions shown in the menu
|
||||
|
|
@ -407,7 +420,7 @@ export default Vue.extend({
|
|||
// Showing inline first for the NcActions inline prop
|
||||
...this.enabledInlineActions,
|
||||
// Then the rest
|
||||
...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN),
|
||||
...this.enabledActions.filter(action => action.default !== DefaultType.HIDDEN && typeof action.renderInline !== 'function'),
|
||||
].filter((value, index, self) => {
|
||||
// Then we filter duplicates to prevent inline actions to be shown twice
|
||||
return index === self.findIndex(action => action.id === value.id)
|
||||
|
|
|
|||
|
|
@ -74,7 +74,7 @@ interface FileActionData {
|
|||
* If defined, the returned html element will be
|
||||
* appended before the actions menu.
|
||||
*/
|
||||
renderInline?: (file: Node, view: Navigation) => HTMLElement,
|
||||
renderInline?: (file: Node, view: Navigation) => Promise<HTMLElement | null>,
|
||||
}
|
||||
|
||||
export class FileAction {
|
||||
|
|
|
|||
|
|
@ -183,19 +183,24 @@ export default Vue.extend({
|
|||
return this.isAscSorting ? results : results.reverse()
|
||||
}
|
||||
|
||||
const identifiers = [
|
||||
// Sort favorites first if enabled
|
||||
...this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : [],
|
||||
// Sort folders first if sorting by name
|
||||
...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [],
|
||||
// Use sorting mode if NOT basename (to be able to use displayName too)
|
||||
...this.sortingMode !== 'basename' ? [v => v[this.sortingMode]] : [],
|
||||
// Use displayName if available, fallback to name
|
||||
v => v.attributes?.displayName || v.basename,
|
||||
// Finally, use basename if all previous sorting methods failed
|
||||
v => v.basename,
|
||||
]
|
||||
const orders = new Array(identifiers.length).fill(this.isAscSorting ? 'asc' : 'desc')
|
||||
|
||||
return orderBy(
|
||||
[...(this.currentFolder?._children || []).map(this.getNode).filter(file => file)],
|
||||
[
|
||||
// Sort favorites first if enabled
|
||||
...this.userConfig.sort_favorites_first ? [v => v.attributes?.favorite !== 1] : [],
|
||||
// Sort folders first if sorting by name
|
||||
...this.sortingMode === 'basename' ? [v => v.type !== 'folder'] : [],
|
||||
// Use sorting mode
|
||||
v => v[this.sortingMode],
|
||||
// Finally, fallback to name
|
||||
v => v.basename,
|
||||
],
|
||||
this.isAscSorting ? ['asc', 'asc', 'asc'] : ['desc', 'desc', 'desc'],
|
||||
identifiers,
|
||||
orders,
|
||||
)
|
||||
},
|
||||
|
||||
|
|
|
|||
|
|
@ -62,5 +62,10 @@ return [
|
|||
'url' => '/api/v1/mounts',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
[
|
||||
'name' => 'Api#askNativeAuth',
|
||||
'url' => '/api/v1/auth',
|
||||
'verb' => 'GET',
|
||||
],
|
||||
],
|
||||
];
|
||||
|
|
|
|||
|
|
@ -96,6 +96,7 @@ return array(
|
|||
'OCA\\Files_External\\Lib\\Storage\\Swift' => $baseDir . '/../lib/Lib/Storage/Swift.php',
|
||||
'OCA\\Files_External\\Lib\\VisibilityTrait' => $baseDir . '/../lib/Lib/VisibilityTrait.php',
|
||||
'OCA\\Files_External\\Listener\\GroupDeletedListener' => $baseDir . '/../lib/Listener/GroupDeletedListener.php',
|
||||
'OCA\\Files_External\\Listener\\LoadAdditionalListener' => $baseDir . '/../lib/Listener/LoadAdditionalListener.php',
|
||||
'OCA\\Files_External\\Listener\\StorePasswordListener' => $baseDir . '/../lib/Listener/StorePasswordListener.php',
|
||||
'OCA\\Files_External\\Listener\\UserDeletedListener' => $baseDir . '/../lib/Listener/UserDeletedListener.php',
|
||||
'OCA\\Files_External\\Migration\\DummyUserSession' => $baseDir . '/../lib/Migration/DummyUserSession.php',
|
||||
|
|
|
|||
|
|
@ -111,6 +111,7 @@ class ComposerStaticInitFiles_External
|
|||
'OCA\\Files_External\\Lib\\Storage\\Swift' => __DIR__ . '/..' . '/../lib/Lib/Storage/Swift.php',
|
||||
'OCA\\Files_External\\Lib\\VisibilityTrait' => __DIR__ . '/..' . '/../lib/Lib/VisibilityTrait.php',
|
||||
'OCA\\Files_External\\Listener\\GroupDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/GroupDeletedListener.php',
|
||||
'OCA\\Files_External\\Listener\\LoadAdditionalListener' => __DIR__ . '/..' . '/../lib/Listener/LoadAdditionalListener.php',
|
||||
'OCA\\Files_External\\Listener\\StorePasswordListener' => __DIR__ . '/..' . '/../lib/Listener/StorePasswordListener.php',
|
||||
'OCA\\Files_External\\Listener\\UserDeletedListener' => __DIR__ . '/..' . '/../lib/Listener/UserDeletedListener.php',
|
||||
'OCA\\Files_External\\Migration\\DummyUserSession' => __DIR__ . '/..' . '/../lib/Migration/DummyUserSession.php',
|
||||
|
|
|
|||
|
|
@ -3,7 +3,7 @@
|
|||
'name' => '__root__',
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
|
||||
'reference' => '706c141fffce928d344fe2f039da549fad065393',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../',
|
||||
'aliases' => array(),
|
||||
|
|
@ -13,7 +13,7 @@
|
|||
'__root__' => array(
|
||||
'pretty_version' => 'dev-master',
|
||||
'version' => 'dev-master',
|
||||
'reference' => 'b1797842784b250fb01ed5e3bf130705eb94751b',
|
||||
'reference' => '706c141fffce928d344fe2f039da549fad065393',
|
||||
'type' => 'library',
|
||||
'install_path' => __DIR__ . '/../',
|
||||
'aliases' => array(),
|
||||
|
|
|
|||
|
|
@ -1,4 +0,0 @@
|
|||
.files-filestable tbody tr.externalErroredRow {
|
||||
/* TODO: As soon as firefox supports it: color-mix(in srgb, var(--color-error) 15%, var(--color-main-background)) */
|
||||
background-color: rgba(255, 0, 0, 0.13);
|
||||
}
|
||||
|
|
@ -1,112 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
if (!OCA.Files_External) {
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.Files_External = {};
|
||||
}
|
||||
/**
|
||||
* @namespace
|
||||
*/
|
||||
OCA.Files_External.App = {
|
||||
|
||||
fileList: null,
|
||||
|
||||
initList: function($el) {
|
||||
if (this.fileList) {
|
||||
return this.fileList;
|
||||
}
|
||||
|
||||
this.fileList = new OCA.Files_External.FileList(
|
||||
$el,
|
||||
{
|
||||
fileActions: this._createFileActions()
|
||||
}
|
||||
);
|
||||
|
||||
this._extendFileList(this.fileList);
|
||||
this.fileList.appName = t('files_external', 'External storage');
|
||||
return this.fileList;
|
||||
},
|
||||
|
||||
removeList: function() {
|
||||
if (this.fileList) {
|
||||
this.fileList.$fileList.empty();
|
||||
}
|
||||
},
|
||||
|
||||
_createFileActions: function() {
|
||||
// inherit file actions from the files app
|
||||
var fileActions = new OCA.Files.FileActions();
|
||||
fileActions.registerDefaultActions();
|
||||
|
||||
// when the user clicks on a folder, redirect to the corresponding
|
||||
// folder in the files app instead of opening it directly
|
||||
fileActions.register('dir', 'Open', OC.PERMISSION_READ, '', function (filename, context) {
|
||||
OCA.Files.App.setActiveView('files', {silent: true});
|
||||
OCA.Files.App.fileList.changeDirectory(OC.joinPaths(context.$file.attr('data-path'), filename), true, true);
|
||||
});
|
||||
fileActions.setDefault('dir', 'Open');
|
||||
return fileActions;
|
||||
},
|
||||
|
||||
_extendFileList: function(fileList) {
|
||||
// remove size column from summary
|
||||
fileList.fileSummary.$el.find('.filesize').remove();
|
||||
}
|
||||
};
|
||||
|
||||
window.addEventListener('DOMContentLoaded', function() {
|
||||
$('#app-content-extstoragemounts').on('show', function(e) {
|
||||
OCA.Files_External.App.initList($(e.target));
|
||||
});
|
||||
$('#app-content-extstoragemounts').on('hide', function() {
|
||||
OCA.Files_External.App.removeList();
|
||||
});
|
||||
|
||||
/* Status Manager */
|
||||
if ($('#filesApp').val()) {
|
||||
|
||||
$('#app-content-files')
|
||||
.add('#app-content-extstoragemounts')
|
||||
.on('changeDirectory', function(e){
|
||||
if (e.dir === '/') {
|
||||
var mount_point = e.previousDir.split('/', 2)[1];
|
||||
// Every time that we return to / root folder from a mountpoint, mount_point status is rechecked
|
||||
OCA.Files_External.StatusManager.getMountPointList(function() {
|
||||
OCA.Files_External.StatusManager.recheckConnectivityForMount([mount_point], true);
|
||||
});
|
||||
}
|
||||
})
|
||||
.on('fileActionsReady', function(e){
|
||||
if ($.isArray(e.$files)) {
|
||||
if (OCA.Files_External.StatusManager.mountStatus === null ||
|
||||
OCA.Files_External.StatusManager.mountPointList === null ||
|
||||
_.size(OCA.Files_External.StatusManager.mountStatus) !== _.size(OCA.Files_External.StatusManager.mountPointList)) {
|
||||
// Will be the very first check when the files view will be loaded
|
||||
OCA.Files_External.StatusManager.launchFullConnectivityCheckOneByOne();
|
||||
} else {
|
||||
// When we change between general files view and external files view
|
||||
OCA.Files_External.StatusManager.getMountPointList(function(){
|
||||
var fileNames = [];
|
||||
$.each(e.$files, function(key, value){
|
||||
fileNames.push(value.attr('data-file'));
|
||||
});
|
||||
// Recheck if launched but work from cache
|
||||
OCA.Files_External.StatusManager.recheckConnectivityForMount(fileNames, false);
|
||||
});
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
/* End Status Manager */
|
||||
});
|
||||
|
|
@ -1,149 +0,0 @@
|
|||
/*
|
||||
* Copyright (c) 2014 Vincent Petry <pvince81@owncloud.com>
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
(function() {
|
||||
|
||||
/**
|
||||
* @class OCA.Files_External.FileList
|
||||
* @augments OCA.Files.FileList
|
||||
*
|
||||
* @classdesc External storage file list.
|
||||
*
|
||||
* Displays a list of mount points visible
|
||||
* for the current user.
|
||||
*
|
||||
* @param $el container element with existing markup for the .files-controls
|
||||
* and a table
|
||||
* @param [options] map of options, see other parameters
|
||||
**/
|
||||
var FileList = function($el, options) {
|
||||
this.initialize($el, options);
|
||||
};
|
||||
|
||||
FileList.prototype = _.extend({}, OCA.Files.FileList.prototype,
|
||||
/** @lends OCA.Files_External.FileList.prototype */ {
|
||||
appName: 'External storage',
|
||||
|
||||
_allowSelection: false,
|
||||
|
||||
/**
|
||||
* @private
|
||||
*/
|
||||
initialize: function($el, options) {
|
||||
OCA.Files.FileList.prototype.initialize.apply(this, arguments);
|
||||
if (this.initialized) {
|
||||
return;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param {OCA.Files_External.MountPointInfo} fileData
|
||||
*/
|
||||
_createRow: function(fileData) {
|
||||
// TODO: hook earlier and render the whole row here
|
||||
var $tr = OCA.Files.FileList.prototype._createRow.apply(this, arguments);
|
||||
var $scopeColumn = $('<td class="column-scope column-last"><span></span></td>');
|
||||
var $backendColumn = $('<td class="column-backend"></td>');
|
||||
var scopeText = t('files_external', 'Personal');
|
||||
if (fileData.scope === 'system') {
|
||||
scopeText = t('files_external', 'System');
|
||||
}
|
||||
$tr.find('.filesize,.date').remove();
|
||||
$scopeColumn.find('span').text(scopeText);
|
||||
$backendColumn.text(fileData.backend);
|
||||
$tr.find('td.filename').after($scopeColumn).after($backendColumn);
|
||||
return $tr;
|
||||
},
|
||||
|
||||
updateEmptyContent: function() {
|
||||
var dir = this.getCurrentDirectory();
|
||||
if (dir === '/') {
|
||||
// root has special permissions
|
||||
this.$el.find('.emptyfilelist.emptycontent').toggleClass('hidden', !this.isEmpty);
|
||||
this.$el.find('.files-filestable thead th').toggleClass('hidden', this.isEmpty);
|
||||
}
|
||||
else {
|
||||
OCA.Files.FileList.prototype.updateEmptyContent.apply(this, arguments);
|
||||
}
|
||||
},
|
||||
|
||||
getDirectoryPermissions: function() {
|
||||
return OC.PERMISSION_READ | OC.PERMISSION_DELETE;
|
||||
},
|
||||
|
||||
updateStorageStatistics: function() {
|
||||
// no op because it doesn't have
|
||||
// storage info like free space / used space
|
||||
},
|
||||
|
||||
reload: function() {
|
||||
this.showMask();
|
||||
if (this._reloadCall?.abort) {
|
||||
this._reloadCall.abort();
|
||||
}
|
||||
|
||||
// there is only root
|
||||
this._setCurrentDir('/', false);
|
||||
|
||||
this._reloadCall = $.ajax({
|
||||
url: OC.linkToOCS('apps/files_external/api/v1') + 'mounts',
|
||||
data: {
|
||||
format: 'json'
|
||||
},
|
||||
type: 'GET',
|
||||
beforeSend: function(xhr) {
|
||||
xhr.setRequestHeader('OCS-APIREQUEST', 'true');
|
||||
}
|
||||
});
|
||||
var callBack = this.reloadCallback.bind(this);
|
||||
return this._reloadCall.then(callBack, callBack);
|
||||
},
|
||||
|
||||
reloadCallback: function(result) {
|
||||
delete this._reloadCall;
|
||||
this.hideMask();
|
||||
|
||||
if (result.ocs && result.ocs.data) {
|
||||
this.setFiles(this._makeFiles(result.ocs.data));
|
||||
return true;
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/**
|
||||
* Converts the OCS API response data to a file info
|
||||
* list
|
||||
* @param OCS API mounts array
|
||||
* @return array of file info maps
|
||||
*/
|
||||
_makeFiles: function(data) {
|
||||
var files = _.map(data, function(fileData) {
|
||||
fileData.icon = OC.imagePath('core', 'filetypes/folder-external');
|
||||
fileData.mountType = 'external';
|
||||
return fileData;
|
||||
});
|
||||
|
||||
files.sort(this._sortComparator);
|
||||
|
||||
return files;
|
||||
}
|
||||
});
|
||||
|
||||
/**
|
||||
* Mount point info attributes.
|
||||
*
|
||||
* @typedef {Object} OCA.Files_External.MountPointInfo
|
||||
*
|
||||
* @property {String} name mount point name
|
||||
* @property {String} scope mount point scope "personal" or "system"
|
||||
* @property {String} backend external storage backend name
|
||||
*/
|
||||
|
||||
OCA.Files_External.FileList = FileList;
|
||||
})();
|
||||
|
|
@ -1,82 +0,0 @@
|
|||
window.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
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 === 'oauth1::oauth1') {
|
||||
var 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', 'oauth1_grant')
|
||||
);
|
||||
|
||||
onCompletion.then(function() {
|
||||
var configured = $tr.find('[data-parameter="configured"]');
|
||||
if ($(configured).val() == 'true') {
|
||||
displayGranted($tr);
|
||||
} else {
|
||||
var app_key = $tr.find('.configuration [data-parameter="app_key"]').val();
|
||||
var app_secret = $tr.find('.configuration [data-parameter="app_secret"]').val();
|
||||
if (app_key != '' && app_secret != '') {
|
||||
var pos = window.location.search.indexOf('oauth_token') + 12;
|
||||
var token = $tr.find('.configuration [data-parameter="token"]');
|
||||
if (pos != -1 && window.location.search.substr(pos, $(token).val().length) == $(token).val()) {
|
||||
var token_secret = $tr.find('.configuration [data-parameter="token_secret"]');
|
||||
var statusSpan = $tr.find('.status span');
|
||||
statusSpan.removeClass();
|
||||
statusSpan.addClass('waiting');
|
||||
$.post(OC.filePath('files_external', 'ajax', 'oauth1.php'), { step: 2, app_key: app_key, app_secret: app_secret, request_token: $(token).val(), request_token_secret: $(token_secret).val() }, function(result) {
|
||||
if (result && result.status == 'success') {
|
||||
$(token).val(result.access_token);
|
||||
$(token_secret).val(result.access_token_secret);
|
||||
$(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 OAuth1'));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
||||
$('#externalStorage').on('click', '[name="oauth1_grant"]', function(event) {
|
||||
event.preventDefault();
|
||||
var tr = $(this).parent().parent();
|
||||
var app_key = $(this).parent().find('[data-parameter="app_key"]').val();
|
||||
var app_secret = $(this).parent().find('[data-parameter="app_secret"]').val();
|
||||
if (app_key != '' && app_secret != '') {
|
||||
var configured = $(this).parent().find('[data-parameter="configured"]');
|
||||
var token = $(this).parent().find('[data-parameter="token"]');
|
||||
var token_secret = $(this).parent().find('[data-parameter="token_secret"]');
|
||||
$.post(OC.filePath('files_external', 'ajax', 'oauth1.php'), { step: 1, app_key: app_key, app_secret: app_secret, callback: location.protocol + '//' + location.host + location.pathname }, function(result) {
|
||||
if (result && result.status == 'success') {
|
||||
$(configured).val('false');
|
||||
$(token).val(result.data.request_token);
|
||||
$(token_secret).val(result.data.request_token_secret);
|
||||
OCA.Files_External.Settings.mountConfig.saveStorageConfig(tr, function() {
|
||||
window.location = result.data.url;
|
||||
});
|
||||
} else {
|
||||
OC.dialogs.alert(result.data.message, t('files_external', 'Error configuring OAuth1'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
OC.dialogs.alert(
|
||||
t('files_external', 'Please provide a valid app key and secret.'),
|
||||
t('files_external', 'Error configuring OAuth1')
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,96 +0,0 @@
|
|||
window.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
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') {
|
||||
var 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() {
|
||||
var configured = $tr.find('[data-parameter="configured"]');
|
||||
if ($(configured).val() == 'true') {
|
||||
displayGranted($tr);
|
||||
} else {
|
||||
var client_id = $tr.find('.configuration [data-parameter="client_id"]').val();
|
||||
var client_secret = $tr.find('.configuration [data-parameter="client_secret"]')
|
||||
.val();
|
||||
if (client_id != '' && client_secret != '') {
|
||||
var params = {};
|
||||
window.location.href.replace(/[?&]+([^=&]+)=([^&]*)/gi, function(m, key, value) {
|
||||
params[key] = value;
|
||||
});
|
||||
if (params['code'] !== undefined) {
|
||||
var token = $tr.find('.configuration [data-parameter="token"]');
|
||||
var statusSpan = $tr.find('.status span');
|
||||
statusSpan.removeClass();
|
||||
statusSpan.addClass('waiting');
|
||||
$.post(OC.filePath('files_external', 'ajax', 'oauth2.php'),
|
||||
{
|
||||
step: 2,
|
||||
client_id: client_id,
|
||||
client_secret: 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();
|
||||
var tr = $(this).parent().parent();
|
||||
var configured = $(this).parent().find('[data-parameter="configured"]');
|
||||
var client_id = $(this).parent().find('[data-parameter="client_id"]').val();
|
||||
var client_secret = $(this).parent().find('[data-parameter="client_secret"]').val();
|
||||
if (client_id != '' && client_secret != '') {
|
||||
var token = $(this).parent().find('[data-parameter="token"]');
|
||||
$.post(OC.filePath('files_external', 'ajax', 'oauth2.php'),
|
||||
{
|
||||
step: 1,
|
||||
client_id: client_id,
|
||||
client_secret: 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')
|
||||
);
|
||||
}
|
||||
}
|
||||
);
|
||||
}
|
||||
});
|
||||
|
||||
});
|
||||
|
|
@ -1,64 +0,0 @@
|
|||
window.addEventListener('DOMContentLoaded', function() {
|
||||
|
||||
OCA.Files_External.Settings.mountConfig.whenSelectAuthMechanism(function($tr, authMechanism, scheme, onCompletion) {
|
||||
if (scheme === 'publickey' && authMechanism === 'publickey::rsa') {
|
||||
var 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();
|
||||
var tr = $(this).parent().parent();
|
||||
generateKeys(tr);
|
||||
});
|
||||
|
||||
function setupTableRow(tr, config) {
|
||||
var selectList = document.createElement('select');
|
||||
selectList.id = 'keyLength';
|
||||
|
||||
var options = [1024, 2048, 4096];
|
||||
for (var i = 0; i < options.length; i++) {
|
||||
var 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')
|
||||
);
|
||||
}
|
||||
|
||||
function generateKeys(tr) {
|
||||
var config = $(tr).find('.configuration');
|
||||
var keyLength = config.find('#keyLength').val();
|
||||
|
||||
$.post(OC.filePath('files_external', 'ajax', 'public_key.php'), {
|
||||
keyLength: 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') );
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
|
|
@ -1,137 +0,0 @@
|
|||
/**
|
||||
* ownCloud
|
||||
*
|
||||
* @author Juan Pablo Villafañez Ramos <jvillafanez@owncloud.com>
|
||||
* @author Jesus Macias Portela <jesus@owncloud.com>
|
||||
* @copyright (C) 2014 ownCloud, Inc.
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
(function(){
|
||||
/**
|
||||
* Launch several functions at thee same time. The number of functions
|
||||
* running at the same time is controlled by the queueWindow param
|
||||
*
|
||||
* The function list come in the following format:
|
||||
*
|
||||
* var flist = [
|
||||
* {
|
||||
* funcName: function () {
|
||||
* var d = $.Deferred();
|
||||
* setTimeout(function(){d.resolve();}, 1000);
|
||||
* return d;
|
||||
* }
|
||||
* },
|
||||
* {
|
||||
* funcName: $.get,
|
||||
* funcArgs: [
|
||||
* OC.filePath('files_external', 'ajax', 'connectivityCheck.php'),
|
||||
* {},
|
||||
* function () {
|
||||
* console.log('titoooo');
|
||||
* }
|
||||
* ]
|
||||
* },
|
||||
* {
|
||||
* funcName: $.get,
|
||||
* funcArgs: [
|
||||
* OC.filePath('files_external', 'ajax', 'connectivityCheck.php')
|
||||
* ],
|
||||
* done: function () {
|
||||
* console.log('yuupi');
|
||||
* },
|
||||
* always: function () {
|
||||
* console.log('always done');
|
||||
* }
|
||||
* }
|
||||
*];
|
||||
*
|
||||
* functions MUST implement the deferred interface
|
||||
*
|
||||
* @param functionList list of functions that the queue will run
|
||||
* (check example above for the expected format)
|
||||
* @param queueWindow specify the number of functions that will
|
||||
* be executed at the same time
|
||||
*/
|
||||
var RollingQueue = function (functionList, queueWindow, callback) {
|
||||
this.queueWindow = queueWindow || 1;
|
||||
this.functionList = functionList;
|
||||
this.callback = callback;
|
||||
this.counter = 0;
|
||||
this.runQueue = function() {
|
||||
this.callbackCalled = false;
|
||||
this.deferredsList = [];
|
||||
if (!$.isArray(this.functionList)) {
|
||||
throw "functionList must be an array";
|
||||
}
|
||||
|
||||
for (var i = 0; i < this.queueWindow; i++) {
|
||||
this.launchNext();
|
||||
}
|
||||
};
|
||||
|
||||
this.hasNext = function() {
|
||||
return (this.counter in this.functionList);
|
||||
};
|
||||
|
||||
this.launchNext = function() {
|
||||
var currentCounter = this.counter++;
|
||||
if (currentCounter in this.functionList) {
|
||||
var funcData = this.functionList[currentCounter];
|
||||
if ($.isFunction(funcData.funcName)) {
|
||||
var defObj = funcData.funcName.apply(funcData.funcName, funcData.funcArgs);
|
||||
this.deferredsList.push(defObj);
|
||||
if ($.isFunction(funcData.done)) {
|
||||
defObj.done(funcData.done);
|
||||
}
|
||||
|
||||
if ($.isFunction(funcData.fail)) {
|
||||
defObj.fail(funcData.fail);
|
||||
}
|
||||
|
||||
if ($.isFunction(funcData.always)) {
|
||||
defObj.always(funcData.always);
|
||||
}
|
||||
|
||||
if (this.hasNext()) {
|
||||
var self = this;
|
||||
defObj.always(function(){
|
||||
_.defer($.proxy(function(){
|
||||
self.launchNext();
|
||||
}, self));
|
||||
});
|
||||
} else {
|
||||
if (!this.callbackCalled) {
|
||||
this.callbackCalled = true;
|
||||
if ($.isFunction(this.callback)) {
|
||||
$.when.apply($, this.deferredsList)
|
||||
.always($.proxy(function(){
|
||||
this.callback();
|
||||
}, this)
|
||||
);
|
||||
}
|
||||
}
|
||||
}
|
||||
return defObj;
|
||||
}
|
||||
}
|
||||
return false;
|
||||
};
|
||||
};
|
||||
|
||||
if (!OCA.Files_External) {
|
||||
OCA.Files_External = {};
|
||||
}
|
||||
|
||||
if (!OCA.Files_External.StatusManager) {
|
||||
OCA.Files_External.StatusManager = {};
|
||||
}
|
||||
|
||||
OCA.Files_External.StatusManager.RollingQueue = RollingQueue;
|
||||
|
||||
})();
|
||||
|
|
@ -1,613 +0,0 @@
|
|||
/**
|
||||
* ownCloud
|
||||
*
|
||||
* @author Juan Pablo Villafañez Ramos <jvillafanez@owncloud.com>
|
||||
* @author Jesus Macias Portela <jesus@owncloud.com>
|
||||
* @copyright (C) 2014 ownCloud, Inc.
|
||||
*
|
||||
* This file is licensed under the Affero General Public License version 3
|
||||
* or later.
|
||||
*
|
||||
* See the COPYING-README file.
|
||||
*
|
||||
*/
|
||||
|
||||
/** @global Handlebars */
|
||||
|
||||
if (!OCA.Files_External) {
|
||||
OCA.Files_External = {};
|
||||
}
|
||||
|
||||
if (!OCA.Files_External.StatusManager) {
|
||||
OCA.Files_External.StatusManager = {};
|
||||
}
|
||||
|
||||
OCA.Files_External.StatusManager = {
|
||||
|
||||
mountStatus: null,
|
||||
mountPointList: null,
|
||||
|
||||
/**
|
||||
* Function
|
||||
* @param {callback} afterCallback
|
||||
*/
|
||||
|
||||
getMountStatus: function (afterCallback) {
|
||||
var self = this;
|
||||
if (typeof afterCallback !== 'function' || self.isGetMountStatusRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.mountStatus) {
|
||||
afterCallback(self.mountStatus);
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Function Check mount point status from cache
|
||||
* @param {string} mount_point
|
||||
*/
|
||||
|
||||
getMountPointListElement: function (mount_point) {
|
||||
var element;
|
||||
$.each(this.mountPointList, function (key, value) {
|
||||
if (value.mount_point === mount_point) {
|
||||
element = value;
|
||||
return false;
|
||||
}
|
||||
});
|
||||
return element;
|
||||
},
|
||||
|
||||
/**
|
||||
* Function Check mount point status from cache
|
||||
* @param {string} mount_point
|
||||
* @param {string} mount_point
|
||||
*/
|
||||
|
||||
getMountStatusForMount: function (mountData, afterCallback) {
|
||||
var self = this;
|
||||
if (typeof afterCallback !== 'function' || self.isGetMountStatusRunning) {
|
||||
return $.Deferred().resolve();
|
||||
}
|
||||
|
||||
var defObj;
|
||||
if (self.mountStatus[mountData.mount_point]) {
|
||||
defObj = $.Deferred();
|
||||
afterCallback(mountData, self.mountStatus[mountData.mount_point]);
|
||||
defObj.resolve(); // not really useful, but it'll keep the same behaviour
|
||||
} else {
|
||||
defObj = $.ajax({
|
||||
type: 'GET',
|
||||
url: OC.getRootPath() + '/index.php/apps/files_external/' + ((mountData.type === 'personal') ? 'userstorages' : 'userglobalstorages') + '/' + mountData.id,
|
||||
data: {'testOnly' : false},
|
||||
success: function (response) {
|
||||
if (response && response.status === 0) {
|
||||
self.mountStatus[mountData.mount_point] = response;
|
||||
} else {
|
||||
var statusCode = response.status ? response.status : 1;
|
||||
var statusMessage = response.statusMessage ? response.statusMessage : t('files_external', 'Empty response from the server')
|
||||
// failure response with error message
|
||||
self.mountStatus[mountData.mount_point] = {
|
||||
type: mountData.type,
|
||||
status: statusCode,
|
||||
id: mountData.id,
|
||||
error: statusMessage,
|
||||
userProvided: response.userProvided,
|
||||
authMechanism: response.authMechanism,
|
||||
canEdit: response.can_edit,
|
||||
};
|
||||
}
|
||||
afterCallback(mountData, self.mountStatus[mountData.mount_point]);
|
||||
},
|
||||
error: function (jqxhr, state, error) {
|
||||
var message;
|
||||
if (mountData.location === 3) {
|
||||
// In this case the error is because mount point use Login credentials and don't exist in the session
|
||||
message = t('files_external', 'Couldn\'t access. Please log out and in again to activate this mount point');
|
||||
} else {
|
||||
message = t('files_external', 'Couldn\'t get the information from the remote server: {code} {type}', {
|
||||
code: jqxhr.status,
|
||||
type: error
|
||||
});
|
||||
}
|
||||
self.mountStatus[mountData.mount_point] = {
|
||||
type: mountData.type,
|
||||
status: 1,
|
||||
location: mountData.location,
|
||||
error: message
|
||||
};
|
||||
afterCallback(mountData, self.mountStatus[mountData.mount_point]);
|
||||
}
|
||||
});
|
||||
}
|
||||
return defObj;
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to get external mount point list from the files_external API
|
||||
* @param {Function} afterCallback function to be executed
|
||||
*/
|
||||
|
||||
getMountPointList: function (afterCallback) {
|
||||
var self = this;
|
||||
if (typeof afterCallback !== 'function' || self.isGetMountPointListRunning) {
|
||||
return;
|
||||
}
|
||||
|
||||
if (self.mountPointList) {
|
||||
afterCallback(self.mountPointList);
|
||||
} else {
|
||||
self.isGetMountPointListRunning = true;
|
||||
$.ajax({
|
||||
type: 'GET',
|
||||
url: OC.linkToOCS('apps/files_external/api/v1') + 'mounts?format=json',
|
||||
success: function (response) {
|
||||
self.mountPointList = [];
|
||||
_.each(response.ocs.data, function (mount) {
|
||||
var element = {};
|
||||
element.mount_point = mount.name;
|
||||
element.type = mount.scope;
|
||||
element.location = "";
|
||||
element.id = mount.id;
|
||||
element.backendText = mount.backend;
|
||||
element.backend = mount.class;
|
||||
|
||||
self.mountPointList.push(element);
|
||||
});
|
||||
afterCallback(self.mountPointList);
|
||||
},
|
||||
error: function (jqxhr, state, error) {
|
||||
self.mountPointList = [];
|
||||
OC.Notification.show(t('files_external', 'Couldn\'t get the list of external mount points: {type}',
|
||||
{type: error}), {type: 'error'}
|
||||
);
|
||||
},
|
||||
complete: function () {
|
||||
self.isGetMountPointListRunning = false;
|
||||
}
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to manage action when a mountpoint status = 1 (Errored). Show a dialog to be redirected to settings page.
|
||||
* @param {string} name MountPoint Name
|
||||
*/
|
||||
|
||||
manageMountPointError: function (name) {
|
||||
this.getMountStatus($.proxy(function (allMountStatus) {
|
||||
if (allMountStatus.hasOwnProperty(name) && allMountStatus[name].status > 0 && allMountStatus[name].status < 7) {
|
||||
var mountData = allMountStatus[name];
|
||||
if (mountData.type === "system") {
|
||||
if (mountData.userProvided || mountData.authMechanism === 'password::global::user') {
|
||||
// personal mount whit credentials problems
|
||||
this.showCredentialsDialog(name, mountData);
|
||||
} else if (mountData.canEdit) {
|
||||
OC.dialogs.confirm(t('files_external', 'There was an error with message: ') + mountData.error + '. Do you want to review mount point config in admin settings page?', t('files_external', 'External mount error'), function (e) {
|
||||
if (e === true) {
|
||||
OC.redirect(OC.generateUrl('/settings/admin/externalstorages'));
|
||||
}
|
||||
});
|
||||
} else {
|
||||
OC.dialogs.info(t('files_external', 'There was an error with message: ') + mountData.error + '. Please contact your system administrator.', t('files_external', 'External mount error'), () => {});
|
||||
}
|
||||
} else {
|
||||
OC.dialogs.confirm(t('files_external', 'There was an error with message: ') + mountData.error + '. Do you want to review mount point config in personal settings page?', t('files_external', 'External mount error'), function (e) {
|
||||
if (e === true) {
|
||||
OC.redirect(OC.generateUrl('/settings/personal#' + t('files_external', 'external-storage')));
|
||||
}
|
||||
});
|
||||
}
|
||||
}
|
||||
}, this));
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to process a mount point in relation with their status, Called from Async Queue.
|
||||
* @param {object} mountData
|
||||
* @param {object} mountStatus
|
||||
*/
|
||||
|
||||
processMountStatusIndividual: function (mountData, mountStatus) {
|
||||
|
||||
var mountPoint = mountData.mount_point;
|
||||
if (mountStatus.status > 0) {
|
||||
var trElement = FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(mountPoint));
|
||||
|
||||
var route = OCA.Files_External.StatusManager.Utils.getIconRoute(trElement) + '-error';
|
||||
|
||||
if (OCA.Files_External.StatusManager.Utils.isCorrectViewAndRootFolder()) {
|
||||
OCA.Files_External.StatusManager.Utils.showIconError(mountPoint, $.proxy(OCA.Files_External.StatusManager.manageMountPointError, OCA.Files_External.StatusManager), route);
|
||||
}
|
||||
return false;
|
||||
} else {
|
||||
if (OCA.Files_External.StatusManager.Utils.isCorrectViewAndRootFolder()) {
|
||||
OCA.Files_External.StatusManager.Utils.restoreFolder(mountPoint);
|
||||
OCA.Files_External.StatusManager.Utils.toggleLink(mountPoint, true, true);
|
||||
}
|
||||
return true;
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to process a mount point in relation with their status
|
||||
* @param {object} mountData
|
||||
* @param {object} mountStatus
|
||||
*/
|
||||
|
||||
processMountList: function (mountList) {
|
||||
var elementList = null;
|
||||
$.each(mountList, function (name, value) {
|
||||
var trElement = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(value.mount_point) + '\"]'); //FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(value.mount_point));
|
||||
trElement.attr('data-external-backend', value.backend);
|
||||
if (elementList) {
|
||||
elementList = elementList.add(trElement);
|
||||
} else {
|
||||
elementList = trElement;
|
||||
}
|
||||
});
|
||||
|
||||
if (elementList instanceof $) {
|
||||
if (OCA.Files_External.StatusManager.Utils.isCorrectViewAndRootFolder()) {
|
||||
// Put their custom icon
|
||||
OCA.Files_External.StatusManager.Utils.changeFolderIcon(elementList);
|
||||
// Save default view
|
||||
OCA.Files_External.StatusManager.Utils.storeDefaultFolderIconAndBgcolor(elementList);
|
||||
OCA.Files_External.StatusManager.Utils.toggleLink(elementList.find('a.name'), false, false);
|
||||
}
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* Function to process the whole mount point list in relation with their status (Async queue)
|
||||
*/
|
||||
|
||||
launchFullConnectivityCheckOneByOne: function () {
|
||||
var self = this;
|
||||
this.getMountPointList(function (list) {
|
||||
// check if we have a list first
|
||||
if (list === undefined && !self.emptyWarningShown) {
|
||||
self.emptyWarningShown = true;
|
||||
OC.Notification.show(t('files_external', 'Couldn\'t fetch list of Windows network drive mount points: Empty response from server'),
|
||||
{type: 'error'}
|
||||
);
|
||||
return;
|
||||
}
|
||||
if (list && list.length > 0) {
|
||||
self.processMountList(list);
|
||||
|
||||
if (!self.mountStatus) {
|
||||
self.mountStatus = {};
|
||||
}
|
||||
|
||||
var ajaxQueue = [];
|
||||
$.each(list, function (key, value) {
|
||||
var queueElement = {
|
||||
funcName: $.proxy(self.getMountStatusForMount, self),
|
||||
funcArgs: [value,
|
||||
$.proxy(self.processMountStatusIndividual, self)]
|
||||
};
|
||||
ajaxQueue.push(queueElement);
|
||||
});
|
||||
|
||||
var rolQueue = new OCA.Files_External.StatusManager.RollingQueue(ajaxQueue, 4, function () {
|
||||
if (!self.notificationHasShown) {
|
||||
$.each(self.mountStatus, function (key, value) {
|
||||
if (value.status === 1) {
|
||||
self.notificationHasShown = true;
|
||||
}
|
||||
});
|
||||
}
|
||||
});
|
||||
rolQueue.runQueue();
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Function to process a mount point list in relation with their status (Async queue)
|
||||
* @param {object} mountListData
|
||||
* @param {boolean} recheck delete cached info and force api call to check mount point status
|
||||
*/
|
||||
|
||||
launchPartialConnectivityCheck: function (mountListData, recheck) {
|
||||
if (mountListData.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var ajaxQueue = [];
|
||||
$.each(mountListData, function (key, value) {
|
||||
if (recheck && value.mount_point in self.mountStatus) {
|
||||
delete self.mountStatus[value.mount_point];
|
||||
}
|
||||
var queueElement = {
|
||||
funcName: $.proxy(self.getMountStatusForMount, self),
|
||||
funcArgs: [value,
|
||||
$.proxy(self.processMountStatusIndividual, self)]
|
||||
};
|
||||
ajaxQueue.push(queueElement);
|
||||
});
|
||||
new OCA.Files_External.StatusManager.RollingQueue(ajaxQueue, 4).runQueue();
|
||||
},
|
||||
|
||||
|
||||
/**
|
||||
* Function to relaunch some mount point status check
|
||||
* @param {string} mountListNames
|
||||
* @param {boolean} recheck delete cached info and force api call to check mount point status
|
||||
*/
|
||||
|
||||
recheckConnectivityForMount: function (mountListNames, recheck) {
|
||||
if (mountListNames.length === 0) {
|
||||
return;
|
||||
}
|
||||
|
||||
var self = this;
|
||||
var mountListData = [];
|
||||
|
||||
if (!self.mountStatus) {
|
||||
self.mountStatus = {};
|
||||
}
|
||||
|
||||
$.each(mountListNames, function (key, value) {
|
||||
var mountData = self.getMountPointListElement(value);
|
||||
if (mountData) {
|
||||
mountListData.push(mountData);
|
||||
}
|
||||
});
|
||||
|
||||
// for all mounts in the list, delete the cached status values
|
||||
if (recheck) {
|
||||
$.each(mountListData, function (key, value) {
|
||||
if (value.mount_point in self.mountStatus) {
|
||||
delete self.mountStatus[value.mount_point];
|
||||
}
|
||||
});
|
||||
}
|
||||
|
||||
self.processMountList(mountListData);
|
||||
self.launchPartialConnectivityCheck(mountListData, recheck);
|
||||
},
|
||||
|
||||
credentialsDialogTemplate:
|
||||
'<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>',
|
||||
|
||||
/**
|
||||
* Function to display custom dialog to enter credentials
|
||||
* @param {any} mountPoint -
|
||||
* @param {any} mountData -
|
||||
*/
|
||||
showCredentialsDialog: function (mountPoint, mountData) {
|
||||
var dialog = $(OCA.Files_External.Templates.credentialsDialog({
|
||||
credentials_text: t('files_external', 'Please enter the credentials for the {mount} mount', {
|
||||
'mount': mountPoint
|
||||
}),
|
||||
placeholder_username: t('files_external', 'Username'),
|
||||
placeholder_password: t('files_external', 'Password')
|
||||
}));
|
||||
|
||||
$('body').append(dialog);
|
||||
|
||||
var apply = function () {
|
||||
var username = dialog.find('[name=username]').val();
|
||||
var password = dialog.find('[name=password]').val();
|
||||
var endpoint = OC.generateUrl('apps/files_external/userglobalstorages/{id}', {
|
||||
id: mountData.id
|
||||
});
|
||||
$('.oc-dialog-close').hide();
|
||||
$.ajax({
|
||||
type: 'PUT',
|
||||
url: endpoint,
|
||||
data: {
|
||||
backendOptions: {
|
||||
user: username,
|
||||
password: password
|
||||
}
|
||||
},
|
||||
success: function (data) {
|
||||
OC.Notification.show(t('files_external', 'Credentials saved'), {type: 'success'});
|
||||
dialog.ocdialog('close');
|
||||
/* Trigger status check again */
|
||||
OCA.Files_External.StatusManager.recheckConnectivityForMount([OC.basename(data.mountPoint)], true);
|
||||
},
|
||||
error: function () {
|
||||
$('.oc-dialog-close').show();
|
||||
OC.Notification.show(t('files_external', 'Credentials saving failed'), {type: 'error'});
|
||||
}
|
||||
});
|
||||
return false;
|
||||
};
|
||||
|
||||
var ocdialogParams = {
|
||||
modal: true,
|
||||
title: t('files_external', 'Credentials required'),
|
||||
buttons: [{
|
||||
text: t('files_external', 'Save'),
|
||||
click: apply,
|
||||
closeOnEscape: true
|
||||
}],
|
||||
closeOnExcape: true
|
||||
};
|
||||
|
||||
dialog.ocdialog(ocdialogParams)
|
||||
.bind('ocdialogclose', function () {
|
||||
dialog.ocdialog('destroy').remove();
|
||||
});
|
||||
|
||||
dialog.find('form').on('submit', apply);
|
||||
dialog.find('form input:first').focus();
|
||||
dialog.find('form input').keyup(function (e) {
|
||||
if ((e.which && e.which === 13) || (e.keyCode && e.keyCode === 13)) {
|
||||
$(e.target).closest('form').submit();
|
||||
return false;
|
||||
} else {
|
||||
return true;
|
||||
}
|
||||
});
|
||||
}
|
||||
};
|
||||
|
||||
OCA.Files_External.StatusManager.Utils = {
|
||||
|
||||
showIconError: function (folder, clickAction, errorImageUrl) {
|
||||
var imageUrl = "url(" + errorImageUrl + ")";
|
||||
var trFolder = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]'); //FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(folder));
|
||||
this.changeFolderIcon(folder, imageUrl);
|
||||
this.toggleLink(folder, false, clickAction);
|
||||
trFolder.addClass('externalErroredRow');
|
||||
},
|
||||
|
||||
/**
|
||||
* @param folder string with the folder or jQuery element pointing to the tr element
|
||||
*/
|
||||
storeDefaultFolderIconAndBgcolor: function (folder) {
|
||||
var trFolder;
|
||||
if (folder instanceof $) {
|
||||
trFolder = folder;
|
||||
} else {
|
||||
trFolder = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]'); //FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(folder)); //$('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]');
|
||||
}
|
||||
trFolder.each(function () {
|
||||
var thisElement = $(this);
|
||||
if (thisElement.data('oldbgcolor') === undefined) {
|
||||
thisElement.data('oldbgcolor', thisElement.css('background-color'));
|
||||
}
|
||||
});
|
||||
|
||||
var icon = trFolder.find('td.filename div.thumbnail');
|
||||
icon.each(function () {
|
||||
var thisElement = $(this);
|
||||
if (thisElement.data('oldImage') === undefined) {
|
||||
thisElement.data('oldImage', thisElement.css('background-image'));
|
||||
}
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param folder string with the folder or jQuery element pointing to the tr element
|
||||
*/
|
||||
restoreFolder: function (folder) {
|
||||
var trFolder;
|
||||
if (folder instanceof $) {
|
||||
trFolder = folder;
|
||||
} else {
|
||||
// can't use here FileList.findFileEl(OCA.Files_External.StatusManager.Utils.jqSelEscape(folder)); return incorrect instance of filelist
|
||||
trFolder = $('.files-fileList tr[data-file=\"' + OCA.Files_External.StatusManager.Utils.jqSelEscape(folder) + '\"]');
|
||||
}
|
||||
var tdChilds = trFolder.find("td.filename div.thumbnail");
|
||||
tdChilds.each(function () {
|
||||
var thisElement = $(this);
|
||||
thisElement.css('background-image', thisElement.data('oldImage'));
|
||||
});
|
||||
},
|
||||
|
||||
/**
|
||||
* @param folder string with the folder or jQuery element pointing to the first td element
|
||||
* of the tr matching the folder name
|
||||
*/
|
||||
changeFolderIcon: function (filename) {
|
||||
var file;
|
||||
var route;
|
||||
if (filename instanceof $) {
|
||||
//trElementList
|
||||
$.each(filename, function (index) {
|
||||
route = OCA.Files_External.StatusManager.Utils.getIconRoute($(this));
|
||||
$(this).attr("data-icon", route);
|
||||
$(this).find('td.filename div.thumbnail').css('background-image', "url(" + route + ")").css('display', 'none').css('display', 'inline');
|
||||
});
|
||||
} else {
|
||||
file = $(".files-fileList tr[data-file=\"" + this.jqSelEscape(filename) + "\"] > td.filename div.thumbnail");
|
||||
var parentTr = file.parents('tr:first');
|
||||
route = OCA.Files_External.StatusManager.Utils.getIconRoute(parentTr);
|
||||
parentTr.attr("data-icon", route);
|
||||
file.css('background-image', "url(" + route + ")").css('display', 'none').css('display', 'inline');
|
||||
}
|
||||
},
|
||||
|
||||
/**
|
||||
* @param backend string with the name of the external storage backend
|
||||
* of the tr matching the folder name
|
||||
*/
|
||||
getIconRoute: function (tr) {
|
||||
if (OCA.Theming) {
|
||||
var icon = OC.generateUrl('/apps/theming/img/core/filetypes/folder-external.svg?v=' + OCA.Theming.cacheBuster);
|
||||
} else {
|
||||
var icon = OC.imagePath('core', 'filetypes/folder-external');
|
||||
}
|
||||
var backend = null;
|
||||
|
||||
if (tr instanceof $) {
|
||||
backend = tr.attr('data-external-backend');
|
||||
}
|
||||
|
||||
switch (backend) {
|
||||
case 'windows_network_drive':
|
||||
icon = OC.imagePath('windows_network_drive', 'folder-windows');
|
||||
break;
|
||||
}
|
||||
|
||||
return icon;
|
||||
},
|
||||
|
||||
toggleLink: function (filename, active, action) {
|
||||
var link;
|
||||
if (filename instanceof $) {
|
||||
link = filename;
|
||||
} else {
|
||||
link = $(".files-fileList tr[data-file=\"" + this.jqSelEscape(filename) + "\"] > td.filename a.name");
|
||||
}
|
||||
if (active) {
|
||||
link.off('click.connectivity');
|
||||
OCA.Files.App.fileList.fileActions.display(link.parent(), true, OCA.Files.App.fileList);
|
||||
} else {
|
||||
link.find('.fileactions, .nametext .action').remove(); // from files/js/fileactions (display)
|
||||
link.off('click.connectivity');
|
||||
link.on('click.connectivity', function (e) {
|
||||
if (action && $.isFunction(action)) {
|
||||
action(filename);
|
||||
}
|
||||
e.preventDefault();
|
||||
return false;
|
||||
});
|
||||
}
|
||||
},
|
||||
|
||||
isCorrectViewAndRootFolder: function () {
|
||||
// correct views = files & extstoragemounts
|
||||
if (OCA.Files.App.getActiveView() === 'files' || OCA.Files.App.getActiveView() === 'extstoragemounts') {
|
||||
return OCA.Files.App.currentFileList.getCurrentDirectory() === '/';
|
||||
}
|
||||
return false;
|
||||
},
|
||||
|
||||
/* escape a selector expression for jQuery */
|
||||
jqSelEscape: function (expression) {
|
||||
if (expression) {
|
||||
return expression.replace(/[!"#$%&'()*+,.\/:;<=>?@\[\\\]^`{|}~]/g, '\\$&');
|
||||
}
|
||||
return null;
|
||||
},
|
||||
|
||||
/* Copied from http://stackoverflow.com/questions/2631001/javascript-test-for-existence-of-nested-object-key */
|
||||
checkNested: function (cobj /*, level1, level2, ... levelN*/) {
|
||||
var args = Array.prototype.slice.call(arguments),
|
||||
obj = args.shift();
|
||||
|
||||
for (var i = 0; i < args.length; i++) {
|
||||
if (!obj || !obj.hasOwnProperty(args[i])) {
|
||||
return false;
|
||||
}
|
||||
obj = obj[args[i]];
|
||||
}
|
||||
return true;
|
||||
}
|
||||
};
|
||||
|
|
@ -29,6 +29,7 @@
|
|||
*/
|
||||
namespace OCA\Files_External\AppInfo;
|
||||
|
||||
use OCA\Files\Event\LoadAdditionalScriptsEvent;
|
||||
use OCA\Files_External\Config\ConfigAdapter;
|
||||
use OCA\Files_External\Config\UserPlaceholderHandler;
|
||||
use OCA\Files_External\Lib\Auth\AmazonS3\AccessKey;
|
||||
|
|
@ -62,6 +63,7 @@ use OCA\Files_External\Lib\Backend\Swift;
|
|||
use OCA\Files_External\Lib\Config\IAuthMechanismProvider;
|
||||
use OCA\Files_External\Lib\Config\IBackendProvider;
|
||||
use OCA\Files_External\Listener\GroupDeletedListener;
|
||||
use OCA\Files_External\Listener\LoadAdditionalListener;
|
||||
use OCA\Files_External\Listener\UserDeletedListener;
|
||||
use OCA\Files_External\Service\BackendService;
|
||||
use OCP\AppFramework\App;
|
||||
|
|
@ -78,6 +80,7 @@ require_once __DIR__ . '/../../3rdparty/autoload.php';
|
|||
* @package OCA\Files_External\AppInfo
|
||||
*/
|
||||
class Application extends App implements IBackendProvider, IAuthMechanismProvider, IBootstrap {
|
||||
public const APP_ID = 'files_external';
|
||||
|
||||
/**
|
||||
* Application constructor.
|
||||
|
|
@ -85,28 +88,19 @@ class Application extends App implements IBackendProvider, IAuthMechanismProvide
|
|||
* @throws \OCP\AppFramework\QueryException
|
||||
*/
|
||||
public function __construct(array $urlParams = []) {
|
||||
parent::__construct('files_external', $urlParams);
|
||||
parent::__construct(self::APP_ID, $urlParams);
|
||||
}
|
||||
|
||||
public function register(IRegistrationContext $context): void {
|
||||
$context->registerEventListener(UserDeletedEvent::class, UserDeletedListener::class);
|
||||
$context->registerEventListener(GroupDeletedEvent::class, GroupDeletedListener::class);
|
||||
$context->registerEventListener(LoadAdditionalScriptsEvent::class, LoadAdditionalListener::class);
|
||||
}
|
||||
|
||||
public function boot(IBootContext $context): void {
|
||||
$context->injectFn(function (IMountProviderCollection $mountProviderCollection, ConfigAdapter $configAdapter) {
|
||||
$mountProviderCollection->registerProvider($configAdapter);
|
||||
});
|
||||
\OCA\Files\App::getNavigationManager()->add(function () {
|
||||
$l = \OC::$server->getL10N('files_external');
|
||||
return [
|
||||
'id' => 'extstoragemounts',
|
||||
'appname' => 'files_external',
|
||||
'script' => 'list.php',
|
||||
'order' => 30,
|
||||
'name' => $l->t('External storage'),
|
||||
];
|
||||
});
|
||||
$context->injectFn(function (BackendService $backendService, UserPlaceholderHandler $userConfigHandler) {
|
||||
$backendService->registerBackendProvider($this);
|
||||
$backendService->registerAuthMechanismProvider($this);
|
||||
|
|
|
|||
|
|
@ -37,30 +37,22 @@ use OCP\AppFramework\Http;
|
|||
use OCP\AppFramework\Http\DataResponse;
|
||||
use OCP\AppFramework\OCSController;
|
||||
use OCP\IRequest;
|
||||
use OCP\IUserSession;
|
||||
|
||||
/**
|
||||
* @psalm-import-type FilesExternalMount from ResponseDefinitions
|
||||
*/
|
||||
class ApiController extends OCSController {
|
||||
|
||||
/** @var IUserSession */
|
||||
private $userSession;
|
||||
/** @var UserGlobalStoragesService */
|
||||
private $userGlobalStoragesService;
|
||||
/** @var UserStoragesService */
|
||||
private $userStoragesService;
|
||||
private UserGlobalStoragesService $userGlobalStoragesService;
|
||||
private UserStoragesService $userStoragesService;
|
||||
|
||||
public function __construct(
|
||||
string $appName,
|
||||
IRequest $request,
|
||||
IUserSession $userSession,
|
||||
UserGlobalStoragesService $userGlobalStorageService,
|
||||
UserStoragesService $userStorageService
|
||||
) {
|
||||
parent::__construct($appName, $request);
|
||||
|
||||
$this->userSession = $userSession;
|
||||
$this->userGlobalStoragesService = $userGlobalStorageService;
|
||||
$this->userStoragesService = $userStorageService;
|
||||
}
|
||||
|
|
@ -89,14 +81,15 @@ class ApiController extends OCSController {
|
|||
}
|
||||
|
||||
$entry = [
|
||||
'id' => $mountConfig->getId(),
|
||||
'type' => 'dir',
|
||||
'name' => basename($mountPoint),
|
||||
'path' => $path,
|
||||
'type' => 'dir',
|
||||
'backend' => $mountConfig->getBackend()->getText(),
|
||||
'scope' => $isSystemMount ? 'system' : 'personal',
|
||||
'permissions' => $permissions,
|
||||
'id' => $mountConfig->getId(),
|
||||
'scope' => $isSystemMount ? 'system' : 'personal',
|
||||
'backend' => $mountConfig->getBackend()->getText(),
|
||||
'class' => $mountConfig->getBackend()->getIdentifier(),
|
||||
'config' => $mountConfig->jsonSerialize(true),
|
||||
];
|
||||
return $entry;
|
||||
}
|
||||
|
|
@ -127,4 +120,31 @@ class ApiController extends OCSController {
|
|||
|
||||
return new DataResponse($entries);
|
||||
}
|
||||
|
||||
/**
|
||||
* @NoAdminRequired
|
||||
*
|
||||
* Ask for credentials using a browser's native basic auth prompt
|
||||
* Then returns it if provided
|
||||
*/
|
||||
public function askNativeAuth(): DataResponse {
|
||||
if (!isset($_SERVER['PHP_AUTH_USER']) || !isset($_SERVER['PHP_AUTH_PW'])) {
|
||||
$response = new DataResponse([], Http::STATUS_UNAUTHORIZED);
|
||||
$response->addHeader('WWW-Authenticate', 'Basic realm="Storage authentification needed"');
|
||||
return $response;
|
||||
}
|
||||
|
||||
$user = $_SERVER['PHP_AUTH_USER'];
|
||||
$password = $_SERVER['PHP_AUTH_PW'];
|
||||
|
||||
// Reset auth
|
||||
unset($_SERVER['PHP_AUTH_USER']);
|
||||
unset($_SERVER['PHP_AUTH_PW']);
|
||||
|
||||
// Using 401 again to ensure we clear any cached Authorization
|
||||
return new DataResponse([
|
||||
'user' => $user,
|
||||
'password' => $password,
|
||||
], Http::STATUS_UNAUTHORIZED);
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -134,7 +134,7 @@ class GlobalStoragesController extends StoragesController {
|
|||
$this->updateStorageStatus($newStorage);
|
||||
|
||||
return new DataResponse(
|
||||
$this->formatStorageForUI($newStorage),
|
||||
$newStorage->jsonSerialize(true),
|
||||
Http::STATUS_CREATED
|
||||
);
|
||||
}
|
||||
|
|
@ -201,7 +201,7 @@ class GlobalStoragesController extends StoragesController {
|
|||
$this->updateStorageStatus($storage, $testOnly);
|
||||
|
||||
return new DataResponse(
|
||||
$this->formatStorageForUI($storage),
|
||||
$storage->jsonSerialize(true),
|
||||
Http::STATUS_OK
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -276,7 +276,7 @@ abstract class StoragesController extends Controller {
|
|||
* @return DataResponse
|
||||
*/
|
||||
public function index() {
|
||||
$storages = $this->formatStoragesForUI($this->service->getStorages());
|
||||
$storages = array_map(static fn ($storage) => $storage->jsonSerialize(true), $this->service->getStorages());
|
||||
|
||||
return new DataResponse(
|
||||
$storages,
|
||||
|
|
@ -284,29 +284,6 @@ abstract class StoragesController extends Controller {
|
|||
);
|
||||
}
|
||||
|
||||
protected function formatStoragesForUI(array $storages): array {
|
||||
return array_map(function ($storage) {
|
||||
return $this->formatStorageForUI($storage);
|
||||
}, $storages);
|
||||
}
|
||||
|
||||
protected function formatStorageForUI(StorageConfig $storage): StorageConfig {
|
||||
/** @var DefinitionParameter[] $parameters */
|
||||
$parameters = array_merge($storage->getBackend()->getParameters(), $storage->getAuthMechanism()->getParameters());
|
||||
|
||||
$options = $storage->getBackendOptions();
|
||||
foreach ($options as $key => $value) {
|
||||
foreach ($parameters as $parameter) {
|
||||
if ($parameter->getName() === $key && $parameter->getType() === DefinitionParameter::VALUE_PASSWORD) {
|
||||
$storage->setBackendOption($key, DefinitionParameter::UNMODIFIED_PLACEHOLDER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
return $storage;
|
||||
}
|
||||
|
||||
/**
|
||||
* Get an external storage entry.
|
||||
*
|
||||
|
|
@ -329,7 +306,7 @@ abstract class StoragesController extends Controller {
|
|||
);
|
||||
}
|
||||
|
||||
$data = $this->formatStorageForUI($storage)->jsonSerialize();
|
||||
$data = $storage->jsonSerialize(true);
|
||||
$isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID());
|
||||
$data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAl || $isAdmin;
|
||||
|
||||
|
|
|
|||
|
|
@ -88,12 +88,13 @@ class UserGlobalStoragesController extends StoragesController {
|
|||
* @NoAdminRequired
|
||||
*/
|
||||
public function index() {
|
||||
$storages = $this->formatStoragesForUI($this->service->getUniqueStorages());
|
||||
|
||||
// remove configuration data, this must be kept private
|
||||
foreach ($storages as $storage) {
|
||||
/** @var UserGlobalStoragesService */
|
||||
$service = $this->service;
|
||||
$storages = array_map(function ($storage) {
|
||||
// remove configuration data, this must be kept private
|
||||
$this->sanitizeStorage($storage);
|
||||
}
|
||||
return $storage->jsonSerialize(true);
|
||||
}, $service->getUniqueStorages());
|
||||
|
||||
return new DataResponse(
|
||||
$storages,
|
||||
|
|
@ -135,7 +136,7 @@ class UserGlobalStoragesController extends StoragesController {
|
|||
|
||||
$this->sanitizeStorage($storage);
|
||||
|
||||
$data = $this->formatStorageForUI($storage)->jsonSerialize();
|
||||
$data = $storage->jsonSerialize(true);
|
||||
$isAdmin = $this->groupManager->isAdmin($this->userSession->getUser()->getUID());
|
||||
$data['can_edit'] = $storage->getType() === StorageConfig::MOUNT_TYPE_PERSONAl || $isAdmin;
|
||||
|
||||
|
|
@ -189,7 +190,7 @@ class UserGlobalStoragesController extends StoragesController {
|
|||
$this->sanitizeStorage($storage);
|
||||
|
||||
return new DataResponse(
|
||||
$this->formatStorageForUI($storage),
|
||||
$storage->jsonSerialize(true),
|
||||
Http::STATUS_OK
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -159,7 +159,7 @@ class UserStoragesController extends StoragesController {
|
|||
$this->updateStorageStatus($newStorage);
|
||||
|
||||
return new DataResponse(
|
||||
$this->formatStorageForUI($newStorage),
|
||||
$newStorage->jsonSerialize(true),
|
||||
Http::STATUS_CREATED
|
||||
);
|
||||
}
|
||||
|
|
@ -219,7 +219,7 @@ class UserStoragesController extends StoragesController {
|
|||
$this->updateStorageStatus($storage, $testOnly);
|
||||
|
||||
return new DataResponse(
|
||||
$this->formatStorageForUI($storage),
|
||||
$storage->jsonSerialize(true),
|
||||
Http::STATUS_OK
|
||||
);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -58,6 +58,10 @@ class SessionCredentials extends AuthMechanism {
|
|||
throw new InsufficientDataForMeaningfulAnswerException('No session credentials saved');
|
||||
}
|
||||
|
||||
if ($user === null) {
|
||||
throw new StorageAuthException('Session unavailable');
|
||||
}
|
||||
|
||||
if ($credentials->getUID() !== $user->getUID()) {
|
||||
throw new StorageAuthException('Session credentials for storage owner not available');
|
||||
}
|
||||
|
|
|
|||
|
|
@ -397,11 +397,17 @@ class StorageConfig implements \JsonSerializable {
|
|||
/**
|
||||
* Serialize config to JSON
|
||||
*/
|
||||
public function jsonSerialize(): array {
|
||||
public function jsonSerialize(bool $obfuscate = false): array {
|
||||
$result = [];
|
||||
if (!is_null($this->id)) {
|
||||
$result['id'] = $this->id;
|
||||
}
|
||||
|
||||
// obfuscate sensitive data if requested
|
||||
if ($obfuscate) {
|
||||
$this->formatStorageForUI();
|
||||
}
|
||||
|
||||
$result['mountPoint'] = $this->mountPoint;
|
||||
$result['backend'] = $this->backend->getIdentifier();
|
||||
$result['authMechanism'] = $this->authMechanism->getIdentifier();
|
||||
|
|
@ -428,4 +434,19 @@ class StorageConfig implements \JsonSerializable {
|
|||
$result['type'] = ($this->getType() === self::MOUNT_TYPE_PERSONAl) ? 'personal': 'system';
|
||||
return $result;
|
||||
}
|
||||
|
||||
protected function formatStorageForUI(): void {
|
||||
/** @var DefinitionParameter[] $parameters */
|
||||
$parameters = array_merge($this->getBackend()->getParameters(), $this->getAuthMechanism()->getParameters());
|
||||
|
||||
$options = $this->getBackendOptions();
|
||||
foreach ($options as $key => $value) {
|
||||
foreach ($parameters as $parameter) {
|
||||
if ($parameter->getName() === $key && $parameter->getType() === DefinitionParameter::VALUE_PASSWORD) {
|
||||
$this->setBackendOption($key, DefinitionParameter::UNMODIFIED_PLACEHOLDER);
|
||||
break;
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
|
|
|||
55
apps/files_external/lib/Listener/LoadAdditionalListener.php
Normal file
55
apps/files_external/lib/Listener/LoadAdditionalListener.php
Normal file
|
|
@ -0,0 +1,55 @@
|
|||
<?php
|
||||
|
||||
declare(strict_types=1);
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2019, Roeland Jago Douma <roeland@famdouma.nl>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @license GNU AGPL version 3 or any later version
|
||||
*
|
||||
* 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/>.
|
||||
*
|
||||
*/
|
||||
namespace OCA\Files_External\Listener;
|
||||
|
||||
use OCA\Files_External\AppInfo\Application;
|
||||
use OCA\Files\Event\LoadAdditionalScriptsEvent;
|
||||
use OCP\AppFramework\Services\IInitialState;
|
||||
use OCP\EventDispatcher\Event;
|
||||
use OCP\EventDispatcher\IEventListener;
|
||||
use OCP\IConfig;
|
||||
use OCP\Util;
|
||||
|
||||
/**
|
||||
* @template-implements IEventListener<Event|LoadAdditionalScriptsEvent>
|
||||
*/
|
||||
class LoadAdditionalListener implements IEventListener {
|
||||
|
||||
public function __construct(
|
||||
private IConfig $config,
|
||||
private IInitialState $initialState,
|
||||
) {}
|
||||
|
||||
public function handle(Event $event): void {
|
||||
if (!($event instanceof LoadAdditionalScriptsEvent)) {
|
||||
return;
|
||||
}
|
||||
|
||||
$allowUserMounting = $this->config->getAppValue('files_external', 'allow_user_mounting', 'no') === 'yes';
|
||||
$this->initialState->provideInitialState('allowUserMounting', $allowUserMounting);
|
||||
Util::addScript(Application::APP_ID, 'main', 'files');
|
||||
}
|
||||
}
|
||||
|
|
@ -35,6 +35,7 @@ namespace OCA\Files_External;
|
|||
* permissions: int,
|
||||
* id: int,
|
||||
* class: string,
|
||||
* config: array<array-key, mixed>,
|
||||
* }
|
||||
*/
|
||||
class ResponseDefinitions {
|
||||
|
|
|
|||
|
|
@ -1,46 +0,0 @@
|
|||
<?php
|
||||
/**
|
||||
* @copyright Copyright (c) 2016, ownCloud, Inc.
|
||||
*
|
||||
* @author Christoph Wurst <christoph@winzerhof-wurst.at>
|
||||
* @author Jesús Macias <jmacias@solidgear.es>
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
* @author Julius Härtl <jus@bitgrid.net>
|
||||
* @author Vincent Petry <vincent@nextcloud.com>
|
||||
*
|
||||
* @license AGPL-3.0
|
||||
*
|
||||
* This code is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License, version 3,
|
||||
* as published by the Free Software Foundation.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License, version 3,
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>
|
||||
*
|
||||
*/
|
||||
|
||||
$config = \OC::$server->getConfig();
|
||||
$userSession = \OC::$server->getUserSession();
|
||||
|
||||
$showgridview = $config->getUserValue($userSession->getUser()->getUID(), 'files', 'show_grid', true);
|
||||
|
||||
$tmpl = new OCP\Template('files_external', 'list', '');
|
||||
|
||||
// gridview not available for ie
|
||||
$tmpl->assign('showgridview', $showgridview);
|
||||
|
||||
/* Load Status Manager */
|
||||
\OCP\Util::addStyle('files_external', 'external');
|
||||
\OCP\Util::addScript('files_external', 'statusmanager');
|
||||
\OCP\Util::addScript('files_external', 'templates');
|
||||
\OCP\Util::addScript('files_external', 'rollingqueue');
|
||||
|
||||
OCP\Util::addScript('files_external', 'app');
|
||||
OCP\Util::addScript('files_external', 'mountsfilelist');
|
||||
|
||||
$tmpl->printPage();
|
||||
145
apps/files_external/src/actions/enterCredentialsAction.spec.ts
Normal file
145
apps/files_external/src/actions/enterCredentialsAction.spec.ts
Normal file
|
|
@ -0,0 +1,145 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
import { action } from './enterCredentialsAction'
|
||||
import { expect } from '@jest/globals'
|
||||
import { File, Folder, Permission } from '@nextcloud/files'
|
||||
import { DefaultType, FileAction } from '../../../files/src/services/FileAction'
|
||||
import type { Navigation } from '../../../files/src/services/Navigation'
|
||||
import type { StorageConfig } from '../services/externalStorage'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
} as Navigation
|
||||
|
||||
const externalStorageView = {
|
||||
id: 'extstoragemounts',
|
||||
name: 'External storage',
|
||||
} as Navigation
|
||||
|
||||
describe('Enter credentials action conditions tests', () => {
|
||||
test('Default values', () => {
|
||||
const storage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('credentials-external-storage')
|
||||
expect(action.displayName([storage], externalStorageView)).toBe('Enter missing credentials')
|
||||
expect(action.iconSvgInline([storage], externalStorageView)).toBe('<svg>SvgMock</svg>')
|
||||
expect(action.default).toBe(DefaultType.DEFAULT)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline!(storage, externalStorageView)).toBe(true)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Enter credentials action enabled tests', () => {
|
||||
const storage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const userProvidedStorage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
status: STORAGE_STATUS.INCOMPLETE_CONF,
|
||||
userProvided: true,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const globalAuthUserStorage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
scope: 'system',
|
||||
backend: 'SFTP',
|
||||
config: {
|
||||
status: STORAGE_STATUS.INCOMPLETE_CONF,
|
||||
authMechanism: 'password::global::user',
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const notAStorage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
})
|
||||
|
||||
test('Disabled with on success storage', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([storage], externalStorageView)).toBe(false)
|
||||
})
|
||||
|
||||
test('Disabled for multiple nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([storage, storage], view)).toBe(false)
|
||||
})
|
||||
|
||||
test('Enabled for missing user auth storage', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([userProvidedStorage], view)).toBe(true)
|
||||
})
|
||||
|
||||
test('Enabled for missing global user auth storage', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([globalAuthUserStorage], view)).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled for normal nodes', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([notAStorage], view)).toBe(false)
|
||||
})
|
||||
})
|
||||
110
apps/files_external/src/actions/enterCredentialsAction.ts
Normal file
110
apps/files_external/src/actions/enterCredentialsAction.ts
Normal file
|
|
@ -0,0 +1,110 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
// eslint-disable-next-line n/no-extraneous-import
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { StorageConfig } from '../services/externalStorage'
|
||||
|
||||
import { generateOcsUrl, generateUrl } from '@nextcloud/router'
|
||||
import { showError, showSuccess } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import axios from '@nextcloud/axios'
|
||||
import LoginSvg from '@mdi/svg/svg/login.svg?raw'
|
||||
import Vue from 'vue'
|
||||
|
||||
import { registerFileAction, FileAction, DefaultType } from '../../../files/src/services/FileAction'
|
||||
import { STORAGE_STATUS, isMissingAuthConfig } from '../utils/credentialsUtils'
|
||||
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
|
||||
|
||||
type OCSAuthResponse = {
|
||||
ocs: {
|
||||
meta: {
|
||||
status: string
|
||||
statuscode: number
|
||||
message: string
|
||||
},
|
||||
data: {
|
||||
user?: string,
|
||||
password?: string,
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'credentials-external-storage',
|
||||
displayName: () => t('files', 'Enter missing credentials'),
|
||||
iconSvgInline: () => LoginSvg,
|
||||
|
||||
enabled: (nodes: Node[]) => {
|
||||
// Only works on single node
|
||||
if (nodes.length !== 1) {
|
||||
return false
|
||||
}
|
||||
|
||||
const node = nodes[0]
|
||||
if (!isNodeExternalStorage(node)) {
|
||||
return false
|
||||
}
|
||||
|
||||
const config = (node.attributes?.config || {}) as StorageConfig
|
||||
if (isMissingAuthConfig(config)) {
|
||||
return true
|
||||
}
|
||||
|
||||
return false
|
||||
},
|
||||
|
||||
async exec(node: Node) {
|
||||
// always resolve auth request, we'll process the data afterwards
|
||||
const response = await axios.get(generateOcsUrl('/apps/files_external/api/v1/auth'), {
|
||||
validateStatus: () => true,
|
||||
})
|
||||
|
||||
const data = (response?.data || {}) as OCSAuthResponse
|
||||
if (data.ocs.data.user && data.ocs.data.password) {
|
||||
const configResponse = await axios.put(generateUrl('apps/files_external/userglobalstorages/{id}', node.attributes), {
|
||||
backendOptions: data.ocs.data,
|
||||
}) as AxiosResponse<StorageConfig>
|
||||
|
||||
const config = configResponse.data
|
||||
if (config.status !== STORAGE_STATUS.SUCCESS) {
|
||||
showError(t('files_external', 'Unable to update this external storage config. {statusMessage}', {
|
||||
statusMessage: config?.statusMessage || '',
|
||||
}))
|
||||
return null
|
||||
}
|
||||
|
||||
// Success update config attribute
|
||||
showSuccess(t('files_external', 'New configuration successfully saved'))
|
||||
Vue.set(node.attributes, 'config', config)
|
||||
}
|
||||
|
||||
return null
|
||||
},
|
||||
|
||||
// Before openFolderAction
|
||||
order: -1000,
|
||||
default: DefaultType.DEFAULT,
|
||||
inline: () => true,
|
||||
})
|
||||
|
||||
registerFileAction(action)
|
||||
96
apps/files_external/src/actions/inlineStorageCheckAction.ts
Normal file
96
apps/files_external/src/actions/inlineStorageCheckAction.ts
Normal file
|
|
@ -0,0 +1,96 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
// eslint-disable-next-line n/no-extraneous-import
|
||||
import type { AxiosError } from 'axios'
|
||||
import type { Node } from '@nextcloud/files'
|
||||
|
||||
import { showWarning } from '@nextcloud/dialogs'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import AlertSvg from '@mdi/svg/svg/alert-circle.svg?raw'
|
||||
import Vue from 'vue'
|
||||
|
||||
import '../css/fileEntryStatus.scss'
|
||||
import { getStatus, type StorageConfig } from '../services/externalStorage'
|
||||
import { isMissingAuthConfig, STORAGE_STATUS } from '../utils/credentialsUtils'
|
||||
import { isNodeExternalStorage } from '../utils/externalStorageUtils'
|
||||
import { registerFileAction, FileAction } from '../../../files/src/services/FileAction'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'check-external-storage',
|
||||
displayName: () => '',
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled: (nodes: Node[]) => {
|
||||
return nodes.every(node => isNodeExternalStorage(node) === true)
|
||||
},
|
||||
exec: async () => null,
|
||||
|
||||
/**
|
||||
* Use this function to check the storage availability
|
||||
* We then update the node attributes directly.
|
||||
*/
|
||||
async renderInline(node: Node) {
|
||||
let config = null as any as StorageConfig
|
||||
try {
|
||||
const response = await getStatus(node.attributes.id, node.attributes.scope === 'system')
|
||||
config = response.data
|
||||
Vue.set(node.attributes, 'config', config)
|
||||
|
||||
if (config.status !== STORAGE_STATUS.SUCCESS) {
|
||||
throw new Error(config?.statusMessage || t('files_external', 'There was an error with this external storage.'))
|
||||
}
|
||||
|
||||
return null
|
||||
} 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,
|
||||
}))
|
||||
return null
|
||||
}
|
||||
|
||||
// Checking if we really have an error
|
||||
const isWarning = isMissingAuthConfig(config)
|
||||
const overlay = document.createElement('span')
|
||||
overlay.classList.add(`files-list__row-status--${isWarning ? 'warning' : 'error'}`)
|
||||
|
||||
const span = document.createElement('span')
|
||||
span.className = 'files-list__row-status'
|
||||
|
||||
// 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)
|
||||
return span
|
||||
}
|
||||
},
|
||||
|
||||
order: 10,
|
||||
})
|
||||
|
||||
registerFileAction(action)
|
||||
140
apps/files_external/src/actions/openInFilesAction.spec.ts
Normal file
140
apps/files_external/src/actions/openInFilesAction.spec.ts
Normal file
|
|
@ -0,0 +1,140 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
import { action } from './openInFilesAction'
|
||||
import { expect } from '@jest/globals'
|
||||
import { File, Folder, Permission } from '@nextcloud/files'
|
||||
import { DefaultType, FileAction } from '../../../files/src/services/FileAction'
|
||||
import type { Navigation } from '../../../files/src/services/Navigation'
|
||||
import type { StorageConfig } from '../services/externalStorage'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils'
|
||||
|
||||
const view = {
|
||||
id: 'files',
|
||||
name: 'Files',
|
||||
} as Navigation
|
||||
|
||||
const externalStorageView = {
|
||||
id: 'extstoragemounts',
|
||||
name: 'External storage',
|
||||
} as Navigation
|
||||
|
||||
describe('Open in files action conditions tests', () => {
|
||||
test('Default values', () => {
|
||||
const storage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
expect(action).toBeInstanceOf(FileAction)
|
||||
expect(action.id).toBe('open-in-files-external-storage')
|
||||
expect(action.displayName([storage], externalStorageView)).toBe('Open in files')
|
||||
expect(action.iconSvgInline([storage], externalStorageView)).toBe('')
|
||||
expect(action.default).toBe(DefaultType.HIDDEN)
|
||||
expect(action.order).toBe(-1000)
|
||||
expect(action.inline).toBeUndefined()
|
||||
})
|
||||
|
||||
test('Default values', () => {
|
||||
const failingStorage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: STORAGE_STATUS.ERROR,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
expect(action.displayName([failingStorage], externalStorageView)).toBe('Examine this faulty external storage configuration')
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open in files action enabled tests', () => {
|
||||
test('Enabled with on valid view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], externalStorageView)).toBe(true)
|
||||
})
|
||||
|
||||
test('Disabled on wrong view', () => {
|
||||
expect(action.enabled).toBeDefined()
|
||||
expect(action.enabled!([], view)).toBe(false)
|
||||
})
|
||||
})
|
||||
|
||||
describe('Open in files action execute tests', () => {
|
||||
test('Open in files', async () => {
|
||||
const goToRouteMock = jest.fn()
|
||||
window.OCP = { Files: { Router: { goToRoute: goToRouteMock } } }
|
||||
|
||||
const storage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: STORAGE_STATUS.SUCCESS,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const exec = await action.exec(storage, externalStorageView, '/')
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(goToRouteMock).toBeCalledTimes(1)
|
||||
expect(goToRouteMock).toBeCalledWith(null, { view: 'files' }, { dir: '/Foo/Bar' })
|
||||
})
|
||||
|
||||
test('Open in files broken storage', async () => {
|
||||
const confirmMock = jest.fn()
|
||||
window.OC = { dialogs: { confirm: confirmMock } }
|
||||
|
||||
const storage = new Folder({
|
||||
id: 1,
|
||||
source: 'https://cloud.domain.com/remote.php/dav/files/admin/Foo/Bar',
|
||||
owner: 'admin',
|
||||
root: '/files/admin',
|
||||
permissions: Permission.ALL,
|
||||
attributes: {
|
||||
config: {
|
||||
status: STORAGE_STATUS.ERROR,
|
||||
} as StorageConfig,
|
||||
},
|
||||
})
|
||||
|
||||
const exec = await action.exec(storage, externalStorageView, '/')
|
||||
// Silent action
|
||||
expect(exec).toBe(null)
|
||||
expect(confirmMock).toBeCalledTimes(1)
|
||||
})
|
||||
})
|
||||
75
apps/files_external/src/actions/openInFilesAction.ts
Normal file
75
apps/files_external/src/actions/openInFilesAction.ts
Normal file
|
|
@ -0,0 +1,75 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
import type { Node } from '@nextcloud/files'
|
||||
import type { StorageConfig } from '../services/externalStorage'
|
||||
|
||||
import { generateUrl } from '@nextcloud/router'
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
|
||||
import { registerFileAction, FileAction, DefaultType } from '../../../files/src/services/FileAction'
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils'
|
||||
|
||||
export const action = new FileAction({
|
||||
id: 'open-in-files-external-storage',
|
||||
displayName: (nodes: Node[]) => {
|
||||
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')
|
||||
},
|
||||
iconSvgInline: () => '',
|
||||
|
||||
enabled: (nodes: Node[], view) => view.id === 'extstoragemounts',
|
||||
|
||||
async exec(node: Node) {
|
||||
const config = node.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 = node.attributes.scope === 'personal' ? 'user' : 'admin'
|
||||
window.location.href = generateUrl(`/settings/${scope}/externalstorages`)
|
||||
}
|
||||
},
|
||||
)
|
||||
return null
|
||||
}
|
||||
|
||||
// Do not use fileid as we don't have that information
|
||||
// from the external storage api
|
||||
window.OCP.Files.Router.goToRoute(
|
||||
null, // use default route
|
||||
{ view: 'files' },
|
||||
{ dir: node.path },
|
||||
)
|
||||
return null
|
||||
},
|
||||
|
||||
// Before openFolderAction
|
||||
order: -1000,
|
||||
default: DefaultType.HIDDEN,
|
||||
})
|
||||
|
||||
registerFileAction(action)
|
||||
36
apps/files_external/src/css/fileEntryStatus.scss
Normal file
36
apps/files_external/src/css/fileEntryStatus.scss
Normal file
|
|
@ -0,0 +1,36 @@
|
|||
.files-list__row-status {
|
||||
display: flex;
|
||||
width: 44px;
|
||||
justify-content: center;
|
||||
align-items: center;
|
||||
height: 100%;
|
||||
|
||||
svg {
|
||||
width: 24px;
|
||||
height: 24px;
|
||||
|
||||
path {
|
||||
fill: currentColor;
|
||||
}
|
||||
}
|
||||
|
||||
&--error,
|
||||
&--warning {
|
||||
position: absolute;
|
||||
display: block;
|
||||
top: 0;
|
||||
left: 0;
|
||||
right: 0;
|
||||
bottom: 0;
|
||||
opacity: .1;
|
||||
z-index: -1;
|
||||
}
|
||||
|
||||
&--error {
|
||||
background: var(--color-error);
|
||||
}
|
||||
|
||||
&--warning {
|
||||
background: var(--color-warning);
|
||||
}
|
||||
}
|
||||
77
apps/files_external/src/main.ts
Normal file
77
apps/files_external/src/main.ts
Normal file
|
|
@ -0,0 +1,77 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
import type NavigationService from '../../files/src/services/Navigation'
|
||||
import type { Navigation } from '../../files/src/services/Navigation'
|
||||
|
||||
import { translate as t } from '@nextcloud/l10n'
|
||||
import { loadState } from '@nextcloud/initial-state'
|
||||
import FolderNetworkSvg from '@mdi/svg/svg/folder-network.svg?raw'
|
||||
|
||||
import './actions/enterCredentialsAction'
|
||||
import './actions/inlineStorageCheckAction'
|
||||
import './actions/openInFilesAction'
|
||||
import { getContents } from './services/externalStorage'
|
||||
|
||||
const allowUserMounting = loadState('files_external', 'allowUserMounting', false)
|
||||
|
||||
const Navigation = window.OCP.Files.Navigation as NavigationService
|
||||
Navigation.register({
|
||||
id: 'extstoragemounts',
|
||||
name: t('files_external', 'External storage'),
|
||||
caption: t('files_external', 'List of external storage.'),
|
||||
|
||||
emptyCaption: allowUserMounting
|
||||
? t('files_external', 'There is no external storage configured. You can configure them in your Personal settings.')
|
||||
: t('files_external', 'There is no external storage configured and you don\'t have the permission to configure them.'),
|
||||
emptyTitle: t('files_external', 'No external storage'),
|
||||
|
||||
icon: FolderNetworkSvg,
|
||||
order: 30,
|
||||
|
||||
columns: [
|
||||
{
|
||||
id: 'storage-type',
|
||||
title: t('files_external', 'Storage type'),
|
||||
render(node) {
|
||||
const backend = node.attributes?.backend || t('files_external', 'Unknown')
|
||||
const span = document.createElement('span')
|
||||
span.textContent = backend
|
||||
return span
|
||||
},
|
||||
},
|
||||
{
|
||||
id: 'scope',
|
||||
title: t('files_external', 'Scope'),
|
||||
render(node) {
|
||||
const span = document.createElement('span')
|
||||
let scope = t('files_external', 'Personal')
|
||||
if (node.attributes?.scope === 'system') {
|
||||
scope = t('files_external', 'System')
|
||||
}
|
||||
span.textContent = scope
|
||||
return span
|
||||
},
|
||||
},
|
||||
],
|
||||
|
||||
getContents,
|
||||
} as Navigation)
|
||||
104
apps/files_external/src/services/externalStorage.ts
Normal file
104
apps/files_external/src/services/externalStorage.ts
Normal file
|
|
@ -0,0 +1,104 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
// eslint-disable-next-line n/no-extraneous-import
|
||||
import type { AxiosResponse } from 'axios'
|
||||
import type { ContentsWithRoot } from '../../../files/src/services/Navigation'
|
||||
import type { OCSResponse } from '../../../files_sharing/src/services/SharingService'
|
||||
|
||||
import { Folder, Permission } from '@nextcloud/files'
|
||||
import { generateOcsUrl, generateRemoteUrl, generateUrl } from '@nextcloud/router'
|
||||
import { getCurrentUser } from '@nextcloud/auth'
|
||||
import axios from '@nextcloud/axios'
|
||||
|
||||
import { STORAGE_STATUS } from '../utils/credentialsUtils'
|
||||
|
||||
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
|
||||
*/
|
||||
export type MountEntry = {
|
||||
name: string
|
||||
path: string,
|
||||
type: 'dir',
|
||||
backend: 'SFTP',
|
||||
scope: 'system' | 'personal',
|
||||
permissions: number,
|
||||
id: number,
|
||||
class: string
|
||||
config: StorageConfig
|
||||
}
|
||||
|
||||
const entryToFolder = (ocsEntry: MountEntry): Folder => {
|
||||
const path = (ocsEntry.path + '/' + ocsEntry.name).replace(/^\//gm, '')
|
||||
return new Folder({
|
||||
id: ocsEntry.id,
|
||||
source: generateRemoteUrl('dav' + rootPath + '/' + path),
|
||||
root: rootPath,
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
permissions: ocsEntry.config.status !== STORAGE_STATUS.SUCCESS
|
||||
? Permission.NONE
|
||||
: ocsEntry?.permissions || Permission.READ,
|
||||
attributes: {
|
||||
displayName: path,
|
||||
...ocsEntry,
|
||||
},
|
||||
})
|
||||
}
|
||||
|
||||
export const getContents = async (): Promise<ContentsWithRoot> => {
|
||||
const response = await axios.get(generateOcsUrl('apps/files_external/api/v1/mounts')) as AxiosResponse<OCSResponse<MountEntry>>
|
||||
const contents = response.data.ocs.data.map(entryToFolder)
|
||||
|
||||
return {
|
||||
folder: new Folder({
|
||||
id: 0,
|
||||
source: generateRemoteUrl('dav' + rootPath),
|
||||
root: rootPath,
|
||||
owner: getCurrentUser()?.uid || null,
|
||||
permissions: Permission.READ,
|
||||
}),
|
||||
contents,
|
||||
}
|
||||
}
|
||||
|
||||
export const getStatus = function(id: number, global = true) {
|
||||
const type = global ? 'userglobalstorages' : 'userstorages'
|
||||
return axios.get(generateUrl(`apps/files_external/${type}/${id}?testOnly=false`)) as Promise<AxiosResponse<StorageConfig>>
|
||||
}
|
||||
42
apps/files_external/src/utils/credentialsUtils.ts
Normal file
42
apps/files_external/src/utils/credentialsUtils.ts
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
import type { StorageConfig } from '../services/externalStorage'
|
||||
|
||||
// @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,
|
||||
}
|
||||
|
||||
export const isMissingAuthConfig = function(config: StorageConfig) {
|
||||
// If we don't know the status, assume it is ok
|
||||
if (!config.status || config.status === STORAGE_STATUS.SUCCESS) {
|
||||
return false
|
||||
}
|
||||
|
||||
return config.userProvided || config.authMechanism === 'password::global::user'
|
||||
}
|
||||
39
apps/files_external/src/utils/externalStorageUtils.ts
Normal file
39
apps/files_external/src/utils/externalStorageUtils.ts
Normal file
|
|
@ -0,0 +1,39 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
import { FileType, Node } from '@nextcloud/files'
|
||||
import type { MountEntry } from '../services/externalStorage'
|
||||
|
||||
export const isNodeExternalStorage = function(node: Node) {
|
||||
// Not a folder, not a storage
|
||||
if (node.type === FileType.File) {
|
||||
return false
|
||||
}
|
||||
|
||||
// No backend or scope, not a storage
|
||||
const attributes = node.attributes as MountEntry
|
||||
if (!attributes.scope || !attributes.backend) {
|
||||
return false
|
||||
}
|
||||
|
||||
// Specific markers that we're sure are ext storage only
|
||||
return attributes.scope === 'personal' || attributes.scope === 'system'
|
||||
}
|
||||
|
|
@ -1,31 +0,0 @@
|
|||
<?php /** @var \OCP\IL10N $l */ ?>
|
||||
<div class="files-controls">
|
||||
<div></div>
|
||||
</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>
|
||||
|
||||
<table class="files-filestable list-container <?php p($_['showgridview'] ? 'view-grid' : '') ?>">
|
||||
<thead>
|
||||
<tr>
|
||||
<th class="hidden column-name">
|
||||
<div class="column-name-container">
|
||||
<a class="name sort columntitle" data-sort="name"><span><?php p($l->t('Name')); ?></span><span class="sort-indicator"></span></a>
|
||||
</div>
|
||||
</th>
|
||||
<th id="headerBackend" class="hidden column-backend">
|
||||
<a class="backend sort columntitle" data-sort="backend"><span><?php p($l->t('Storage type')); ?></span><span class="sort-indicator"></span></a>
|
||||
</th>
|
||||
<th id="headerScope" class="hidden column-scope column-last">
|
||||
<a class="scope sort columntitle" data-sort="scope"><span><?php p($l->t('Scope')); ?></span><span class="sort-indicator"></span></a>
|
||||
</th>
|
||||
</tr>
|
||||
</thead>
|
||||
<tbody class="files-fileList">
|
||||
</tbody>
|
||||
<tfoot>
|
||||
</tfoot>
|
||||
</table>
|
||||
|
|
@ -129,7 +129,7 @@ abstract class StoragesControllerTest extends \Test\TestCase {
|
|||
|
||||
$data = $response->getData();
|
||||
$this->assertEquals(Http::STATUS_CREATED, $response->getStatus());
|
||||
$this->assertEquals($storageConfig, $data);
|
||||
$this->assertEquals($storageConfig->jsonSerialize(), $data);
|
||||
}
|
||||
|
||||
public function testAddLocalStorageWhenDisabled() {
|
||||
|
|
@ -201,7 +201,7 @@ abstract class StoragesControllerTest extends \Test\TestCase {
|
|||
|
||||
$data = $response->getData();
|
||||
$this->assertEquals(Http::STATUS_OK, $response->getStatus());
|
||||
$this->assertEquals($storageConfig, $data);
|
||||
$this->assertEquals($storageConfig->jsonSerialize(), $data);
|
||||
}
|
||||
|
||||
public function mountPointNamesProvider() {
|
||||
|
|
|
|||
|
|
@ -48,9 +48,9 @@ export const action = new FileAction({
|
|||
return null
|
||||
},
|
||||
|
||||
default: DefaultType.HIDDEN,
|
||||
// Before openFolderAction
|
||||
order: -1000,
|
||||
default: DefaultType.HIDDEN,
|
||||
})
|
||||
|
||||
registerFileAction(action)
|
||||
|
|
|
|||
|
|
@ -45,7 +45,7 @@ describe('SharingService methods definitions', () => {
|
|||
},
|
||||
data: [],
|
||||
},
|
||||
} as OCSResponse,
|
||||
} as OCSResponse<any>,
|
||||
}
|
||||
})
|
||||
})
|
||||
|
|
|
|||
|
|
@ -31,14 +31,14 @@ import logger from './logger'
|
|||
|
||||
export const rootPath = `/files/${getCurrentUser()?.uid}`
|
||||
|
||||
export type OCSResponse = {
|
||||
export type OCSResponse<T> = {
|
||||
ocs: {
|
||||
meta: {
|
||||
status: string
|
||||
statuscode: number
|
||||
message: string
|
||||
},
|
||||
data: []
|
||||
data: T[]
|
||||
}
|
||||
}
|
||||
|
||||
|
|
@ -87,7 +87,7 @@ const ocsEntryToNode = function(ocsEntry: any): Folder | File | null {
|
|||
}
|
||||
}
|
||||
|
||||
const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse> {
|
||||
const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse<any>> {
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/shares')
|
||||
return axios.get(url, {
|
||||
headers,
|
||||
|
|
@ -98,15 +98,15 @@ const getShares = function(shared_with_me = false): AxiosPromise<OCSResponse> {
|
|||
})
|
||||
}
|
||||
|
||||
const getSharedWithYou = function(): AxiosPromise<OCSResponse> {
|
||||
const getSharedWithYou = function(): AxiosPromise<OCSResponse<any>> {
|
||||
return getShares(true)
|
||||
}
|
||||
|
||||
const getSharedWithOthers = function(): AxiosPromise<OCSResponse> {
|
||||
const getSharedWithOthers = function(): AxiosPromise<OCSResponse<any>> {
|
||||
return getShares()
|
||||
}
|
||||
|
||||
const getRemoteShares = function(): AxiosPromise<OCSResponse> {
|
||||
const getRemoteShares = function(): AxiosPromise<OCSResponse<any>> {
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/remote_shares')
|
||||
return axios.get(url, {
|
||||
headers,
|
||||
|
|
@ -116,7 +116,7 @@ const getRemoteShares = function(): AxiosPromise<OCSResponse> {
|
|||
})
|
||||
}
|
||||
|
||||
const getPendingShares = function(): AxiosPromise<OCSResponse> {
|
||||
const getPendingShares = function(): AxiosPromise<OCSResponse<any>> {
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/shares/pending')
|
||||
return axios.get(url, {
|
||||
headers,
|
||||
|
|
@ -126,7 +126,7 @@ const getPendingShares = function(): AxiosPromise<OCSResponse> {
|
|||
})
|
||||
}
|
||||
|
||||
const getRemotePendingShares = function(): AxiosPromise<OCSResponse> {
|
||||
const getRemotePendingShares = function(): AxiosPromise<OCSResponse<any>> {
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/remote_shares/pending')
|
||||
return axios.get(url, {
|
||||
headers,
|
||||
|
|
@ -136,7 +136,7 @@ const getRemotePendingShares = function(): AxiosPromise<OCSResponse> {
|
|||
})
|
||||
}
|
||||
|
||||
const getDeletedShares = function(): AxiosPromise<OCSResponse> {
|
||||
const getDeletedShares = function(): AxiosPromise<OCSResponse<any>> {
|
||||
const url = generateOcsUrl('apps/files_sharing/api/v1/deletedshares')
|
||||
return axios.get(url, {
|
||||
headers,
|
||||
|
|
@ -147,7 +147,7 @@ const getDeletedShares = function(): AxiosPromise<OCSResponse> {
|
|||
}
|
||||
|
||||
export const getContents = async (sharedWithYou = true, sharedWithOthers = true, pendingShares = false, deletedshares = false, filterTypes: number[] = []): Promise<ContentsWithRoot> => {
|
||||
const promises = [] as AxiosPromise<OCSResponse>[]
|
||||
const promises = [] as AxiosPromise<OCSResponse<any>>[]
|
||||
|
||||
if (sharedWithYou) {
|
||||
promises.push(getSharedWithYou(), getRemoteShares())
|
||||
|
|
|
|||
|
|
@ -112,7 +112,7 @@ describe('Sharing views contents', () => {
|
|||
},
|
||||
data: [],
|
||||
},
|
||||
} as OCSResponse,
|
||||
} as OCSResponse<any>,
|
||||
}
|
||||
})
|
||||
|
||||
|
|
|
|||
|
|
@ -28,7 +28,7 @@ import AccountGroupSvg from '@mdi/svg/svg/account-group.svg?raw'
|
|||
import AccountSvg from '@mdi/svg/svg/account.svg?raw'
|
||||
import DeleteSvg from '@mdi/svg/svg/delete.svg?raw'
|
||||
import LinkSvg from '@mdi/svg/svg/link.svg?raw'
|
||||
import ShareVariantSvg from '@mdi/svg/svg/share-variant.svg?raw'
|
||||
import AccouontPlusSvg from '@mdi/svg/svg/account-plus.svg?raw'
|
||||
|
||||
import { getContents } from '../services/SharingService'
|
||||
|
||||
|
|
@ -49,7 +49,7 @@ export default () => {
|
|||
emptyTitle: t('files_sharing', 'No shares'),
|
||||
emptyCaption: t('files_sharing', 'Files and folders you shared or have been shared with you will show up here'),
|
||||
|
||||
icon: ShareVariantSvg,
|
||||
icon: AccouontPlusSvg,
|
||||
order: 20,
|
||||
|
||||
columns: [],
|
||||
|
|
|
|||
4
dist/core-common.js
vendored
4
dist/core-common.js
vendored
File diff suppressed because one or more lines are too long
2
dist/core-common.js.map
vendored
2
dist/core-common.js.map
vendored
File diff suppressed because one or more lines are too long
4
dist/files-main.js
vendored
4
dist/files-main.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files-main.js.map
vendored
2
dist/files-main.js.map
vendored
File diff suppressed because one or more lines are too long
3
dist/files_external-main.js
vendored
Normal file
3
dist/files_external-main.js
vendored
Normal file
File diff suppressed because one or more lines are too long
65
dist/files_external-main.js.LICENSE.txt
vendored
Normal file
65
dist/files_external-main.js.LICENSE.txt
vendored
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
/**
|
||||
* @copyright Copyright (c) 2021 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2022 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
|
||||
/**
|
||||
* @copyright Copyright (c) 2023 John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @author John Molakvoæ <skjnldsv@protonmail.com>
|
||||
*
|
||||
* @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/>.
|
||||
*
|
||||
*/
|
||||
1
dist/files_external-main.js.map
vendored
Normal file
1
dist/files_external-main.js.map
vendored
Normal file
File diff suppressed because one or more lines are too long
4
dist/files_sharing-files_sharing.js
vendored
4
dist/files_sharing-files_sharing.js
vendored
File diff suppressed because one or more lines are too long
2
dist/files_sharing-files_sharing.js.map
vendored
2
dist/files_sharing-files_sharing.js.map
vendored
File diff suppressed because one or more lines are too long
12
package-lock.json
generated
12
package-lock.json
generated
|
|
@ -26166,9 +26166,9 @@
|
|||
}
|
||||
},
|
||||
"node_modules/webdav": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.2.2.tgz",
|
||||
"integrity": "sha512-CTnhTTKug7pKbMqcvrnGNr4rV9qhWXV1sLk1PpN4BOskqDT+cEfFx4Y4VlcFXUX6lSUFsQBm9Ka8+6dIe0doQQ==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.2.3.tgz",
|
||||
"integrity": "sha512-u5wqJULZhB7IwO3qVD9r0ikt6SMHZ4P4YYtLJ6JrCmSoZuW6KvanXWJAA4LZDm548lK7aCNUsy0VxbBKBXAGrg==",
|
||||
"dependencies": {
|
||||
"@buttercup/fetch": "^0.1.1",
|
||||
"base-64": "^1.0.0",
|
||||
|
|
@ -46694,9 +46694,9 @@
|
|||
"optional": true
|
||||
},
|
||||
"webdav": {
|
||||
"version": "5.2.2",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.2.2.tgz",
|
||||
"integrity": "sha512-CTnhTTKug7pKbMqcvrnGNr4rV9qhWXV1sLk1PpN4BOskqDT+cEfFx4Y4VlcFXUX6lSUFsQBm9Ka8+6dIe0doQQ==",
|
||||
"version": "5.2.3",
|
||||
"resolved": "https://registry.npmjs.org/webdav/-/webdav-5.2.3.tgz",
|
||||
"integrity": "sha512-u5wqJULZhB7IwO3qVD9r0ikt6SMHZ4P4YYtLJ6JrCmSoZuW6KvanXWJAA4LZDm548lK7aCNUsy0VxbBKBXAGrg==",
|
||||
"requires": {
|
||||
"@buttercup/fetch": "^0.1.1",
|
||||
"base-64": "^1.0.0",
|
||||
|
|
|
|||
|
|
@ -72,19 +72,6 @@ module.exports = function(config) {
|
|||
],
|
||||
testFiles: ['apps/files_sharing/tests/js/*.js']
|
||||
},
|
||||
{
|
||||
name: 'files_external',
|
||||
srcFiles: [
|
||||
// only test these files, others are not ready and mess
|
||||
// up with the global namespace/classes/state
|
||||
'apps/files_external/js/app.js',
|
||||
'apps/files_external/js/templates.js',
|
||||
'apps/files_external/js/mountsfilelist.js',
|
||||
'apps/files_external/js/settings.js',
|
||||
'apps/files_external/js/statusmanager.js'
|
||||
],
|
||||
testFiles: ['apps/files_external/tests/js/*.js']
|
||||
},
|
||||
'systemtags',
|
||||
'files_trashbin',
|
||||
];
|
||||
|
|
|
|||
|
|
@ -54,6 +54,9 @@ module.exports = {
|
|||
'personal-settings': path.join(__dirname, 'apps/files/src', 'main-personal-settings.js'),
|
||||
'reference-files': path.join(__dirname, 'apps/files/src', 'reference-files.js'),
|
||||
},
|
||||
files_external: {
|
||||
main: path.join(__dirname, 'apps/files_external/src', 'main.ts'),
|
||||
},
|
||||
files_sharing: {
|
||||
additionalScripts: path.join(__dirname, 'apps/files_sharing/src', 'additionalScripts.js'),
|
||||
collaboration: path.join(__dirname, 'apps/files_sharing/src', 'collaborationresourceshandler.js'),
|
||||
|
|
|
|||
Loading…
Reference in a new issue