Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add File Manager / File Browser based on form.FileUpload #6553

Merged
merged 3 commits into from
Apr 9, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
15 changes: 15 additions & 0 deletions applications/luci-app-filebrowser/Makefile
Original file line number Diff line number Diff line change
@@ -0,0 +1,15 @@
# This is free software, licensed under the Apache License, Version 2.0 .

include $(TOPDIR)/rules.mk

LUCI_TITLE:=LuCI File Browser module
LUCI_DEPENDS:=+luci-base

PKG_LICENSE:=Apache-2.0
PKG_VERSION:=1.1.0
PKG_RELEASE:=1
PKG_MAINTAINER:=Sergey Ponomarev <[email protected]>

include ../../luci.mk

# call BuildPackage - OpenWrt buildroot signature
Original file line number Diff line number Diff line change
@@ -0,0 +1,34 @@
'use strict';
'require view';
'require ui';
'require form';

var formData = {
files: {
root: null,
}
};

return view.extend({
render: function() {
var m, s, o;

m = new form.JSONMap(formData, _('File Browser'), '');

s = m.section(form.NamedSection, 'files', 'files');

o = s.option(form.FileUpload, 'root', '');
o.root_directory = '/';
o.browser = true;
o.show_hidden = true;
o.enable_upload = true;
o.enable_remove = true;
o.enable_download = true;

return m.render();
},

handleSave: null,
handleSaveApply: null,
handleReset: null
})
11 changes: 11 additions & 0 deletions applications/luci-app-filebrowser/po/templates/filebrowser.pot
Original file line number Diff line number Diff line change
@@ -0,0 +1,11 @@
msgid ""
msgstr "Content-Type: text/plain; charset=UTF-8"

#: applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js:16
#: applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json:3
msgid "File Browser"
msgstr ""

#: applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json:3
msgid "Grant access to File Browser"
msgstr ""
Original file line number Diff line number Diff line change
@@ -0,0 +1,13 @@
{
"admin/system/filebrowser": {
"title": "File Browser",
"order": 80,
"action": {
"type": "view",
"path": "system/filebrowser"
},
"depends": {
"acl": [ "luci-app-filebrowser" ]
}
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,14 @@
{
"luci-app-filebrowser": {
"description": "Grant access to File Browser",
"write": {
"cgi-io": [ "upload", "download" ],
"ubus": {
"file": [ "*" ]
},
"file": {
"/*": [ "list", "read", "write" ]
}
}
}
}
21 changes: 21 additions & 0 deletions modules/luci-base/htdocs/luci-static/resources/form.js
Original file line number Diff line number Diff line change
Expand Up @@ -4543,12 +4543,23 @@ var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */
__init__: function(/* ... */) {
this.super('__init__', arguments);

this.browser = false;
this.show_hidden = false;
this.enable_upload = true;
this.enable_remove = true;
this.enable_download = false;
this.root_directory = '/etc/luci-uploads';
},


/**
* Open in a file browser mode instead of selecting for a file
*
* @name LuCI.form.FileUpload.prototype#browser
* @type boolean
* @default false
*/

/**
* Toggle display of hidden files.
*
Expand Down Expand Up @@ -4593,6 +4604,14 @@ var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */
* @default true
*/

/**
* Toggle download file functionality.
*
* @name LuCI.form.FileUpload.prototype#enable_download
* @type boolean
* @default false
*/

/**
* Specify the root directory for file browsing.
*
Expand All @@ -4614,9 +4633,11 @@ var CBIFileUpload = CBIValue.extend(/** @lends LuCI.form.FileUpload.prototype */
var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
id: this.cbid(section_id),
name: this.cbid(section_id),
browser: this.browser,
show_hidden: this.show_hidden,
enable_upload: this.enable_upload,
enable_remove: this.enable_remove,
enable_download: this.enable_download,
root_directory: this.root_directory,
disabled: (this.readonly != null) ? this.readonly : this.map.readonly
});
Expand Down
62 changes: 50 additions & 12 deletions modules/luci-base/htdocs/luci-static/resources/ui.js
Original file line number Diff line number Diff line change
Expand Up @@ -2613,6 +2613,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
* @typedef {LuCI.ui.AbstractElement.InitOptions} InitOptions
* @memberof LuCI.ui.FileUpload
*
* @property {boolean} [browser=false]
* Use a file browser mode.
*
* @property {boolean} [show_hidden=false]
* Specifies whether hidden files should be displayed when browsing remote
* files. Note that this is not a security feature, hidden files are always
Expand All @@ -2633,6 +2636,9 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
* remotely depends on the ACL setup for the current session. This option
* merely controls whether the file remove controls are rendered or not.
*
* @property {boolean} [enable_download=false]
* Specifies whether the widget allows the user to download files.
*
* @property {string} [root_directory=/etc/luci-uploads]
* Specifies the remote directory the upload and file browsing actions take
* place in. Browsing to directories outside the root directory is
Expand All @@ -2643,9 +2649,11 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
__init__: function(value, options) {
this.value = value;
this.options = Object.assign({
browser: false,
show_hidden: false,
enable_upload: true,
enable_remove: true,
enable_download: false,
root_directory: '/etc/luci-uploads'
}, options);
},
Expand All @@ -2664,7 +2672,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {

/** @override */
render: function() {
return L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
var renderFileBrowser = L.resolveDefault(this.value != null ? fs.stat(this.value) : null).then(L.bind(function(stat) {
var label;

if (L.isObject(stat) && stat.type != 'directory')
Expand All @@ -2676,13 +2684,13 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
label = [ this.iconForType('file'), ' %s (%s)'.format(this.truncatePath(this.value), _('File not accessible')) ];
else
label = [ _('Select file…') ];

return this.bind(E('div', { 'id': this.options.id }, [
E('button', {
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser'),
'disabled': this.options.disabled ? '' : null
}, label),
let btnOpenFileBrowser = E('button', {
'class': 'btn open-file-browser',
'click': UI.prototype.createHandlerFn(this, 'handleFileBrowser'),
'disabled': this.options.disabled ? '' : null
}, label);
var fileBrowserEl = E('div', { 'id': this.options.id }, [
btnOpenFileBrowser,
E('div', {
'class': 'cbi-filebrowser'
}),
Expand All @@ -2691,8 +2699,18 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
'name': this.options.name,
'value': this.value
})
]));
]);
return this.bind(fileBrowserEl);
}, this));
// in a browser mode open dir listing after render by clicking on a Select button
if (this.options.browser) {
return renderFileBrowser.then(function (fileBrowserEl) {
var btnOpenFileBrowser = fileBrowserEl.getElementsByClassName('open-file-browser').item(0);
btnOpenFileBrowser.click();
return fileBrowserEl;
});
}
return renderFileBrowser
},

