diff --git a/applications/luci-app-filebrowser/Makefile b/applications/luci-app-filebrowser/Makefile new file mode 100644 index 000000000000..28b38021d52a --- /dev/null +++ b/applications/luci-app-filebrowser/Makefile @@ -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 + +include ../../luci.mk + +# call BuildPackage - OpenWrt buildroot signature diff --git a/applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js b/applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js new file mode 100644 index 000000000000..33fe2614e8cc --- /dev/null +++ b/applications/luci-app-filebrowser/htdocs/luci-static/resources/view/system/filebrowser.js @@ -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 +}) diff --git a/applications/luci-app-filebrowser/po/templates/filebrowser.pot b/applications/luci-app-filebrowser/po/templates/filebrowser.pot new file mode 100644 index 000000000000..9970b2c8ff13 --- /dev/null +++ b/applications/luci-app-filebrowser/po/templates/filebrowser.pot @@ -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 "" diff --git a/applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json b/applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json new file mode 100644 index 000000000000..506706957cd2 --- /dev/null +++ b/applications/luci-app-filebrowser/root/usr/share/luci/menu.d/luci-app-filebrowser.json @@ -0,0 +1,13 @@ +{ + "admin/system/filebrowser": { + "title": "File Browser", + "order": 80, + "action": { + "type": "view", + "path": "system/filebrowser" + }, + "depends": { + "acl": [ "luci-app-filebrowser" ] + } + } +} diff --git a/applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json b/applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json new file mode 100644 index 000000000000..858a8dd70d36 --- /dev/null +++ b/applications/luci-app-filebrowser/root/usr/share/rpcd/acl.d/luci-app-filebrowser.json @@ -0,0 +1,14 @@ +{ + "luci-app-filebrowser": { + "description": "Grant access to File Browser", + "write": { + "cgi-io": [ "upload", "download" ], + "ubus": { + "file": [ "*" ] + }, + "file": { + "/*": [ "list", "read", "write" ] + } + } + } +} diff --git a/modules/luci-base/htdocs/luci-static/resources/form.js b/modules/luci-base/htdocs/luci-static/resources/form.js index 70aad7ee7d07..889f6edd8dcd 100644 --- a/modules/luci-base/htdocs/luci-static/resources/form.js +++ b/modules/luci-base/htdocs/luci-static/resources/form.js @@ -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. * @@ -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. * @@ -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 }); diff --git a/modules/luci-base/htdocs/luci-static/resources/ui.js b/modules/luci-base/htdocs/luci-static/resources/ui.js index afb590d8f8a5..2533f45cec70 100644 --- a/modules/luci-base/htdocs/luci-static/resources/ui.js +++ b/modules/luci-base/htdocs/luci-static/resources/ui.js @@ -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 @@ -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 @@ -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); }, @@ -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') @@ -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' }), @@ -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 */ @@ -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]) @@ -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')) : '' ]), ]); }, @@ -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'), @@ -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;