luci-base: use cgi-io and rpcd-mod-file to handle file upload and browsing
authorJo-Philipp Wich <jo@mein.io>
Tue, 3 Sep 2019 17:34:33 +0000 (19:34 +0200)
committerJo-Philipp Wich <jo@mein.io>
Tue, 10 Sep 2019 13:28:16 +0000 (15:28 +0200)
Remove the old server side support for file browsing and file uploading
and switch to a client side widget instead which uses XMLHTTPRequests to
upload files via cgi-io and RPC calls for file listing and status queries.

Signed-off-by: Jo-Philipp Wich <jo@mein.io>
modules/luci-base/Makefile
modules/luci-base/htdocs/luci-static/resources/cbi.js
modules/luci-base/htdocs/luci-static/resources/form.js
modules/luci-base/htdocs/luci-static/resources/ui.js
modules/luci-base/luasrc/view/cbi/filebrowser.htm [deleted file]
modules/luci-base/luasrc/view/cbi/upload.htm
modules/luci-base/root/usr/share/rpcd/acl.d/luci-base.json
modules/luci-mod-admin-full/luasrc/controller/admin/filebrowser.lua [deleted file]

index 9bc8ec17a173fb5d574aea10abce595e558a4b51..92da37a2224492d635a49ce915535eb17ead5457 100644 (file)
@@ -12,7 +12,7 @@ LUCI_TYPE:=mod
 LUCI_BASENAME:=base
 
 LUCI_TITLE:=LuCI core libraries
-LUCI_DEPENDS:=+lua +luci-lib-nixio +luci-lib-ip +rpcd +libubus-lua +luci-lib-jsonc +liblucihttp-lua
+LUCI_DEPENDS:=+lua +luci-lib-nixio +luci-lib-ip +rpcd +libubus-lua +luci-lib-jsonc +liblucihttp-lua +rpcd-mod-file +cgi-io
 
 
 PKG_SOURCE:=v1.0.0.tar.gz
index 5aa687b670831a0351a5e7fe4edbb397029cf6e0..9144fbaf62e8a2d219d04519d9b07a3f24514111 100644 (file)
@@ -298,8 +298,6 @@ function cbi_init() {
                                   node.getAttribute('data-type'));
        }
 
-       document.querySelectorAll('[data-browser]').forEach(cbi_browser_init);
-
        document.querySelectorAll('.cbi-tooltip:not(:empty)').forEach(function(s) {
                s.parentNode.classList.add('cbi-tooltip-container');
        });
@@ -330,29 +328,6 @@ function cbi_init() {
        cbi_d_update();
 }
 
-function cbi_filebrowser(id, defpath) {
-       var field   = L.dom.elem(id) ? id : document.getElementById(id);
-       var browser = window.open(
-               cbi_strings.path.browser + (field.value || defpath || '') + '?field=' + field.id,
-               "luci_filebrowser", "width=300,height=400,left=100,top=200,scrollbars=yes"
-       );
-
-       browser.focus();
-}
-
-function cbi_browser_init(field)
-{
-       field.parentNode.insertBefore(
-               E('img', {
-                       'src': L.resource('cbi/folder.gif'),
-                       'class': 'cbi-image-button',
-                       'click': function(ev) {
-                               cbi_filebrowser(field, field.getAttribute('data-browser'));
-                               ev.preventDefault();
-                       }
-               }), field.nextSibling);
-}
-
 function cbi_validate_form(form, errmsg)
 {
        /* if triggered by a section removal or addition, don't validate */
index be9b53a88c02fff255ae4a9fe6e8c92505e7ccf3..6832a16460afbcac15b03482fbb4a29cb22f4cbd 100644 (file)
@@ -1673,6 +1673,32 @@ var CBIHiddenValue = CBIValue.extend({
        }
 });
 
