Fix archiver error, add backup options and SQLite maintenance (#6487)

PTR committed Nov 3, 2024 at 19:44 UTC e58d659fa94a74e6bbbef5198f85ec10e8a3993e
3 files changed +425 -245
db.js
+380 -235
@@ -32,6 +32,20 @@ module.exports.CreateDB = function (parent, func) {
32 var expirePowerEventsSeconds = (60 * 60 * 24 * 10); // By default, expire power events after 10 days (864000). (Seconds * Minutes * Hours * Days)
33 var expireServerStatsSeconds = (60 * 60 * 24 * 30); // By default, expire power events after 30 days (2592000). (Seconds * Minutes * Hours * Days)
34 const common = require('./common.js');
35 + const path = require('path');
36 + const fs = require('fs');
37 + const DB_NEDB = 1, DB_MONGOJS = 2, DB_MONGODB = 3,DB_MARIADB = 4, DB_MYSQL = 5, DB_POSTGRESQL = 6, DB_ACEBASE = 7, DB_SQLITE = 8;
38 + const DB_LIST = ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'PostgreSQL', 'AceBase', 'SQLite']; //for the info command
39 + let databaseName = 'meshcentral';
40 + let datapathParentPath = path.dirname(parent.datapath);
41 + let datapathFoldername = path.basename(parent.datapath);
42 + obj.performingBackup = false;
43 + const BACKUPFAIL_ZIPCREATE = 0x0001;
44 + const BACKUPFAIL_ZIPMODULE = 0x0010;
45 + const BACKUPFAIL_DBDUMP = 0x0100;
46 + let backupStatus = 0x0;
47 + let newAutoBackupFile;
48 + let newDBDumpFile;
49 obj.identifier = null;
50 obj.dbKey = null;
51 obj.dbRecordsEncryptKey = null;
@@ -105,16 +119,16 @@ module.exports.CreateDB = function (parent, func) {
119
120 // Perform database maintenance
121 obj.maintenance = function () {
108 - if (obj.databaseType == 1) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
122 + if (obj.databaseType == DB_NEDB) { // NeDB will not remove expired records unless we try to access them. This will force the removal.
123 obj.eventsfile.remove({ time: { '$lt': new Date(Date.now() - (expireEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
124 obj.powerfile.remove({ time: { '$lt': new Date(Date.now() - (expirePowerEventsSeconds * 1000)) } }, { multi: true }); // Force delete older events
125 obj.serverstatsfile.remove({ time: { '$lt': new Date(Date.now() - (expireServerStatsSeconds * 1000)) } }, { multi: true }); // Force delete older events
112 - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) { // MariaDB or MySQL
126 + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) { // MariaDB or MySQL
127 sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expireEventsSeconds
128 sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { }); // Delete events older than expirePowerSeconds
129 sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
130 sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { }); // Delete events where expiration date is in the past
117 - } else if (obj.databaseType == 7) { // AceBase
131 + } else if (obj.databaseType == DB_ACEBASE) { // AceBase
132 //console.log('Performing AceBase maintenance');
133 obj.file.query('events').filter('time', '<', new Date(Date.now() - (expireEventsSeconds * 1000))).remove().then(function () {
134 obj.file.query('stats').filter('time', '<', new Date(Date.now() - (expireServerStatsSeconds * 1000))).remove().then(function () {
@@ -123,8 +137,13 @@ module.exports.CreateDB = function (parent, func) {
137 });
138 });
139 });
126 - } else if (obj.databaseType == 8) { // SQLite3
127 - // TODO
140 + } else if (obj.databaseType == DB_SQLITE) { // SQLite3
141 + // TODO: Combine with others?
142 + sqlDbQuery('DELETE FROM events WHERE time < ?', [new Date(Date.now() - (expireEventsSeconds * 1000))], function (doc, err) { });
143 + sqlDbQuery('DELETE FROM power WHERE time < ?', [new Date(Date.now() - (expirePowerEventsSeconds * 1000))], function (doc, err) { });
144 + sqlDbQuery('DELETE FROM serverstats WHERE expire < ?', [new Date()], function (doc, err) { });
145 + sqlDbQuery('DELETE FROM smbios WHERE expire < ?', [new Date()], function (doc, err) { });
146 + obj.file.run( 'PRAGMA optimize;' ); //see https://sqlite.org/pragma.html#pragma_optimize
147 }
148 obj.removeInactiveDevices();
149 }
@@ -245,18 +264,18 @@ module.exports.CreateDB = function (parent, func) {
264 obj.removeDomain = function (domainName, func) {
265 var pendingCalls;
266 // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
248 - if (obj.databaseType == 7) {
267 + if (obj.databaseType == DB_ACEBASE) {
268 // AceBase
269 pendingCalls = 3;
270 obj.file.query('meshcentral').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
271 obj.file.query('events').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
272 obj.file.query('power').filter('domain', '==', domainName).remove().then(function () { if (--pendingCalls == 0) { func(); } });
254 - } else if ((obj.databaseType == 4) || (obj.databaseType == 5) || (obj.databaseType == 6)) {
273 + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
274 // MariaDB, MySQL or PostgreSQL
275 pendingCalls = 2;
276 sqlDbQuery('DELETE FROM main WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
277 sqlDbQuery('DELETE FROM events WHERE domain = $1', [domainName], function () { if (--pendingCalls == 0) { func(); } });
259 - } else if (obj.databaseType == 3) {
278 + } else if (obj.databaseType == DB_MONGODB) {
279 // MongoDB
280 pendingCalls = 3;
281 obj.file.deleteMany({ domain: domainName }, { multi: true }, function () { if (--pendingCalls == 0) { func(); } });
@@ -276,17 +295,17 @@ module.exports.CreateDB = function (parent, func) {
295 // TODO: Remove all meshes that dont have any links
296
297 // Remove all events, power events and SMBIOS data from the main collection. They are all in seperate collections now.
279 - if ((obj.databaseType == 4) || (obj.databaseType == 5) || (obj.databaseType == 6)) {
298 + if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) {
299 // MariaDB, MySQL or PostgreSQL
300 obj.RemoveAllOfType('event', function () { });
301 obj.RemoveAllOfType('power', function () { });
302 obj.RemoveAllOfType('smbios', function () { });
284 - } else if (obj.databaseType == 3) {
303 + } else if (obj.databaseType == DB_MONGODB) {
304 // MongoDB
305 obj.file.deleteMany({ type: 'event' }, { multi: true });
306 obj.file.deleteMany({ type: 'power' }, { multi: true });
307 obj.file.deleteMany({ type: 'smbios' }, { multi: true });
289 - } else if ((obj.databaseType == 1) || (obj.databaseType == 2)) {
308 + } else if ((obj.databaseType == DB_NEDB) || (obj.databaseType == DB_MONGOJS)) {
309 // NeDB or MongoJS
310 obj.file.remove({ type: 'event' }, { multi: true });
311 obj.file.remove({ type: 'power' }, { multi: true });
@@ -387,19 +406,19 @@ module.exports.CreateDB = function (parent, func) {
406 if (meshChange) { obj.Set(docs[i]); }
407 }
408 }
390 - if (obj.databaseType == 8) {
409 + if (obj.databaseType == DB_SQLITE) {
410 // SQLite
411
393 - } else if (obj.databaseType == 7) {
412 + } else if (obj.databaseType == DB_ACEBASE) {
413 // AceBase
414
396 - } else if (obj.databaseType == 6) {
415 + } else if (obj.databaseType == DB_POSTGRESQL) {
416 // Postgres
417 sqlDbQuery('DELETE FROM Main WHERE ((extra != NULL) AND (extra LIKE (\'mesh/%\')) AND (extra != ANY ($1)))', [meshlist], function (err, response) { });
399 - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
418 + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
419 // MariaDB
420 sqlDbQuery('DELETE FROM Main WHERE (extra LIKE ("mesh/%") AND (extra NOT IN ?)', [meshlist], function (err, response) { });
402 - } else if (obj.databaseType == 3) {
421 + } else if (obj.databaseType == DB_MONGODB) {
422 // MongoDB
423 obj.file.deleteMany({ meshid: { $exists: true, $nin: meshlist } }, { multi: true });
424 } else {
@@ -504,33 +523,33 @@ module.exports.CreateDB = function (parent, func) {
523 // Get the number of records in the database for various types, this is the slow NeDB way.
524 // WARNING: This is a terrible query for database performance. Only do this when needed. This query will look at almost every document in the database.
525 obj.getStats = function (func) {
507 - if (obj.databaseType == 7) {
526 + if (obj.databaseType == DB_ACEBASE) {
527 // AceBase
528 // TODO
510 - } else if (obj.databaseType == 6) {
529 + } else if (obj.databaseType == DB_POSTGRESQL) {
530 // PostgreSQL
531 // TODO
513 - } else if (obj.databaseType == 5) {
532 + } else if (obj.databaseType == DB_MYSQL) {
533 // MySQL
534 // TODO
516 - } else if (obj.databaseType == 4) {
535 + } else if (obj.databaseType == DB_MARIADB) {
536 // MariaDB
537 // TODO
519 - } else if (obj.databaseType == 3) {
538 + } else if (obj.databaseType == DB_MONGODB) {
539 // MongoDB
540 obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }]).toArray(function (err, docs) {
541 var counters = {}, totalCount = 0;
542 if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
543 func(counters);
544 });
526 - } else if (obj.databaseType == 2) {
545 + } else if (obj.databaseType == DB_MONGOJS) {
546 // MongoJS
547 obj.file.aggregate([{ "$group": { _id: "$type", count: { $sum: 1 } } }], function (err, docs) {
548 var counters = {}, totalCount = 0;
549 if (err == null) { for (var i in docs) { if (docs[i]._id != null) { counters[docs[i]._id] = docs[i].count; totalCount += docs[i].count; } } }
550 func(counters);
551 });
533 - } else if (obj.databaseType == 1) {
552 + } else if (obj.databaseType == DB_NEDB) {
553 // NeDB version
554 obj.file.count({ type: 'node' }, function (err, nodeCount) {
555 obj.file.count({ type: 'mesh' }, function (err, meshCount) {
@@ -570,7 +589,7 @@ module.exports.CreateDB = function (parent, func) {
589 if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
590 obj.GetAllType('mesh', function (err, docs) {
591 if (err == null) { for (var i in docs) { count++; obj.Set(docs[i]); } }
573 - if (obj.databaseType == 1) { // If we are using NeDB, compact the database.
592 + if (obj.databaseType == DB_NEDB) { // If we are using NeDB, compact the database.
593 obj.file.persistence.compactDatafile();
594 obj.file.on('compaction.done', function () { func(count); }); // It's important to wait for compaction to finish before exit, otherwise NeDB may corrupt.
595 } else {
@@ -721,13 +740,15 @@ module.exports.CreateDB = function (parent, func) {
740
741 if (parent.args.sqlite3) {
742 // SQLite3 database setup
724 - obj.databaseType = 8;
743 + obj.databaseType = DB_SQLITE;
744 const sqlite3 = require('sqlite3');
726 - obj.file = new sqlite3.Database(parent.path.join(parent.datapath, 'meshcentral.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
745 + if (typeof parent.config.settings.sqlite3 == 'string') {databaseName = parent.config.settings.sqlite3};
746 + //use sqlite3 cache mode https://github.com/TryGhost/node-sqlite3/wiki/Caching#caching
747 + obj.file = new sqlite3.cached.Database(parent.path.join(parent.datapath, databaseName + '.sqlite'), sqlite3.OPEN_READWRITE, function (err) {
748 if (err && (err.code == 'SQLITE_CANTOPEN')) {
749 // Database needs to be created
729 - obj.file = new sqlite3.Database(parent.path.join(parent.datapath, 'meshcentral.sqlite'), function (err) {
730 - if (err) { console.log("SQLite Error: " + err); exit(1); return; }
750 + obj.file = new sqlite3.Database(parent.path.join(parent.datapath, databaseName + '.sqlite'), function (err) {
751 + if (err) { console.log("SQLite Error: " + err); process.exit(1);; return; }
752 obj.file.exec(`
753 CREATE TABLE main (id VARCHAR(256) PRIMARY KEY NOT NULL, type CHAR(32), domain CHAR(64), extra CHAR(255), extraex CHAR(255), doc JSON);
754 CREATE TABLE events(id INTEGER PRIMARY KEY, time TIMESTAMP, domain CHAR(64), action CHAR(255), nodeid CHAR(255), userid CHAR(255), doc JSON);
@@ -749,20 +770,29 @@ module.exports.CreateDB = function (parent, func) {
770 CREATE INDEX ndxsmbiostime ON smbios (time);
771 CREATE INDEX ndxsmbiosexpire ON smbios (expire);
772 `, function (err) {
752 - // Completed setup of SQLite3
773 + // Completed DB creation of SQLite3
774 + //WAL mode instead of roll-back/delete
775 + obj.file.run( 'PRAGMA journal_mode=WAL;' );
776 + //Together with the optimize in the maintenance run, see https://sqlite.org/pragma.html#pragma_optimize
777 + obj.file.run( 'PRAGMA optimize=0x10002;' );
778 setupFunctions(func);
779 }
780 );
781 });
782 return;
758 - } else if (err) { console.log("SQLite Error: " + err); exit(1); return; }
783 + } else if (err) { console.log("SQLite Error: " + err); process.exit(0); return; }
784
785 // Completed setup of SQLite3
786 + //for existing db's
787 + //WAL mode instead of roll-back/delete
788 + obj.file.run( 'PRAGMA journal_mode=WAL;' );
789 + //Together with the optimize in the maintenance run, see https://sqlite.org/pragma.html#pragma_optimize
790 + obj.file.run( 'PRAGMA optimize=0x10002;' );
791 setupFunctions(func);
792 });
793 } else if (parent.args.acebase) {
794 // AceBase database setup
765 - obj.databaseType = 7;
795 + obj.databaseType = DB_ACEBASE;
796 const { AceBase } = require('acebase');
797 // For information on AceBase sponsor: https://github.com/appy-one/acebase/discussions/100
798 obj.file = new AceBase('meshcentral', { sponsor: ((typeof parent.args.acebase == 'object') && (parent.args.acebase.sponsor)), logLevel: 'error', storage: { path: parent.datapath } });
@@ -818,7 +848,7 @@ module.exports.CreateDB = function (parent, func) {
848
849 if (parent.args.mariadb) {
850 // Use MariaDB
821 - obj.databaseType = 4;
851 + obj.databaseType = DB_MARIADB;
852 var tempDatastore = require('mariadb').createPool(connectionObject);
853 tempDatastore.getConnection().then(function (conn) {
854 conn.query('CREATE DATABASE IF NOT EXISTS ' + dbname).then(function (result) {
@@ -832,7 +862,7 @@ module.exports.CreateDB = function (parent, func) {
862 createTablesIfNotExist(dbname);
863 } else if (parent.args.mysql) {
864 // Use MySQL
835 - obj.databaseType = 5;
865 + obj.databaseType = DB_MYSQL;
866 var tempDatastore = require('mysql2').createPool(connectionObject);
867 tempDatastore.query('CREATE DATABASE IF NOT EXISTS ' + dbname, function (error) {
868 if (error != null) {
@@ -846,37 +876,50 @@ module.exports.CreateDB = function (parent, func) {
876 }
877 } else if (parent.args.postgres) {
878 // Postgres SQL
849 - var connectinArgs = parent.args.postgres;
850 - var dbname = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral';
851 - delete connectinArgs.database;
852 - obj.databaseType = 6;
853 - const { Pool, Client } = require('pg');
854 - connectinArgs.database = dbname;
879 + let connectinArgs = parent.args.postgres;
880 + connectinArgs.Database = (databaseName = (connectinArgs.database != null) ? connectinArgs.database : 'meshcentral');
881 +
882 + let DatastoreTest;
883 + obj.databaseType = DB_POSTGRESQL;
884 + const { Client } = require('pg');
885 Datastore = new Client(connectinArgs);
856 - Datastore.connect();
857 - sqlDbQuery('SELECT 1 FROM pg_database WHERE datname = $1', [dbname], function (dberr, dbdocs) { // check database exists first before creating
858 - if (dberr == null) { // database exists now check tables exists
859 - sqlDbQuery('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, docs) {
860 - if (err == null) { setupFunctions(func); } else { postgreSqlCreateTables(func); } // If not present, create the tables and indexes
886 + //Connect to and check pg db first to check if own db exists. Otherwise errors out on 'database does not exist'
887 + connectinArgs.database = 'postgres';
888 + DatastoreTest = new Client(connectinArgs);
889 + DatastoreTest.connect();
890 +
891 + DatastoreTest.query('SELECT 1 FROM pg_catalog.pg_database WHERE datname = $1', [databaseName], function (err, res) { // check database exists first before creating
892 + if (res.rowCount != 0) { // database exists now check tables exists
893 + DatastoreTest.end();
894 + Datastore.connect();
895 + Datastore.query('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, res) {
896 + if (err == null) {
897 + (res.rowCount ==0) ? postgreSqlCreateTables(func) : setupFunctions(func)
898 + } else
899 + if (err.code == '42P01') { //42P01 = undefined table, https://www.postgresql.org/docs/current/errcodes-appendix.html
900 + postgreSqlCreateTables(func);
901 + } else {
902 + console.log('Postgresql database exists, other error: ', err.message); process.exit(0);
903 + };
904 });
905 } else { // If not present, create the tables and indexes
863 - const pgtools = require('pgtools');
864 - pgtools.createdb(connectinArgs, dbname, function (err, res) {
906 + //not needed, just use a create db statement: const pgtools = require('pgtools');
907 + DatastoreTest.query('CREATE DATABASE '+ databaseName + ';', [], function (err, res) {
908 if (err == null) {
909 // Create the tables and indexes
910 + DatastoreTest.end();
911 + Datastore.connect();
912 postgreSqlCreateTables(func);
913 } else {
869 - // Database already existed, perform a test query to see if the main table is present
870 - sqlDbQuery('SELECT doc FROM main WHERE id = $1', ['DatabaseIdentifier'], function (err, docs) {
871 - if (err == null) { setupFunctions(func); } else { postgreSqlCreateTables(func); } // If not present, create the tables and indexes
872 - });
914 + console.log('Postgresql database create error: ', err.message);
915 + process.exit(0);
916 }
917 });
918 }
919 });
920 } else if (parent.args.mongodb) {
921 // Use MongoDB
879 - obj.databaseType = 3;
922 + obj.databaseType = DB_MONGODB;
923
924 // If running an older NodeJS version, TextEncoder/TextDecoder is required
925 if (global.TextEncoder == null) { global.TextEncoder = require('util').TextEncoder; }
@@ -1059,7 +1102,7 @@ module.exports.CreateDB = function (parent, func) {
1102 });
1103 } else if (parent.args.xmongodb) {
1104 // Use MongoJS, this is the old system.
1062 - obj.databaseType = 2;
1105 + obj.databaseType = DB_MONGOJS;
1106 Datastore = require('mongojs');
1107 var db = Datastore(parent.args.xmongodb);
1108 var dbcollection = 'meshcentral';
@@ -1163,7 +1206,7 @@ module.exports.CreateDB = function (parent, func) {
1206 setupFunctions(func); // Completed setup of MongoJS
1207 } else {
1208 // Use NeDB (The default)
1166 - obj.databaseType = 1;
1209 + obj.databaseType = DB_NEDB;
1210 try { Datastore = require('@yetzt/nedb'); } catch (ex) { } // This is the NeDB with fixed security dependencies.
1211 if (Datastore == null) { Datastore = require('nedb'); } // So not to break any existing installations, if the old NeDB is present, use it.
1212 var datastoreOptions = { filename: parent.getConfigFilePath('meshcentral.db'), autoload: true };
@@ -1275,7 +1318,7 @@ module.exports.CreateDB = function (parent, func) {
1318
1319 // Query the database
1320 function sqlDbQuery(query, args, func, debug) {
1278 - if (obj.databaseType == 8) { // SQLite
1321 + if (obj.databaseType == DB_SQLITE) { // SQLite
1322 if (args == null) { args = []; }
1323 obj.file.all(query, args, function (err, docs) {
1324 if (err != null) { console.log(query, args, err, docs); }
@@ -1290,7 +1333,7 @@ module.exports.CreateDB = function (parent, func) {
1333 }
1334 if (func) { func(err, docs); }
1335 });
1293 - } else if (obj.databaseType == 4) { // MariaDB
1336 + } else if (obj.databaseType == DB_MARIADB) { // MariaDB
1337 Datastore.getConnection()
1338 .then(function (conn) {
1339 conn.query(query, args)
@@ -1309,7 +1352,7 @@ module.exports.CreateDB = function (parent, func) {
1352 })
1353 .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log('SQLERR2', ex); } });
1354 }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log('SQLERR3', ex); } } });
1312 - } else if (obj.databaseType == 5) { // MySQL
1355 + } else if (obj.databaseType == DB_MYSQL) { // MySQL
1356 Datastore.query(query, args, function (error, results, fields) {
1357 if (error != null) {
1358 if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
@@ -1330,7 +1373,7 @@ module.exports.CreateDB = function (parent, func) {
1373 if (func) { try { func(null, docs); } catch (ex) { console.log('SQLERR5', ex); } }
1374 }
1375 });
1333 - } else if (obj.databaseType == 6) { // Postgres SQL
1376 + } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres SQL
1377 Datastore.query(query, args, function (error, results) {
1378 if (error != null) {
1379 if (func) try { func(error); } catch (ex) { console.log('SQLERR4', ex); }
@@ -1359,7 +1402,7 @@ module.exports.CreateDB = function (parent, func) {
1402
1403 // Exec on the database
1404 function sqlDbExec(query, args, func) {
1362 - if (obj.databaseType == 4) { // MariaDB
1405 + if (obj.databaseType == DB_MARIADB) { // MariaDB
1406 Datastore.getConnection()
1407 .then(function (conn) {
1408 conn.query(query, args)
@@ -1369,7 +1412,7 @@ module.exports.CreateDB = function (parent, func) {
1412 })
1413 .catch(function (err) { conn.release(); if (func) try { func(err); } catch (ex) { console.log(ex); } });
1414 }).catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
1372 - } else if ((obj.databaseType == 5) || (obj.databaseType == 6)) { // MySQL or Postgres SQL
1415 + } else if ((obj.databaseType == DB_MYSQL) || (obj.databaseType == DB_POSTGRESQL)) { // MySQL or Postgres SQL
1416 Datastore.query(query, args, function (error, results, fields) {
1417 if (func) try { func(error, results ? results[0] : null); } catch (ex) { console.log(ex); }
1418 });
@@ -1378,7 +1421,7 @@ module.exports.CreateDB = function (parent, func) {
1421
1422 // Execute a batch of commands on the database
1423 function sqlDbBatchExec(queries, func) {
1381 - if (obj.databaseType == 4) { // MariaDB
1424 + if (obj.databaseType == DB_MARIADB) { // MariaDB
1425 Datastore.getConnection()
1426 .then(function (conn) {
1427 var Promises = [];
@@ -1388,7 +1431,7 @@ module.exports.CreateDB = function (parent, func) {
1431 .catch(function (err) { conn.release(); if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
1432 })
1433 .catch(function (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } });
1391 - } else if (obj.databaseType == 5) { // MySQL
1434 + } else if (obj.databaseType == DB_MYSQL) { // MySQL
1435 Datastore.getConnection(function(err, connection) {
1436 if (err) { if (func) { try { func(err); } catch (ex) { console.log(ex); } } return; }
1437 var Promises = [];
@@ -1397,7 +1440,7 @@ module.exports.CreateDB = function (parent, func) {
1440 .then(function (error, results, fields) { connection.release(); if (func) { try { func(error, results); } catch (ex) { console.log(ex); } } })
1441 .catch(function (error, results, fields) { connection.release(); if (func) { try { func(error); } catch (ex) { console.log(ex); } } });
1442 });
1400 - } else if (obj.databaseType == 6) { // Postgres
1443 + } else if (obj.databaseType == DB_POSTGRESQL) { // Postgres
1444 var Promises = [];
1445 for (var i in queries) { if (typeof queries[i] == 'string') { Promises.push(Datastore.query(queries[i])); } else { Promises.push(Datastore.query(queries[i][0], queries[i][1])); } }
1446 Promise.all(Promises)
@@ -1407,7 +1450,7 @@ module.exports.CreateDB = function (parent, func) {
1450 }
1451
1452 function setupFunctions(func) {
1410 - if (obj.databaseType == 8) {
1453 + if (obj.databaseType == DB_SQLITE) {
1454 // Database actions on the main collection. SQLite3: https://www.linode.com/docs/guides/getting-started-with-nodejs-sqlite/
1455 obj.Set = function (value, func) {
1456 obj.dbCounters.fileSet++;
@@ -1733,7 +1776,7 @@ module.exports.CreateDB = function (parent, func) {
1776 obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE plugin SET doc=JSON_SET(doc,"$.status",$1) WHERE id=$2', [status,id], func); };
1777 obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc=json_patch(doc,$1) WHERE id=$2', [JSON.stringify(args),id], func); };
1778 }
1736 - } else if (obj.databaseType == 7) {
1779 + } else if (obj.databaseType == DB_ACEBASE) {
1780 // Database actions on the main collection. AceBase: https://github.com/appy-one/acebase
1781 obj.Set = function (data, func) {
1782 data = common.escapeLinksFieldNameEx(data);
@@ -2025,7 +2068,7 @@ module.exports.CreateDB = function (parent, func) {
2068 obj.setPluginStatus = function (id, status, func) { obj.file.ref('plugin').child(encodeURIComponent(id)).update({ status: status }).then(function (ref) { if (func) { func(); } }) };
2069 obj.updatePlugin = function (id, args, func) { delete args._id; obj.file.ref('plugin').child(encodeURIComponent(id)).set(args).then(function (ref) { if (func) { func(); } }) };
2070 }
2028 - } else if (obj.databaseType == 6) {
2071 + } else if (obj.databaseType == DB_POSTGRESQL) {
2072 // Database actions on the main collection (Postgres)
2073 obj.Set = function (value, func) {
2074 obj.dbCounters.fileSet++;
@@ -2285,7 +2328,7 @@ module.exports.CreateDB = function (parent, func) {
2328 obj.setPluginStatus = function (id, status, func) { sqlDbQuery("UPDATE plugin SET doc= jsonb_set(doc::jsonb,'{status}',$1) WHERE id=$2", [status,id], func); };
2329 obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE plugin SET doc= doc::jsonb || ($1) WHERE id=$2', [args,id], func); };
2330 }
2288 - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
2331 + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
2332 // Database actions on the main collection (MariaDB or MySQL)
2333 obj.Set = function (value, func) {
2334 obj.dbCounters.fileSet++;
@@ -2535,7 +2578,7 @@ module.exports.CreateDB = function (parent, func) {
2578 obj.setPluginStatus = function (id, status, func) { sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_SET(doc,"$.status",?) WHERE id=?', [status,id], func); };
2579 obj.updatePlugin = function (id, args, func) { delete args._id; sqlDbQuery('UPDATE meshcentral.plugin SET doc=JSON_MERGE_PATCH(doc,?) WHERE id=?', [JSON.stringify(args),id], func); };
2580 }
2538 - } else if (obj.databaseType == 3) {
2581 + } else if (obj.databaseType == DB_MONGODB) {
2582 // Database actions on the main collection (MongoDB)
2583
2584 // Bulk operations
@@ -2919,7 +2962,7 @@ module.exports.CreateDB = function (parent, func) {
2962 obj.GetEvents = function (ids, domain, filter, func) {
2963 var finddata = { domain: domain, ids: { $in: ids } };
2964 if (filter != null) finddata.action = filter;
2922 - if (obj.databaseType == 1) {
2965 + if (obj.databaseType == DB_NEDB) {
2966 obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
2967 } else {
2968 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
@@ -2928,7 +2971,7 @@ module.exports.CreateDB = function (parent, func) {
2971 obj.GetEventsWithLimit = function (ids, domain, limit, filter, func) {
2972 var finddata = { domain: domain, ids: { $in: ids } };
2973 if (filter != null) finddata.action = filter;
2931 - if (obj.databaseType == 1) {
2974 + if (obj.databaseType == DB_NEDB) {
2975 obj.eventsfile.find(finddata, { _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
2976 } else {
2977 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
@@ -2937,7 +2980,7 @@ module.exports.CreateDB = function (parent, func) {
2980 obj.GetUserEvents = function (ids, domain, userid, filter, func) {
2981 var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
2982 if (filter != null) finddata.action = filter;
2940 - if (obj.databaseType == 1) {
2983 + if (obj.databaseType == DB_NEDB) {
2984 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).exec(func);
2985 } else {
2986 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }, func);
@@ -2946,21 +2989,21 @@ module.exports.CreateDB = function (parent, func) {
2989 obj.GetUserEventsWithLimit = function (ids, domain, userid, limit, filter, func) {
2990 var finddata = { domain: domain, $or: [{ ids: { $in: ids } }, { userid: userid }] };
2991 if (filter != null) finddata.action = filter;
2949 - if (obj.databaseType == 1) {
2992 + if (obj.databaseType == DB_NEDB) {
2993 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit).exec(func);
2994 } else {
2995 obj.eventsfile.find(finddata, { type: 0, _id: 0, domain: 0, ids: 0, node: 0 }).sort({ time: -1 }).limit(limit, func);
2996 }
2997 };
2998 obj.GetEventsTimeRange = function (ids, domain, msgids, start, end, func) {
2956 - if (obj.databaseType == 1) {
2999 + if (obj.databaseType == DB_NEDB) {
3000 obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }).exec(func);
3001 } else {
3002 obj.eventsfile.find({ domain: domain, $or: [{ ids: { $in: ids } }], msgid: { $in: msgids }, time: { $gte: start, $lte: end } }, { type: 0, _id: 0, domain: 0, node: 0 }).sort({ time: 1 }, func);
3003 }
3004 };
3005 obj.GetUserLoginEvents = function (domain, userid, func) {
2963 - if (obj.databaseType == 1) {
3006 + if (obj.databaseType == DB_NEDB) {
3007 obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }).exec(func);
3008 } else {
3009 obj.eventsfile.find({ domain: domain, action: { $in: ['authfail', 'login'] }, userid: userid, msgArgs: { $exists: true } }, { action: 1, time: 1, msgid: 1, msgArgs: 1, tokenName: 1 }).sort({ time: -1 }, func);
@@ -2969,7 +3012,7 @@ module.exports.CreateDB = function (parent, func) {
3012 obj.GetNodeEventsWithLimit = function (nodeid, domain, limit, filter, func) {
3013 var finddata = { domain: domain, nodeid: nodeid };
3014 if (filter != null) finddata.action = filter;
2972 - if (obj.databaseType == 1) {
3015 + if (obj.databaseType == DB_NEDB) {
3016 obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
3017 } else {
3018 obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
@@ -2978,7 +3021,7 @@ module.exports.CreateDB = function (parent, func) {
3021 obj.GetNodeEventsSelfWithLimit = function (nodeid, domain, userid, limit, filter, func) {
3022 var finddata = { domain: domain, nodeid: nodeid, userid: { $in: [userid, null] } };
3023 if (filter != null) finddata.action = filter;
2981 - if (obj.databaseType == 1) {
3024 + if (obj.databaseType == DB_NEDB) {
3025 obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit).exec(func);
3026 } else {
3027 obj.eventsfile.find(finddata, { type: 0, etype: 0, _id: 0, domain: 0, ids: 0, node: 0, nodeid: 0 }).sort({ time: -1 }).limit(limit, func);
@@ -2992,7 +3035,7 @@ module.exports.CreateDB = function (parent, func) {
3035 // Database actions on the power collection
3036 obj.getAllPower = function (func) { obj.powerfile.find({}, func); };
3037 obj.storePowerEvent = function (event, multiServer, func) { if (multiServer != null) { event.server = multiServer.serverid; } obj.powerfile.insert(event, func); };
2995 - obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == 1) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
3038 + obj.getPowerTimeline = function (nodeid, func) { if (obj.databaseType == DB_NEDB) { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }).exec(func); } else { obj.powerfile.find({ nodeid: { $in: ['*', nodeid] } }, { _id: 0, nodeid: 0, s: 0 }).sort({ time: 1 }, func); } };
3039 obj.removeAllPowerEvents = function () { obj.powerfile.remove({}, { multi: true }); };
3040 obj.removeAllPowerEventsForNode = function (nodeid) { if (nodeid == null) return; obj.powerfile.remove({ nodeid: nodeid }, { multi: true }); };
3041
@@ -3072,21 +3115,20 @@ module.exports.CreateDB = function (parent, func) {
3115 var r = '', backupPath = parent.backuppath;
3116 if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3117
3075 - var dbname = 'meshcentral';
3118 + let dbname = 'meshcentral';
3119 if (parent.args.mongodbname) { dbname = parent.args.mongodbname; }
3120 else if ((typeof parent.args.mariadb == 'object') && (typeof parent.args.mariadb.database == 'string')) { dbname = parent.args.mariadb.database; }
3121 else if ((typeof parent.args.mysql == 'object') && (typeof parent.args.mysql.database == 'string')) { dbname = parent.args.mysql.database; }
3122 + else if (typeof parent.config.settings.sqlite3 == 'string') {dbname = parent.config.settings.sqlite3 + '.sqlite'};
3123
3124 const currentDate = new Date();
3125 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3082 - const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
3083 - const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile);
3126 + const newAutoBackupFile = ((typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-') + fileSuffix;
3127
3128 r += 'DB Name: ' + dbname + '\r\n';
3086 - r += 'DB Type: ' + ['None', 'NeDB', 'MongoJS', 'MongoDB', 'MariaDB', 'MySQL', 'AceBase'][obj.databaseType] + '\r\n';
3129 + r += 'DB Type: ' + DB_LIST[obj.databaseType] + '\r\n';
3130 r += 'BackupPath: ' + backupPath + '\r\n';
3088 - r += 'newAutoBackupFile: ' + newAutoBackupFile + '\r\n';
3089 - r += 'newAutoBackupPath: ' + newAutoBackupPath + '\r\n';
3131 + r += 'BackupFile: ' + newAutoBackupFile + '.zip\r\n';
3132
3133 if (parent.config.settings.autobackup == null) {
3134 r += 'No Settings/AutoBackup\r\n';
@@ -3117,11 +3159,33 @@ module.exports.CreateDB = function (parent, func) {
3159 if (typeof parent.config.settings.autobackup.mysqldumppath != 'string') { r += 'Bad mysqldump type\r\n'; }
3160 else { r += parent.config.settings.autobackup.mysqldumppath + '\r\n'; }
3161 }
3162 + if (parent.config.settings.autobackup.backupotherfolders) {
3163 + r += 'Backup other folders: ';
3164 + r += parent.filespath + ', ' + parent.recordpath + '\r\n';
3165 + }
3166 + if (parent.config.settings.autobackup.backupwebfolders) {
3167 + r += 'Backup webfolders: ';
3168 + if (parent.webViewsOverridePath) {r += parent.webViewsOverridePath };
3169 + if (parent.webPublicOverridePath) {r += ', '+ parent.webPublicOverridePath};
3170 + if (parent.webEmailsOverridePath) {r += ',' + parent.webEmailsOverridePath};
3171 + r+= '\r\n';
3172 + }
3173 + if (parent.config.settings.autobackup.backupignorefilesglob != []) {
3174 + r += 'Backup IgnoreFilesGlob: ';
3175 + { r += parent.config.settings.autobackup.backupignorefilesglob + '\r\n'; }
3176 + }
3177 + if (parent.config.settings.autobackup.backupskipfoldersglob != []) {
3178 + r += 'Backup SkipFoldersGlob: ';
3179 + { r += parent.config.settings.autobackup.backupskipfoldersglob + '\r\n'; }
3180 + }
3181 +
3182 if (typeof parent.config.settings.autobackup.s3 == 'object') {
3183 r += 'S3 Backups: Enabled\r\n';
3184 }
3185 if (typeof parent.config.settings.autobackup.webdav == 'object') {
3186 r += 'WebDAV Backups: Enabled\r\n';
3187 + r += 'WebDAV backup path: ' + ((typeof parent.config.settings.autobackup.webdav.foldername == 'string') ? parent.config.settings.autobackup.webdav.foldername : 'MeshCentral-Backups') + '\r\n';
3188 + r += 'WebDAV maximum files: '+ ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') ? parent.config.settings.autobackup.webdav.maxfiles : 'no limit') + '\r\n';
3189 }
3190 if (typeof parent.config.settings.autobackup.googledrive == 'object') {
3191 r += 'Google Drive Backups: Enabled\r\n';
@@ -3134,7 +3198,7 @@ module.exports.CreateDB = function (parent, func) {
3198 }
3199
3200 function buildSqlDumpCommand() {
3137 - var props = (obj.databaseType == 4) ? parent.args.mariadb : parent.args.mysql;
3201 + var props = (obj.databaseType == DB_MARIADB) ? parent.args.mariadb : parent.args.mysql;
3202
3203 var mysqldumpPath = 'mysqldump';
3204 if (parent.config.settings.autobackup && parent.config.settings.autobackup.mysqldumppath) {
@@ -3151,7 +3215,7 @@ module.exports.CreateDB = function (parent, func) {
3215
3216 // SSL options different on mariadb/mysql
3217 var sslOptions = '';
3154 - if (obj.databaseType == 4) {
3218 + if (obj.databaseType == DB_MARIADB) {
3219 if (props.ssl) {
3220 sslOptions = ' --ssl';
3221 if (props.ssl.cacertpath) sslOptions = ' --ssl-ca=' + props.ssl.cacertpath;
@@ -3193,14 +3257,14 @@ module.exports.CreateDB = function (parent, func) {
3257
3258 // Check that the server is capable of performing a backup
3259 obj.checkBackupCapability = function (func) {
3196 - if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { func(); }
3197 - if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
3198 - // Check that we have access to MongoDump
3199 - var backupPath = parent.backuppath;
3200 - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3201 - try { parent.fs.mkdirSync(backupPath); } catch (ex) { }
3202 - if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, database auto-backup will not be performed."); return; }
3260 + if ((parent.config.settings.autobackup == null) || (parent.config.settings.autobackup == false)) { func(); return; };
3261 + let backupPath = parent.backuppath;
3262 + if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3263 + try { parent.fs.mkdirSync(backupPath); } catch (e) { }
3264 + if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, auto-backup will not be performed."); return; }
3265
3266 + if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
3267 + // Check that we have access to MongoDump
3268 var cmd = buildMongoDumpCommand();
3269 cmd += (parent.platform == 'win32') ? ' --archive=\"nul\"' : ' --archive=\"/dev/null\"';
3270 const child_process = require('child_process');
@@ -3217,13 +3281,8 @@ module.exports.CreateDB = function (parent, func) {
3281 }
3282 } catch (ex) { console.log(ex); }
3283 });
3220 - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
3284 + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
3285 // Check that we have access to mysqldump
3222 - var backupPath = parent.backuppath;
3223 - if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3224 - try { parent.fs.mkdirSync(backupPath); } catch (e) { }
3225 - if (parent.fs.existsSync(backupPath) == false) { func(1, "Backup folder \"" + backupPath + "\" does not exist, database auto-backup will not be performed."); return; }
3226 -
3286 var cmd = buildSqlDumpCommand();
3287 cmd += ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
3288 const child_process = require('child_process');
@@ -3240,6 +3299,23 @@ module.exports.CreateDB = function (parent, func) {
3299 }
3300 } catch (ex) { console.log(ex); }
3301 });
3302 + } else if (obj.databaseType == DB_POSTGRESQL) {
3303 + // Check that we have access to pg_dump
3304 + parent.config.settings.autobackup.pgdumppath = path.normalize(parent.config.settings.autobackup.pgdumppath ? parent.config.settings.autobackup.pgdumppath : 'pg_dump');
3305 + let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
3306 + + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
3307 + + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
3308 + + ' > ' + ((parent.platform == 'win32') ? '\"nul\"' : '\"/dev/null\"');
3309 + const child_process = require('child_process');
3310 + child_process.exec(cmd, { cwd: backupPath }, function(error, stdout, stdin) {
3311 + try {
3312 + if ((error != null) && (error != '')) {
3313 + func(1, "Unable to find pg_dump, PostgreSQL database auto-backup will not be performed.");
3314 + } else {
3315 + func();
3316 + }
3317 + } catch (ex) { console.log(ex); }
3318 + });
3319 } else {
3320 func();
3321 }
@@ -3355,167 +3431,234 @@ module.exports.CreateDB = function (parent, func) {
3431 }
3432
3433 // Perform a server backup
3358 - obj.performingBackup = false;
3434 obj.performBackup = function (func) {
3435 + parent.debug('db','Entering performBackup');
3436 try {
3437 if (obj.performingBackup) return 1;
3438 obj.performingBackup = true;
3363 - //console.log('Performing backup...');
3439 + let backupPath = parent.backuppath;
3440 + let dataPath = parent.datapath;
3441
3365 - var backupPath = parent.backuppath;
3442 if (parent.config.settings.autobackup && parent.config.settings.autobackup.backuppath) { backupPath = parent.config.settings.autobackup.backuppath; }
3443 try { parent.fs.mkdirSync(backupPath); } catch (e) { }
3368 - const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
3369 - const dburl = parent.args.mongodb;
3444 const currentDate = new Date();
3445 const fileSuffix = currentDate.getFullYear() + '-' + padNumber(currentDate.getMonth() + 1, 2) + '-' + padNumber(currentDate.getDate(), 2) + '-' + padNumber(currentDate.getHours(), 2) + '-' + padNumber(currentDate.getMinutes(), 2);
3372 - const newAutoBackupFile = 'meshcentral-autobackup-' + fileSuffix;
3373 - const newAutoBackupPath = parent.path.join(backupPath, newAutoBackupFile);
3446 + newAutoBackupFile = path.join(backupPath, ((typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-') + fileSuffix + '.zip');
3447
3375 - if ((obj.databaseType == 2) || (obj.databaseType == 3)) {
3376 - // Perform a MongoDump backup
3377 - const newBackupFile = 'mongodump-' + fileSuffix;
3378 - var newBackupPath = parent.path.join(backupPath, newBackupFile);
3448 + if ((obj.databaseType == DB_MONGOJS) || (obj.databaseType == DB_MONGODB)) {
3449 + // Perform a MongoDump in the datadir
3450 + const dbname = (parent.args.mongodbname) ? (parent.args.mongodbname) : 'meshcentral';
3451 + const dburl = parent.args.mongodb;
3452 +
3453 + //const newDBDumpFile = 'mongodump-' + fileSuffix;
3454 + newDBDumpFile = path.join(dataPath, (dbname + '-mongodump-' + fileSuffix + '.archive'));
3455
3456 var cmd = buildMongoDumpCommand();
3381 - cmd += (dburl) ? ' --archive=\"' + newBackupPath + '.archive\"' :
3382 - ' --db=\"' + dbname + '\" --archive=\"' + newBackupPath + '.archive\"';
3457 + cmd += (dburl) ? ' --archive=\"' + newDBDumpFile + '\"' :
3458 + ' --db=\"' + dbname + '\" --archive=\"' + newDBDumpFile + '\"';
3459
3460 const child_process = require('child_process');
3385 - var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
3386 - try {
3387 - var mongoDumpSuccess = true;
3388 - backupProcess = null;
3389 - if ((error != null) && (error != '')) { mongoDumpSuccess = false; console.log('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); }
3390 -
3391 - // Perform archive compression
3392 - var archiver = require('archiver');
3393 - var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
3394 - var archive = null;
3395 - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
3396 - try {
3397 - archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
3398 - archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
3399 - if (func) { func('Creating encrypted ZIP'); }
3400 - } catch (ex) { // registering encryption failed, so create without encryption
3401 - archive = archiver('zip', { zlib: { level: 9 } });
3402 - if (func) { func('Creating encrypted ZIP failed, so falling back to normal ZIP'); }
3403 - }
3404 - } else {
3405 - archive = archiver('zip', { zlib: { level: 9 } });
3406 - }
3407 - output.on('close', function () {
3408 - obj.performingBackup = false;
3409 - if (func) { if (mongoDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without mongodb database: ' + error); } }
3410 - obj.performCloudBackup(newAutoBackupPath + '.zip', func);
3411 - setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.archive', function () { }); } catch (ex) { console.log(ex); } }, 5000);
3412 - });
3413 - output.on('end', function () { });
3414 - output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
3415 - archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
3416 - archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
3417 - archive.pipe(output);
3418 - if (mongoDumpSuccess == true) { archive.file(newBackupPath + '.archive', { name: newBackupFile + '.archive' }); }
3419 - archive.directory(parent.datapath, 'meshcentral-data');
3420 - archive.finalize();
3421 - } catch (ex) { console.log(ex); }
3422 - });
3423 - } else if ((obj.databaseType == 4) || (obj.databaseType == 5)) {
3461 + const dumpProcess = child_process.exec(
3462 + cmd,
3463 + { cwd: parent.parentpath },
3464 + (error)=> {if (error) {backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform MongoDB backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3465 + );
3466 + dumpProcess.on('exit', (code) => {
3467 + if (code != 0) {console.log(`Mongodump child process exited with code ${code}`); backupStatus |= BACKUPFAIL_DBDUMP;}
3468 + obj.createBackupfile(func);
3469 + });
3470 +
3471 + } else if ((obj.databaseType == DB_MARIADB) || (obj.databaseType == DB_MYSQL)) {
3472 // Perform a MySqlDump backup
3473 const newBackupFile = 'mysqldump-' + fileSuffix;
3426 - var newBackupPath = parent.path.join(backupPath, newBackupFile);
3474 + newDBDumpFile = path.join(dataPath, newBackupFile + '.sql');
3475
3476 var cmd = buildSqlDumpCommand();
3429 - cmd += ' --result-file=\"' + newBackupPath + '.sql\"';
3477 + cmd += ' --result-file=\"' + newDBDumpFile + '\"';
3478 +
3479 const child_process = require('child_process');
3431 - var backupProcess = child_process.exec(cmd, { cwd: backupPath }, function (error, stdout, stderr) {
3432 - try {
3433 - var sqlDumpSuccess = true;
3434 - backupProcess = null;
3435 - if ((error != null) && (error != '')) { sqlDumpSuccess = false; console.log('ERROR: Unable to perform MySQL/MariaDB backup: ' + error + '\r\n'); }
3436 -
3437 - var archiver = require('archiver');
3438 - var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
3439 - var archive = null;
3440 - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
3441 - try {
3442 - archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
3443 - archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
3444 - if (func) { func('Creating encrypted ZIP'); }
3445 - } catch (ex) { // registering encryption failed, so create without encryption
3446 - archive = archiver('zip', { zlib: { level: 9 } });
3447 - if (func) { func('Creating encrypted ZIP failed, so falling back to normal ZIP'); }
3448 - }
3449 - } else {
3450 - archive = archiver('zip', { zlib: { level: 9 } });
3451 - }
3452 - output.on('close', function () {
3453 - obj.performingBackup = false;
3454 - if (func) { if (sqlDumpSuccess) { func('Auto-backup completed.'); } else { func('Auto-backup completed without MySQL/MariaDB database: ' + error); } }
3455 - obj.performCloudBackup(newAutoBackupPath + '.zip', func);
3456 - setTimeout(function () { try { parent.fs.unlink(newBackupPath + '.sql', function () { }); } catch (ex) { console.log(ex); } }, 5000);
3457 - });
3458 - output.on('end', function () { });
3459 - output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
3460 - archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
3461 - archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
3462 - archive.pipe(output);
3463 - if (sqlDumpSuccess == true) { archive.file(newBackupPath + '.sql', { name: newBackupFile + '.sql' }); }
3464 - archive.directory(parent.datapath, 'meshcentral-data');
3465 - archive.finalize();
3466 - } catch (ex) { console.log(ex); }
3480 + const dumpProcess = child_process.exec(
3481 + cmd,
3482 + { cwd: parent.parentpath },
3483 + (error)=> {if (error) {backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform MySQL backup: ' + error + '\r\n'); obj.createBackupfile(func);}}
3484 + );
3485 + dumpProcess.on('exit', (code) => {
3486 + if (code != 0) {console.log(`MySQLdump child process exited with code ${code}`); backupStatus |= BACKUPFAIL_DBDUMP;}
3487 + obj.createBackupfile(func);
3488 + });
3489 +
3490 + } else if (obj.databaseType == DB_SQLITE) {
3491 + //.db3 suffix to escape escape backupfile glob to exclude the sqlite db files
3492 + newDBDumpFile = path.join(dataPath, databaseName + '-sqlitedump-' + fileSuffix + '.db3');
3493 + /*undocumented in node-sqlite3 API, check https://github.com/TryGhost/node-sqlite3/blob/593c9d498be2510d286349134537e3bf89401c4a/test/backup.test.js
3494 + var backup = obj.file.backup(newDBDumpFile);
3495 + backup.step(-1, function (err) {
3496 + if (err) { console.log('SQLite start-backup error: ' + err); backupStatus |=BACKUPFAIL_DBDUMP; obj.createBackupfile(func); };
3497 + backup.finish(function (err) {
3498 + if (err) { console.log('SQLite backup error: ' + err); backupStatus |=BACKUPFAIL_DBDUMP;};
3499 + obj.createBackupfile(func);
3500 + });
3501 + });
3502 + */
3503 + // do a VACUUM INTO in favor of the backup API to compress the export, see https://www.sqlite.org/backup.html
3504 + obj.file.exec('VACUUM INTO \'' + newDBDumpFile + '\'', function (err) {
3505 + if (err) { console.log('SQLite start-backup error: ' + err); backupStatus |=BACKUPFAIL_DBDUMP;};
3506 + //always finish/clean up
3507 + obj.createBackupfile(func);
3508 + });
3509 + } else if (obj.databaseType == DB_POSTGRESQL) {
3510 + // Perform a PostgresDump backup
3511 + const newBackupFile = databaseName + '-pgdump-' + fileSuffix + '.sql';
3512 + newDBDumpFile = path.join(dataPath, newBackupFile);
3513 + let cmd = '"' + parent.config.settings.autobackup.pgdumppath + '"'
3514 + + ' --dbname=postgresql://' + parent.config.settings.postgres.user + ":" +parent.config.settings.postgres.password
3515 + + "@" + parent.config.settings.postgres.host + ":" + parent.config.settings.postgres.port + "/" + databaseName
3516 + + " --file=" + newDBDumpFile;
3517 + const child_process = require('child_process');
3518 + const dumpProcess = child_process.exec(
3519 + cmd,
3520 + { cwd: dataPath },
3521 + (error)=> {if (error) {backupStatus |= BACKUPFAIL_DBDUMP; console.log('ERROR: Unable to perform PostgreSQL dump: ' + error.message + '\r\n'); obj.createBackupfile(func);}}
3522 + );
3523 + dumpProcess.on('exit', (code) => {
3524 + if (code != 0) {console.log(`PostgreSQLdump child process exited with code: ` + code); backupStatus |= BACKUPFAIL_DBDUMP;}
3525 + obj.createBackupfile(func);
3526 });
3527 } else {
3469 - // Perform a NeDB backup
3470 - var archiver = require('archiver');
3471 - var output = parent.fs.createWriteStream(newAutoBackupPath + '.zip');
3472 - var archive = null;
3473 - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
3474 - try {
3475 - archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted'));
3476 - archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
3477 - if (func) { func('Creating encrypted ZIP'); }
3478 - } catch (ex) { // registering encryption failed, so create without encryption
3479 - archive = archiver('zip', { zlib: { level: 9 } });
3480 - if (func) { func('Creating encrypted ZIP failed, so falling back to normal ZIP'); }
3481 - }
3482 - } else {
3483 - archive = archiver('zip', { zlib: { level: 9 } });
3484 - }
3485 - output.on('close', function () { obj.performingBackup = false; if (func) { func('Auto-backup completed.'); } obj.performCloudBackup(newAutoBackupPath + '.zip', func); });
3486 - output.on('end', function () { });
3487 - output.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
3488 - archive.on('warning', function (err) { console.log('Backup warning: ' + err); if (func) { func('Backup warning: ' + err); } });
3489 - archive.on('error', function (err) { console.log('Backup error: ' + err); if (func) { func('Backup error: ' + err); } });
3490 - archive.pipe(output);
3491 - archive.directory(parent.datapath, 'meshcentral-data');
3492 - archive.finalize();
3493 - }
3494 -
3495 - // Remove old backups
3496 - if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
3497 - var cutoffDate = new Date();
3498 - cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
3499 - parent.fs.readdir(parent.backuppath, function (err, dir) {
3500 - try {
3501 - if ((err == null) && (dir.length > 0)) {
3502 - for (var i in dir) {
3503 - var name = dir[i];
3504 - if (name.startsWith('meshcentral-autobackup-') && name.endsWith('.zip')) {
3505 - var timex = name.substring(23, name.length - 4).split('-');
3506 - if (timex.length == 5) {
3507 - var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
3508 - if (fileDate && (cutoffDate > fileDate)) { try { parent.fs.unlink(parent.path.join(parent.backuppath, name), function () { }); } catch (ex) { } }
3528 + //NeDB backup, no db dump needed, just make a file backup
3529 + obj.createBackupfile(func);
3530 + }
3531 + } catch (ex) { console.log(ex); };
3532 + return(0);
3533 + };
3534 +
3535 + obj.createBackupfile = function(func) {
3536 + parent.debug('db', 'Entering createFileBackup');
3537 + let archiver = require('archiver');
3538 + let archive = null;
3539 + //if password defined, create encrypted zip
3540 + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.zippassword == 'string')) {
3541 + try {
3542 + //Only register format once, otherwise it triggers an error
3543 + if (archiver.isRegisteredFormat('zip-encrypted') == false) { archiver.registerFormat('zip-encrypted', require('archiver-zip-encrypted')); }
3544 + archive = archiver.create('zip-encrypted', { zlib: { level: 9 }, encryptionMethod: 'aes256', password: parent.config.settings.autobackup.zippassword });
3545 + if (func) { func('Creating encrypted ZIP'); }
3546 + } 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
3547 + backupStatus |= BACKUPFAIL_ZIPMODULE;
3548 + if (func) { func('Zipencryptionmodule failed, aborting'); }
3549 + console.log('Zipencryptionmodule failed, aborting');
3550 + }
3551 + } else {
3552 + if (func) { func('Creating a NON-ENCRYPTED ZIP'); }
3553 + archive = archiver('zip', { zlib: { level: 9 } });
3554 + }
3555 +
3556 + //original behavior, just a filebackup if dbdump fails : (backupStatus == 0 || backupStatus == BACKUPFAIL_DBDUMP)
3557 + if (backupStatus == 0) {
3558 + // Zip the data directory with the dbdump|NeDB files
3559 + let output = parent.fs.createWriteStream(newAutoBackupFile);
3560 + output.on('close', function () {
3561 + if (backupStatus == 0) {
3562 + //remove dump archive file, because zipped and otherwise fills up
3563 + if (obj.databaseType != DB_NEDB) {
3564 + try { parent.fs.unlink(newDBDumpFile, function () { }); } catch (ex) {console.log('Failed to clean up dbdump file')};
3565 + };
3566 + obj.performCloudBackup(newAutoBackupFile, func);
3567 + // Remove old backups
3568 + if (parent.config.settings.autobackup && (typeof parent.config.settings.autobackup.keeplastdaysbackup == 'number')) {
3569 + let cutoffDate = new Date();
3570 + cutoffDate.setDate(cutoffDate.getDate() - parent.config.settings.autobackup.keeplastdaysbackup);
3571 + parent.fs.readdir(parent.backuppath, function (err, dir) {
3572 + try {
3573 + if ((err == null) && (dir.length > 0)) {
3574 + let fileName = (typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-';
3575 + for (var i in dir) {
3576 + var name = dir[i];
3577 + if (name.startsWith(fileName) && name.endsWith('.zip')) {
3578 + var timex = name.substring(23, name.length - 4).split('-');
3579 + if (timex.length == 5) {
3580 + var fileDate = new Date(parseInt(timex[0]), parseInt(timex[1]) - 1, parseInt(timex[2]), parseInt(timex[3]), parseInt(timex[4]));
3581 + if (fileDate && (cutoffDate > fileDate)) { try { parent.fs.unlink(parent.path.join(parent.backuppath, name), function () { }); } catch (ex) { } }
3582 + }
3583 + }
3584 }
3585 }
3511 - }
3512 - }
3513 - } catch (ex) { console.log(ex); }
3586 + } catch (ex) { console.log(ex); }
3587 + });
3588 + }
3589 + console.log('Auto-backup completed.');
3590 + if (func) { func('Auto-backup completed.'); };
3591 + } else {
3592 + console.log('Zipbackup failed ('+ (+backupStatus).toString(16).slice(-4) + '), deleting incomplete backup: ' + newAutoBackupFile );
3593 + if (func) { func('Zipbackup failed ('+ (+backupStatus).toString(16).slice(-4) + '), deleting incomplete backup: ' + newAutoBackupFile) };
3594 + try { parent.fs.unlink(newAutoBackupFile, function () { }); parent.fs.unlink(newDBDumpFile, function () { }); } catch (ex) {console.log('Failed to delete incomplete backup files')};
3595 + };
3596 + obj.performingBackup = false;
3597 + backupStatus = 0x0;
3598 + });
3599 + output.on('end', function () { });
3600 + output.on('error', function (err) {
3601 + if ((backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3602 + console.log('Output error: ' + err);
3603 + if (func) { func('Output error: ' + err); };
3604 + backupStatus |= BACKUPFAIL_ZIPCREATE;
3605 + archive.abort();
3606 + };
3607 + });
3608 + archive.on('warning', function (err) {
3609 + //if files added to the archiver object aren't reachable anymore (e.g. sqlite-journal files)
3610 + //an ENOENT warning is given, but the archiver module has no option to/does not skip/resume
3611 + //so the backup needs te be aborted as it otherwise leaves an incomplete zip and never 'ends'
3612 + if ((backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3613 + console.log('Zip warning: ' + err);
3614 + if (func) { func('Zip warning: ' + err); };
3615 + backupStatus |= BACKUPFAIL_ZIPCREATE;
3616 + archive.abort();
3617 + };
3618 + });
3619 + archive.on('error', function (err) {
3620 + if ((backupStatus & BACKUPFAIL_ZIPCREATE) == 0) {
3621 + console.log('Zip error: ' + err);
3622 + if (func) { func('Zip error: ' + err); };
3623 + backupStatus |= BACKUPFAIL_ZIPCREATE;
3624 + archive.abort();
3625 + }
3626 });
3515 - }
3516 - } catch (ex) { console.log(ex); }
3517 - return 0;
3518 - }
3627 + archive.pipe(output);
3628 +
3629 + let globIgnoreFiles;
3630 + //slice in case exclusion gets pushed
3631 + globIgnoreFiles = parent.config.settings.autobackup.backupignorefilesglob.slice();
3632 + if (parent.config.settings.sqlite3) { globIgnoreFiles.push (datapathFoldername + '/' + databaseName + '.sqlite*'); }; //skip sqlite database file, and temp files with ext -journal, -wal & -shm
3633 + //archiver.glob doesn't seem to use the third param, archivesubdir. Bug?
3634 + //workaround: go up a dir and add data dir explicitly to keep the zip tidy
3635 + archive.glob((datapathFoldername + '/**'), {
3636 + cwd: datapathParentPath,
3637 + ignore: globIgnoreFiles,
3638 + skip: parent.config.settings.autobackup.backupskipfoldersglob
3639 + });
3640 +
3641 + if (parent.config.settings.autobackup.backupwebfolders) {
3642 + if (parent.webViewsOverridePath) { archive.directory(parent.webViewsOverridePath, 'meshcentral-views'); }
3643 + if (parent.webPublicOverridePath) { archive.directory(parent.webPublicOverridePath, 'meshcentral-public'); }
3644 + if (parent.webEmailsOverridePath) { archive.directory(parent.webEmailsOverridePath, 'meshcentral-emails'); }
3645 + };
3646 + if (parent.config.settings.autobackup.backupotherfolders) {
3647 + archive.directory(parent.filespath, 'meshcentral-files');
3648 + archive.directory(parent.recordpath, 'meshcentral-recordings');
3649 + };
3650 +
3651 + archive.finalize();
3652 + } else {
3653 + //failed somewhere before zipping
3654 + console.log('Backup failed ('+ (+backupStatus).toString(16).slice(-4) + ')');
3655 + if (func) { func('Backup failed ('+ (+backupStatus).toString(16).slice(-4) + ')') };
3656 + //Just in case something's there
3657 + try { parent.fs.unlink(newDBDumpFile, function () { }); } catch (ex) { };
3658 + backupStatus = 0x0;
3659 + obj.performingBackup = false;
3660 + };
3661 + };
3662
3663 // Perform cloud backup
3664 obj.performCloudBackup = function (filename, func) {
@@ -3531,7 +3674,9 @@ module.exports.CreateDB = function (parent, func) {
3674 // Clean up our WebDAV folder
3675 function performWebDavCleanup(client) {
3676 if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) {
3534 - var directoryItems = client.getDirectoryContents(webdavfolderName);
3677 + let fileName = (typeof parent.config.settings.autobackup.backupname == 'string') ? parent.config.settings.autobackup.backupname : 'meshcentral-autobackup-';
3678 + //only files matching our backupfilename
3679 + let directoryItems = client.getDirectoryContents(webdavfolderName, { deep: false, glob: "/**/" + fileName + "*.zip" });
3680 directoryItems.then(
3681 function (files) {
3682 for (var i in files) { files[i].xdate = new Date(files[i].lastmod); }
meshcentral-config-schema.json
+32 -2
@@ -94,9 +94,9 @@
94 }
95 },
96 "sqlite3": {
97 - "type": "boolean",
97 + "type": [ "boolean", "string" ],
98 "default": false,
99 - "description": "Set true to use SQLite3 as a local MeshCentral database."
99 + "description": "Set boolean true to use SQLite3 as a local MeshCentral database with default db filename 'meshcentral' or enter a string for a different db filename. Extension .sqlite is appended"
100 },
101 "mySQL": {
102 "type": "object",
@@ -836,6 +836,11 @@
836 "default": "mysqldump",
837 "description": "The file path of where \"mysqldump\" is located. Default is \"mysqldump\""
838 },
839 + "pgDumpPath": {
840 + "type": "string",
841 + "default": "pg_dump",
842 + "description": "The file path of where \"pg_dump\" is located. Default is \"pg_dump\""
843 + },
844 "backupIntervalHours": {
845 "type": "integer",
846 "default": 24,
@@ -857,6 +862,31 @@
862 "default": "meshcentral-backups",
863 "description": "The file path where backup files are kept. The default is \"meshcentral-backups\" which sits next to \"meshcentral-data\"."
864 },
865 + "backupName": {
866 + "type": "string",
867 + "default": "meshcentral-autobackup-",
868 + "description": "The filename of the backupfile. The default is \"meshcentral-autobackup-\", the filename is appended with the time of backup."
869 + },
870 + "backupWebFolders": {
871 + "type": "boolean",
872 + "default": false,
873 + "description": "Add views, public and emails directories if overridden"
874 + },
875 + "backupOtherFolders": {
876 + "type": "boolean",
877 + "default": false,
878 + "description": "Also add files and recordings folder to the backup"
879 + },
880 + "backupIgnoreFilesGlob": {
881 + "type": "array",
882 + "default": [],
883 + "description": "Glob for ignoring files in the data directory. For example [\"**/*.log\"] !! If a string instead of an array is passed, it will be split by ',' so *{.txt,.log} won't work in that case !! Don't do string..."
884 + },
885 + "backupSkipFoldersGlob":{
886 + "type": "array",
887 + "default": [],
888 + "description": "Glob for ignoring directories in the data directory. For example [\"**/signedagents\"]"
889 + },
890 "googleDrive": {
891 "type": "object",
892 "description": "Enabled automated upload of the server backups to a Google Drive account, once enabled you need to go in \"My Server\" tab as administrator to associate the account.",
meshcentral.js
+13 -8
@@ -2089,11 +2089,16 @@ function CreateMeshCentralServer(config, args) {
2089 obj.updateServerState('state', "running");
2090
2091 // Setup auto-backup defaults
2092 - if (obj.config.settings.autobackup == null || obj.config.settings.autobackup === true) { obj.config.settings.autobackup = { backupintervalhours: 24, keeplastdaysbackup: 10 }; }
2093 - else if (obj.config.settings.autobackup === false) { delete obj.config.settings.autobackup; }
2094 - else if (typeof obj.config.settings.autobackup == 'object'){
2095 - if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; }
2096 - if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; }
2092 + if (obj.config.settings.autobackup == null || obj.config.settings.autobackup == false || obj.config.settings.autobackup == 'false') { delete obj.config.settings.autobackup; }
2093 + else {
2094 + if (obj.config.settings.autobackup === true) {obj.config.settings.autobackup = {backupintervalhours: 24, keeplastdaysbackup: 10}; };
2095 + if (typeof obj.config.settings.autobackup.backupintervalhours != 'number') { obj.config.settings.autobackup.backupintervalhours = 24; };
2096 + if (typeof obj.config.settings.autobackup.keeplastdaysbackup != 'number') { obj.config.settings.autobackup.keeplastdaysbackup = 10; };
2097 + //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 !!
2098 + if (!obj.config.settings.autobackup.backupignorefilesglob) {obj.config.settings.autobackup.backupignorefilesglob = []}
2099 + else if (typeof obj.config.settings.autobackup.backupignorefilesglob == 'string') { obj.config.settings.autobackup.backupignorefilesglob = obj.config.settings.autobackup.backupignorefilesglob.replaceAll(', ', ',').split(','); };
2100 + if (!obj.config.settings.autobackup.backupskipfoldersglob) {obj.config.settings.autobackup.backupskipfoldersglob = []}
2101 + else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
2102 }
2103
2104 // Check that autobackup path is not within the "meshcentral-data" folder.
@@ -4220,10 +4225,10 @@ function mainStart() {
4225 if (config.settings.mysql != null) { modules.push('mysql2@3.6.2'); } // Add MySQL.
4226 //if (config.settings.mysql != null) { modules.push('@mysql/xdevapi@8.0.33'); } // Add MySQL, official driver (https://dev.mysql.com/doc/dev/connector-nodejs/8.0/)
4227 if (config.settings.mongodb != null) { modules.push('mongodb@4.13.0'); modules.push('saslprep@1.0.3'); } // Add MongoDB, official driver.
4223 - if (config.settings.postgres != null) { modules.push('pg@8.7.1'); modules.push('pgtools@0.3.2'); } // Add Postgres, Postgres driver.
4228 + if (config.settings.postgres != null) { modules.push('pg@8.13.1') } // Add Postgres, official driver.
4229 if (config.settings.mariadb != null) { modules.push('mariadb@3.2.2'); } // Add MariaDB, official driver.
4230 if (config.settings.acebase != null) { modules.push('acebase@1.29.5'); } // Add AceBase, official driver.
4226 - if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.6'); } // Add sqlite3, official driver.
4231 + if (config.settings.sqlite3 != null) { modules.push('sqlite3@5.1.7'); } // Add sqlite3, official driver.
4232 if (config.settings.vault != null) { modules.push('node-vault@0.10.2'); } // Add official HashiCorp's Vault module.
4233 if (config.settings.plugins != null) { modules.push('semver@7.5.4'); } // Required for version compat testing and update checks
4234 if ((config.settings.plugins != null) && (config.settings.plugins.proxy != null)) { modules.push('https-proxy-agent@7.0.2'); } // Required for HTTP/HTTPS proxy support
@@ -4240,7 +4245,7 @@ function mainStart() {
4245 if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
4246 // Enable WebDAV Support
4247 if (typeof config.settings.autobackup.webdav == 'object') {
4243 - if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@4.11.3'); }
4248 + if ((typeof config.settings.autobackup.webdav.url != 'string') || (typeof config.settings.autobackup.webdav.username != 'string') || (typeof config.settings.autobackup.webdav.password != 'string')) { addServerWarning("Missing WebDAV parameters.", 2, null, !args.launch); } else { modules.push('webdav@4.11.4'); }
4249 }
4250 // Enable S3 Support
4251 if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.1'); }