luci-app-opkg: full convert to client side actions
authorJo-Philipp Wich <jo@mein.io>
Mon, 24 Feb 2020 15:23:49 +0000 (16:23 +0100)
committerJo-Philipp Wich <jo@mein.io>
Mon, 24 Feb 2020 15:23:49 +0000 (16:23 +0100)
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js
applications/luci-app-opkg/luasrc/controller/opkg.lua [deleted file]
applications/luci-app-opkg/luasrc/view/opkg.htm [deleted file]
applications/luci-app-opkg/root/usr/libexec/opkg-list [new file with mode: 0755]
applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json
applications/luci-app-opkg/root/usr/share/rpcd/acl.d/luci-app-opkg.json [new file with mode: 0644]

index c7cb55fc47fb7afd1dd11f043b69083c939a7fc3..59ca2cd951a3dbbb40bc529cad68af3c718d4a84 100644 (file)
@@ -1,3 +1,95 @@
+'use strict';
+'require fs';
+'require ui';
+'require rpc';
+
+var css = '                                                            \
+       .controls {                                                     \
+               display: flex;                                  \
+               margin: .5em 0 1em 0;                   \
+               flex-wrap: wrap;                                \
+               justify-content: space-around;  \
+       }                                                                       \
+                                                                               \
+       .controls > * {                                         \
+               padding: .25em;                                 \
+               white-space: nowrap;                    \
+               flex: 1 1 33%;                                  \
+               box-sizing: border-box;                 \
+               display: flex;                                  \
+               flex-wrap: wrap;                                \
+       }                                                                       \
+                                                                               \
+       .controls > *:first-child,                      \
+       .controls > * > label {                         \
+               flex-basis: 100%;                               \
+               min-width: 250px;                               \
+       }                                                                       \
+                                                                               \
+       .controls > *:nth-child(2),                     \
+       .controls > *:nth-child(3) {            \
+               flex-basis: 20%;                                \
+       }                                                                       \
+                                                                               \
+       .controls > * > .btn {                          \
+               flex-basis: 20px;                               \
+               text-align: center;                             \
+       }                                                                       \
+                                                                               \
+       .controls > * > * {                                     \
+               flex-grow: 1;                                   \
+               align-self: center;                             \
+       }                                                                       \
+                                                                               \
+       .controls > div > input {                       \
+               width: auto;                                    \
+       }                                                                       \
+                                                                               \
+       .td.version,                                            \
+       .td.size {                                                      \
+               white-space: nowrap;                    \
+       }                                                                       \
+                                                                               \
+       ul.deps, ul.deps ul, ul.errors {        \
+               margin-left: 1em;                               \
+       }                                                                       \
+                                                                               \
+       ul.deps li, ul.errors li {                      \
+               list-style: none;                               \
+       }                                                                       \
+                                                                               \
+       ul.deps li:before {                                     \
+               content: "↳";                                 \
+               display: inline-block;                  \
+               width: 1em;                                             \
+               margin-left: -1em;                              \
+       }                                                                       \
+                                                                               \
+       ul.deps li > span {                                     \
+               white-space: nowrap;                    \
+       }                                                                       \
+                                                                               \
+       ul.errors li {                                          \
+               color: #c44;                                    \
+               font-size: 90%;                                 \
+               font-weight: bold;                              \
+               padding-left: 1.5em;                    \
+       }                                                                       \
+                                                                               \
+       ul.errors li:before {                           \
+               content: "⚠";                                 \
+               display: inline-block;                  \
+               width: 1.5em;                                   \
+               margin-left: -1.5em;                    \
+       }                                                                       \
+';
+
+var callMountPoints = rpc.declare({
+       object: 'luci',
+       method: 'getMountPoints',
+       expect: { result: [] }
+});
+
 var packages = {
        available: { providers: {}, pkgs: {} },
        installed: { providers: {}, pkgs: {} }
@@ -471,6 +563,8 @@ function renderDependencies(depends, info)
        info.seen = info.seen || [];
 
        for (var i = 0; i < deps.length; i++) {
+               var dep, vop, ver;
+
                if (deps[i] === 'libc')
                        continue;
 
@@ -580,7 +674,7 @@ function handleInstall(ev)
                ]);
        }
 