+var CBIFileUpload = CBIValue.extend({
+       __name__: 'CBI.FileSelect',
+
+       __init__: function(/* ... */) {
+               this.super('__init__', arguments);
+
+               this.show_hidden = false;
+               this.enable_upload = true;
+               this.enable_remove = true;
+               this.root_directory = '/etc/luci-uploads';
+       },
+
+       renderWidget: function(section_id, option_index, cfgvalue) {
+               var browserEl = new ui.FileUpload((cfgvalue != null) ? cfgvalue : this.default, {
+                       id: this.cbid(section_id),
+                       name: this.cbid(section_id),
+                       show_hidden: this.show_hidden,
+                       enable_upload: this.enable_upload,
+                       enable_remove: this.enable_remove,
+                       root_directory: this.root_directory
+               });
+
+               return browserEl.render();
+       }
+});
+
 var CBISectionValue = CBIValue.extend({
        __name__: 'CBI.ContainerValue',
        __init__: function(map, section, option, cbiClass /*, ... */) {
@@ -1726,5 +1752,6 @@ return L.Class.extend({
        DummyValue: CBIDummyValue,
        Button: CBIButtonValue,
        HiddenValue: CBIHiddenValue,
+       FileUpload: CBIFileUpload,
        SectionValue: CBISectionValue
 });
index 3444549e04ca5e72284e24de84c630c0d6c1aa00..c27dd7ebfc3fd4d58779b41d2a6649f5d17b17fc 100644 (file)
@@ -1,4 +1,5 @@
 'use strict';
+'require rpc';
 'require uci';
 'require validation';
 
@@ -1453,6 +1454,417 @@ var UIHiddenfield = UIElement.extend({
        }
 });
 
+var UIFileUpload = UIElement.extend({
+       __init__: function(value, options) {
+               this.value = value;
+               this.options = Object.assign({
+                       show_hidden: false,
+                       enable_upload: true,
+                       enable_remove: true,
+                       root_directory: '/etc/luci-uploads'
+               }, options);
+       },
+
+       callFileStat: rpc.declare({
+               'object': 'file',
+               'method': 'stat',
+               'params': [ 'path' ],
+               'expect': { '': {} }
+       }),
+
+       callFileList: rpc.declare({
+               'object': 'file',
+               'method': 'list',
+               'params': [ 'path' ],
+               'expect': { 'entries': [] }
+       }),
+
+       callFileRemove: rpc.declare({
+               'object': 'file',
+               'method': 'remove',
+               'params': [ 'path' ]
+       }),
+
+       bind: function(browserEl) {
+               this.node = browserEl;
+
+               this.setUpdateEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+               this.setChangeEvents(browserEl, 'cbi-fileupload-select', 'cbi-fileupload-cancel');
+
+               L.dom.bindClassInstance(browserEl, this);
+
+               return browserEl;
+       },
+
+       render: function() {
+               return Promise.resolve(this.value != null ? this.callFileStat(this.value) : null).then(L.bind(function(stat) {
+                       var label;
+
+                       if (L.isObject(stat) && stat.type != 'directory')
+                               this.stat = stat;
+
+                       if (this.stat != null)
+                               label = [ this.iconForType(this.stat.type), ' %s (%1000mB)'.format(this.truncatePath(this.stat.path), this.stat.size) ];
+                       else if (this.value != null)
+                               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': L.ui.createHandlerFn(this, 'handleFileBrowser')
+                               }, label),
+                               E('div', {
+                                       'class': 'cbi-filebrowser'
+                               }),
+                               E('input', {
+                                       'type': 'hidden',
+                                       'name': this.options.name,
+                                       'value': this.value
+                               })
+                       ]));
+               }, this));
+       },
+
+       truncatePath: function(path) {
+               if (path.length > 50)
+                       path = path.substring(0, 25) + '…' + path.substring(path.length - 25);
+
+               return path;
+       },
+
+       iconForType: function(type) {
+               switch (type) {
+               case 'symlink':
+                       return E('img', {
+                               'src': L.resource('cbi/link.gif'),
+                               'title': _('Symbolic link'),
+                               'class': 'middle'
+                       });
+
+               case 'directory':
+                       return E('img', {
+                               'src': L.resource('cbi/folder.gif'),
+                               'title': _('Directory'),
+                               'class': 'middle'
+                       });
+
+               default:
+                       return E('img', {
+                               'src': L.resource('cbi/file.gif'),
+                               'title': _('File'),
+                               'class': 'middle'
+                       });
+               }
+       },
+
+       canonicalizePath: function(path) {
+               return path.replace(/\/{2,}/, '/')
+                       .replace(/\/\.(\/|$)/g, '/')
+                       .replace(/[^\/]+\/\.\.(\/|$)/g, '/')
+                       .replace(/\/$/, '');
+       },
+
+       splitPath: function(path) {
+               var croot = this.canonicalizePath(this.options.root_directory || '/'),
+                   cpath = this.canonicalizePath(path || '/');
+
+               if (cpath.length <= croot.length)
+                       return [ croot ];
+
+               if (cpath.charAt(croot.length) != '/')
+                       return [ croot ];
+
+               var parts = cpath.substring(croot.length + 1).split(/\//);
+
+               parts.unshift(croot);
+
+               return parts;
+       },
+
+       handleUpload: function(path, list, ev) {
+               var form = ev.target.parentNode,
+                   fileinput = form.querySelector('input[type="file"]'),
+                   nameinput = form.querySelector('input[type="text"]'),
+                   filename = (nameinput.value != null ? nameinput.value : '').trim();
+
+               ev.preventDefault();
+
+               if (filename == '' || filename.match(/\//) || fileinput.files[0] == null)
+                       return;
+
+               var existing = list.filter(function(e) { return e.name == filename })[0];
+
+               if (existing != null && existing.type == 'directory')
+                       return alert(_('A directory with the same name already exists.'));
+               else if (existing != null && !confirm(_('Overwrite existing file "%s" ?').format(filename)))
+                       return;
+
+               var data = new FormData();
+
+               data.append('sessionid', L.env.sessionid);
+               data.append('filename', path + '/' + filename);
+               data.append('filedata', fileinput.files[0]);
+
+               return L.Request.post('/cgi-bin/cgi-upload', data, {
+                       progress: L.bind(function(btn, ev) {
+                               btn.firstChild.data = '%.2f%%'.format((ev.loaded / ev.total) * 100);
+                       }, this, ev.target)
+               }).then(L.bind(function(path, ev, res) {
+                       var reply = res.json();
+
+                       if (L.isObject(reply) && reply.failure)
+                               alert(_('Upload request failed: %s').format(reply.message));
+
+                       return this.handleSelect(path, null, ev);
+               }, this, path, ev));
+       },
+
+       handleDelete: function(path, fileStat, ev) {
+               var parent = path.replace(/\/[^\/]+$/, '') || '/',
+                   name = path.replace(/^.+\//, ''),
+                   msg;
+
+               ev.preventDefault();
+
+               if (fileStat.type == 'directory')
+                       msg = _('Do you really want to recursively delete the directory "%s" ?').format(name);
+               else
+                       msg = _('Do you really want to delete "%s" ?').format(name);
+
+               if (confirm(msg)) {
+                       var button = this.node.firstElementChild,
+                           hidden = this.node.lastElementChild;
+
+                       if (path == hidden.value) {
+                               L.dom.content(button, _('Select file…'));
+                               hidden.value = '';
+                       }
+
+                       return this.callFileRemove(path).then(L.bind(function(parent, ev, rc) {
+                               if (rc == 0)
+                                       return this.handleSelect(parent, null, ev);
+                               else if (rc == 6)
+                                       alert(_('Delete permission denied'));
+                               else
+                                       alert(_('Delete request failed: %d %s').format(rc, rpc.getStatusText(rc)));
+
+                       }, this, parent, ev));
+               }
+       },
+
+       renderUpload: function(path, list) {
+               if (!this.options.enable_upload)
+                       return E([]);
+
+               return E([
+                       E('a', {
+                               'href': '#',
+                               'class': 'btn cbi-button-positive',
+                               'click': function(ev) {
+                                       var uploadForm = ev.target.nextElementSibling,
+                                           fileInput = uploadForm.querySelector('input[type="file"]');
+
+                                       ev.target.style.display = 'none';
+                                       uploadForm.style.display = '';
+                                       fileInput.click();
+                               }
+                       }, _('Upload file…')),
+                       E('div', { 'class': 'upload', 'style': 'display:none' }, [
+                               E('input', {
+                                       'type': 'file',
+                                       'style': 'display:none',
+                                       'change': function(ev) {
+                                               var nameinput = ev.target.parentNode.querySelector('input[type="text"]'),
+                                                   uploadbtn = ev.target.parentNode.querySelector('button.cbi-button-save');
+
+                                               nameinput.value = ev.target.value.replace(/^.+[\/\\]/, '');
+                                               uploadbtn.disabled = false;
+                                       }
+                               }),
+                               E('button', {
+                                       'class': 'btn',
+                                       'click': function(ev) {
+                                               ev.preventDefault();
+                                               ev.target.previousElementSibling.click();
+                                       }
+                               }, _('Browse…')),
+                               E('div', {}, E('input', { 'type': 'text', 'placeholder': _('Filename') })),
+                               E('button', {
+                                       'class': 'btn cbi-button-save',
+                                       'click': L.ui.createHandlerFn(this, 'handleUpload', path, list),
+                                       'disabled': true
+                               }, _('Upload file'))
+                       ])
+               ]);
+       },
+
+       renderListing: function(container, path, list) {
+               var breadcrumb = E('p'),
+                   rows = E('ul');
+
+               list.sort(function(a, b) {
+                       var isDirA = (a.type == 'directory'),
+                           isDirB = (b.type == 'directory');
+
+                       if (isDirA != isDirB)
+                               return isDirA < isDirB;
+
+                       return a.name > b.name;
+               });
+
+               for (var i = 0; i < list.length; i++) {
+                       if (!this.options.show_hidden && list[i].name.charAt(0) == '.')
+                               continue;
+
+                       var entrypath = this.canonicalizePath(path + '/' + list[i].name),
+                           selected = (entrypath == this.node.lastElementChild.value),
+                           mtime = new Date(list[i].mtime * 1000);
+
+                       rows.appendChild(E('li', [
+                               E('div', { 'class': 'name' }, [
+                                       this.iconForType(list[i].type),
+                                       ' ',
+                                       E('a', {
+                                               'href': '#',
+                                               'style': selected ? 'font-weight:bold' : null,
+                                               'click': L.ui.createHandlerFn(this, 'handleSelect',
+                                                       entrypath, list[i].type != 'directory' ? list[i] : null)
+                                       }, '%h'.format(list[i].name))
+                               ]),
+                               E('div', { 'class': 'mtime hide-xs' }, [
+                                       ' %04d-%02d-%02d %02d:%02d:%02d '.format(
+                                               mtime.getFullYear(),
+                                               mtime.getMonth() + 1,
+                                               mtime.getDate(),
+                                               mtime.getHours(),
+                                               mtime.getMinutes(),
+                                               mtime.getSeconds())
+                               ]),
+                               E('div', [
+                                       selected ? E('button', {
+                                               'class': 'btn',
+                                               'click': L.ui.createHandlerFn(this, 'handleReset')
+                                       }, _('Deselect')) : '',
+                                       this.options.enable_remove ? E('button', {
+                                               'class': 'btn cbi-button-negative',
+                                               'click': L.ui.createHandlerFn(this, 'handleDelete', entrypath, list[i])
+                                       }, _('Delete')) : ''
+                               ])
+                       ]));
+               }
+
+               if (!rows.firstElementChild)
+                       rows.appendChild(E('em', _('No entries in this directory')));
+
+               var dirs = this.splitPath(path),
+                   cur = '';
+
+               for (var i = 0; i < dirs.length; i++) {
+                       cur = cur ? cur + '/' + dirs[i] : dirs[i];
+                       L.dom.append(breadcrumb, [
+                               i ? ' » ' : '',
+                               E('a', {
+                                       'href': '#',
+                                       'click': L.ui.createHandlerFn(this, 'handleSelect', cur || '/', null)
+                               }, dirs[i] != '' ? '%h'.format(dirs[i]) : E('em', '(root)')),
+                       ]);
+               }
+
+               L.dom.content(container, [
+                       breadcrumb,
+                       rows,
+                       E('div', { 'class': 'right' }, [
+                               this.renderUpload(path, list),
+                               E('a', {
+                                       'href': '#',
+                                       'class': 'btn',
+                                       'click': L.ui.createHandlerFn(this, 'handleCancel')
+                               }, _('Cancel'))
+                       ]),
+               ]);
+       },
+
+       handleCancel: function(ev) {
+               var button = this.node.firstElementChild,
+                   browser = button.nextElementSibling;
+
+               browser.classList.remove('open');
+               button.style.display = '';
+
+               this.node.dispatchEvent(new CustomEvent('cbi-fileupload-cancel', {}));
+       },
+
+       handleReset: function(ev) {
+               var button = this.node.firstElementChild,
+                   hidden = this.node.lastElementChild;
+
+               hidden.value = '';
+               L.dom.content(button, _('Select file…'));
+
+               this.handleCancel(ev);
+       },
+
+       handleSelect: function(path, fileStat, ev) {
+               var browser = L.dom.parent(ev.target, '.cbi-filebrowser'),
+                   ul = browser.querySelector('ul');
+
+               if (fileStat == null) {
+                       L.dom.content(ul, E('em', { 'class': 'spinning' }, _('Loading directory contents…')));
+                       this.callFileList(path).then(L.bind(this.renderListing, this, browser, path));
+               }
+               else {
+                       var button = this.node.firstElementChild,
+                           hidden = this.node.lastElementChild;
+
+                       path = this.canonicalizePath(path);
+
+                       L.dom.content(button, [
+                               this.iconForType(fileStat.type),
+                               ' %s (%1000mB)'.format(this.truncatePath(path), fileStat.size)
+                       ]);
+
+                       browser.classList.remove('open');
+                       button.style.display = '';
+                       hidden.value = path;
+
+                       this.stat = Object.assign({ path: path }, fileStat);
+                       this.node.dispatchEvent(new CustomEvent('cbi-fileupload-select', { detail: this.stat }));
+               }
+       },
+
+       handleFileBrowser: function(ev) {
+               var button = ev.target,
+                   browser = button.nextElementSibling,
+                   path = this.stat ? this.stat.path.replace(/\/[^\/]+$/, '') : this.options.root_directory;
+
+               if (this.options.root_directory.indexOf(path) != 0)
+                       path = this.options.root_directory;
+
+               ev.preventDefault();
+
+               return this.callFileList(path).then(L.bind(function(button, browser, path, list) {
+                       document.querySelectorAll('.cbi-filebrowser.open').forEach(function(browserEl) {
+                               L.dom.findClassInstance(browserEl).handleCancel(ev);
+                       });
+
+                       button.style.display = 'none';
+                       browser.classList.add('open');
+
+                       return this.renderListing(browser, path, list);
+               }, this, button, browser, path));
+       },
+
+       getValue: function() {
+               return this.node.lastElementChild.value;
+       },
+
+       setValue: function(value) {
+               this.node.lastElementChild.value = value;
+       }
+});
+
 
 return L.Class.extend({
        __init__: function() {
@@ -2173,5 +2585,6 @@ return L.Class.extend({
        Dropdown: UIDropdown,
        DynamicList: UIDynamicList,
        Combobox: UICombobox,
-       Hiddenfield: UIHiddenfield
+       Hiddenfield: UIHiddenfield,
+       FileUpload: UIFileUpload
 });
diff --git a/modules/luci-base/luasrc/view/cbi/filebrowser.htm b/modules/luci-base/luasrc/view/cbi/filebrowser.htm
deleted file mode 100644 (file)
index 806b1b5..0000000
+++ /dev/null
@@ -1,113 +0,0 @@
-<?xml version="1.0" encoding="UTF-8"?>
-<!DOCTYPE html PUBLIC "-//W3C//DTD XHTML 1.0 Strict//EN" "http://www.w3.org/TR/xhtml1/DTD/xhtml1-strict.dtd">
-<html xmlns="http://www.w3.org/1999/xhtml" xml:lang="en" lang="en">
-<head>
-    <title>Filebrowser - LuCI</title>
-       <style type="text/css">
-               #path, #listing {
-                       font-size: 85%;
-               }
-
-               ul {
-                       padding-left: 0;
-                       list-style-type: none;
-               }
-
-               li img {
-                       vertical-align: bottom;
-                       margin-right: 0.2em;
-               }
-       </style>
-
-       <script type="text/javascript">
-               function callback(path) {
-                       if( window.opener ) {
-                               var input = window.opener.document.getElementById(decodeURIComponent('<%=luci.http.urlencode(luci.http.formvalue('field'))%>'));
-                               if( input ) {
-                                       input.value = decodeURIComponent(path);
-                                       window.close();
-                               }
-                       }
-               }
-       </script>
-</head>
-<body>
-       <%
-               require("nixio.fs")
-               require("nixio.util")
-               require("luci.http")
-               require("luci.dispatcher")
-
-               local field   = luci.http.formvalue('field')
-               local request = luci.dispatcher.context.args
-               local path    = { '' }
-
-               for i = 1, #request do
-                       if request[i] ~= '..' and #request[i] > 0 then
-                               path[#path+1] = request[i]
-                       end
-               end
-
-               local filestat = nixio.fs.stat(table.concat(path, '/'))
-               local baseurl  = { 'admin', 'filebrowser' }
-
-               if filestat and filestat.type == "reg" then
-                       path[#path] = ''
-               elseif not (filestat and filestat.type == "dir") then
-                       path = { '', '' }
-               else
-                       path[#path+1] = ''
-               end
-
-               filepath = table.concat(path, '/')
-
-               local entries = {}
-               local _, e
-               for _, e in luci.util.vspairs(nixio.util.consume((nixio.fs.dir(filepath)))) do
-                       local p = filepath .. e
-                       local s = nixio.fs.stat(p)
-                       if s then
-                               entries[#entries+1] = {
-                                       name = e,
-                                       path = p,
-                                       type = s.type
-                               }
-                       end
-               end
-       -%>
-       <div id="path">
-               Location:
-               <% for i, dir in ipairs(path) do %>
-                       <% if i == 1 then %>
-                               <a href="<%=url(unpack(baseurl))%>?field=<%=luci.http.urlencode(field)%>">(root)</a>
-                       <% elseif next(path, i) then %>
-                               <% baseurl[#baseurl+1] = luci.http.urlencode(dir) %>
-                               / <a href="<%=url(unpack(baseurl))%>?field=<%=luci.http.urlencode(field)%>"><%=pcdata(dir)%></a>
-                       <% else %>
-                               <% baseurl[#baseurl+1] = luci.http.urlencode(dir) %>
-                               / <%=pcdata(dir)%>
-                       <% end %>
-               <% end %>
-       </div>
-
-       <hr />
-
-       <div id="listing">
-               <ul>
-                       <% for _, e in ipairs(entries) do if e.type == 'dir' then -%>
-                               <li class="dir">
-                                       <img src="<%=resource%>/cbi/folder.gif" alt="<%:Directory%>" />
-                                       <a href="<%=url(unpack(baseurl))%>/<%=luci.http.urlencode(e.name)%>?field=<%=luci.http.urlencode(field)%>"><%=pcdata(e.name)%>/</a>
-                               </li>
-                       <% end end -%>
-
-                       <% for _, e in ipairs(entries) do if e.type ~= 'dir' then -%>
-                               <li class="file">
-                                       <img src="<%=resource%>/cbi/file.gif" alt="<%:File%>" />
-                                       <a href="#" onclick="callback('<%=luci.http.urlencode(e.path)%>')"><%=pcdata(e.name)%></a>
-                               </li>
-                       <% end end -%>
-               </ul>
-       </div>
-</body>
-</html>
index 3c3d82b653885079b4755d06f3f2d37ae52a4784..e610495380a87340a14cf4d69f944067636f5215 100644 (file)
@@ -1,24 +1,14 @@
-<%
-       local t = require("luci.tools.webadmin")
-       local v = self:cfgvalue(section)
-       local s = v and nixio.fs.stat(v)
--%>
 <%+cbi/valueheader%>
-       <% if s then %>
-               <%:Uploaded File%> (<%=t.byte_format(s.size)%>)
-                <% if self.unsafeupload then %>
-                   <input type="hidden"<%= attr("value", v) .. attr("name", cbid) .. attr("id", cbid) %> />
-                   <input class="cbi-button cbi-button-image" type="image" value="<%:Replace entry%>" name="cbi.rlf.<%=section .. "." .. self.option%>" alt="<%:Replace entry%>" title="<%:Replace entry%>" src="<%=resource%>/cbi/reload.gif" />
-                <% end %>
-       <% end %>
 
-        <% if not self.unsafeupload then %>
-               <input type="hidden"<%= attr("value", v) .. attr("name", "cbi.rlf." .. section .. "." .. self.option) .. attr("id", "cbi.rlf." .. section .. "." .. self.option) %> />
-       <% end %>
+<div<%=attr("data-ui-widget", luci.util.serialize_json({
+       "FileUpload", self:cfgvalue(section) or self.default, {
+               id = cbid,
+               name = cbid,
+               show_hidden = self.show_hidden,
+               enable_remove = self.enable_remove,
+               enable_upload = self.enable_upload,
+               root_directory = "/" --self.root_directory
+       }
+}))%>></div>
 
-       <% if (not s) or (s and not self.unsafeupload) then %>
-               <input class="cbi-input-file" type="file"<%= attr("name", cbid) .. attr("id", cbid) %> />
-       <% end %>
-       <input type="text" class="cbi-input-text" data-update="change"<%=
-               attr("name", cbid .. ".textbox") .. attr("id", cbid .. ".textbox") .. attr("value", luci.cbi.AbstractValue.cfgvalue(self, section) or self.default) .. ifattr(self.size, "size") .. ifattr(self.placeholder, "placeholder") .. ifattr(self.readonly, "readonly") .. ifattr(self.maxlength, "maxlength") %> />
 <%+cbi/valuefooter%>
index 2804cc7003dbc7ef1be2fd67487d753fcb1a8192..57e0ae384b5a13e102c3b746551a7c683089a28e 100644 (file)
        "luci-access": {
                "description": "Grant access to basic LuCI procedures",
                "read": {
+                       "file": {
+                               "/": [ "list" ],
+                               "/*": [ "list" ]
+                       },
                        "ubus": {
+                               "file": [ "list", "stat" ],
                                "iwinfo": [ "assoclist" ],
                                "luci": [ "getBoardJSON", "getDUIDHints", "getHostHints", "getIfaddrs", "getInitList", "getLocaltime", "getTimezones", "getDHCPLeases", "getLEDs", "getNetworkDevices", "getUSBDevices", "getHostname", "getTTYDevices", "getWirelessDevices" ],
                                "network.device": [ "status" ],
                        "uci": [ "*" ]
                },
                "write": {
+                       "cgi-io": [ "upload", "/etc/luci-uploads/*" ],
+                       "file": {
+                               "/etc/luci-uploads/*": [ "write" ]
+                       },
                        "ubus": {
+                               "file": [ "remove" ],
                                "iwinfo": [ "scan" ],
                                "luci": [ "setInitAction", "setLocaltime" ],
                                "uci": [ "add", "apply", "confirm", "delete", "order", "set" ]
diff --git a/modules/luci-mod-admin-full/luasrc/controller/admin/filebrowser.lua b/modules/luci-mod-admin-full/luasrc/controller/admin/filebrowser.lua
deleted file mode 100644 (file)
index 2572615..0000000
+++ /dev/null
@@ -1,9 +0,0 @@
--- Copyright 2008 Steven Barth <steven@midlink.org>
--- Copyright 2008 Jo-Philipp Wich <jow@openwrt.org>
--- Licensed to the public under the Apache License 2.0.
-
-module("luci.controller.admin.filebrowser", package.seeall)
-
-function index()
-       entry( {"admin", "filebrowser"}, template("cbi/filebrowser") ).leaf = true
-end