Autobackup update (#6695)

* add backupHours option and many debug messages * Cleanup debug messages, add backupinfo * Add full path to remove log message * Put backupcheck after config init, check proper backuppath * Handle absolute backuppath, check access in checkBackupCapability, seperated expired files check to function, more message edits, serverwarnings * Revert fallback to default backuppath * Cleanup checkBackupCapability and messages * add WebDAV messages

PTR committed Jan 26, 2025 at 15:24 UTC f7b958d28b2f19fb0cd7a61831c16aa6a96a6b8a
3 files changed +203 -129
db.js
+179 -119
@@ -781,10 +781,10 @@ module.exports.CreateDB = function (parent, func) {
781 parent.debug('db', 'SQlite config options: ' + JSON.stringify(obj.sqliteConfig, null, 4));
782 if (obj.sqliteConfig.journalMode == 'memory') { console.log('[WARNING] journal_mode=memory: this can lead to database corruption if there is a crash during a transaction. See https://www.sqlite.org/pragma.html#pragma_journal_mode') };
783 //.cached not usefull
784 - obj.file = new sqlite3.Database(parent.path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
784 + obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
785 if (err && (err.code == 'SQLITE_CANTOPEN')) {
786 // Database needs to be created
787 - obj.file = new sqlite3.Database(parent.path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
787 + obj.file = new sqlite3.Database(path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
788 if (err) { console.log("SQLite Error: " + err); process.exit(1); }
789 obj.file.exec(`
790 CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
@@ -975,7 +975,7 @@ module.exports.CreateDB = function (parent, func) {
975 } else {
976 if ((info.versionArray[0] < 3) || ((info.versionArray[0] == 3) && (info.versionArray[1] < 6))) {
977 // We are running with mongoDB older than 3.6, this is not good.
978 - parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.");
978 + parent.addServerWarning("Current version of MongoDB (" + info.version + ") is too old, please upgrade to MongoDB 3.6 or better.", true);
979 }
980 }
981 });
@@ -1294,7 +1294,7 @@ module.exports.CreateDB = function (parent, func) {
1294
1295 // Setup the SMBIOS collection, for NeDB we don't setup SMBIOS since NeDB will corrupt the database. Remove any existing ones.
1296 //obj.smbiosfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-smbios.db'), autoload: true, corruptAlertThreshold: 1 });
1297 - parent.fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
1297 + fs.unlink(parent.getConfigFilePath('meshcentral-smbios.db'), function () { });
1298
1299 // Setup the server stats collection and setup indexes
1300 obj.serverstatsfile = new Datastore({ filename: parent.getConfigFilePath('meshcentral-stats.db'), autoload: true, corruptAlertThreshold: 1 });
@@ -3187,7 +3187,6 @@ module.exports.CreateDB = function (parent, func) {
3187 // Return a human readable string with current backup configuration
3188 obj.getBackupConfig = function () {
3189 var r = '', backupPath = parent.backuppath;
3190 - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3190
3191 let dbname = 'meshcentral';
3192 if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
@@ -3197,7 +3196,7 @@ module.exports.CreateDB = function (parent, func) {
3196
3197 const currentDate = new Date();
3198 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3200 - obj.newAutoBackupFile = ((typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-') + fileSuffix;
3199 + obj.newAutoBackupFile = parent.config.settings.autobackup.backupname + fileSuffix;
3200
3201 r += 'DB Name: ' + dbname + '\r\n';
3202 r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
@@ -3207,15 +3206,14 @@ module.exports.CreateDB = function (parent, func) {
3206 if (parent.config.settings.autobackup == null) {
3207 r += 'No Settings/AutoBackup\r\n';
3208 } else {
3209 + if (parent.config.settings.autobackup.backuphour != null && parent.config.settings.autobackup.backuphour != -1) {
3210 + r += 'Backup between: ' + parent.config.settings.autobackup.backuphour + 'H-' + (parent.config.settings.autobackup.backuphour + 1) + 'H\r\n';
3211 + }
3212 if (parent.config.settings.autobackup.backupintervalhours != null) {
3211 - r += 'Backup Interval (Hours): ';
3212 - if (typeof parent.config.settings.autobackup.backupintervalhours != 'number') { r += 'Bad backupintervalhours type\r\n'; }
3213 - else { r += parent.config.settings.autobackup.backupintervalhours + '\r\n'; }
3213 + r += 'Backup Interval (Hours): ' + parent.config.settings.autobackup.backupintervalhours + '\r\n';
3214 }
3215 if (parent.config.settings.autobackup.keeplastdaysbackup != null) {
3216 - r += 'Keep Last Backups (Days): ';
3217 - if (typeof parent.config.settings.autobackup.keeplastdaysbackup != 'number') { r += 'Bad keeplastdaysbackup type\r\n'; }
3218 - else { r += parent.config.settings.autobackup.keeplastdaysbackup + '\r\n'; }
3216 + r += 'Keep Last Backups (Days): ' + parent.config.settings.autobackup.keeplastdaysbackup + '\r\n';
3217 }
3218 if (parent.config.settings.autobackup.zippassword != null) {
3219 r += 'ZIP Password: ';
@@ -3330,48 +3328,70 @@ module.exports.CreateDB = function (parent, func) {
3328 }
3329
3330 // Check that the server is capable of performing a backup
3331 + // Tries configured custom location with fallback to default location
3332 + // Now runs after autobackup config init in meshcentral.js so config options are checked
3333 obj.checkBackupCapability = function (func) {
3334 - if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { func(); return; };
3334 + if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { return; };
3335 + //block backup until validated. Gets put back if all checks are ok.
3336 + let backupInterval = parent.config.settings.autobackup.backupintervalhours;
3337 + parent.config.settings.autobackup.backupintervalhours = -1;
3338 let backupPath = parent.backuppath;
3336 - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3337 - try { parent.fs.mkdirSync(backupPath); } catch (e) { }
3338 - if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, auto-backup will not be performed."); return; }
3339
3340 + if (backupPath.startsWith(parent.datapath)) {
3341 + func(1, "Backup path can't be set within meshcentral-data folder. No backups will be made.");
3342 + return;
3343 + }
3344 + // Check create/write backupdir
3345 + try { fs.mkdirSync(backupPath); }
3346 + catch (e) {
3347 + // EEXIST error = dir already exists
3348 + if (e.code != 'EEXIST' ) {
3349 + //Unable to create backuppath
3350 + console.error(e.message);
3351 + func(1, 'Unable to create ' + backupPath + '. No backups will be made. Error: ' + e.message);
3352 + return;
3353 + }
3354 + }
3355 + const testFile = path.join(backupPath, (parent.config.settings.autobackup.backupname + ".test"));
3356 +
3357 + try { fs.writeFileSync( testFile, "DeleteMe"); }
3358 + catch (e) {
3359 + //Unable to create file
3360 + console.error (e.message);
3361 + func(1, "Backuppath (" + backupPath + ") can't be written to. No backups will be made. Error: " + e.message);
3362 + return;
3363 + }
3364 + try { fs.unlinkSync(testFile); parent.debug('backup', 'Backuppath ' + backupPath + ' accesscheck successful');}
3365 + catch (e) {
3366 + console.error (e.message);
3367 + func(1, "Backuppathtestfile (" + testFile + ") can't be deleted, check filerights. Error: " + e.message);
3368 + // Assume write rights, no delete rights. Continue with warning.
3369 + //return;
3370 + }
3371 +
3372 + // Check database dumptools
3373 if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
3374 // Check that we have access to MongoDump
3375 var cmd = buildMongoDumpCommand();
3376 cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
3377 const child_process = require('child_process');
3378 child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
3346 - try {
3347 - if ((error != null) && (error != '')) {
3348 - if (parent.platform == 'win32') {
3349 - func(1, "Unable to find mongodump.exe, MongoDB database auto-backup will not be performed.");
3350 - } else {
3351 - func(1, "Unable to find mongodump, MongoDB database auto-backup will not be performed.");
3352 - }
3353 - } else {
3354 - func();
3355 - }
3356 - } catch (ex) { console.log(ex); }
3379 + if ((error != null) && (error != '')) {
3380 + func(1, "Unable to find mongodump tool, backup will not be performed. Command tried: " + cmd);
3381 + return;
3382 + } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3383 });
3384 } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
3385 // Check that we have access to mysqldump
3386 var cmd = buildSqlDumpCommand();
3387 cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
3388 const child_process = require('child_process');
3363 - child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
3364 - try {
3365 - if ((error != null) && (error != '')) {
3366 - if (parent.platform == 'win32') {
3367 - func(1, "Unable to find mysqldump.exe, MySQL/MariaDB database auto-backup will not be performed.");
3368 - } else {
3369 - func(1, "Unable to find mysqldump, MySQL/MariaDB database auto-backup will not be performed.");
3370 - }
3371 - } else {
3372 - func();
3373 - }
3374 - } catch (ex) { console.log(ex); }
3389 + child_process.exec(cmd, { cwd: backupPath, timeout: 1000*30 }, function(error, stdout, stdin) {
3390 + if ((error != null) && (error != '')) {
3391 + func(1, "Unable to find mysqldump tool, backup will not be performed. Command tried: " + cmd);
3392 + return;
3393 + } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3394 +
3395 });
3396 } else if (obj.databaseType == DB_POSTGRESQL) {
3397 // Check that we have access to pg_dump
@@ -3382,17 +3402,14 @@ module.exports.CreateDB = function (parent, func) {
3402 + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
3403 const child_process = require('child_process');
3404 child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
3385 - try {
3386 - if ((error != null) && (error != '')) {
3387 - func(1, "Unable to find pg_dump, PostgreSQL database auto-backup will not be performed.");
3388 - } else {
3389 - func();
3390 - }
3391 - } catch (ex) { console.log(ex); }
3405 + if ((error != null) && (error != '')) {
3406 + func(1, "Unable to find pg_dump tool, backup will not be performed. Command tried: " + cmd);
3407 + return;
3408 + } else {parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3409 });
3410 } else {
3394 - func();
3395 - }
3411 + //all ok, enable backup
3412 + parent.config.settings.autobackup.backupintervalhours = backupInterval;}
3413 }
3414
3415 // MongoDB pending bulk read operation, perform fast bulk document reads.
@@ -3506,19 +3523,18 @@ module.exports.CreateDB = function (parent, func) {
3523
3524 // Perform a server backup
3525 obj.performBackup = function (func) {
3509 - parent.debug('db','Entering performBackup');
3526 + parent.debug('backup','Entering performBackup');
3527 try {
3528 if (obj.performingBackup) return 'Backup alreay in progress.';
3512 - if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Unable to create backup if backuppath is set to the data folder.'); return 'Backup aborted.' }};
3529 + if (parent.config.settings.autobackup.backupintervalhours == -1) { if (func) { func('Backup disabled.'); return 'Backup disabled.' }};
3530 obj.performingBackup = true;
3531 let backupPath = parent.backuppath;
3532 let dataPath = parent.datapath;
3533
3517 - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3518 - try { parent.fs.mkdirSync(backupPath); } catch (e) { }
3534 const currentDate = new Date();
3535 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3521 - obj.newAutoBackupFile = path.join(backupPath, ((typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-') + fileSuffix + '.zip');
3536 + obj.newAutoBackupFile = path.join(backupPath, parent.config.settings.autobackup.backupname + fileSuffix + '.zip');
3537 + parent.debug('backup','newAutoBackupFile=' + obj.newAutoBackupFile);
3538
3539 if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
3540 // Perform a MongoDump
@@ -3530,13 +3546,14 @@ module.exports.CreateDB = function (parent, func) {
3546 var cmd = buildMongoDumpCommand();
3547 cmd += (dburl) ? ' --archive=\"' + obj.newDBDumpFile + '\"' :
3548 ' --db=\"' + dbname + '\" --archive=\"' + obj.newDBDumpFile + '\"';
3533 -
3549 + parent.debug('backup','Mongodump cmd: ' + cmd);
3550 const child_process = require('child_process');
3551 const dumpProcess = child_process.exec(
3552 cmd,
3553 { cwd: parent.parentpath },
3538 - (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3554 + (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3555 );
3556 +
3557 dumpProcess.on('exit', (code) => {
3558 if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
3559 obj.createBackupfile(func);
@@ -3549,15 +3566,16 @@ module.exports.CreateDB = function (parent, func) {
3566
3567 var cmd = buildSqlDumpCommand();
3568 cmd += ' --result-file=\"' + obj.newDBDumpFile + '\"';
3569 + parent.debug('backup','Maria/MySQLdump cmd: ' + cmd);
3570
3571 const child_process = require('child_process');
3572 const dumpProcess = child_process.exec(
3573 cmd,
3574 { cwd: parent.parentpath },
3557 - (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3575 + (error)=> {if (error) {obj.backupStatus |= BACKUPFAIL_DBDUMP; console.error('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3576 );
3577 dumpProcess.on('exit', (code) => {
3560 - if (code != 0) {console.log(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
3578 + if (code != 0) {console.error(`MySQLdump child process exited with code ${code}`); obj.backupStatus |= BACKUPFAIL_DBDUMP;}
3579 obj.createBackupfile(func);
3580 });
3581
@@ -3565,8 +3583,9 @@ module.exports.CreateDB = function (parent, func) {
3583 //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
3584 obj.newDBDumpFile = path.join(backupPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
3585 // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
3586 + parent.debug('backup','SQLitedump: VACUUM INTO ' + obj.newDBDumpFile);
3587 obj.file.exec('VACUUM INTO \'' + obj.newDBDumpFile + '\'', function (err) {
3569 - if (err) { console.log('SQLite start-backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
3588 + if (err) { console.error('SQLite backup error: ' + err); obj.backupStatus |=BACKUPFAIL_DBDUMP;};
3589 //always finish/clean up
3590 obj.createBackupfile(func);
3591 });
@@ -3578,6 +3597,7 @@ module.exports.CreateDB = function (parent, func) {
3597 + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
3598 + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
3599 + " --file=" + obj.newDBDumpFile;
3600 + parent.debug('backup','Postgresqldump cmd: ' + cmd);
3601 const child_process = require('child_process');
3602 const dumpProcess = child_process.exec(
3603 cmd,
@@ -3589,15 +3609,15 @@ module.exports.CreateDB = function (parent, func) {
3609 obj.createBackupfile(func);
3610 });
3611 } else {
3592 - //NeDB backup, no db dump needed, just make a file backup
3612 + // NeDB/Acebase backup, no db dump needed, just make a file backup
3613 obj.createBackupfile(func);
3614 }
3595 - } catch (ex) { console.log(ex); };
3615 + } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during performBackup, check errorlog: ' +ex.message, true); };
3616 return 'Starting auto-backup...';
3617 };
3618
3619 obj.createBackupfile = function(func) {
3600 - parent.debug('db', 'Entering createFileBackup');
3620 + parent.debug('backup', 'Entering createBackupfile');
3621 let archiver = require('archiver');
3622 let archive = null;
3623 let zipLevel = Math.min(Math.max(Number(parent.config.settings.autobackup.zipcompression ? parent.config.settings.autobackup.zipcompression : 5),1),9);
@@ -3611,8 +3631,8 @@ module.exports.CreateDB = function (parent, func) {
3631 if (func) { func('Creating encrypted ZIP'); }
3632 } catch (ex) { // registering encryption failed, do not fall back to non-encrypted, fail backup and skip old backup removal as a precaution to not lose any backups
3633 obj.backupStatus |= BACKUPFAIL_ZIPMODULE;
3614 - if (func) { func('Zipencryptionmodule failed, aborting'); }
3615 - console.log('Zipencryptionmodule failed, aborting');
3634 + if (func) { func('Zipencryptionmodule failed, aborting');}
3635 + console.error('Zipencryptionmodule failed, aborting');
3636 }
3637 } else {
3638 if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
@@ -3622,51 +3642,36 @@ module.exports.CreateDB = function (parent, func) {
3642 //original behavior, just a filebackup if dbdump fails : (obj.backupStatus == 0 || obj.backupStatus == BACKUPFAIL_DBDUMP)
3643 if (obj.backupStatus == 0) {
3644 // Zip the data directory with the dbdump|NeDB files
3625 - let output = parent.fs.createWriteStream(obj.newAutoBackupFile);
3645 + let output = fs.createWriteStream(obj.newAutoBackupFile);
3646 +
3647 + // Archive finalized and closed
3648 output.on('close', function () {
3649 if (obj.backupStatus == 0) {
3628 - //remove dump archive file, because zipped and otherwise fills up
3629 - if (obj.databaseType != DB_NEDB) {
3630 - try { parent.fs.unlink(obj.newDBDumpFile, function () { }); } catch (ex) {console.log('Failed to clean up dbdump file')};
3631 - };
3650 + let mesg = 'Auto-backup completed: ' + obj.newAutoBackupFile + ', backup-size: ' + ((archive.pointer() / 1048576).toFixed(2)) + "Mb";
3651 + console.log(mesg);
3652 + if (func) { func(mesg); };
3653 obj.performCloudBackup(obj.newAutoBackupFile, func);
3633 - // Remove old backups
3634 - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
3635 - let cutoffDate = new Date();
3636 - cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
3637 - parent.fs.readdir(parent.backuppath, function (err, dir) {
3638 - try {
3639 - if ((err == null) && (dir.length > 0)) {
3640 - let fileName = (typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-';
3641 - for (var i in dir) {
3642 - var name = dir[i];
3643 - if (name.startsWith(fileName) && name.endsWith('.zip')) {
3644 - var timex = name.substring(23, name.length - 4).split('-');
3645 - if (timex.length == 5) {
3646 - var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
3647 - if (fileDate && (cutoffDate > fileDate)) { try { parent.fs.unlink(parent.path.join(parent.backuppath, name), function () { }); } catch (ex) { } }
3648 - }
3649 - }
3650 - }
3651 - }
3652 - } catch (ex) { console.log(ex); }
3653 - });
3654 - }
3655 - console.log('Auto-backup completed.');
3656 - if (func) { func('Auto-backup completed.'); };
3654 + obj.removeExpiredBackupfiles(func);
3655 +
3656 } else {
3658 - console.log('Zipbackup failed ('+ (+obj.backupStatus).toString(16).slice(-4) + '), deleting incomplete backup: ' + obj.newAutoBackupFile );
3659 - if (func) { func('Zipbackup failed ('+ (+obj.backupStatus).toString(16).slice(-4) + '), deleting incomplete backup: ' + obj.newAutoBackupFile) };
3660 - try { parent.fs.unlink(obj.newAutoBackupFile, function () { }); parent.fs.unlink(obj.newDBDumpFile, function () { }); } catch (ex) {console.log('Failed to delete incomplete backup files')};
3657 + let mesg = 'Zipbackup failed (' + obj.backupStatus.toString(2).slice(-8) + '), deleting incomplete backup: ' + obj.newAutoBackupFile;
3658 + if (func) { func(mesg) }
3659 + else { parent.addServerWarning(mesg, true ) };
3660 + if (fs.existsSync(obj.newAutoBackupFile)) { fs.unlink(obj.newAutoBackupFile, function (err) { console.error('Failed to clean up backupfile: ' + err.message) }) };
3661 + };
3662 + if (obj.databaseType != DB_NEDB) {
3663 + //remove dump archive file, because zipped and otherwise fills up
3664 + if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }) };
3665 };
3666 obj.performingBackup = false;
3667 obj.backupStatus = 0x0;
3664 - });
3668 + }
3669 + );
3670 output.on('end', function () { });
3671 output.on('error', function (err) {
3672 if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3668 - console.log('Output error: ' + err);
3669 - if (func) { func('Output error: ' + err); };
3673 + console.error('Output error: ' + err.message);
3674 + if (func) { func('Output error: ' + err.message); };
3675 obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
3676 archive.abort();
3677 };
@@ -3676,16 +3681,16 @@ module.exports.CreateDB = function (parent, func) {
3681 //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
3682 //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
3683 if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3679 - console.log('Zip warning: ' + err);
3680 - if (func) { func('Zip warning: ' + err); };
3684 + console.log('Zip warning: ' + err.message);
3685 + if (func) { func('Zip warning: ' + err.message); };
3686 obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
3687 archive.abort();
3688 };
3689 });
3690 archive.on('error', function (err) {
3691 if ((obj.backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3687 - console.log('Zip error: ' + err);
3688 - if (func) { func('Zip error: ' + err); };
3692 + console.error('Zip error: ' + err.message);
3693 + if (func) { func('Zip error: ' + err.message); };
3694 obj.backupStatus |= BACKUPFAIL_ZIPCREATE;
3695 archive.abort();
3696 }
@@ -3718,22 +3723,67 @@ module.exports.CreateDB = function (parent, func) {
3723 archive.finalize();
3724 } else {
3725 //failed somewhere before zipping
3721 - console.log('Backup failed ('+ (+obj.backupStatus).toString(16).slice(-4) + ')');
3722 - if (func) { func('Backup failed ('+ (+obj.backupStatus).toString(16).slice(-4) + ')') };
3726 + console.error('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')');
3727 + if (func) { func('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')') }
3728 + else {
3729 + parent.addServerWarning('Backup failed ('+ obj.backupStatus.toString(2).slice(-8) + ')', true);
3730 + }
3731 //Just in case something's there
3724 - try { parent.fs.unlink(obj.newDBDumpFile, function () { }); } catch (ex) { };
3732 + if (fs.existsSync(obj.newDBDumpFile)) { fs.unlink(obj.newDBDumpFile, function (err) { if (err) {console.error('Failed to clean up dbdump file: ' + err.message) } }); };
3733 obj.backupStatus = 0x0;
3734 obj.performingBackup = false;
3735 };
3736 };
3737
3738 + // Remove expired backupfiles by filenamedate
3739 + obj.removeExpiredBackupfiles = function (func) {
3740 + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
3741 + let cutoffDate = new Date();
3742 + cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
3743 + fs.readdir(parent.backuppath, function (err, dir) {
3744 + try {
3745 + if (err == null) {
3746 + if (dir.length > 0) {
3747 + let fileName = parent.config.settings.autobackup.backupname;
3748 + let checked = 0;
3749 + let removed = 0;
3750 + for (var i in dir) {
3751 + var name = dir[i];
3752 + parent.debug('backup', "checking file: ", path.join(parent.backuppath, name));
3753 + if (name.startsWith(fileName) && name.endsWith('.zip')) {
3754 + var timex = name.substring(fileName.length, name.length - 4).split('-');
3755 + if (timex.length == 5) {
3756 + checked++;
3757 + var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
3758 + if (fileDate && (cutoffDate > fileDate)) {
3759 + console.log("Removing expired backup file: ", path.join(parent.backuppath, name));
3760 + fs.unlink(path.join(parent.backuppath, name), function (err) { if (err) { console.error(err.message); if (func) {func('Error removing: ' + err.message); } } });
3761 + removed++;
3762 + }
3763 + }
3764 + else { parent.debug('backup', "file: " + name + " timestamp failure: ", timex); }
3765 + }
3766 + }
3767 + let mesg= 'Checked ' + checked + ' candidates in ' + parent.backuppath + '. Removed ' + removed + ' expired backupfiles using cutoffDate: '+ cutoffDate.toLocaleString('default', { dateStyle: 'short', timeStyle: 'short' });
3768 + parent.debug (mesg);
3769 + if (func) { func(mesg); }
3770 + } else { console.error('No files found in ' + parent.backuppath + '. There should be at least one.')}
3771 + }
3772 + else
3773 + { console.error(err); parent.addServerWarning( 'Reading files in backup directory ' + parent.backuppath + ' failed, check errorlog: ' + err.message, true); }
3774 + } catch (ex) { console.error(ex); parent.addServerWarning( 'Something went wrong during removeExpiredBackupfiles, check errorlog: ' +ex.message, true); }
3775 + });
3776 + }
3777 + }
3778 +
3779 // Perform cloud backup
3780 obj.performCloudBackup = function (filename, func) {
3732 -
3781 // WebDAV Backup
3782 if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
3735 - const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
3783 + parent.debug( 'backup', 'Entering WebDAV backup');
3784 + if (func) { func('Entering WebDAV backup.'); }
3785
3786 + const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
3787 // Fetch the folder name
3788 var webdavfolderName = 'MeshCentral-Backups';
3789 if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; }
@@ -3741,23 +3791,28 @@ module.exports.CreateDB = function (parent, func) {
3791 // Clean up our WebDAV folder
3792 function performWebDavCleanup(client) {
3793 if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) {
3744 - let fileName = (typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-';
3794 + let fileName = parent.config.settings.autobackup.backupname;
3795 //only files matching our backupfilename
3796 let directoryItems = client.getDirectoryContents(webdavfolderName, { deep: false, glob: "/**/" + fileName + "*.zip" });
3797 directoryItems.then(
3798 function (files) {
3799 for (var i in files) { files[i].xdate = new Date(files[i].lastmod); }
3800 files.sort(xdateTimeSort);
3801 + parent.debug('backup','WebDAV filtered directory contents: ' + JSON.stringify(files, null, 4));
3802 while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) {
3752 - client.deleteFile(files.shift().filename).then(function (state) {
3753 - if (func) { func('WebDAV file deleted.'); }
3803 + let delFile = files.shift().filename;
3804 + client.deleteFile(delFile).then(function (state) {
3805 + parent.debug('backup','WebDAV file deleted: ' + delFile);
3806 + if (func) { func('WebDAV file deleted: ' + delFile); }
3807 }).catch(function (err) {
3755 - if (func) { func('WebDAV (deleteFile) error: ' + err); }
3808 + console.error(err);
3809 + if (func) { func('WebDAV (deleteFile) error: ' + err.message); }
3810 });
3811 }
3812 }
3813 ).catch(function (err) {
3760 - if (func) { func('WebDAV (getDirectoryContents) error: ' + err); }
3814 + console.error(err);
3815 + if (func) { func('WebDAV (getDirectoryContents) error: ' + err.message); }
3816 });
3817 }
3818 }
@@ -3766,14 +3821,14 @@ module.exports.CreateDB = function (parent, func) {
3821 function performWebDavUpload(client, filepath) {
3822 require('fs').stat(filepath, function(err,stat){
3823 var fileStream = require('fs').createReadStream(filepath);
3769 - fileStream.on('close', function () { if (func) { func('WebDAV upload completed'); } })
3770 - fileStream.on('error', function (err) { if (func) { func('WebDAV (fileUpload) error: ' + err); } })
3824 + fileStream.on('close', function () { console.log('WebDAV upload completed: ' + webdavfolderName + '/' + require('path').basename(filepath)); if (func) { func('WebDAV upload completed: ' + webdavfolderName + '/' + require('path').basename(filepath)); } })
3825 + fileStream.on('error', function (err) { console.error(err); if (func) { func('WebDAV (fileUpload) error: ' + err.message); } })
3826 fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath), { headers: { "Content-Length": stat.size } }));
3772 - if (func) { func('Uploading using WebDAV...'); }
3827 + parent.debug('backup', 'Uploading using WebDAV to: ' + parent.config.settings.autobackup.webdav.url);
3828 + if (func) { func('Uploading using WebDAV to: ' + parent.config.settings.autobackup.webdav.url); }
3829 });
3830 }
3831
3776 - if (func) { func('Attempting WebDAV upload...'); }
3832 const { createClient } = require('webdav');
3833 const client = createClient(parent.config.settings.autobackup.webdav.url, {
3834 username: parent.config.settings.autobackup.webdav.username,
@@ -3787,19 +3842,23 @@ module.exports.CreateDB = function (parent, func) {
3842 performWebDavUpload(client, filename);
3843 }else{
3844 client.createDirectory(webdavfolderName, {recursive: true}).then(function (a) {
3790 - if (func) { func('WebDAV folder created'); }
3845 + console.log('backup','WebDAV folder created: ' + webdavfolderName);
3846 + if (func) { func('WebDAV folder created: ' + webdavfolderName); }
3847 performWebDavUpload(client, filename);
3848 }).catch(function (err) {
3793 - if (func) { func('WebDAV (createDirectory) error: ' + err); }
3849 + console.error(err);
3850 + if (func) { func('WebDAV (createDirectory) error: ' + err.message); }
3851 });
3852 }
3853 }).catch(function (err) {
3797 - if (func) { func('WebDAV (exists) error: ' + err); }
3854 + console.error(err);
3855 + if (func) { func('WebDAV (exists) error: ' + err.message); }
3856 });
3857 }
3858
3859 // Google Drive Backup
3860 if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
3861 + parent.debug( 'backup', 'Entering Google Drive backup');
3862 obj.Get('GoogleDriveBackup', function (err, docs) {
3863 if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
3864 if (func) { func('Attempting Google Drive upload...'); }
@@ -3878,6 +3937,7 @@ module.exports.CreateDB = function (parent, func) {
3937
3938 // S3 Backup
3939 if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.s3 == 'object')) {
3940 + parent.debug( 'backup', 'Entering S3 backup');
3941 var s3folderName = 'MeshCentral-Backups';
3942 if (typeof parent.config.settings.autobackup.s3.foldername == 'string') { s3folderName = parent.config.settings.autobackup.s3.foldername; }
3943 // Construct the config object
meshcentral-config-schema.json
+5
@@ -886,6 +886,11 @@
886 "default": 24,
887 "description": "How often should the autobackup run in hours from the second meshcentral starts up? Default is every 24 hours"
888 },
889 + "backupHour": {
890 + "type": "integer",
891 + "default": 0,
892 + "description": "At which hour the autobackup should run. This forces a daily backup, overrules a custom 'backupIntervalHours'."
893 + },
894 "keepLastDaysBackup": {
895 "type": "integer",
896 "default": 10,
meshcentral.js
+19 -10
@@ -1348,7 +1348,7 @@ function CreateMeshCentralServer(config, args) {
1348 }
1349
1350 // Check if the database is capable of performing a backup
1351 - obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
1351 + // Moved behind autobackup config init in startex4: obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
1352
1353 // Load configuration for database if needed
1354 if (obj.args.loadconfigfromdb) {
@@ -2016,6 +2016,7 @@ function CreateMeshCentralServer(config, args) {
2016
2017 // Start periodic maintenance
2018 obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 60 * 60); // Run this every hour
2019 + //obj.maintenanceTimer = setInterval(obj.maintenanceActions, 1000 * 10 * 1); // DEBUG: Run this more often
2020
2021 // Dispatch an event that the server is now running
2022 obj.DispatchEvent(['*'], obj, { etype: 'server', action: 'started', msg: 'Server started' });
@@ -2105,18 +2106,19 @@ function CreateMeshCentralServer(config, args) {
2106 if (obj.config.settings.autobackup == null || obj.config.settings.autobackup === true) { obj.config.settings.autobackup = {backupintervalhours: 24, keeplastdaysbackup: 10}; };
2107 if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
2108 if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
2109 + if (obj.config.settings.autobackup.backuphour != null ) { obj.config.settings.autobackup.backupintervalhours = 24; if ((typeof obj.config.settings.autobackup.backuphour != 'number') || (obj.config.settings.autobackup.backuphour > 23 || obj.config.settings.autobackup.backuphour < 0 )) { obj.config.settings.autobackup.backuphour = 0; }}
2110 + else {obj.config.settings.autobackup.backuphour = -1 };
2111 //arrayfi in case of string and remove possible ', ' space. !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !!
2112 if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
2113 else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
2114 if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
2115 else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
2116 + if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
2117 + if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
2118 }
2119
2115 - // Check that autobackup path is not within the "meshcentral-data" folder.
2116 - if ((typeof obj.config.settings.autobackup == 'object') && (typeof obj.config.settings.autobackup.backuppath == 'string') && (obj.path.normalize(obj.config.settings.autobackup.backuppath).startsWith(obj.path.normalize(obj.datapath)))) {
2117 - addServerWarning("Backup path can't be set within meshcentral-data folder, backup settings ignored.", 21);
2118 - obj.config.settings.autobackup = {backupintervalhours: -1}; //block console autobackup
2119 - }
2120 + // Check if the database is capable of performing a backup
2121 + obj.db.checkBackupCapability(function (err, msg) { if (msg != null) { obj.addServerWarning(msg, true) } });
2122
2123 // Load Intel AMT passwords from the "amtactivation.log" file
2124 obj.loadAmtActivationLogPasswords(function (amtPasswords) {
@@ -2278,14 +2280,19 @@ function CreateMeshCentralServer(config, args) {
2280
2281 // Check if we need to perform an automatic backup
2282 function checkAutobackup() {
2281 - if (obj.config.settings.autobackup.backupintervalhours >= 1) {
2283 + if (obj.config.settings.autobackup.backupintervalhours >= 1 ) {
2284 obj.db.Get('LastAutoBackupTime', function (err, docs) {
2283 - if (err != null) return;
2285 + if (err != null) { console.error("checkAutobackup: Error getting LastBackupTime from DB"); return}
2286 var lastBackup = 0;
2285 - const now = new Date().getTime();
2287 + const currentdate = new Date();
2288 + let currentHour = currentdate.getHours();
2289 + let now = currentdate.getTime();
2290 if (docs.length == 1) { lastBackup = docs[0].value; }
2291 const delta = now - lastBackup;
2288 - if (delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) {
2292 + //const delta = 9999999999; // DEBUG: backup always
2293 + obj.debug ('backup', 'Entering checkAutobackup, lastAutoBackupTime: ' + new Date(lastBackup).toLocaleString('default', { dateStyle: 'medium', timeStyle: 'short' }) + ', delta: ' + (delta/(1000*60*60)).toFixed(2) + ' hours');
2294 + //start autobackup if interval has passed or at configured hour, whichever comes first. When an hour schedule is missed, it will make a backup immediately.
2295 + if ((delta > (obj.config.settings.autobackup.backupintervalhours * 60 * 60 * 1000)) || ((currentHour == obj.config.settings.autobackup.backuphour) && (delta >= 2 * 60 * 60 * 1000))) {
2296 // A new auto-backup is required.
2297 obj.db.Set({ _id: 'LastAutoBackupTime', value: now }); // Save the current time in the database
2298 obj.db.performBackup(); // Perform the backup
@@ -3936,6 +3943,7 @@ function CreateMeshCentralServer(config, args) {
3943 function logWarnEvent(msg) { if (obj.servicelog != null) { obj.servicelog.warn(msg); } console.log(msg); }
3944 function logErrorEvent(msg) { if (obj.servicelog != null) { obj.servicelog.error(msg); } console.error(msg); }
3945 obj.getServerWarnings = function () { return serverWarnings; }
3946 + // TODO: migrate from other addServerWarning function and add timestamp
3947 obj.addServerWarning = function (msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
3948
3949 // auth.log functions
@@ -4106,6 +4114,7 @@ function InstallModuleEx(modulenames, args, func) {
4114 process.on('SIGINT', function () { if (meshserver != null) { meshserver.Stop(); meshserver = null; } console.log('Server Ctrl-C exit...'); process.exit(); });
4115
4116 // Add a server warning, warnings will be shown to the administrator on the web application
4117 +// TODO: migrate to obj.addServerWarning?
4118 const serverWarnings = [];
4119 function addServerWarning(msg, id, args, print) { serverWarnings.push({ msg: msg, id: id, args: args }); if (print !== false) { console.log("WARNING: " + msg); } }
4120