/** @private */
Expand Down Expand Up @@ -2917,6 +2935,10 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleReset')
}, [ _('Deselect') ]) : '',
this.options.enable_download && list[i].type == 'file' ? E('button', {
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleDownload', entrypath, list[i])
}, [ _('Download') ]) : '',
this.options.enable_remove ? E('button', {
'class': 'btn cbi-button-negative',
'click': UI.prototype.createHandlerFn(this, 'handleDelete', entrypath, list[i])
Expand Down Expand Up @@ -2947,11 +2969,11 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
rows,
E('div', { 'class': 'right' }, [
this.renderUpload(path, list),
E('a', {
!this.options.browser ? E('a', {
'href': '#',
'class': 'btn',
'click': UI.prototype.createHandlerFn(this, 'handleCancel')
}, _('Cancel'))
}, _('Cancel')) : ''
]),
]);
},
Expand Down Expand Up @@ -2980,6 +3002,22 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
this.handleCancel(ev);
},

/** @private */
handleDownload: function(path, fileStat, ev) {
fs.read_direct(path, 'blob').then(function (blob) {
var url = window.URL.createObjectURL(blob);
var a = document.createElement('a');
a.style.display = 'none';
a.href = url;
a.download = fileStat.name;
document.body.appendChild(a);
a.click();
window.URL.revokeObjectURL(url);
}).catch(function(err) {
alert(_('Download failed: %s').format(err.message));
});
},

/** @private */
handleSelect: function(path, fileStat, ev) {
var browser = dom.parent(ev.target, '.cbi-filebrowser'),
Expand All @@ -2989,7 +3027,7 @@ var UIFileUpload = UIElement.extend(/** @lends LuCI.ui.FileUpload.prototype */ {
dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
L.resolveDefault(fs.list(path), []).then(L.bind(this.renderListing, this, browser, path));
}
else {
else if (!this.options.browser) {
var button = this.node.firstElementChild,
hidden = this.node.lastElementChild;

Expand Down