6 var callSystemValidateFirmwareImage = rpc.declare({
8 method: 'validate_firmware_image',
10 expect: { '': { valid: false, forcable: true } }
13 function findStorageSize(procmtd, procpart) {
14 var kernsize = 0, rootsize = 0, wholesize = 0;
16 procmtd.split(/\n/).forEach(function(ln) {
17 var match = ln.match(/^mtd\d+: ([0-9a-f]+) [0-9a-f]+ "(.+)"$/),
18 size = match ? parseInt(match[1], 16) : 0;
20 switch (match ? match[2] : '') {
43 else if (kernsize > 0 && rootsize > kernsize)
44 return kernsize + rootsize;
46 procpart.split(/\n/).forEach(function(ln) {
47 var match = ln.match(/^\s*\d+\s+\d+\s+(\d+)\s+(\S+)$/);
49 var size = parseInt(match[1], 10);
51 if (!match[2].match(/\d/) && size > 2048 && wholesize == 0)
52 wholesize = size * 1024;
60 var mapdata = { actions: {}, config: {} };
62 return L.view.extend({
65 L.resolveDefault(fs.stat('/lib/upgrade/platform.sh'), {}),
66 fs.trimmed('/proc/sys/kernel/hostname'),
67 fs.trimmed('/proc/mtd'),
68 fs.trimmed('/proc/partitions'),
69 fs.trimmed('/proc/mounts')
72 return Promise.all(tasks);
75 handleBackup: function(ev) {
76 var form = E('form', {
78 action: '/cgi-bin/cgi-backup',
79 enctype: 'application/x-www-form-urlencoded'
80 }, E('input', { type: 'hidden', name: 'sessionid', value: rpc.getSessionID() }));
82 ev.currentTarget.parentNode.appendChild(form);
85 form.parentNode.removeChild(form);
88 handleFirstboot: function(ev) {
89 if (!confirm(_('Do you really want to erase all settings?')))
92 L.ui.showModal(_('Erasing...'), [
93 E('p', { 'class': 'spinning' }, _('The system is erasing the configuration partition now and will reboot itself when finished.'))
96 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
97 fs.exec('/sbin/firstboot', [ '-r', '-y' ]);
99 L.ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
102 handleRestore: function(ev) {
103 return L.ui.uploadFile('/tmp/backup.tar.gz', ev.target)
104 .then(L.bind(function(btn, res) {
105 btn.firstChild.data = _('Checking archiveā¦');
106 return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
108 .then(L.bind(function(btn, res) {
110 L.ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
111 return fs.remove('/tmp/backup.tar.gz');
114 L.ui.showModal(_('Apply backup?'), [
115 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.')),
116 E('pre', {}, [ res.stdout ]),
117 E('div', { 'class': 'right' }, [
120 'click': L.ui.createHandlerFn(this, function(ev) {
121 return fs.remove('/tmp/backup.tar.gz').finally(L.ui.hideModal);
123 }, [ _('Cancel') ]), ' ',
125 'class': 'btn cbi-button-action important',
126 'click': L.ui.createHandlerFn(this, 'handleRestoreConfirm', btn)
127 }, [ _('Continue') ])
131 .catch(function(e) { L.ui.addNotification(null, E('p', e.message)) })
132 .finally(L.bind(function(btn, input) {
133 btn.firstChild.data = _('Upload archive...');
134 }, this, ev.target));
137 handleRestoreConfirm: function(btn, ev) {
138 return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
139 .then(L.bind(function(btn, res) {
141 L.ui.addNotification(null, [
142 E('p', _('The restore command failed with code %d').format(res.code)),
143 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
145 L.raise('Error', 'Unpack failed');
148 btn.firstChild.data = _('Rebootingā¦');
149 return fs.exec('/sbin/reboot');
151 .then(L.bind(function(res) {
153 L.ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
154 L.raise('Error', 'Reboot failed');
157 L.ui.showModal(_('Rebootingā¦'), [
158 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.'))
161 L.ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
163 .catch(function(e) { L.ui.addNotification(null, E('p', e.message)) })
164 .finally(function() { btn.firstChild.data = _('Upload archive...') });
167 handleBlock: function(hostname, ev) {
168 var mtdblock = L.dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select').value;
169 var form = E('form', {
171 'action': '/cgi-bin/cgi-download',
172 'enctype': 'application/x-www-form-urlencoded'
174 E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
175 E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/mtdblock%d'.format(mtdblock) }),
176 E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.mtd%d.bin'.format(hostname, mtdblock) })
179 ev.currentTarget.parentNode.appendChild(form);
182 form.parentNode.removeChild(form);
185 handleSysupgrade: function(storage_size, ev) {
186 return L.ui.uploadFile('/tmp/firmware.bin', ev.target.firstChild)
187 .then(L.bind(function(btn, reply) {
188 btn.firstChild.data = _('Checking imageā¦');
190 L.ui.showModal(_('Checking imageā¦'), [
191 E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.'))
194 return callSystemValidateFirmwareImage('/tmp/firmware.bin')
195 .then(function(res) { return [ reply, res ]; });
197 .then(L.bind(function(btn, reply) {
198 return fs.exec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ])
199 .then(function(res) { reply.push(res); return reply; });
201 .then(L.bind(function(btn, res) {
202 var keep = E('input', { type: 'checkbox' }),
203 force = E('input', { type: 'checkbox' }),
204 is_valid = res[1].valid,
205 is_forceable = res[1].forceable,
206 allow_backup = res[1].allow_backup,
207 is_too_big = (storage_size > 0 && res[0].size > storage_size),
210 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.')));
211 body.push(E('ul', {}, [
212 res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '',
213 res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '',
214 res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : ''
217 body.push(E('p', {}, E('label', { 'class': 'btn' }, [
218 keep, ' ', _('Keep settings and retain the current configuration')
221 if (!is_valid || is_too_big)
225 body.push(E('p', { 'class': 'alert-message' }, [
226 _('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!')
230 body.push(E('p', { 'class': 'alert-message' }, [
231 res[2].stderr ? res[2].stderr : '',
232 res[2].stderr ? E('br') : '',
233 res[2].stderr ? E('br') : '',
234 _('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.')
238 body.push(E('p', { 'class': 'alert-message' }, [
239 _('The uploaded firmware does not allow keeping current configuration.')
245 keep.disabled = true;
248 if ((!is_valid || is_too_big) && is_forceable)
249 body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [
250 force, ' ', _('Force upgrade'),
252 _('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!')
255 var cntbtn = E('button', {
256 'class': 'btn cbi-button-action important',
257 'click': L.ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, keep, force),
258 'disabled': (!is_valid || is_too_big) ? true : null
259 }, [ _('Continue') ]);
261 body.push(E('div', { 'class': 'right' }, [
264 'click': L.ui.createHandlerFn(this, function(ev) {
265 return fs.remove('/tmp/firmware.bin').finally(L.ui.hideModal);
267 }, [ _('Cancel') ]), ' ', cntbtn
270 force.addEventListener('change', function(ev) {
271 cntbtn.disabled = !ev.target.checked;
274 L.ui.showModal(_('Flash image?'), body);
276 .catch(function(e) { L.ui.addNotification(null, E('p', e.message)) })
277 .finally(L.bind(function(btn) {
278 btn.firstChild.data = _('Flash image...');
279 }, this, ev.target));
282 handleSysupgradeConfirm: function(btn, keep, force, ev) {
283 btn.firstChild.data = _('Flashingā¦');
285 L.ui.showModal(_('Flashingā¦'), [
286 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.'))
295 opts.push('--force');
297 opts.push('/tmp/firmware.bin');
299 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
300 fs.exec('/sbin/sysupgrade', opts);
302 L.ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
305 handleBackupList: function(ev) {
306 return fs.exec('/sbin/sysupgrade', [ '--list-backup' ]).then(function(res) {
308 L.ui.addNotification(null, [
309 E('p', _('The sysupgrade command failed with code %d').format(res.code)),
310 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
312 L.raise('Error', 'Sysupgrade failed');
315 L.ui.showModal(_('Backup file list'), [
316 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.')),
317 E('ul', {}, (res.stdout || '').trim().split(/\n/).map(function(ln) { return E('li', {}, ln) })),
318 E('div', { 'class': 'right' }, [
321 'click': L.ui.hideModal
328 handleBackupSave: function(m, ev) {
329 return m.save(function() {
330 return fs.write('/etc/sysupgrade.conf', mapdata.config.editlist.trim().replace(/\r\n/g, '\n') + '\n');
332 L.ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
333 }).catch(function(e) {
334 L.ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e)));
338 render: function(rpc_replies) {
339 var has_sysupgrade = (rpc_replies[0].type == 'file'),
340 hostname = rpc_replies[1],
341 procmtd = rpc_replies[2],
342 procpart = rpc_replies[3],
343 procmounts = rpc_replies[4],
344 has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:\/overlay \/ ") != null),
345 storage_size = findStorageSize(procmtd, procpart),
348 m = new form.JSONMap(mapdata, _('Flash operations'));
351 s = m.section(form.NamedSection, 'actions', _('Actions'));
354 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Backup'), _('Click "Generate archive" to download a tar archive of the current configuration files.'));
357 o = ss.option(form.Button, 'dl_backup', _('Download backup'));
358 o.inputstyle = 'action important';
359 o.inputtitle = _('Generate archive');
360 o.onclick = this.handleBackup;
363 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).'));
366 if (has_rootfs_data) {
367 o = ss.option(form.Button, 'reset', _('Reset to defaults'));
368 o.inputstyle = 'negative important';
369 o.inputtitle = _('Perform reset');
370 o.onclick = this.handleFirstboot;
373 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.'));
374 o.inputstyle = 'action important';
375 o.inputtitle = _('Upload archive...');
376 o.onclick = L.bind(this.handleRestore, this);
379 if (procmtd.length) {
380 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! )'));
383 o = ss.option(form.ListValue, 'mtdselect', _('Choose mtdblock'));
384 procmtd.split(/\n/).forEach(function(ln) {
385 var match = ln.match(/^mtd(\d+): .+ "(.+?)"$/);
387 o.value(match[1], match[2]);
390 o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
391 o.inputstyle = 'action important';
392 o.inputtitle = _('Save mtdblock');
393 o.onclick = L.bind(this.handleBlock, this, hostname);
397 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
399 ? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
400 : _('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.'));
404 if (has_sysupgrade) {
405 o = ss.option(form.Button, 'sysupgrade', _('Image'));
406 o.inputstyle = 'action important';
407 o.inputtitle = _('Flash image...');
408 o.onclick = L.bind(this.handleSysupgrade, this, storage_size);
412 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.'));
413 s.render = L.bind(function(view /*, ... */) {
414 return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
415 .then(L.bind(function(node) {
416 node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
418 'class': 'cbi-button cbi-button-save',
419 'click': L.ui.createHandlerFn(view, 'handleBackupSave', this.map)
427 o = s.option(form.Button, 'showlist', _('Show current backup file list'));
428 o.inputstyle = 'action';
429 o.inputtitle = _('Open list...');
430 o.onclick = L.bind(this.handleBackupList, this);
432 o = s.option(form.TextValue, 'editlist');
435 o.load = function(section_id) {
436 return L.resolveDefault(fs.read('/etc/sysupgrade.conf'), '');
443 handleSaveApply: null,