luci-app-opkg: full convert to client side actions
Signed-off-by: Jo-Philipp Wich <jo@mein.io>
This commit is contained in:
parent
562425605a
commit
c6ce80e36f
6 changed files with 295 additions and 341 deletions
|
@ -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 = {
|
var packages = {
|
||||||
available: { providers: {}, pkgs: {} },
|
available: { providers: {}, pkgs: {} },
|
||||||
installed: { providers: {}, pkgs: {} }
|
installed: { providers: {}, pkgs: {} }
|
||||||
|
@ -471,6 +563,8 @@ function renderDependencies(depends, info)
|
||||||
info.seen = info.seen || [];
|
info.seen = info.seen || [];
|
||||||
|
|
||||||
for (var i = 0; i < deps.length; i++) {
|
for (var i = 0; i < deps.length; i++) {
|
||||||
|
var dep, vop, ver;
|
||||||
|
|
||||||
if (deps[i] === 'libc')
|
if (deps[i] === 'libc')
|
||||||
continue;
|
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('ul', {}, [
|
||||||
E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
|
E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
|
||||||
E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
|
E('li', '<strong>%s:</strong> %h'.format(_('Size'), size)),
|
||||||
|
@ -595,7 +689,7 @@ function handleInstall(ev)
|
||||||
]),
|
]),
|
||||||
E('div', {
|
E('div', {
|
||||||
'class': 'btn',
|
'class': 'btn',
|
||||||
'click': L.hideModal
|
'click': ui.hideModal
|
||||||
}, _('Cancel')),
|
}, _('Cancel')),
|
||||||
' ',
|
' ',
|
||||||
E('div', {
|
E('div', {
|
||||||
|
@ -635,11 +729,11 @@ function handleManualInstall(ev)
|
||||||
warning = E('p', {}, _('Really attempt to install <em>%h</em>?').format(name_or_url));
|
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,
|
warning,
|
||||||
E('div', { 'class': 'right' }, [
|
E('div', { 'class': 'right' }, [
|
||||||
E('div', {
|
E('div', {
|
||||||
'click': L.hideModal,
|
'click': ui.hideModal,
|
||||||
'class': 'btn cbi-button-neutral'
|
'class': 'btn cbi-button-neutral'
|
||||||
}, _('Cancel')),
|
}, _('Cancel')),
|
||||||
' ', install
|
' ', install
|
||||||
|
@ -649,11 +743,29 @@ function handleManualInstall(ev)
|
||||||
|
|
||||||
function handleConfig(ev)
|
function handleConfig(ev)
|
||||||
{
|
{
|
||||||
L.showModal(_('OPKG Configuration'), [
|
var conf = {};
|
||||||
|
|
||||||
|
ui.showModal(_('OPKG Configuration'), [
|
||||||
E('p', { 'class': 'spinning' }, _('Loading configuration data…'))
|
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 = [
|
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>.'))
|
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' }, [
|
body.push(E('div', { 'class': 'right' }, [
|
||||||
E('div', {
|
E('div', {
|
||||||
'class': 'btn cbi-button-neutral',
|
'class': 'btn cbi-button-neutral',
|
||||||
'click': L.hideModal
|
'click': ui.hideModal
|
||||||
}, _('Cancel')),
|
}, _('Cancel')),
|
||||||
' ',
|
' ',
|
||||||
E('div', {
|
E('div', {
|
||||||
|
@ -681,16 +793,20 @@ function handleConfig(ev)
|
||||||
data[textarea.getAttribute('name')] = textarea.value
|
data[textarea.getAttribute('name')] = textarea.value
|
||||||
});
|
});
|
||||||
|
|
||||||
L.showModal(_('OPKG Configuration'), [
|
ui.showModal(_('OPKG Configuration'), [
|
||||||
E('p', { 'class': 'spinning' }, _('Saving configuration data…'))
|
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')),
|
}, _('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('ul', {}, [
|
||||||
E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
|
E('li', '<strong>%s:</strong> %h'.format(_('Version'), pkg.version)),
|
||||||
E('li', '<strong>%s:</strong> %h'.format(_('Size'), size))
|
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', { 'style': 'flex-grow:1', 'class': 'right' }, [
|
||||||
E('div', {
|
E('div', {
|
||||||
'class': 'btn',
|
'class': 'btn',
|
||||||
'click': L.hideModal
|
'click': ui.hideModal
|
||||||
}, _('Cancel')),
|
}, _('Cancel')),
|
||||||
' ',
|
' ',
|
||||||
E('div', {
|
E('div', {
|
||||||
|
@ -749,15 +865,27 @@ function handleOpkg(ev)
|
||||||
var cmd = ev.target.getAttribute('data-command'),
|
var cmd = ev.target.getAttribute('data-command'),
|
||||||
pkg = ev.target.getAttribute('data-package'),
|
pkg = ev.target.getAttribute('data-package'),
|
||||||
rem = document.querySelector('input[name="autoremove"]'),
|
rem = document.querySelector('input[name="autoremove"]'),
|
||||||
owr = document.querySelector('input[name="overwrite"]'),
|
owr = document.querySelector('input[name="overwrite"]');
|
||||||
url = 'admin/system/opkg/exec/' + encodeURIComponent(cmd);
|
|
||||||
|
|
||||||
var dlg = L.showModal(_('Executing package manager'), [
|
var dlg = ui.showModal(_('Executing package manager'), [
|
||||||
E('p', { 'class': 'spinning' },
|
E('p', { 'class': 'spinning' },
|
||||||
_('Waiting for the <em>opkg %h</em> command to complete…').format(cmd))
|
_('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);
|
dlg.removeChild(dlg.lastChild);
|
||||||
|
|
||||||
if (res.stdout)
|
if (res.stdout)
|
||||||
|
@ -775,7 +903,7 @@ function handleOpkg(ev)
|
||||||
E('div', {
|
E('div', {
|
||||||
'class': 'btn',
|
'class': 'btn',
|
||||||
'click': L.bind(function(res) {
|
'click': L.bind(function(res) {
|
||||||
L.hideModal();
|
ui.hideModal();
|
||||||
updateLists();
|
updateLists();
|
||||||
|
|
||||||
if (res.code !== 0)
|
if (res.code !== 0)
|
||||||
|
@ -784,6 +912,9 @@ function handleOpkg(ev)
|
||||||
resolveFn(res);
|
resolveFn(res);
|
||||||
}, this, res)
|
}, this, res)
|
||||||
}, _('Dismiss'))));
|
}, _('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)
|
function handleUpload(ev)
|
||||||
{
|
{
|
||||||
var path = '/tmp/upload.ipk';
|
var path = '/tmp/upload.ipk';
|
||||||
return L.ui.uploadFile(path).then(L.bind(function(btn, res) {
|
return ui.uploadFile(path).then(L.bind(function(btn, res) {
|
||||||
L.showModal(_('Manually install package'), [
|
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('p', {}, _('Installing packages from untrusted sources is a potential security risk! Really attempt to install <em>%h</em>?').format(res.name)),
|
||||||
E('ul', {}, [
|
E('ul', {}, [
|
||||||
res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '',
|
res.size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res.size)) : '',
|
||||||
|
@ -802,8 +933,8 @@ function handleUpload(ev)
|
||||||
E('div', { 'class': 'right' }, [
|
E('div', { 'class': 'right' }, [
|
||||||
E('div', {
|
E('div', {
|
||||||
'click': function(ev) {
|
'click': function(ev) {
|
||||||
L.hideModal();
|
ui.hideModal();
|
||||||
L.fs.remove(path);
|
fs.remove(path);
|
||||||
},
|
},
|
||||||
'class': 'btn cbi-button-neutral'
|
'class': 'btn cbi-button-neutral'
|
||||||
}, _('Cancel')), ' ',
|
}, _('Cancel')), ' ',
|
||||||
|
@ -813,7 +944,7 @@ function handleUpload(ev)
|
||||||
'data-package': path,
|
'data-package': path,
|
||||||
'click': function(ev) {
|
'click': function(ev) {
|
||||||
handleOpkg(ev).finally(function() {
|
handleOpkg(ev).finally(function() {
|
||||||
L.fs.remove(path)
|
fs.remove(path)
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
}, _('Install'))
|
}, _('Install'))
|
||||||
|
@ -822,7 +953,16 @@ function handleUpload(ev)
|
||||||
}, this, ev.target));
|
}, 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', [],
|
cbi_update_table('#packages', [],
|
||||||
E('div', { 'class': 'spinning' }, _('Loading package information…')));
|
E('div', { 'class': 'spinning' }, _('Loading package information…')));
|
||||||
|
@ -830,42 +970,104 @@ function updateLists()
|
||||||
packages.available = { providers: {}, pkgs: {} };
|
packages.available = { providers: {}, pkgs: {} };
|
||||||
packages.installed = { 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'),
|
var pg = document.querySelector('.cbi-progressbar'),
|
||||||
total = stat.blocks || 0,
|
mount = L.toArray(data[0].filter(function(m) { return m.mount == '/' || m.mount == '/overlay' }))
|
||||||
free = stat.bfree || 0;
|
.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.firstElementChild.style.width = Math.floor(mount.size ? ((100 / mount.size) * mount.free) : 100) + '%';
|
||||||
pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, free * (stat.frsize || 0)));
|
pg.setAttribute('title', '%s (%.1024mB)'.format(pg.firstElementChild.style.width, mount.free));
|
||||||
|
|
||||||
L.get('admin/system/opkg/list/available', null, function(xhr) {
|
parseList(data[1], packages.available);
|
||||||
parseList(xhr.responseText, packages.available);
|
parseList(data[2], packages.installed);
|
||||||
L.get('admin/system/opkg/list/installed', null, function(xhr) {
|
|
||||||
parseList(xhr.responseText, packages.installed);
|
display(document.querySelector('input[name="filter"]').value);
|
||||||
display(document.querySelector('input[name="filter"]').value);
|
|
||||||
});
|
|
||||||
});
|
|
||||||
});
|
});
|
||||||
}
|
}
|
||||||
|
|
||||||
window.requestAnimationFrame(function() {
|
var keyTimeout = null;
|
||||||
var filter = document.querySelector('input[name="filter"]'),
|
|
||||||
keyTimeout = null;
|
|
||||||
|
|
||||||
filter.value = filter.getAttribute('value');
|
function handleKeyUp(ev) {
|
||||||
filter.addEventListener('keyup',
|
if (keyTimeout !== null)
|
||||||
function(ev) {
|
window.clearTimeout(keyTimeout);
|
||||||
if (keyTimeout !== null)
|
|
||||||
window.clearTimeout(keyTimeout);
|
|
||||||
|
|
||||||
keyTimeout = window.setTimeout(function() {
|
keyTimeout = window.setTimeout(function() {
|
||||||
display(ev.target.value);
|
display(ev.target.value);
|
||||||
}, 250);
|
}, 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') ]))
|
||||||
|
]),
|
||||||
|
|
||||||
|
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' ])
|
||||||
|
])
|
||||||
|
])
|
||||||
|
]);
|
||||||
|
|
||||||
|
requestAnimationFrame(function() {
|
||||||
|
updateLists(listData)
|
||||||
});
|
});
|
||||||
|
|
||||||
document.querySelector('#pager > .prev').addEventListener('click', handlePage);
|
return view;
|
||||||
document.querySelector('#pager > .next').addEventListener('click', handlePage);
|
},
|
||||||
document.querySelector('.cbi-tabmenu.mode').addEventListener('click', handleMode);
|
|
||||||
|
|
||||||
updateLists();
|
handleSave: null,
|
||||||
|
handleSaveApply: null,
|
||||||
|
handleReset: 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
|
|
|
@ -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> </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>
|
|
||||||
 
|
|
||||||
<button class="btn cbi-button-action" onclick="handleUpload(event)"><%:Upload Package…%></button>
|
|
||||||
 
|
|
||||||
<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"> </div>
|
|
||||||
</div>
|
|
||||||
</div>
|
|
||||||
|
|
||||||
<script type="text/javascript" src="<%=resource%>/view/opkg.js"></script>
|
|
||||||
|
|
||||||
<%+footer%>
|
|
15
applications/luci-app-opkg/root/usr/libexec/opkg-list
Executable file
15
applications/luci-app-opkg/root/usr/libexec/opkg-list
Executable file
|
@ -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
|
|
@ -3,42 +3,8 @@
|
||||||
"title": "Software",
|
"title": "Software",
|
||||||
"order": 30,
|
"order": 30,
|
||||||
"action": {
|
"action": {
|
||||||
"type": "template",
|
"type": "view",
|
||||||
"path": "opkg"
|
"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"
|
|
||||||
}
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
|
@ -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" ]
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
||||||
|
}
|
Loading…
Reference in a new issue