-       L.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
+       ui.showModal(_('Details for package <em>%h</em>').format(pkg.name), [
                E('ul', {}, [
                        E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
                        E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
@@ -595,7 +689,7 @@ function handleInstall(ev)
                        ]),
                        E('div', {
                                'class': 'btn',
-                               'click': L.hideModal
+                               'click': ui.hideModal
                        }, _('Cancel')),
                        ' ',
                        E('div', {
@@ -635,11 +729,11 @@ function handleManualInstall(ev)
                warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
        }
 
-       L.showModal(_('Manually install package'), [
+       ui.showModal(_('Manually install package'), [
                warning,
                E('div', { 'class': 'right' }, [
                        E('div', {
-                               'click': L.hideModal,
+                               'click': ui.hideModal,
                                'class': 'btn cbi-button-neutral'
                        }, _('Cancel')),
                        ' ', install
@@ -649,11 +743,29 @@ function handleManualInstall(ev)
 
 function handleConfig(ev)
 {
-       L.showModal(_('OPKG Configuration'), [
+       var conf = {};
+
+       ui.showModal(_('OPKG Configuration'), [
                E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
        ]);
 
-       L.get('admin/system/opkg/config', null, function(xhr, conf) {
+       fs.list('/etc/opkg').then(function(partials) {
+               var files = [ '/etc/opkg.conf' ];
+
+               for (var i = 0; i < partials.length; i++)
+                       if (partials[i].type == 'file' && partials[i].name.match(/\.conf$/))
+                               files.push('/etc/opkg/' + partials[i].name);
+
+               return Promise.all(files.map(function(file) {
+                       return fs.read(file)
+                               .then(L.bind(function(conf, file, res) { conf[file] = res }, this, conf, file))
+                               .catch(function(err) {
+                                       ui.addNotification(null, E('p', {}, [ _('Unable to read %s: %s').format(file, err) ]));
+                                       ui.hideModal();
+                                       throw err;
+                               });
+               }));
+       }).then(function() {
                var body = [
                        E('p', {}, _('Below is a listing of the various configuration files used by <em>opkg</em>. Use <em>opkg.conf</em> for global settings and <em>customfeeds.conf</em> for custom repository entries. The configuration in the other files may be changed but is usually not preserved by <em>sysupgrade</em>.'))
                ];
@@ -669,7 +781,7 @@ function handleConfig(ev)
                body.push(E('div', { 'class': 'right' }, [
                        E('div', {
                                'class': 'btn cbi-button-neutral',
-                               'click': L.hideModal
+                               'click': ui.hideModal
                        }, _('Cancel')),
                        ' ',
                        E('div', {
@@ -681,16 +793,20 @@ function handleConfig(ev)
                                                        data[textarea.getAttribute('name')] = textarea.value
                                                });
 
-                                       L.showModal(_('OPKG Configuration'), [
+                                       ui.showModal(_('OPKG Configuration'), [
                                                E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
                                        ]);
 
-                                       L.post('admin/system/opkg/config', { data: JSON.stringify(data) }, L.hideModal);
+                                       Promise.all(Object.keys(data).map(function(file) {
+                                               return fs.write(file, data[file]).catch(function(err) {
+                                                       ui.addNotification(null, E('p', {}, [ _('Unable to save %s: %s').format(file, err) ]));
+                                               });
+                                       })).then(ui.hideModal);
                                }
                        }, _('Save')),
                ]));
 
-               L.showModal(_('OPKG Configuration'), body);
+               ui.showModal(_('OPKG Configuration'), body);
        });
 }
 
@@ -715,7 +831,7 @@ function handleRemove(ev)
                ]);
        }
 
-       L.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
+       ui.showModal(_('Remove package <em>%h</em>').format(pkg.name), [
                E('ul', {}, [
                        E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
                        E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
@@ -729,7 +845,7 @@ function handleRemove(ev)
                        E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
                                E('div', {
                                        'class': 'btn',
-                                       'click': L.hideModal
+                                       'click': ui.hideModal
                                }, _('Cancel')),
                                ' ',
                                E('div', {
@@ -749,15 +865,27 @@ function handleOpkg(ev)
                var cmd = ev.target.getAttribute('data-command'),
                    pkg = ev.target.getAttribute('data-package'),
                    rem = document.querySelector('input[name="autoremove"]'),
-                   owr = document.querySelector('input[name="overwrite"]'),
-                   url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
+                   owr = document.querySelector('input[name="overwrite"]');
 
-               var dlg = L.showModal(_('Executing package manager'), [
+               var dlg = ui.showModal(_('Executing package manager'), [
                        E('p', { 'class': 'spinning' },
                                _('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
                ]);
 
-               L.post(url, { package: pkg, autoremove: rem ? rem.checked : false, overwrite: owr ? owr.checked : false }, function(xhr, res) {
+               var argv = [ '--force-removal-of-dependent-packages' ];
+
+               if (rem && rem.checked)
+                       argv.push('--autoremove');
+
+               if (owr && owr.checked)
+                       argv.push('--force-overwrite');
+
+               argv.push(cmd);
+
+               if (pkg != null)
+                       argv.push(pkg);
+
+               fs.exec('/bin/opkg', argv).then(function(res) {
                        dlg.removeChild(dlg.lastChild);
 
                        if (res.stdout)
@@ -775,7 +903,7 @@ function handleOpkg(ev)
                                E('div', {
                                        'class': 'btn',
                                        'click': L.bind(function(res) {
-                                               L.hideModal();
+                                               ui.hideModal();
                                                updateLists();
 
                                                if (res.code !== 0)
@@ -784,6 +912,9 @@ function handleOpkg(ev)
                                                        resolveFn(res);
                                        }, this, res)
                                }, _('Dismiss'))));
+               }).catch(function(err) {
+                       ui.addNotification(null, E('p', _('Unable to execute <em>opkg %s</em> command: %s').format(cmd, err)));
+                       ui.hideModal();
                });
        });
 }
@@ -791,8 +922,8 @@ function handleOpkg(ev)
 function handleUpload(ev)
 {
        var path = '/tmp/upload.ipk';
-       return L.ui.uploadFile(path).then(L.bind(function(btn, res) {
-               L.showModal(_('Manually install package'), [
+       return ui.uploadFile(path).then(L.bind(function(btn, res) {
+               ui.showModal(_('Manually install package'), [
                        E('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res.name)),
                        E('ul', {}, [
                                res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '',
@@ -802,8 +933,8 @@ function handleUpload(ev)
                        E('div', { 'class': 'right' }, [
                                E('div', {
                                        'click': function(ev) {
-                                               L.hideModal();
-                                               L.fs.remove(path);
+                                               ui.hideModal();
+                                               fs.remove(path);
                                        },
                                        'class': 'btn cbi-button-neutral'
                                }, _('Cancel')), ' ',
@@ -813,7 +944,7 @@ function handleUpload(ev)
                                        'data-package': path,
                                        'click': function(ev) {
                                                handleOpkg(ev).finally(function() {
-                                                       L.fs.remove(path)
+                                                       fs.remove(path)
                                                });
                                        }
                                }, _('Install'))
@@ -822,7 +953,16 @@ function handleUpload(ev)
        }, this, ev.target));
 }
 
-function updateLists()
+function downloadLists()
+{
+       return Promise.all([
+               callMountPoints(),
+               fs.exec_direct('/usr/libexec/opkg-list', [ 'available' ]),
+               fs.exec_direct('/usr/libexec/opkg-list', [ 'installed' ])
+       ]);
+}
+
+function updateLists(data)
 {
        cbi_update_table('#packages', [],
                E('div', { 'class': 'spinning' }, _('Loading package information…')));
@@ -830,42 +970,104 @@ function updateLists()
        packages.available = { providers: {}, pkgs: {} };
        packages.installed = { providers: {}, pkgs: {} };
 
-       L.get('admin/system/opkg/statvfs', null, function(xhr, stat) {
+       return (data ? Promise.resolve(data) : downloadLists()).then(function(data) {
                var pg = document.querySelector('.cbi-progressbar'),
-                   total = stat.blocks || 0,
-                   free = stat.bfree || 0;
+                   mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' }))
+                       .sort(function(a, b) { return a.mount > b.mount })[0] || { size: 0, free: 0 };
 
-               pg.firstElementChild.style.width = Math.floor(total ? ((100 / total) * free) : 100) + '%';
-               pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
+               pg.firstElementChild.style.width = Math.floor(mount.size ? ((100 / mount.size) * mount.free) : 100) + '%';
+               pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, mount.free));
 
-               L.get('admin/system/opkg/list/available', null, function(xhr) {
-                       parseList(xhr.responseText, packages.available);
-                       L.get('admin/system/opkg/list/installed', null, function(xhr) {
-                               parseList(xhr.responseText, packages.installed);
-                               display(document.querySelector('input[name="filter"]').value);
-                       });
-               });
+               parseList(data[1], packages.available);
+               parseList(data[2], packages.installed);
+
+               display(document.querySelector('input[name="filter"]').value);
        });
 }
 
-window.requestAnimationFrame(function() {
-       var filter = document.querySelector('input[name="filter"]'),
-           keyTimeout = null;
+var keyTimeout = null;
+
+function handleKeyUp(ev) {
+       if (keyTimeout !== null)
+               window.clearTimeout(keyTimeout);
+
+       keyTimeout = window.setTimeout(function() {
+               display(ev.target.value);
+       }, 250);
+}
+
+return L.view.extend({
+       load: function() {
+               return downloadLists();
+       },
+
+       render: function(listData) {
+               var query = decodeURIComponent(L.toArray(location.search.match(/\bquery=([^=]+)\b/))[1] || '');
+
+               var view = E([], [
+                       E('style', { 'type': 'text/css' }, [ css ]),
+
+                       E('h2', {}, _('Software')),
+
+                       E('div', { 'class': 'controls' }, [
+                               E('div', {}, [
+                                       E('label', {}, _('Free space') + ':'),
+                                       E('div', { 'class': 'cbi-progressbar', 'title': _('unknown') }, E('div', {}, [ '\u00a0' ]))
+                               ]),
+
+                               E('div', {}, [
+                                       E('label', {}, _('Filter') + ':'),
+                                       E('input', { 'type': 'text', 'name': 'filter', 'placeholder': _('Type to filter…'), 'value': query, 'keyup': handleKeyUp }),
+                                       E('button', { 'class': 'btn cbi-button', 'click': handleReset }, [ _('Clear') ])
+                               ]),
+
+                               E('div', {}, [
+                                       E('label', {}, _('Download and install package') + ':'),
+                                       E('input', { 'type': 'text', 'name': 'install', 'placeholder': _('Package name or URL…'), 'keydown': function(ev) { if (ev.keyCode === 13) handleManualInstall(ev) } }),
+                                       E('button', { 'class': 'btn cbi-button cbi-button-action', 'click': handleManualInstall }, [ _('OK') ])
+                               ]),
+
+                               E('div', {}, [
+                                       E('label', {}, _('Actions') + ':'), ' ',
+                                       E('button', { 'class': 'btn cbi-button-positive', 'data-command': 'update', 'click': handleOpkg }, [ _('Update lists…') ]), '\u00a0',
+                                       E('button', { 'class': 'btn cbi-button-action', 'click': handleUpload }, [ _('Upload Package…') ]), '\u00a0',
+                                       E('button', { 'class': 'btn cbi-button-neutral', 'click': handleConfig }, [ _('Configure opkg…') ])
+                               ])
+                       ]),
+
+                       E('ul', { 'class': 'cbi-tabmenu mode' }, [
+                               E('li', { 'data-mode': 'available', 'class': 'available cbi-tab', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Available') ])),
+                               E('li', { 'data-mode': 'installed', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Installed') ])),
+                               E('li', { 'data-mode': 'updates', 'class': 'installed cbi-tab-disabled', 'click': handleMode }, E('a', { 'href': '#' }, [ _('Updates') ]))
+                       ]),
 
-       filter.value = filter.getAttribute('value');
-       filter.addEventListener('keyup',
-               function(ev) {
-                       if (keyTimeout !== null)
-                               window.clearTimeout(keyTimeout);
+                       E('div', { 'class': 'controls', 'style': 'display:none' }, [
+                               E('div', { 'id': 'pager', 'class': 'center' }, [
+                                       E('button', { 'class': 'btn cbi-button-neutral prev', 'aria-label': _('Previous page'), 'click': handlePage }, [ '«' ]),
+                                       E('div', { 'class': 'text' }, [ 'dummy' ]),
+                                       E('button', { 'class': 'btn cbi-button-neutral next', 'aria-label': _('Next page'), 'click': handlePage }, [ '»' ])
+                               ])
+                       ]),
+
+                       E('div', { 'id': 'packages', 'class': 'table' }, [
+                               E('div', { 'class': 'tr cbi-section-table-titles' }, [
+                                       E('div', { 'class': 'th col-2 left' }, [ _('Package name') ]),
+                                       E('div', { 'class': 'th col-2 left version' }, [ _('Version') ]),
+                                       E('div', { 'class': 'th col-1 center size'}, [ _('Size (.ipk)') ]),
+                                       E('div', { 'class': 'th col-10 left' }, [ _('Description') ]),
+                                       E('div', { 'class': 'th right' }, [ '\u00a0' ])
+                               ])
+                       ])
+               ]);
 
-                       keyTimeout = window.setTimeout(function() {
-                               display(ev.target.value);
-                       }, 250);
+               requestAnimationFrame(function() {
+                       updateLists(listData)
                });
 
-       document.querySelector('#pager > .prev').addEventListener('click', handlePage);
-       document.querySelector('#pager > .next').addEventListener('click', handlePage);
-       document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
+               return view;
+       },
 
-       updateLists();
+       handleSave: null,
+       handleSaveApply: null,
+       handleReset: null
 });
diff --git a/applications/luci-app-opkg/luasrc/controller/opkg.lua b/applications/luci-app-opkg/luasrc/controller/opkg.lua
deleted file mode 100644 (file)
index ebdcf1b..0000000
+++ /dev/null
@@ -1,109 +0,0 @@
--- Copyright 2018 Jo-Philipp Wich <jo@mein.io>
--- Licensed to the public under the Apache License 2.0.
-
-module("luci.controller.opkg", package.seeall)
-
-function action_list(mode)
-       local util = require "luci.util"
-       local cmd
-
-       if mode == "installed" then
-               cmd = { "/bin/cat", "/usr/lib/opkg/status" }
-       else
-               local lists_dir = nil
-
-               local fd = io.popen([[sed -rne 's#^lists_dir \S+ (\S+)#\1#p' /etc/opkg.conf /etc/opkg/*.conf 2>/dev/null]], "r")
-               if fd then
-                       lists_dir = fd:read("*l")
-                       fd:close()
-               end
-
-               if not lists_dir or #lists_dir == 0 then
-                       lists_dir = "/tmp/opkg-lists"
-               end
-
-               cmd = { "/bin/sh", "-c", [[find %s -type f '!' -name '*.sig' | xargs -r gzip -cd]] % util.shellquote(lists_dir) }
-       end
-
-       luci.http.prepare_content("text/plain; charset=utf-8")
-       luci.sys.process.exec(cmd, luci.http.write)
-end
-
-function action_exec(command, package)
-       local sys = require "luci.sys"
-       local cmd = { "/bin/opkg", "--force-removal-of-dependent-packages" }
-       local pkg = luci.http.formvalue("package")
-
-       if luci.http.formvalue("autoremove") == "true" then
-               cmd[#cmd + 1] = "--autoremove"
-       end
-
-       if luci.http.formvalue("overwrite") == "true" then
-               cmd[#cmd + 1] = "--force-overwrite"
-       end
-
-       cmd[#cmd + 1] = command
-
-       if pkg then
-               cmd[#cmd + 1] = pkg
-       end
-
-       luci.http.prepare_content("application/json")
-       luci.http.write_json(sys.process.exec(cmd, true, true))
-end
-
-function action_statvfs()
-       local fs = require "nixio.fs"
-
-       luci.http.prepare_content("application/json")
-       luci.http.write_json(fs.statvfs("/") or {})
-end
-
-function action_config()
-       local fs = require "nixio.fs"
-       local js = require "luci.jsonc"
-       local data = luci.http.formvalue("data")
-
-       if data then
-               data = js.parse(data)
-
-               if not data then
-                       luci.http.status(400, "Bad Request")
-                       return
-               end
-
-               local file, content
-               for file, content in pairs(data) do
-                       if type(content) ~= "string" or
-                          (file ~= "opkg.conf" and not file:match("^opkg/[^/]+%.conf$"))
-                       then
-                               luci.http.status(400, "Bad Request")
-                               return
-                       end
-
-                       local path = "/etc/%s" % file
-                       if not fs.access(path, "w") then
-                               luci.http.status(403, "Permission denied")
-                               return
-                       end
-
-                       fs.writefile(path, content:gsub("\r\n", "\n"))
-               end
-
-               luci.http.status(204, "Saved")
-       else
-               local rv = { ["opkg.conf"] = fs.readfile("/etc/opkg.conf") }
-               local entries = fs.dir("/etc/opkg")
-               if entries then
-                       local entry
-                       for entry in entries do
-                               if entry:match("%.conf$") then
-                                       rv["opkg/%s" % entry] = fs.readfile("/etc/opkg/%s" % entry)
-                               end
-                       end
-               end
-
-               luci.http.prepare_content("application/json")
-               luci.http.write_json(rv)
-       end
-end
diff --git a/applications/luci-app-opkg/luasrc/view/opkg.htm b/applications/luci-app-opkg/luasrc/view/opkg.htm
deleted file mode 100644 (file)
index 297891d..0000000
+++ /dev/null
@@ -1,147 +0,0 @@
-<%#
- Copyright 2018 Jo-Philipp Wich <jo@mein.io>
- Licensed to the public under the Apache License 2.0.
--%>
-
-<%+header%>
-
-<style type="text/css">
-       .controls {
-               display: flex;
-               margin: .5em 0 1em 0;
-               flex-wrap: wrap;
-               justify-content: space-around;
-       }
-
-       .controls > * {
-               padding: .25em;
-               white-space: nowrap;
-               flex: 1 1 33%;
-               box-sizing: border-box;
-               display: flex;
-               flex-wrap: wrap;
-       }
-
-       .controls > *:first-child,
-       .controls > * > label {
-               flex-basis: 100%;
-               min-width: 250px;
-       }
-
-       .controls > *:nth-child(2),
-       .controls > *:nth-child(3) {
-               flex-basis: 20%;
-       }
-
-       .controls > * > .btn {
-               flex-basis: 20px;
-               text-align: center;
-       }
-
-       .controls > * > * {
-               flex-grow: 1;
-               align-self: center;
-       }
-
-       .controls > div > input {
-               width: auto;
-       }
-
-       .td.version,
-       .td.size {
-               white-space: nowrap;
-       }
-
-       ul.deps, ul.deps ul, ul.errors {
-               margin-left: 1em;
-       }
-
-       ul.deps li, ul.errors li {
-               list-style: none;
-       }
-
-       ul.deps li:before {
-               content: "↳";
-               display: inline-block;
-               width: 1em;
-               margin-left: -1em;
-       }
-
-       ul.deps li > span {
-               white-space: nowrap;
-       }
-
-       ul.errors li {
-               color: #c44;
-               font-size: 90%;
-               font-weight: bold;
-               padding-left: 1.5em;
-       }
-
-       ul.errors li:before {
-               content: "⚠";
-               display: inline-block;
-               width: 1.5em;
-               margin-left: -1.5em;
-       }
-</style>
-
-<h2><%:Software%></h2>
-
-<div class="controls">
-       <div>
-               <label><%:Free space%>:</label>
-               <div class="cbi-progressbar" title="<%:unknown%>">
-                       <div>&#160;</div>
-               </div>
-       </div>
-
-       <div>
-               <label><%:Filter%>:</label>
-               <input type="text" name="filter" placeholder="<%:Type to filter…%>"<%=attr("value", luci.http.formvalue("query") or "")%> /><!--
-               --><button class="btn cbi-button" onclick="handleReset(event)"><%:Clear%></button>
-       </div>
-
-       <div>
-               <label><%:Download and install package%>:</label>
-               <input type="text" name="install" placeholder="<%:Package name or URL…%>" onkeydown="if (event.keyCode === 13) handleManualInstall(event)" /><!--
-               --><button class="btn cbi-button cbi-button-action" onclick="handleManualInstall(event)"><%:OK%></button>
-       </div>
-
-       <div>
-               <label><%:Actions%>:</label>
-               <button class="btn cbi-button-positive" data-command="update" onclick="handleOpkg(event)"><%:Update lists…%></button>
-               &#160;
-               <button class="btn cbi-button-action" onclick="handleUpload(event)"><%:Upload Package…%></button>
-               &#160;
-               <button class="btn cbi-button-neutral" onclick="handleConfig(event)"><%:Configure opkg…%></button>
-       </div>
-</div>
-
-<ul class="cbi-tabmenu mode">
-       <li data-mode="available" class="available cbi-tab"><a href="#"><%:Available%></a></li>
-       <li data-mode="installed" class="installed cbi-tab-disabled"><a href="#"><%:Installed%></a></li>
-       <li data-mode="updates" class="installed cbi-tab-disabled"><a href="#"><%:Updates%></a></li>
-</ul>
-
-<div class="controls" style="display:none">
-       <div id="pager" class="center">
-               <button class="btn cbi-button-neutral prev" aria-label="<%:Previous page%>">«</button>
-               <div class="text">dummy</div>
-               <button class="btn cbi-button-neutral next" aria-label="<%:Next page%>">»</button>
-       </div>
-</div>
-
-<div class="table" id="packages">
-       <div class="tr cbi-section-table-titles">
-               <div class="th col-2 left"><%:Package name%></div>
-               <div class="th col-2 left version"><%:Version%></div>
-               <div class="th col-1 center size"><%:Size (.ipk)%></div>
-               <div class="th col-10 left"><%:Description%></div>
-               <div class="th right">&#160;</div>
-       </div>
-</div>
-
-<script type="text/javascript" src="<%=resource%>/view/opkg.js"></script>
-
-<%+footer%>
diff --git a/applications/luci-app-opkg/root/usr/libexec/opkg-list b/applications/luci-app-opkg/root/usr/libexec/opkg-list
new file mode 100755 (executable)
index 0000000..088bc63
--- /dev/null
@@ -0,0 +1,15 @@
+#!/bin/sh
+
+case "$1" in
+       installed)
+               cat /usr/lib/opkg/status
+       ;;
+       available)
+               lists_dir=$(sed -rne 's#^lists_dir \S+ (\S+)#\1#p' /etc/opkg.conf /etc/opkg/*.conf 2>/dev/null | tail -n 1)
+               find "${lists_dir:-/tmp/opkg-lists}" -type f '!' -name '*.sig' | xargs -r gzip -cd
+       ;;
+       *)
+               echo "Usage: $0 {installed|available}" >&2
+               exit 1
+       ;;
+esac
index 9356b586dd97269de420aa1e32dc3a4fc5f2c19f..8632a41b3ea6ddc2d539a050ed51b87badf984c2 100644 (file)
@@ -3,42 +3,8 @@
                "title": "Software",
                "order": 30,
                "action": {
-                       "type": "template",
+                       "type": "view",
                        "path": "opkg"
                }
-       },
-
-       "admin/system/opkg/list/*": {
-               "action": {
-                       "type": "call",
-                       "module": "luci.controller.opkg",
-                       "function": "action_list"
-               }
-       },
-
-       "admin/system/opkg/exec/*": {
-               "action": {
-                       "type": "call",
-                       "post": true,
-                       "module": "luci.controller.opkg",
-                       "function": "action_exec"
-               }
-       },
-
-       "admin/system/opkg/statvfs/*": {
-               "action": {
-                       "type": "call",
-                       "module": "luci.controller.opkg",
-                       "function": "action_statvfs"
-               }
-       },
-
-       "admin/system/opkg/config/*": {
-               "action": {
-                       "type": "call",
-                       "post": { "data": true },
-                       "module": "luci.controller.opkg",
-                       "function": "action_config"
-               }
        }
 }
diff --git a/applications/luci-app-opkg/root/usr/share/rpcd/acl.d/luci-app-opkg.json b/applications/luci-app-opkg/root/usr/share/rpcd/acl.d/luci-app-opkg.json
new file mode 100644 (file)
index 0000000..66ef81f
--- /dev/null
@@ -0,0 +1,27 @@
+{
+       "luci-app-opkg": {
+               "description": "Grant access to opkg management",
+               "read": {
+                       "cgi-io": [ "exec" ],
+                       "file": {
+                               "/usr/libexec/opkg-list installed": [ "exec" ],
+                               "/usr/libexec/opkg-list available": [ "exec" ],
+                               "/etc/opkg.conf": [ "read" ],
+                               "/etc/opkg/*.conf": [ "read" ]
+                       },
+                       "ubus": {
+                               "luci": [ "getMountPoints" ]
+                       }
+               },
+               "write": {
+                       "file": {
+                               "/bin/opkg * install *": [ "exec" ],
+                               "/bin/opkg * remove *": [ "exec" ],
+                               "/bin/opkg * update": [ "exec" ],
+                               "/etc/opkg.conf": [ "write" ],
+                               "/etc/opkg/*.conf": [ "write" ],
+                               "/tmp/upload.ipk": [ "write" ]
+                       }
+               }
+       }
+}