+'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: {} }
info.seen = info.seen || [];
for (var i = 0; i < deps.length; i++) {
+ var dep, vop, ver;
+
if (deps[i] === 'libc')
continue;
]);
}
- 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)),
]),
E('div', {
'class': 'btn',
- 'click': L.hideModal
+ 'click': ui.hideModal
}, _('Cancel')),
' ',
E('div', {
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
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>.'))
];
body.push(E('div', { 'class': 'right' }, [
E('div', {
'class': 'btn cbi-button-neutral',
- 'click': L.hideModal
+ 'click': ui.hideModal
}, _('Cancel')),
' ',
E('div', {
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);
});
}
]);
}
- 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))
E('div', { 'style': 'flex-grow:1', 'class': 'right' }, [
E('div', {
'class': 'btn',
- 'click': L.hideModal
+ 'click': ui.hideModal
}, _('Cancel')),
' ',
E('div', {
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)
E('div', {
'class': 'btn',
'click': L.bind(function(res) {
- L.hideModal();
+ ui.hideModal();
updateLists();
if (res.code !== 0)
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();
});
});
}
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)) : '',
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')), ' ',
'data-package': path,
'click': function(ev) {
handleOpkg(ev).finally(function() {
- L.fs.remove(path)
+ fs.remove(path)
});
}
}, _('Install'))
}, 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…')));
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
});
+++ /dev/null
--- 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