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": {