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