7 var callSystemValidateFirmwareImage = rpc.declare({
9 method: 'validate_firmware_image',
11 expect: { '': { valid: false, forcable: true } }
14 function findStorageSize(procmtd, procpart) {
15 var kernsize = 0, rootsize = 0, wholesize = 0;
17 procmtd.split(/\n/).forEach(function(ln) {
18 var match = ln.match(/^mtd\d+: ([0-9a-f]+) [0-9a-f]+ "(.+)"$/),
19 size = match ? parseInt(match[1], 16) : 0;
21 switch (match ? match[2] : '') {
44 else if (kernsize > 0 && rootsize > kernsize)
45 return kernsize + rootsize;
47 procpart.split(/\n/).forEach(function(ln) {
48 var match = ln.match(/^\s*\d+\s+\d+\s+(\d+)\s+(\S+)$/);
50 var size = parseInt(match[1], 10);
52 if (!match[2].match(/\d/) && size > 2048 && wholesize == 0)
53 wholesize = size * 1024;
61 var mapdata = { actions: {}, config: {} };
63 return L.view.extend({
66 L.resolveDefault(fs.stat('/lib/upgrade/platform.sh'), {}),
67 fs.trimmed('/proc/sys/kernel/hostname'),
68 fs.trimmed('/proc/mtd'),
69 fs.trimmed('/proc/partitions'),
70 fs.trimmed('/proc/mounts')
73 return Promise.all(tasks);
76 handleBackup: function(ev) {
77 var form = E('form', {
79 action: '/cgi-bin/cgi-backup',
80 enctype: 'application/x-www-form-urlencoded'
81 }, E('input', { type: 'hidden', name: 'sessionid', value: rpc.getSessionID() }));
83 ev.currentTarget.parentNode.appendChild(form);
86 form.parentNode.removeChild(form);
89 handleFirstboot: function(ev) {
90 if (!confirm(_('Do you really want to erase all settings?')))
93 ui.showModal(_('Erasing...'), [
94 E('p', { 'class': 'spinning' }, _('The system is erasing the configuration partition now and will reboot itself when finished.'))
97 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
98 fs.exec('/sbin/firstboot', [ '-r', '-y' ]);
100 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
103 handleRestore: function(ev) {
104 return ui.uploadFile('/tmp/backup.tar.gz', ev.target)
105 .then(L.bind(function(btn, res) {
106 btn.firstChild.data = _('Checking archiveā¦');
107 return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
109 .then(L.bind(function(btn, res) {
111 ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
112 return fs.remove('/tmp/backup.tar.gz');
115 ui.showModal(_('Apply backup?'), [
116 E('p', _('The uploaded backup archive appears to be valid and contains the files listed below. Press "Continue" to restore the backup and reboot, or "Cancel" to abort the operation.')),
117 E('pre', {}, [ res.stdout ]),
118 E('div', { 'class': 'right' }, [
121 'click': ui.createHandlerFn(this, function(ev) {
122 return fs.remove('/tmp/backup.tar.gz').finally(ui.hideModal);
124 }, [ _('Cancel') ]), ' ',
126 'class': 'btn cbi-button-action important',
127 'click': ui.createHandlerFn(this, 'handleRestoreConfirm', btn)
128 }, [ _('Continue') ])
132 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
133 .finally(L.bind(function(btn, input) {
134 btn.firstChild.data = _('Upload archive...');
135 }, this, ev.target));
138 handleRestoreConfirm: function(btn, ev) {
139 return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
140 .then(L.bind(function(btn, res) {
142 ui.addNotification(null, [
143 E('p', _('The restore command failed with code %d').format(res.code)),
144 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
146 L.raise('Error', 'Unpack failed');
149 btn.firstChild.data = _('Rebootingā¦');
150 return fs.exec('/sbin/reboot');
152 .then(L.bind(function(res) {
154 ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
155 L.raise('Error', 'Reboot failed');
158 ui.showModal(_('Rebootingā¦'), [
159 E('p', { 'class': 'spinning' }, _('The system is rebooting now. If the restored configuration changed the current LAN IP address, you might need to reconnect manually.'))
162 ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
164 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
165 .finally(function() { btn.firstChild.data = _('Upload archive...') });
168 handleBlock: function(hostname, ev) {
169 var mtdblock = L.dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select').value;
170 var form = E('form', {
172 'action': '/cgi-bin/cgi-download',
173 'enctype': 'application/x-www-form-urlencoded'
175 E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
176 E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/mtdblock%d'.format(mtdblock) }),
177 E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.mtd%d.bin'.format(hostname, mtdblock) })
180 ev.currentTarget.parentNode.appendChild(form);
183 form.parentNode.removeChild(form);
186 handleSysupgrade: function(storage_size, ev) {
187 return ui.uploadFile('/tmp/firmware.bin', ev.target.firstChild)
188 .then(L.bind(function(btn, reply) {
189 btn.firstChild.data = _('Checking imageā¦');
191 ui.showModal(_('Checking imageā¦'), [
192 E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.'))
195 return callSystemValidateFirmwareImage('/tmp/firmware.bin')
196 .then(function(res) { return [ reply, res ]; });
198 .then(L.bind(function(btn, reply) {
199 return fs.exec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ])
200 .then(function(res) { reply.push(res); return reply; });
202 .then(L.bind(function(btn, res) {
203 var keep = E('input', { type: 'checkbox' }),
204 force = E('input', { type: 'checkbox' }),
205 is_valid = res[1].valid,
206 is_forceable = res[1].forceable,
207 allow_backup = res[1].allow_backup,
208 is_too_big = (storage_size > 0 && res[0].size > storage_size),
211 body.push(E('p', _('The flash image was uploaded. Below is the checksum and file size listed, compare them with the original file to ensure data integrity. <br /> Click "Proceed" below to start the flash procedure.')));
212 body.push(E('ul', {}, [
213 res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '',
214 res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '',
215 res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : ''
218 body.push(E('p', {}, E('label', { 'class': 'btn' }, [
219 keep, ' ', _('Keep settings and retain the current configuration')
222 if (!is_valid || is_too_big)
226 body.push(E('p', { 'class': 'alert-message' }, [
227 _('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!')
231 body.push(E('p', { 'class': 'alert-message' }, [
232 res[2].stderr ? res[2].stderr : '',
233 res[2].stderr ? E('br') : '',
234 res[2].stderr ? E('br') : '',
235 _('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.')
239 body.push(E('p', { 'class': 'alert-message' }, [
240 _('The uploaded firmware does not allow keeping current configuration.')
246 keep.disabled = true;
249 if ((!is_valid || is_too_big) && is_forceable)
250 body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [
251 force, ' ', _('Force upgrade'),
253 _('Select \'Force upgrade\' to flash the image even if the image format check fails. Use only if you are sure that the firmware is correct and meant for your device!')
256 var cntbtn = E('button', {
257 'class': 'btn cbi-button-action important',
258 'click': ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, keep, force),
259 'disabled': (!is_valid || is_too_big) ? true : null
260 }, [ _('Continue') ]);
262 body.push(E('div', { 'class': 'right' }, [
265 'click': ui.createHandlerFn(this, function(ev) {
266 return fs.remove('/tmp/firmware.bin').finally(ui.hideModal);
268 }, [ _('Cancel') ]), ' ', cntbtn
271 force.addEventListener('change', function(ev) {
272 cntbtn.disabled = !ev.target.checked;
275 ui.showModal(_('Flash image?'), body);
277 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
278 .finally(L.bind(function(btn) {
279 btn.firstChild.data = _('Flash image...');
280 }, this, ev.target));
283 handleSysupgradeConfirm: function(btn, keep, force, ev) {
284 btn.firstChild.data = _('Flashingā¦');
286 ui.showModal(_('Flashingā¦'), [
287 E('p', { 'class': 'spinning' }, _('The system is flashing now.<br /> DO NOT POWER OFF THE DEVICE!<br /> Wait a few minutes before you try to reconnect. It might be necessary to renew the address of your computer to reach the device again, depending on your settings.'))
296 opts.push('--force');
298 opts.push('/tmp/firmware.bin');
300 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
301 fs.exec('/sbin/sysupgrade', opts);
304 ui.awaitReconnect(window.location.host);
306 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
309 handleBackupList: function(ev) {
310 return fs.exec('/sbin/sysupgrade', [ '--list-backup' ]).then(function(res) {
312 ui.addNotification(null, [
313 E('p', _('The sysupgrade command failed with code %d').format(res.code)),
314 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
316 L.raise('Error', 'Sysupgrade failed');
319 ui.showModal(_('Backup file list'), [
320 E('p', _('Below is the determined list of files to backup. It consists of changed configuration files marked by opkg, essential base files and the user defined backup patterns.')),
321 E('ul', {}, (res.stdout || '').trim().split(/\n/).map(function(ln) { return E('li', {}, ln) })),
322 E('div', { 'class': 'right' }, [
325 'click': ui.hideModal
332 handleBackupSave: function(m, ev) {
333 return m.save(function() {
334 return fs.write('/etc/sysupgrade.conf', mapdata.config.editlist.trim().replace(/\r\n/g, '\n') + '\n');
336 ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
337 }).catch(function(e) {
338 ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e)));
342 render: function(rpc_replies) {
343 var has_sysupgrade = (rpc_replies[0].type == 'file'),
344 hostname = rpc_replies[1],
345 procmtd = rpc_replies[2],
346 procpart = rpc_replies[3],
347 procmounts = rpc_replies[4],
348 has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:\/overlay \/ ") != null),
349 storage_size = findStorageSize(procmtd, procpart),
352 m = new form.JSONMap(mapdata, _('Flash operations'));
355 s = m.section(form.NamedSection, 'actions', _('Actions'));
358 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Backup'), _('Click "Generate archive" to download a tar archive of the current configuration files.'));
361 o = ss.option(form.Button, 'dl_backup', _('Download backup'));
362 o.inputstyle = 'action important';
363 o.inputtitle = _('Generate archive');
364 o.onclick = this.handleBackup;
367 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Restore'), _('To restore configuration files, you can upload a previously generated backup archive here. To reset the firmware to its initial state, click "Perform reset" (only possible with squashfs images).'));
370 if (has_rootfs_data) {
371 o = ss.option(form.Button, 'reset', _('Reset to defaults'));
372 o.inputstyle = 'negative important';
373 o.inputtitle = _('Perform reset');
374 o.onclick = this.handleFirstboot;
377 o = ss.option(form.Button, 'restore', _('Restore backup'), _('Custom files (certificates, scripts) may remain on the system. To prevent this, perform a factory-reset first.'));
378 o.inputstyle = 'action important';
379 o.inputtitle = _('Upload archive...');
380 o.onclick = L.bind(this.handleRestore, this);
383 if (procmtd.length) {
384 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Save mtdblock contents'), _('Click "Save mtdblock" to download specified mtdblock file. (NOTE: THIS FEATURE IS FOR PROFESSIONALS! )'));
387 o = ss.option(form.ListValue, 'mtdselect', _('Choose mtdblock'));
388 procmtd.split(/\n/).forEach(function(ln) {
389 var match = ln.match(/^mtd(\d+): .+ "(.+?)"$/);
391 o.value(match[1], match[2]);
394 o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
395 o.inputstyle = 'action important';
396 o.inputtitle = _('Save mtdblock');
397 o.onclick = L.bind(this.handleBlock, this, hostname);
401 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
403 ? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
404 : _('Sorry, there is no sysupgrade support present; a new firmware image must be flashed manually. Please refer to the wiki for device specific install instructions.'));
408 if (has_sysupgrade) {
409 o = ss.option(form.Button, 'sysupgrade', _('Image'));
410 o.inputstyle = 'action important';
411 o.inputtitle = _('Flash image...');
412 o.onclick = L.bind(this.handleSysupgrade, this, storage_size);
416 s = m.section(form.NamedSection, 'config', 'config', _('Configuration'), _('This is a list of shell glob patterns for matching files and directories to include during sysupgrade. Modified files in /etc/config/ and certain other configurations are automatically preserved.'));
417 s.render = L.bind(function(view /*, ... */) {
418 return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
419 .then(L.bind(function(node) {
420 node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
422 'class': 'cbi-button cbi-button-save',
423 'click': ui.createHandlerFn(view, 'handleBackupSave', this.map)
431 o = s.option(form.Button, 'showlist', _('Show current backup file list'));
432 o.inputstyle = 'action';
433 o.inputtitle = _('Open list...');
434 o.onclick = L.bind(this.handleBackupList, this);
436 o = s.option(form.TextValue, 'editlist');
439 o.load = function(section_id) {
440 return L.resolveDefault(fs.read('/etc/sysupgrade.conf'), '');
447 handleSaveApply: null,