/* * Primary app code */ // Global app settings APP_DATA = null; APP_EVENTS= {}; APP_UTIL = { parseBoolOption: function(val, def) { if (val == null) { if (def === undefined) return true; else return def == true; } var str = (val + '').toLowerCase(); return str == '' || str == 'true' || str == '1' || str == 'yes' || str == 'on'; }, isValidBoolOption: function(val) { if (val == null) return true; var t = (val + '').toLowerCase(); return val == '' || t == '1' || t == 'true' || t == 'on' || t == 'yes' || t == '0' || t == 'false' || t == 'off' || t == 'no'; }, parseOptionStrings: function(val, dict, validateCallback) { dict = dict || {}; var lines = val.replace('\r', '\n').split('\n'); for(var i in lines) { var line = lines[i].trim(); if (line != '' && line[0] != '#') { if (line.indexOf('--') == 0) { line = line.substr(2); } var eqpos = line.indexOf('='); var key = line; var value = true; if (eqpos > 0) { key = line.substr(0, eqpos).trim(); value = line.substr(eqpos + 1).trim(); if (value == '') value = true; } if (validateCallback) if (!validateCallback(dict, key, value)) return null; dict['--' + key] = value; } } return dict; }, fill_form: function(form, data, map, extra) { map = map || {}; data = data || {}; for(var k in data) { var key = k; var m = map[key]; var v = data[k]; if (m !== false) { if (m && typeof(m) == typeof('')) key = m; if (m && typeof(m) == typeof(function() {})) { m(data, key, v, extra); } else { var n = form.find('#' + key); if (n.attr('type') == 'checkbox') { n.attr('checked', APP_UTIL.parseBoolOption(v)); } else { n.val(v); } n.change(); } } } }, read_form: function(form, map, values, extra) { values = values || {}; map = map || {}; form.find('select').each(function(i, e) { var key = e.id; var m = map[e.id]; if (m !== false) { if (m && typeof(m) == typeof(function() {})) { m(values, key, e, extra); } else { if (m && typeof(m) == typeof('')) key = m; values[key] = $(e).val(); } } }); form.find('input').each(function(i, e) { var key = e.id; var m = map[e.id]; if (m !== false) { if (m && typeof(m) == typeof(function() {})) { m(values, key, e, extra); } else { if (m && typeof(m) == typeof('')) key = m; if (e.type == 'checkbox') values[key] = $(e).is(':checked'); else values[key] = $(e).val(); } } }); return values; } }; $(document).ready(function() { // Flag as loaded in case we have plugins APP_DATA = { plugins: { backend: {}, compression: {}, encryption: {}, primary: {} } }; var PRIVATE_DATA = {}; $('#main-appname').text(APP_CONFIG.branded_name); if ((APP_CONFIG.branded_subtitle || '').length > 0) { var subdiv = $('
'); subdiv.text(APP_CONFIG.branded_subtitle); $('#main-appname').append(subdiv); $('#main-appname').addClass('has-subtitle'); } $('.button').button(); $('#main-list-container > div.main-backup-entry').remove(); $('#loading-dialog').dialog({modal: true}).show(); // Register a global function for password strength $.passwordStrength = function(password, callback) { if (callback == null) return; var onUpdate = function(res) { try { callback(res); } catch (e) { } }; try { onUpdate(zxcvbn(password)); } catch (e) { // Not loaded, try this: $.getScript('/scripts/zxcvbn.js', function() { try { onUpdate(zxcvbn(password)); } catch (e) { onUpdate(null); } }); } }; var serverWithCallback = function(data, callback, errorhandler, refreshMethod) { if (typeof(data) == typeof('')) data = { action: data }; var method = 'GET'; if (data.HTTP_METHOD) { method = data.HTTP_METHOD; delete data.HTTP_METHOD; } $.ajax({ url: APP_CONFIG.server_url, type: method, dataType: 'json', data: data }) .done(function(data) { if (refreshMethod) refreshMethod(data, true, null); if (callback != null) callback(data, true, null); }) .fail(function(data, status) { if (refreshMethod) refreshMethod(data, false, data.statusText); if (errorhandler) errorhandler(data, false, data.statusText); }); }; PRIVATE_DATA.server_state = { polling: false, eventId: -1, state: null, failed: false, dataEventId: -1, retryTimer: null, pauseUpdateTimer: null, activeTask: null, scheduled: [] }; PRIVATE_DATA.progress_state_text = { 'Backup_Begin': 'Starting ...', 'Backup_PreBackupVerify': 'Verifying backend data ...', 'Backup_PostBackupTest': 'Verifying remote data ...', 'Backup_PreviousBackupFinalize': 'Completing previous backup ...', 'Backup_ProcessingFiles': null, 'Backup_Finalize': 'Completing backup ...', 'Backup_WaitForUpload': 'Waiting for upload ...', 'Backup_Delete': 'Deleting unwanted files ...', 'Backup_Compact': 'Compacting remote data ...', 'Backup_VerificationUpload': 'Verifying upload ...', 'Backup_PostBackupVerify': 'Verifying backend data ...', 'Backup_Complete': 'Finished!', 'Restore_Begin': 'Starting ...', 'Restore_RecreateDatabase': 'Rebuilding local database ...', 'Restore_PreRestoreVerify': 'Verifying remote data ...', 'Restore_CreateFileList': 'Building list of files to restore ...', 'Restore_CreateTargetFolders': 'Creating target folders ...', 'Restore_ScanForExistingFiles': 'Scanning existing files ...', 'Restore_ScanForLocalBlocks': 'Scanning for local blocks ...', 'Restore_PatchWithLocalBlocks': 'Patching files with local blocks ...', 'Restore_DownloadingRemoteFiles': 'Downloading files ...', 'Restore_PostRestoreVerify': 'Verifying restored files ...', 'Restore_Complete': 'Finished!', 'Error': 'Error!' }; PRIVATE_DATA.update_progress_and_schedules = function() { var scheduledMap = {}; var backupMap = {}; var state = PRIVATE_DATA.server_state; var current = state.activeTask == null ? false : 'backup-' + state.activeTask; for(var n in state.scheduled) scheduledMap['backup-' + state.scheduled[n]] = n + 1; for(var n in PRIVATE_DATA.backup_list) backupMap['backup-' + PRIVATE_DATA.backup_list[n].Backup.ID] = PRIVATE_DATA.backup_list[n]; $('#main-list').find('.main-backup-entry').each(function(i, e) { var el = $(e); if (scheduledMap[e.id]) { el.find('.backup-progress-overall').css('width', '0px'); el.find('.main-progress-text').removeClass('in-progress').text('#' + parseInt(scheduledMap[e.id]) + ' in queue'); el.find('.backup-state-icon').addClass('main-icon-hourglass').removeClass('main-icon-clock'); } else if (e.id == current) { var txt = 'Running ...'; var pg = 0; if (PRIVATE_DATA.server_progress.lastEvent != null) { var filesleft = PRIVATE_DATA.server_progress.lastEvent.TotalFileCount - PRIVATE_DATA.server_progress.lastEvent.ProcessedFileCount; var sizeleft = PRIVATE_DATA.server_progress.lastEvent.TotalFileSize - PRIVATE_DATA.server_progress.lastEvent.ProcessedFileSize; pg = PRIVATE_DATA.server_progress.lastEvent.ProcessedFileSize / PRIVATE_DATA.server_progress.lastEvent.TotalFileSize; if (PRIVATE_DATA.server_progress.lastEvent.Phase == 'Backup_ProcessingFiles') { el.find('.backup-progress-overall').removeClass('backup-progress-indeterminate'); txt = filesleft + ' files remaining'; if (PRIVATE_DATA.server_progress.lastEvent.ProcessedFileCount == 0) pg = 0; else if (pg == 1) pg = 0.95; if (PRIVATE_DATA.server_progress.lastEvent.StillCounting) txt = "~" + txt; } else { pg = 1; el.find('.backup-progress-overall').addClass('backup-progress-indeterminate'); txt = PRIVATE_DATA.progress_state_text[PRIVATE_DATA.server_progress.lastEvent.Phase] || txt; } } el.find('.backup-progress-overall').css('width', parseInt(pg*100) + '%'); el.find('.main-progress-text').addClass('in-progress').text(txt); el.find('.backup-state-icon').removeClass('main-icon-hourglass main-icon-clock'); } else if (backupMap[e.id] && backupMap[e.id].Schedule) { el.find('.backup-progress-overall').css('width', '0px'); el.find('.main-progress-text').removeClass('in-progress').text(new Date(Date.parse(backupMap[e.id].Schedule.Time)).toLocaleString()); el.find('.backup-state-icon').removeClass('main-icon-hourglass').addClass('main-icon-clock'); } else { el.find('.backup-progress-overall').css('width', '0px'); el.find('.main-progress-text').removeClass('in-progress').text(''); el.find('.backup-state-icon').removeClass('main-icon-hourglass main-icon-clock'); } }); }; PRIVATE_DATA.long_poll_for_status = function() { var state = PRIVATE_DATA.server_state; if (state.polling) return; if (state.retryTimer != null) { $(document).trigger('server-state-connecting'); clearInterval(state.retryTimer); state.retryTimer = null; } state.polling = true; var req = { action: 'get-current-state', longpoll: true, lastEventId: state.eventId }; var timeout = 240000; if (state.failed || state.eventId == -1) { req.longpoll = false; timeout = 5000 } req.duration = parseInt((timeout-1000) / 1000) + 's'; $.ajax({ url: APP_CONFIG.server_url, type: 'GET', timeout: timeout, dataType: 'json', data: req }) .done(function(data) { state.polling = false; state.state = data; state.eventId = data.LastEventID; var oldDataId = state.dataEventId; var oldState = state.programState; state.dataEventId = data.LastDataUpdateID; state.programState = data.ProgramState; state.scheduled = data.SchedulerQueueIds || []; if (state.failed) { state.failed = false; $(document).trigger('server-state-restored'); } //If there is an active backup, (re)start the progress monitor if (data.ActiveScheduleId != null && parseInt(data.ActiveScheduleId) > 0) { state.activeTask = parseInt(data.ActiveScheduleId); PRIVATE_DATA.poll_for_progress(); } else { state.activeTask = null; } $(document).trigger('server-state-updated', data); if (oldDataId != state.dataEventId) $(document).trigger('server-state-data-updated', data); if (oldState != state.programState) { if (state.pauseUpdateTimer != null) { clearInterval(state.pauseUpdateTimer); state.pauseUpdateTimer = null; } $(document).trigger('server-state-changed', state.programState); if (state.programState == 'Running') $(document).trigger('server-state-running'); else if (state.programState == 'Paused') { $(document).trigger('server-state-paused'); var estimatedEnd = Date.parse(data.EstimatedPauseEnd); if (estimatedEnd != 0) { estimatedEnd = new Date(estimatedEnd); var updateTimer = function() { var left = Math.max(0, estimatedEnd - new Date()); $(document).trigger('server-state-pause-countdown', {millisecondsLeft: left}); } setInterval(updateTimer, 500); updateTimer(); } } } PRIVATE_DATA.update_progress_and_schedules(); PRIVATE_DATA.long_poll_for_status(); }) .fail(function(data) { state.polling = false; if (!state.failed) { state.failed = true; $(document).trigger('server-state-lost'); } state.retryStartTime = new Date(); var updateTimer = function() { var n = new Date() - state.retryStartTime; var left = Math.max(0, 15000 - n); $(document).trigger('server-state-countdown', {millisecondsLeft: left}); if (left <= 0) PRIVATE_DATA.long_poll_for_status(); } state.retryTimer = setInterval(updateTimer, 500); }); }; PRIVATE_DATA.server_progress = { polling: false, eventId: -1, throttleTimer: null, lastEvent: null, updateFreq: 2000 }; PRIVATE_DATA.poll_for_progress = function() { var state = PRIVATE_DATA.server_progress; if (state.polling) return; if (state.throttleTimer != null) { clearTimeout(state.throttleTimer); state.throttleTimer = null; } state.polling = true; var req = { action: 'get-progress-state', longpoll: false }; state.requestStart = new Date(); $.ajax({ url: APP_CONFIG.server_url, type: 'GET', timeout: 5000, dataType: 'json', data: req }) .done(function(data) { state.polling = false; state.eventId = data.LastEventID; state.lastEvent = data; PRIVATE_DATA.update_progress_and_schedules(); var timeSinceLast = new Date() - state.requestStart; if (timeSinceLast < state.updateFreq) { state.throttleTimer = setTimeout(function(){ if (PRIVATE_DATA.server_state.activeTask != null) PRIVATE_DATA.poll_for_progress(); }, Math.max(500, state.updateFreq - timeSinceLast)); } else { if (PRIVATE_DATA.server_state.activeTask != null) PRIVATE_DATA.poll_for_progress(); } }) .fail(function(data, a, b, c, d) { state.polling = false; // We will be restarted on the next status update }); }; PRIVATE_DATA.refresh_server_settings = function(callback, errorhandler) { serverWithCallback( 'system-info', callback, errorhandler, function(data, success) { if (success && data != null) PRIVATE_DATA.server_config = data; } ); }; PRIVATE_DATA.refresh_backup_list = function(callback, errorhandler) { serverWithCallback( 'list-backups', callback, errorhandler, function(data, success) { if (success && data != null) { PRIVATE_DATA.backup_list = data; // Clear existing stuff $('#main-list-container > div.main-backup-entry').remove(); if ($('#backup-item-template').length > 0 && data.length > 0) { // Pre-processing of data for(var n in data) { var b = data[n].Backup; b.Metadata = b.Metadata || {}; if (!b.Metadata['TargetSizeString']) b.Metadata['TargetSizeString'] = '< unknown >' if (!b.Metadata['SourceSizeString']) b.Metadata['SourceSizeString'] = '< unknown >' } if (APP_DATA.plugins.primary['backup-list-preprocess']) APP_DATA.plugins.primary['backup-list-preprocess'](data); //Fill with jQuery template $.tmpl($('#backup-item-template'), data).prependTo($('#main-list-container')); // Post processing of data for(var n in data) { var id = data[n].Backup.ID; $('#backup-details-run-' + id).click(function() { APP_DATA.runBackup(id); }); $('#backup-details-restore-' + id).click(function() { APP_DATA.restoreBackup(id); }); $('#backup-details-edit-' + id).click(function() { APP_DATA.editBackup(id); }); $('#backup-details-delete-' + id).click(function() { APP_DATA.deleteBackup(id); }); } if (APP_DATA.plugins.primary['backup-list-postrocess']) APP_DATA.plugins.primary['backup-list-postrocess']($('#main-list-container'), $('#main-list-container > div.main-backup-entry'), data); } } PRIVATE_DATA.update_progress_and_schedules(); } ); }; APP_DATA.getServerConfig = function(callback, errorhandler) { if (PRIVATE_DATA.server_config == null) { PRIVATE_DATA.refresh_server_settings(callback, errorhandler); } else { callback(PRIVATE_DATA.server_config); } }; APP_DATA.validatePath = function(path, callback) { serverWithCallback({ action: 'validate-path', path: path }, callback, callback); }; APP_DATA.getLabels = function(callback) { serverWithCallback('list-tags', callback, callback); }; APP_DATA.getBackupDefaults = function(callback, errorhandler) { serverWithCallback('get-backup-defaults', callback, errorhandler); }; APP_DATA.getBackupData = function(id, callback, errorhandler) { serverWithCallback({ action: 'get-backup', id: id}, callback, errorhandler); }; APP_DATA.addBackup = function(cfg, callback, errorhandler) { serverWithCallback( { action: 'add-backup', HTTP_METHOD: 'POST', data: JSON.stringify(cfg)}, callback, errorhandler ); }; APP_DATA.updateBackup = function(cfg, callback, errorhandler) { serverWithCallback( { action: 'update-backup', HTTP_METHOD: 'POST', data: JSON.stringify(cfg)}, callback, errorhandler ); }; APP_DATA.editNewBackup = function() { APP_DATA.editBackup(); }; APP_DATA.deleteBackup = function(id, callback, errorhandler) { serverWithCallback( { action: 'delete-backup', id: id }, callback, errorhandler ); }; APP_DATA.editBackup = function(id) { APP_DATA.getServerConfig(function(data) { var callback = function(data) { $("#edit-dialog").dialog('open'); // Bug-fix, this will remove style="width: auto", which breaks Chrome a bit $("#edit-dialog").css('width', ''); // Send data to the dialog $("#edit-dialog").trigger('setup-dialog', data.data); }; var errorhandler = function() { alert('Failed to get server setup...') }; if (id == undefined || parseInt(id) < 0) APP_DATA.getBackupDefaults(callback, errorhandler); else APP_DATA.getBackupData(id, callback, errorhandler); }, function() { alert('Failed to get server setup...') }); }; APP_DATA.runBackup = function(id) { serverWithCallback( { action: 'send-command', command: 'run-backup', id: id }, function() {}, function(d,s,m) { alert('Failed to start backup: ' + m); } ); }; APP_DATA.restoreBackup = function(id) { alert('Not implemented'); }; APP_DATA.pauseServer = function(duration) { serverWithCallback({action: 'send-command', command: 'pause', duration: duration}); }; APP_DATA.resumeServer = function() { serverWithCallback({action: 'send-command', command: 'resume'}); }; $('#main-settings').click(function() { var pos = $('#main-settings').position(); var barheight = $('#main-topbar').outerHeight(); var menuwidth = $('#main-control-menu').outerWidth(); var buttonwidth = $('#main-settings').outerWidth(); $('#main-control-menu').css({ position: 'absolute', top: barheight + 'px', right: ($(document).outerWidth() - (pos.left + buttonwidth)) + 'px' }); $('#main-control-menu').toggle(); }); $('#main-donate').click(function() { }); $('#main-control').click(function() { }); $('#main-newbackup').click(function() { APP_DATA.editNewBackup(); }); $('#edit-dialog').tabs({ active: 0 }); $("#edit-dialog").dialog({ minWidth: 320, width: $('body').width > 600 ? 320 : 600, minHeight: 480, height: 500, modal: true, autoOpen: false, closeOnEscape: true, buttons: [ { text: 'Previous', disabled: true, click: function(event, ui) { var cur = parseInt($('#edit-dialog').tabs( "option", "active")); cur = Math.max(cur-1, 0); $('#edit-dialog').tabs( "option", "active", cur); }}, { text: 'Next', click: function(event, ui) { var cur = parseInt($('#edit-dialog').tabs( "option", "active")); var max = $('#edit-dialog').parent().find('[role=tablist] > li').size() - 1; if (!EDIT_BACKUP.validate_tab(cur)) return; event.curPage = cur; event.currentTarget.curPage = cur; cur = Math.min(cur+1, max); $('#edit-dialog').tabs( "option", "active", cur); }} ] }); // Hack the tabs into the dialog title $('#edit-dialog').parent().find('.ui-dialog-titlebar').after($('#edit-dialog').find('.ui-tabs-nav')); $('#edit-dialog').parent().addClass('ui-tabs'); $(document).on('server-state-updated', function() { $('#loading-dialog').dialog("close"); }); $(document).on('server-state-lost', function() { $('#connection-lost-dialog').dialog('open'); $('#connection-lost-dialog-text').text('Server connection lost...'); var button = $('#connection-lost-dialog').parent().find('.ui-dialog-buttonpane').find('.ui-button').first(); button.attr("disabled",false).removeClass( 'ui-button-disabled ui-state-disabled' ); }); $(document).on('server-state-restored', function() { $('#connection-lost-dialog').dialog('close'); }); $(document).on('server-state-countdown', function(e, f) { var s = parseInt(f.millisecondsLeft / 1000) + 1; $('#connection-lost-dialog-text').html('Server connection lost,
retry in ' + s + ' seconds'); var button = $('#connection-lost-dialog').parent().find('.ui-dialog-buttonpane').find('.ui-button').first(); button.attr("disabled",false).removeClass( 'ui-button-disabled ui-state-disabled' ); }); $(document).on('server-state-connecting', function(e) { $('#connection-lost-dialog-text').text('Retrying...'); var button = $('#connection-lost-dialog').parent().find('.ui-dialog-buttonpane').find('.ui-button').first(); button.attr("disabled",true).addClass( 'ui-button-disabled ui-state-disabled' ); }); $(document).on('server-state-running', function() { $('#main-control').removeClass('main-icon-pause').addClass('main-icon-running'); }); $(document).on('server-state-paused', function() { $('#main-control').removeClass('main-icon-running').addClass('main-icon-pause'); }); $(document).on('server-state-pause-countdown', function(e, f) { var s = parseInt(f.millisecondsLeft / 1000) + 1; }); $('#main-control').click(function() { if ($('#main-control').hasClass('main-icon-pause')) APP_DATA.resumeServer(); else APP_DATA.pauseServer(); }); $('#connection-lost-dialog').dialog({ modal: true, autoOpen: false, closeOnEscape: false, buttons: [ { text: 'Retry now', disabled: true, click: function(event, ui) { PRIVATE_DATA.long_poll_for_status(); } } ] }); $('#connection-lost-dialog').parent().find('.ui-dialog-titlebar-close').remove().first().remove(); $('#connection-lost-dialog').on('dialogbeforeclose', function( event, ui ) { return !PRIVATE_DATA.server_state.failed; }); $(document).on('server-state-data-updated', function() { PRIVATE_DATA.refresh_backup_list(); }); PRIVATE_DATA.long_poll_for_status(); PRIVATE_DATA.refresh_server_settings(); PRIVATE_DATA.refresh_backup_list(); });