Merge pull request #6553 from stokito/luc-mod-system_filemanager
authorJo-Philipp Wich <jo@mein.io>
Tue, 9 Apr 2024 08:13:35 +0000 (10:13 +0200)
committerGitHub <noreply@github.com>
Tue, 9 Apr 2024 08:13:35 +0000 (10:13 +0200)
Add File Manager / File Browser based on form.FileUpload

1  2 
modules/luci-base/htdocs/luci-static/resources/form.js
modules/luci-base/htdocs/luci-static/resources/ui.js

index 70aad7ee7d07fe8a27f7d8d14c8fe59f7353e4e4,df594a3a34068730a7ad9759c7477de33838312a..889f6edd8dcde833f66b5d83ff85c6452b8fb144
@@@ -1007,14 -1007,14 +1007,14 @@@ var CBIAbstractSection = CBIAbstractEle
         *
         * @throws {TypeError}
         * Throws a `TypeError` exception in case the passed class value is not a
 -       * descendent of `AbstractValue`.
 +       * descendant of `AbstractValue`.
         *
         * @returns {LuCI.form.AbstractValue}
         * Returns the instantiated option class instance.
         */
        option: function(cbiClass /*, ... */) {
                if (!CBIAbstractValue.isSubclass(cbiClass))
 -                      throw L.error('TypeError', 'Class must be a descendent of CBIAbstractValue');
 +                      throw L.error('TypeError', 'Class must be a descendant of CBIAbstractValue');
  
                var obj = cbiClass.instantiate(this.varargs(arguments, 1, this.map, this));
                this.append(obj);
         *
         * @throws {TypeError}
         * Throws a `TypeError` exception in case the passed class value is not a
 -       * descendent of `AbstractValue`.
 +       * descendant of `AbstractValue`.
         *
         * @returns {LuCI.form.AbstractValue}
         * Returns the instantiated option class instance.
@@@ -4543,12 -4543,23 +4543,23 @@@ var CBIFileUpload = CBIValue.extend(/*
        __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.
         *
         * @default true
         */
  
+       /**
+        * Toggle download file functionality.
+        *
+        * @name LuCI.form.FileUpload.prototype#enable_download
+        * @type boolean
+        * @default false
+        */
        /**
         * Specify the root directory for file browsing.
         *
                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
                });
index afb590d8f8a50048602b7c731dc0f30a13b7f65e,8d41962ac9e81b75dbf6b92059b1820e8e887dc2..2533f45cec70da42e78ab5db3af6a008ead4b0d8
@@@ -2613,6 -2613,9 +2613,9 @@@ var UIFileUpload = UIElement.extend(/*
         * @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
         * 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
        __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);
        },
  
        /** @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')
                                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'
                                }),
                                        '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 */
                                                '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])
                        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')) : ''
                        ]),
                ]);
        },
                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'),
                        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;
  
@@@ -4554,7 -4592,7 +4592,7 @@@ var UI = baseclass.extend(/** @lends Lu
                                        E('p', _('Failed to confirm apply within %ds, waiting for rollback…')
                                                .format(L.env.apply_rollback)));
  
 -                              var call = function(r, data, duration) {
 +                              var call = function(r) {
                                        if (r.status === 204) {
                                                UI.prototype.changes.displayStatus('warning', [
                                                        E('h4', _('Configuration changes have been rolled back!')),
                                                return;
                                        }
  
 -                                      var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
 +                                      var delay = isNaN(r.duration) ? 0 : Math.max(1000 - r.duration, 0);
                                        window.setTimeout(function() {
                                                request.request(L.url('admin/uci/confirm'), {
                                                        method: 'post',
                                                        timeout: L.env.apply_timeout * 1000,
                                                        query: { sid: L.env.sessionid, token: L.env.token }
 -                                              }).then(call, call.bind(null, { status: 0 }, null, 0));
 +                                              }).then(call, call.bind(null, { status: 0, duration: 0 }));
                                        }, delay);
                                };
  
                        if (override_token)
                                this.confirm_auth = { token: override_token };
  
 -                      var call = function(r, data, duration) {
 +                      var call = function(r) {
                                if (Date.now() >= deadline) {
                                        window.clearTimeout(tt);
                                        UI.prototype.changes.rollback(checked);
                                        return;
                                }
 -                              else if (r && (r.status === 200 || r.status === 204)) {
 +                              else if (r.status === 200 || r.status === 204) {
                                        document.dispatchEvent(new CustomEvent('uci-applied'));
  
                                        UI.prototype.changes.setIndicator(0);
                                        return;
                                }
  
 -                              var delay = isNaN(duration) ? 0 : Math.max(1000 - duration, 0);
 +                              var delay = isNaN(r.duration) ? 0 : Math.max(1000 - r.duration, 0);
                                window.setTimeout(function() {
                                        request.request(L.url('admin/uci/confirm'), {
                                                method: 'post',
                                                timeout: L.env.apply_timeout * 1000,
                                                query: UI.prototype.changes.confirm_auth
 -                                      }).then(call, call);
 +                                      }).then(call, call.bind(null, { status: 0, duration: 0 }));
                                }, delay);
                        };
  
                        tick();
  
                        /* wait a few seconds for the settings to become effective */
 -                      window.setTimeout(call, Math.max(L.env.apply_holdoff * 1000 - ((ts + L.env.apply_rollback * 1000) - deadline), 1));
 +                      window.setTimeout(call.bind(null, { status: 0 }), L.env.apply_holdoff * 1000);
                },
  
                /**