Added WebDAV auto-backup support
Ylian Saint-Hilaire committed
Sep 10, 2020 at 12:43 UTC
1918006bc601c82b0c7a45834bef30db37d504b3
5 files changed
+168
-63
db.js
+132
-61
@@ -1412,81 +1412,152 @@ module.exports.CreateDB = function (parent, func) {
1412
1413
// Perform cloud backup
1414
obj.performCloudBackup = function (filename, func) {
1415
- if (typeof parent.config.settings.autobackup.googledrive != 'object') return;
1416
- obj.Get('GoogleDriveBackup', function (err, docs) {
1417
- if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
1418
- if (func) { func('Attempting Google Drive upload...'); }
1419
- const {google} = require('googleapis');
1420
- const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
1421
- oAuth2Client.on('tokens', function(tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
1422
- oAuth2Client.setCredentials(docs[0].token);
1423
- const drive = google.drive({ version: 'v3', auth: oAuth2Client });
1424
- const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
1425
-
1426
- // Called once we know our folder id, clean up and upload a backup.
1427
- var useGoogleDrive = function (folderid) {
1428
- // List files to see if we need to delete older ones
1429
- if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
1430
- drive.files.list({
1431
- q: 'trashed = false and \'' + folderid + '\' in parents',
1432
- fields: 'nextPageToken, files(id, name, size, createdTime)',
1415
+
1416
+ // WebDAV Backup
1417
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
1418
+ const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
1419
+
1420
+ // Fetch the folder name
1421
+ var webdavfolderName = 'MeshCentral-Backups';
1422
+ if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; }
1423
+
1424
+ // Clean up our WebDAV folder
1425
+ function performWebDavCleanup(client) {
1426
+ if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) {
1427
+ var directoryItems = client.getDirectoryContents(webdavfolderName);
1428
+ directoryItems.then(
1429
+ function (files) {
1430
+ for (var i in files) { files[i].xdate = new Date(files[i].lastmod); }
1431
+ files.sort(xdateTimeSort);
1432
+ while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) {
1433
+ client.deleteFile(files.shift().filename).then(function (state) {
1434
+ if (func) { func('WebDAV file deleted.'); }
1435
+ }).catch(function (err) {
1436
+ if (func) { func('WebDAV (deleteFile) error: ' + err); }
1437
+ });
1438
+ }
1439
+ }
1440
+ ).catch(function (err) {
1441
+ if (func) { func('WebDAV (getDirectoryContents) error: ' + err); }
1442
+ });
1443
+ }
1444
+ }
1445
+
1446
+ // Upload to the WebDAV folder
1447
+ function performWebDavUpload(client, filepath) {
1448
+ var fileStream = require('fs').createReadStream(filepath);
1449
+ fileStream.on('close', function () { if (func) { func('WebDAV upload completed'); } })
1450
+ fileStream.on('error', function (err) { if (func) { func('WebDAV (fileUpload) error: ' + err); } })
1451
+ fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath)));
1452
+ if (func) { func('Uploading using WebDAV...'); }
1453
+ }
1454
+
1455
+ if (func) { func('Attempting WebDAV upload...'); }
1456
+ const { createClient } = require('webdav');
1457
+ const client = createClient(parent.config.settings.autobackup.webdav.url, { username: parent.config.settings.autobackup.webdav.username, password: parent.config.settings.autobackup.webdav.password });
1458
+ var directoryItems = client.getDirectoryContents('/');
1459
+ directoryItems.then(
1460
+ function (files) {
1461
+ var folderFound = false;
1462
+ for (var i in files) { if ((files[i].basename == webdavfolderName) && (files[i].type == 'directory')) { folderFound = true; } }
1463
+ if (folderFound == false) {
1464
+ client.createDirectory(webdavfolderName).then(function (a) {
1465
+ if (a.statusText == 'Created') {
1466
+ if (func) { func('WebDAV folder created'); }
1467
+ performWebDavUpload(client, filename);
1468
+ } else {
1469
+ if (func) { func('WebDAV (createDirectory) status: ' + a.statusText); }
1470
+ }
1471
+ }).catch(function (err) {
1472
+ if (func) { func('WebDAV (createDirectory) error: ' + err); }
1473
+ });
1474
+ } else {
1475
+ performWebDavCleanup(client);
1476
+ performWebDavUpload(client, filename);
1477
+ }
1478
+ }
1479
+ ).catch(function (err) {
1480
+ if (func) { func('WebDAV (getDirectoryContents) error: ' + err); }
1481
+ });
1482
+ }
1483
+
1484
+ // Google Drive Backup
1485
+ if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) {
1486
+ obj.Get('GoogleDriveBackup', function (err, docs) {
1487
+ if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return;
1488
+ if (func) { func('Attempting Google Drive upload...'); }
1489
+ const {google} = require('googleapis');
1490
+ const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob");
1491
+ oAuth2Client.on('tokens', function (tokens) { if (tokens.refresh_token) { docs[0].token = tokens.refresh_token; parent.db.Set(docs[0]); } }); // Update the token in the database
1492
+ oAuth2Client.setCredentials(docs[0].token);
1493
+ const drive = google.drive({ version: 'v3', auth: oAuth2Client });
1494
+ const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; }
1495
+
1496
+ // Called once we know our folder id, clean up and upload a backup.
1497
+ var useGoogleDrive = function (folderid) {
1498
+ // List files to see if we need to delete older ones
1499
+ if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') {
1500
+ drive.files.list({
1501
+ q: 'trashed = false and \'' + folderid + '\' in parents',
1502
+ fields: 'nextPageToken, files(id, name, size, createdTime)',
1503
+ }, function (err, res) {
1504
+ if (err) {
1505
+ console.log('GoogleDrive (files.list) error: ' + err);
1506
+ if (func) { func('GoogleDrive (files.list) error: ' + err); }
1507
+ return;
1508
+ }
1509
+ // Delete any old files if more than 10 files are present in the backup folder.
1510
+ res.data.files.sort(createdTimeSort);
1511
+ while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
1512
+ });
1513
+ }
1514
+
1515
+ //console.log('Uploading...');
1516
+ if (func) { func('Uploading to Google Drive...'); }
1517
+
1518
+ // Upload the backup
1519
+ drive.files.create({
1520
+ requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
1521
+ media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
1522
}, function (err, res) {
1523
if (err) {
1435
- console.log('GoogleDrive (files.list) error: ' + err);
1436
- if (func) { func('GoogleDrive (files.list) error: ' + err); }
1524
+ console.log('GoogleDrive (files.create) error: ' + err);
1525
+ if (func) { func('GoogleDrive (files.create) error: ' + err); }
1526
return;
1527
}
1439
- // Delete any old files if more than 10 files are present in the backup folder.
1440
- res.data.files.sort(createdTimeSort);
1441
- while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); }
1528
+ //console.log('Upload done.');
1529
+ if (func) { func('Google Drive upload completed.'); }
1530
});
1531
}
1532
1445
- //console.log('Uploading...');
1446
- if (func) { func('Uploading to Google Drive...'); }
1533
+ // Fetch the folder name
1534
+ var folderName = 'MeshCentral-Backups';
1535
+ if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
1536
1448
- // Upload the backup
1449
- drive.files.create({
1450
- requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] },
1451
- media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) },
1537
+ // Find our backup folder, create one if needed.
1538
+ drive.files.list({
1539
+ q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
1540
+ fields: 'nextPageToken, files(id, name)',
1541
}, function (err, res) {
1542
if (err) {
1454
- console.log('GoogleDrive (files.create) error: ' + err);
1455
- if (func) { func('GoogleDrive (files.create) error: ' + err); }
1543
+ console.log('GoogleDrive error: ' + err);
1544
+ if (func) { func('GoogleDrive error: ' + err); }
1545
return;
1546
}
1458
- //console.log('Upload done.');
1459
- if (func) { func('Google Drive upload completed.'); }
1547
+ if (res.data.files.length == 0) {
1548
+ // Create a folder
1549
+ drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
1550
+ if (err) {
1551
+ console.log('GoogleDrive (folder.create) error: ' + err);
1552
+ if (func) { func('GoogleDrive (folder.create) error: ' + err); }
1553
+ return;
1554
+ }
1555
+ useGoogleDrive(file.data.id);
1556
+ });
1557
+ } else { useGoogleDrive(res.data.files[0].id); }
1558
});
1461
- }
1462
-
1463
- // Fetch the folder name
1464
- var folderName = 'MeshCentral-Backups';
1465
- if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; }
1466
-
1467
- // Find our backup folder, create one if needed.
1468
- drive.files.list({
1469
- q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false',
1470
- fields: 'nextPageToken, files(id, name)',
1471
- }, function (err, res) {
1472
- if (err) {
1473
- console.log('GoogleDrive error: ' + err);
1474
- if (func) { func('GoogleDrive error: ' + err); }
1475
- return;
1476
- }
1477
- if (res.data.files.length == 0) {
1478
- // Create a folder
1479
- drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) {
1480
- if (err) {
1481
- console.log('GoogleDrive (folder.create) error: ' + err);
1482
- if (func) { func('GoogleDrive (folder.create) error: ' + err); }
1483
- return;
1484
- }
1485
- useGoogleDrive(file.data.id);
1486
- });
1487
- } else { useGoogleDrive(res.data.files[0].id); }
1559
});
1489
- });
1560
+ }
1561
}
1562
1563
function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; }
meshcentral-config-schema.json
+11
@@ -115,6 +115,17 @@
115
"folderName": { "type": "integer", "default": "MeshCentral-Backups", "description": "The name of the folder to create in the Google Drive account." },
116
"maxFiles": { "type": "string", "default": null, "description": "The maximum number of files to keep in the Google Drive folder, older files will be removed if needed." }
117
}
118
+ },
119
+ "webDAV": {
120
+ "type": "object",
121
+ "description": "Enabled automated upload of the server backups to a WebDAV account.",
122
+ "properties": {
123
+ "url": { "type": "string", "description": "WebDAV account URL." },
124
+ "username": { "type": "string", "description": "WebDAV account username." },
125
+ "password": { "type": "string", "description": "WebDAV account password." },
126
+ "folderName": { "type": "integer", "default": "MeshCentral-Backups", "description": "The name of the folder to create in the WebDAV account." },
127
+ "maxFiles": { "type": "string", "default": null, "description": "The maximum number of files to keep in the WebDAV folder, older files will be removed if needed." }
128
+ }
129
}
130
}
131
},
meshcentral.js
+18
-1
@@ -2725,7 +2725,24 @@ function mainStart() {
2725
// Setup encrypted zip support if needed
2726
if (config.settings.autobackup && config.settings.autobackup.zippassword) {
2727
modules.push('archiver-zip-encrypted');
2728
- if (typeof config.settings.autobackup.googledrive == 'object') { if (nodeVersion >= 8) { modules.push('googleapis'); } else { delete config.settings.autobackup.googledrive; } } // Enable Google Drive Support
2728
+ // Enable Google Drive Support
2729
+ if (typeof config.settings.autobackup.googledrive == 'object') {
2730
+ if (nodeVersion >= 8) {
2731
+ modules.push('googleapis');
2732
+ } else {
2733
+ addServerWarning("Google Drive requires Node v8.x or higher.", !args.launch);
2734
+ delete config.settings.autobackup.googledrive;
2735
+ }
2736
+ }
2737
+ // Enable WebDAV Support
2738
+ if (typeof config.settings.autobackup.webdav == 'object') {
2739
+ if (nodeVersion >= 10) {
2740
+ 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.", !args.launch); } else { modules.push('webdav'); }
2741
+ } else {
2742
+ addServerWarning("WebDAV requires Node v10.x or higher.", !args.launch);
2743
+ delete config.settings.autobackup.webdav;
2744
+ }
2745
+ }
2746
}
2747
2748
// Setup common password blocking
package.json
-1
@@ -36,7 +36,6 @@
36
"sample-config-advanced.json"
37
],
38
"dependencies": {
39
- "archiver": "^4.0.2",
39
"body-parser": "^1.19.0",
40
"cbor": "^4.1.5",
41
"compression": "^1.7.4",
sample-config-advanced.json
+7
@@ -92,6 +92,13 @@
92
"_googleDrive": {
93
"folderName": "MeshCentral-Backups",
94
"maxFiles": 10
95
+ },
96
+ "webdav": {
97
+ "url": "https://server/remote.php/dav/files/xxxxx@server.com/",
98
+ "username": "user",
99
+ "password": "pass",
100
+ "folderName": "MeshCentral-Backups",
101
+ "maxFiles": 10
102
}
103
},
104
"_redirects": {