From c6ce80e36f2e70c941492254f0ac2e8f159cb832 Mon Sep 17 00:00:00 2001 From: Jo-Philipp Wich Date: Mon, 24 Feb 2020 16:23:49 +0100 Subject: [PATCH] luci-app-opkg: full convert to client side actions Signed-off-by: Jo-Philipp Wich --- .../htdocs/luci-static/resources/view/opkg.js | 302 +++++++++++++++--- .../luci-app-opkg/luasrc/controller/opkg.lua | 109 ------- .../luci-app-opkg/luasrc/view/opkg.htm | 147 --------- .../luci-app-opkg/root/usr/libexec/opkg-list | 15 + .../usr/share/luci/menu.d/luci-app-opkg.json | 36 +-- .../usr/share/rpcd/acl.d/luci-app-opkg.json | 27 ++ 6 files changed, 295 insertions(+), 341 deletions(-) delete mode 100644 applications/luci-app-opkg/luasrc/controller/opkg.lua delete mode 100644 applications/luci-app-opkg/luasrc/view/opkg.htm create mode 100755 applications/luci-app-opkg/root/usr/libexec/opkg-list create mode 100644 applications/luci-app-opkg/root/usr/share/rpcd/acl.d/luci-app-opkg.json diff --git a/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js index c7cb55fc4..59ca2cd95 100644 --- a/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js +++ b/applications/luci-app-opkg/htdocs/luci-static/resources/view/opkg.js @@ -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 %h').format(pkg.name), [ + ui.showModal(_('Details for package %h').format(pkg.name), [ E('ul', {}, [ E('li', '%s: %h'.format(_('Version'), pkg.version)), E('li', '%s: %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 %h?').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 opkg. Use opkg.conf for global settings and customfeeds.conf for custom repository entries. The configuration in the other files may be changed but is usually not preserved by sysupgrade.')) ]; @@ -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 %h').format(pkg.name), [ + ui.showModal(_('Remove package %h').format(pkg.name), [ E('ul', {}, [ E('li', '%s: %h'.format(_('Version'), pkg.version)), E('li', '%s: %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 opkg %h 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 opkg %s 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 %h?').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 index ebdcf1b09..000000000 --- a/applications/luci-app-opkg/luasrc/controller/opkg.lua +++ /dev/null @@ -1,109 +0,0 @@ --- Copyright 2018 Jo-Philipp Wich --- 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 index 297891dbd..000000000 --- a/applications/luci-app-opkg/luasrc/view/opkg.htm +++ /dev/null @@ -1,147 +0,0 @@ -<%# - Copyright 2018 Jo-Philipp Wich - Licensed to the public under the Apache License 2.0. --%> - -<%+header%> - - - -

<%:Software%>

- -
-
- -
-
 
-
-
- -
- - /> -
- -
- - -
- -
- - -   - -   - -
-
- - - - - -
-
-
<%:Package name%>
-
<%:Version%>
-
<%:Size (.ipk)%>
-
<%:Description%>
-
 
-
-
- - - -<%+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 index 000000000..088bc6339 --- /dev/null +++ b/applications/luci-app-opkg/root/usr/libexec/opkg-list @@ -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 diff --git a/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json b/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json index 9356b586d..8632a41b3 100644 --- a/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json +++ b/applications/luci-app-opkg/root/usr/share/luci/menu.d/luci-app-opkg.json @@ -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 index 000000000..66ef81f10 --- /dev/null +++ b/applications/luci-app-opkg/root/usr/share/rpcd/acl.d/luci-app-opkg.json @@ -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" ] + } + } + } +} -- 2.25.1