9 var isReadonlyView = !L.hasViewPermission();
11 var callSystemValidateFirmwareImage = rpc.declare({
13 method: 'validate_firmware_image',
15 expect: { '': { valid: false, forcable: true } }
18 function findStorageSize(procmtd, procpart) {
19 var kernsize = 0, rootsize = 0, wholesize = 0;
21 procmtd.split(/\n/).forEach(function(ln) {
22 var match = ln.match(/^mtd\d+: ([0-9a-f]+) [0-9a-f]+ "(.+)"$/),
23 size = match ? parseInt(match[1], 16) : 0;
25 switch (match ? match[2] : '') {
48 else if (kernsize > 0 && rootsize > kernsize)
49 return kernsize + rootsize;
51 procpart.split(/\n/).forEach(function(ln) {
52 var match = ln.match(/^\s*\d+\s+\d+\s+(\d+)\s+(\S+)$/);
54 var size = parseInt(match[1], 10);
56 if (!match[2].match(/\d/) && size > 2048 && wholesize == 0)
57 wholesize = size * 1024;
65 var mapdata = { actions: {}, config: {} };
70 L.resolveDefault(fs.stat('/lib/upgrade/platform.sh'), {}),
71 fs.trimmed('/proc/sys/kernel/hostname'),
72 fs.trimmed('/proc/mtd'),
73 fs.trimmed('/proc/partitions'),
74 fs.trimmed('/proc/mounts')
77 return Promise.all(tasks);
80 handleBackup: function(ev) {
81 var form = E('form', {
83 action: L.env.cgi_base + '/cgi-backup',
84 enctype: 'application/x-www-form-urlencoded'
85 }, E('input', { type: 'hidden', name: 'sessionid', value: rpc.getSessionID() }));
87 ev.currentTarget.parentNode.appendChild(form);
90 form.parentNode.removeChild(form);
93 handleFirstboot: function(ev) {
94 if (!confirm(_('Do you really want to erase all settings?')))
97 ui.showModal(_('Erasing...'), [
98 E('p', { 'class': 'spinning' }, _('The system is erasing the configuration partition now and will reboot itself when finished.'))
101 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
102 fs.exec('/sbin/firstboot', [ '-r', '-y' ]);
104 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
107 handleRestore: function(ev) {
108 return ui.uploadFile('/tmp/backup.tar.gz', ev.target)
109 .then(L.bind(function(btn, res) {
110 btn.firstChild.data = _('Checking archive…');
111 return fs.exec('/bin/tar', [ '-tzf', '/tmp/backup.tar.gz' ]);
113 .then(L.bind(function(btn, res) {
115 ui.addNotification(null, E('p', _('The uploaded backup archive is not readable')));
116 return fs.remove('/tmp/backup.tar.gz');
119 ui.showModal(_('Apply backup?'), [
120 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.')),
121 E('pre', {}, [ res.stdout ]),
122 E('div', { 'class': 'right' }, [
125 'click': ui.createHandlerFn(this, function(ev) {
126 return fs.remove('/tmp/backup.tar.gz').finally(ui.hideModal);
128 }, [ _('Cancel') ]), ' ',
130 'class': 'btn cbi-button-action important',
131 'click': ui.createHandlerFn(this, 'handleRestoreConfirm', btn)
132 }, [ _('Continue') ])
136 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
137 .finally(L.bind(function(btn, input) {
138 btn.firstChild.data = _('Upload archive...');
139 }, this, ev.target));
142 handleRestoreConfirm: function(btn, ev) {
143 return fs.exec('/sbin/sysupgrade', [ '--restore-backup', '/tmp/backup.tar.gz' ])
144 .then(L.bind(function(btn, res) {
146 ui.addNotification(null, [
147 E('p', _('The restore command failed with code %d').format(res.code)),
148 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
150 L.raise('Error', 'Unpack failed');
153 btn.firstChild.data = _('Rebooting…');
154 return fs.exec('/sbin/reboot');
156 .then(L.bind(function(res) {
158 ui.addNotification(null, E('p', _('The reboot command failed with code %d').format(res.code)));
159 L.raise('Error', 'Reboot failed');
162 ui.showModal(_('Rebooting…'), [
163 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.'))
166 ui.awaitReconnect(window.location.host, '192.168.1.1', 'openwrt.lan');
168 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
169 .finally(function() { btn.firstChild.data = _('Upload archive...') });
172 handleBlock: function(hostname, ev) {
173 var mtdblock = dom.parent(ev.target, '.cbi-section').querySelector('[data-name="mtdselect"] select').value;
174 var form = E('form', {
176 'action': L.env.cgi_base + '/cgi-download',
177 'enctype': 'application/x-www-form-urlencoded'
179 E('input', { 'type': 'hidden', 'name': 'sessionid', 'value': rpc.getSessionID() }),
180 E('input', { 'type': 'hidden', 'name': 'path', 'value': '/dev/mtdblock%d'.format(mtdblock) }),
181 E('input', { 'type': 'hidden', 'name': 'filename', 'value': '%s.mtd%d.bin'.format(hostname, mtdblock) })
184 ev.currentTarget.parentNode.appendChild(form);
187 form.parentNode.removeChild(form);
190 handleSysupgrade: function(storage_size, ev) {
191 return ui.uploadFile('/tmp/firmware.bin', ev.target.firstChild)
192 .then(L.bind(function(btn, reply) {
193 btn.firstChild.data = _('Checking image…');
195 ui.showModal(_('Checking image…'), [
196 E('span', { 'class': 'spinning' }, _('Verifying the uploaded image file.'))
199 return callSystemValidateFirmwareImage('/tmp/firmware.bin')
200 .then(function(res) { return [ reply, res ]; });
202 .then(L.bind(function(btn, reply) {
203 return fs.exec('/sbin/sysupgrade', [ '--test', '/tmp/firmware.bin' ])
204 .then(function(res) { reply.push(res); return reply; });
206 .then(L.bind(function(btn, res) {
207 var keep = E('input', { type: 'checkbox' }),
208 force = E('input', { type: 'checkbox' }),
209 is_valid = res[1].valid,
210 is_forceable = res[1].forceable,
211 allow_backup = res[1].allow_backup,
212 is_too_big = (storage_size > 0 && res[0].size > storage_size),
215 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.')));
216 body.push(E('ul', {}, [
217 res[0].size ? E('li', {}, '%s: %1024.2mB'.format(_('Size'), res[0].size)) : '',
218 res[0].checksum ? E('li', {}, '%s: %s'.format(_('MD5'), res[0].checksum)) : '',
219 res[0].sha256sum ? E('li', {}, '%s: %s'.format(_('SHA256'), res[0].sha256sum)) : ''
222 body.push(E('p', {}, E('label', { 'class': 'btn' }, [
223 keep, ' ', _('Keep settings and retain the current configuration')
226 if (!is_valid || is_too_big)
230 body.push(E('p', { 'class': 'alert-message' }, [
231 _('It appears that you are trying to flash an image that does not fit into the flash memory, please verify the image file!')
235 body.push(E('p', { 'class': 'alert-message' }, [
236 res[2].stderr ? res[2].stderr : '',
237 res[2].stderr ? E('br') : '',
238 res[2].stderr ? E('br') : '',
239 _('The uploaded image file does not contain a supported format. Make sure that you choose the generic image format for your platform.')
243 body.push(E('p', { 'class': 'alert-message' }, [
244 _('The uploaded firmware does not allow keeping current configuration.')
250 keep.disabled = true;
253 if ((!is_valid || is_too_big) && is_forceable)
254 body.push(E('p', {}, E('label', { 'class': 'btn alert-message danger' }, [
255 force, ' ', _('Force upgrade'),
257 _('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!')
260 var cntbtn = E('button', {
261 'class': 'btn cbi-button-action important',
262 'click': ui.createHandlerFn(this, 'handleSysupgradeConfirm', btn, keep, force),
263 'disabled': (!is_valid || is_too_big) ? true : null
264 }, [ _('Continue') ]);
266 body.push(E('div', { 'class': 'right' }, [
269 'click': ui.createHandlerFn(this, function(ev) {
270 return fs.remove('/tmp/firmware.bin').finally(ui.hideModal);
272 }, [ _('Cancel') ]), ' ', cntbtn
275 force.addEventListener('change', function(ev) {
276 cntbtn.disabled = !ev.target.checked;
279 ui.showModal(_('Flash image?'), body);
281 .catch(function(e) { ui.addNotification(null, E('p', e.message)) })
282 .finally(L.bind(function(btn) {
283 btn.firstChild.data = _('Flash image...');
284 }, this, ev.target));
287 handleSysupgradeConfirm: function(btn, keep, force, ev) {
288 btn.firstChild.data = _('Flashing…');
290 ui.showModal(_('Flashing…'), [
291 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.'))
300 opts.push('--force');
302 opts.push('/tmp/firmware.bin');
304 /* Currently the sysupgrade rpc call will not return, hence no promise handling */
305 fs.exec('/sbin/sysupgrade', opts);
308 ui.awaitReconnect(window.location.host);
310 ui.awaitReconnect('192.168.1.1', 'openwrt.lan');
313 handleBackupList: function(ev) {
314 return fs.exec('/sbin/sysupgrade', [ '--list-backup' ]).then(function(res) {
316 ui.addNotification(null, [
317 E('p', _('The sysupgrade command failed with code %d').format(res.code)),
318 res.stderr ? E('pre', {}, [ res.stderr ]) : ''
320 L.raise('Error', 'Sysupgrade failed');
323 ui.showModal(_('Backup file list'), [
324 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.')),
325 E('ul', {}, (res.stdout || '').trim().split(/\n/).map(function(ln) { return E('li', {}, ln) })),
326 E('div', { 'class': 'right' }, [
329 'click': ui.hideModal
336 handleBackupSave: function(m, ev) {
337 return m.save(function() {
338 return fs.write('/etc/sysupgrade.conf', mapdata.config.editlist.trim().replace(/\r\n/g, '\n') + '\n');
340 ui.addNotification(null, E('p', _('Contents have been saved.')), 'info');
341 }).catch(function(e) {
342 ui.addNotification(null, E('p', _('Unable to save contents: %s').format(e)));
346 render: function(rpc_replies) {
347 var has_sysupgrade = (rpc_replies[0].type == 'file'),
348 hostname = rpc_replies[1],
349 procmtd = rpc_replies[2],
350 procpart = rpc_replies[3],
351 procmounts = rpc_replies[4],
352 has_rootfs_data = (procmtd.match(/"rootfs_data"/) != null) || (procmounts.match("overlayfs:\/overlay \/ ") != null),
353 storage_size = findStorageSize(procmtd, procpart),
356 m = new form.JSONMap(mapdata, _('Flash operations'));
358 m.readonly = isReadonlyView;
360 s = m.section(form.NamedSection, 'actions', _('Actions'));
363 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Backup'), _('Click "Generate archive" to download a tar archive of the current configuration files.'));
366 o = ss.option(form.Button, 'dl_backup', _('Download backup'));
367 o.inputstyle = 'action important';
368 o.inputtitle = _('Generate archive');
369 o.onclick = this.handleBackup;
372 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).'));
375 if (has_rootfs_data) {
376 o = ss.option(form.Button, 'reset', _('Reset to defaults'));
377 o.inputstyle = 'negative important';
378 o.inputtitle = _('Perform reset');
379 o.onclick = this.handleFirstboot;
382 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.'));
383 o.inputstyle = 'action important';
384 o.inputtitle = _('Upload archive...');
385 o.onclick = L.bind(this.handleRestore, this);
388 if (procmtd.length) {
389 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! )'));
392 o = ss.option(form.ListValue, 'mtdselect', _('Choose mtdblock'));
393 procmtd.split(/\n/).forEach(function(ln) {
394 var match = ln.match(/^mtd(\d+): .+ "(.+?)"$/);
396 o.value(match[1], match[2]);
399 o = ss.option(form.Button, 'mtddownload', _('Download mtdblock'));
400 o.inputstyle = 'action important';
401 o.inputtitle = _('Save mtdblock');
402 o.onclick = L.bind(this.handleBlock, this, hostname);
406 o = s.option(form.SectionValue, 'actions', form.NamedSection, 'actions', 'actions', _('Flash new firmware image'),
408 ? _('Upload a sysupgrade-compatible image here to replace the running firmware.')
409 : _('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.'));
413 if (has_sysupgrade) {
414 o = ss.option(form.Button, 'sysupgrade', _('Image'));
415 o.inputstyle = 'action important';
416 o.inputtitle = _('Flash image...');
417 o.onclick = L.bind(this.handleSysupgrade, this, storage_size);
421 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.'));
422 s.render = L.bind(function(view /*, ... */) {
423 return form.NamedSection.prototype.render.apply(this, this.varargs(arguments, 1))
424 .then(L.bind(function(node) {
425 node.appendChild(E('div', { 'class': 'cbi-page-actions' }, [
427 'class': 'cbi-button cbi-button-save',
428 'click': ui.createHandlerFn(view, 'handleBackupSave', this.map),
429 'disabled': isReadonlyView || null
437 o = s.option(form.Button, 'showlist', _('Show current backup file list'));
438 o.inputstyle = 'action';
439 o.inputtitle = _('Open list...');
440 o.onclick = L.bind(this.handleBackupList, this);
442 o = s.option(form.TextValue, 'editlist');
445 o.load = function(section_id) {
446 return L.resolveDefault(fs.read('/etc/sysupgrade.conf'), '');
453 handleSaveApply: null,