diff --git a/db.js b/db.js index 773e680e..be600099 100644 --- a/db.js +++ b/db.js @@ -1412,81 +1412,152 @@ module.exports.CreateDB = function (parent, func) { // Perform cloud backup obj.performCloudBackup = function (filename, func) { - if (typeof parent.config.settings.autobackup.googledrive != 'object') return; - obj.Get('GoogleDriveBackup', function (err, docs) { - if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return; - if (func) { func('Attempting Google Drive upload...'); } - const {google} = require('googleapis'); - const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob"); - 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 - oAuth2Client.setCredentials(docs[0].token); - const drive = google.drive({ version: 'v3', auth: oAuth2Client }); - const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; } - // Called once we know our folder id, clean up and upload a backup. - var useGoogleDrive = function (folderid) { - // List files to see if we need to delete older ones - if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') { - drive.files.list({ - q: 'trashed = false and \'' + folderid + '\' in parents', - fields: 'nextPageToken, files(id, name, size, createdTime)', - }, function (err, res) { - if (err) { - console.log('GoogleDrive (files.list) error: ' + err); - if (func) { func('GoogleDrive (files.list) error: ' + err); } - return; - } - // Delete any old files if more than 10 files are present in the backup folder. - res.data.files.sort(createdTimeSort); - while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); } - }); - } - - //console.log('Uploading...'); - if (func) { func('Uploading to Google Drive...'); } - - // Upload the backup - drive.files.create({ - requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] }, - media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) }, - }, function (err, res) { - if (err) { - console.log('GoogleDrive (files.create) error: ' + err); - if (func) { func('GoogleDrive (files.create) error: ' + err); } - return; - } - //console.log('Upload done.'); - if (func) { func('Google Drive upload completed.'); } - }); - } + // WebDAV Backup + if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) { + const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; } // Fetch the folder name - var folderName = 'MeshCentral-Backups'; - if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; } + var webdavfolderName = 'MeshCentral-Backups'; + if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; } - // Find our backup folder, create one if needed. - drive.files.list({ - q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false', - fields: 'nextPageToken, files(id, name)', - }, function (err, res) { - if (err) { - console.log('GoogleDrive error: ' + err); - if (func) { func('GoogleDrive error: ' + err); } - return; + // Clean up our WebDAV folder + function performWebDavCleanup(client) { + if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) { + var directoryItems = client.getDirectoryContents(webdavfolderName); + directoryItems.then( + function (files) { + for (var i in files) { files[i].xdate = new Date(files[i].lastmod); } + files.sort(xdateTimeSort); + while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) { + client.deleteFile(files.shift().filename).then(function (state) { + if (func) { func('WebDAV file deleted.'); } + }).catch(function (err) { + if (func) { func('WebDAV (deleteFile) error: ' + err); } + }); + } + } + ).catch(function (err) { + if (func) { func('WebDAV (getDirectoryContents) error: ' + err); } + }); } - if (res.data.files.length == 0) { - // Create a folder - drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) { + } + + // Upload to the WebDAV folder + function performWebDavUpload(client, filepath) { + var fileStream = require('fs').createReadStream(filepath); + fileStream.on('close', function () { if (func) { func('WebDAV upload completed'); } }) + fileStream.on('error', function (err) { if (func) { func('WebDAV (fileUpload) error: ' + err); } }) + fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath))); + if (func) { func('Uploading using WebDAV...'); } + } + + if (func) { func('Attempting WebDAV upload...'); } + const { createClient } = require('webdav'); + const client = createClient(parent.config.settings.autobackup.webdav.url, { username: parent.config.settings.autobackup.webdav.username, password: parent.config.settings.autobackup.webdav.password }); + var directoryItems = client.getDirectoryContents('/'); + directoryItems.then( + function (files) { + var folderFound = false; + for (var i in files) { if ((files[i].basename == webdavfolderName) && (files[i].type == 'directory')) { folderFound = true; } } + if (folderFound == false) { + client.createDirectory(webdavfolderName).then(function (a) { + if (a.statusText == 'Created') { + if (func) { func('WebDAV folder created'); } + performWebDavUpload(client, filename); + } else { + if (func) { func('WebDAV (createDirectory) status: ' + a.statusText); } + } + }).catch(function (err) { + if (func) { func('WebDAV (createDirectory) error: ' + err); } + }); + } else { + performWebDavCleanup(client); + performWebDavUpload(client, filename); + } + } + ).catch(function (err) { + if (func) { func('WebDAV (getDirectoryContents) error: ' + err); } + }); + } + + // Google Drive Backup + if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.googledrive == 'object')) { + obj.Get('GoogleDriveBackup', function (err, docs) { + if ((err != null) || (docs.length != 1) || (docs[0].state != 3)) return; + if (func) { func('Attempting Google Drive upload...'); } + const {google} = require('googleapis'); + const oAuth2Client = new google.auth.OAuth2(docs[0].clientid, docs[0].clientsecret, "urn:ietf:wg:oauth:2.0:oob"); + 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 + oAuth2Client.setCredentials(docs[0].token); + const drive = google.drive({ version: 'v3', auth: oAuth2Client }); + const createdTimeSort = function (a, b) { if (a.createdTime > b.createdTime) return 1; if (a.createdTime < b.createdTime) return -1; return 0; } + + // Called once we know our folder id, clean up and upload a backup. + var useGoogleDrive = function (folderid) { + // List files to see if we need to delete older ones + if (typeof parent.config.settings.autobackup.googledrive.maxfiles == 'number') { + drive.files.list({ + q: 'trashed = false and \'' + folderid + '\' in parents', + fields: 'nextPageToken, files(id, name, size, createdTime)', + }, function (err, res) { + if (err) { + console.log('GoogleDrive (files.list) error: ' + err); + if (func) { func('GoogleDrive (files.list) error: ' + err); } + return; + } + // Delete any old files if more than 10 files are present in the backup folder. + res.data.files.sort(createdTimeSort); + while (res.data.files.length >= parent.config.settings.autobackup.googledrive.maxfiles) { drive.files.delete({ fileId: res.data.files.shift().id }, function (err, res) { }); } + }); + } + + //console.log('Uploading...'); + if (func) { func('Uploading to Google Drive...'); } + + // Upload the backup + drive.files.create({ + requestBody: { name: require('path').basename(filename), mimeType: 'text/plain', parents: [folderid] }, + media: { mimeType: 'application/zip', body: require('fs').createReadStream(filename) }, + }, function (err, res) { if (err) { - console.log('GoogleDrive (folder.create) error: ' + err); - if (func) { func('GoogleDrive (folder.create) error: ' + err); } + console.log('GoogleDrive (files.create) error: ' + err); + if (func) { func('GoogleDrive (files.create) error: ' + err); } return; } - useGoogleDrive(file.data.id); + //console.log('Upload done.'); + if (func) { func('Google Drive upload completed.'); } }); - } else { useGoogleDrive(res.data.files[0].id); } + } + + // Fetch the folder name + var folderName = 'MeshCentral-Backups'; + if (typeof parent.config.settings.autobackup.googledrive.foldername == 'string') { folderName = parent.config.settings.autobackup.googledrive.foldername; } + + // Find our backup folder, create one if needed. + drive.files.list({ + q: 'mimeType = \'application/vnd.google-apps.folder\' and name=\'' + folderName + '\' and trashed = false', + fields: 'nextPageToken, files(id, name)', + }, function (err, res) { + if (err) { + console.log('GoogleDrive error: ' + err); + if (func) { func('GoogleDrive error: ' + err); } + return; + } + if (res.data.files.length == 0) { + // Create a folder + drive.files.create({ resource: { 'name': folderName, 'mimeType': 'application/vnd.google-apps.folder' }, fields: 'id' }, function (err, file) { + if (err) { + console.log('GoogleDrive (folder.create) error: ' + err); + if (func) { func('GoogleDrive (folder.create) error: ' + err); } + return; + } + useGoogleDrive(file.data.id); + }); + } else { useGoogleDrive(res.data.files[0].id); } + }); }); - }); + } } function padNumber(number, digits) { return Array(Math.max(digits - String(number).length + 1, 0)).join(0) + number; } diff --git a/meshcentral-config-schema.json b/meshcentral-config-schema.json index 228fe0e8..dd3458d0 100644 --- a/meshcentral-config-schema.json +++ b/meshcentral-config-schema.json @@ -115,6 +115,17 @@ "folderName": { "type": "integer", "default": "MeshCentral-Backups", "description": "The name of the folder to create in the Google Drive account." }, "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." } } + }, + "webDAV": { + "type": "object", + "description": "Enabled automated upload of the server backups to a WebDAV account.", + "properties": { + "url": { "type": "string", "description": "WebDAV account URL." }, + "username": { "type": "string", "description": "WebDAV account username." }, + "password": { "type": "string", "description": "WebDAV account password." }, + "folderName": { "type": "integer", "default": "MeshCentral-Backups", "description": "The name of the folder to create in the WebDAV account." }, + "maxFiles": { "type": "string", "default": null, "description": "The maximum number of files to keep in the WebDAV folder, older files will be removed if needed." } + } } } }, diff --git a/meshcentral.js b/meshcentral.js index 4de53b52..ec994c65 100644 --- a/meshcentral.js +++ b/meshcentral.js @@ -2725,7 +2725,24 @@ function mainStart() { // Setup encrypted zip support if needed if (config.settings.autobackup && config.settings.autobackup.zippassword) { modules.push('archiver-zip-encrypted'); - if (typeof config.settings.autobackup.googledrive == 'object') { if (nodeVersion >= 8) { modules.push('googleapis'); } else { delete config.settings.autobackup.googledrive; } } // Enable Google Drive Support + // Enable Google Drive Support + if (typeof config.settings.autobackup.googledrive == 'object') { + if (nodeVersion >= 8) { + modules.push('googleapis'); + } else { + addServerWarning("Google Drive requires Node v8.x or higher.", !args.launch); + delete config.settings.autobackup.googledrive; + } + } + // Enable WebDAV Support + if (typeof config.settings.autobackup.webdav == 'object') { + if (nodeVersion >= 10) { + 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'); } + } else { + addServerWarning("WebDAV requires Node v10.x or higher.", !args.launch); + delete config.settings.autobackup.webdav; + } + } } // Setup common password blocking diff --git a/package.json b/package.json index 19ae4c2d..7aee7f1e 100644 --- a/package.json +++ b/package.json @@ -36,7 +36,6 @@ "sample-config-advanced.json" ], "dependencies": { - "archiver": "^4.0.2", "body-parser": "^1.19.0", "cbor": "^4.1.5", "compression": "^1.7.4", diff --git a/sample-config-advanced.json b/sample-config-advanced.json index ab0c660a..f2fc37f2 100644 --- a/sample-config-advanced.json +++ b/sample-config-advanced.json @@ -92,6 +92,13 @@ "_googleDrive": { "folderName": "MeshCentral-Backups", "maxFiles": 10 + }, + "webdav": { + "url": "https://server/remote.php/dav/files/xxxxx@server.com/", + "username": "user", + "password": "pass", + "folderName": "MeshCentral-Backups", + "maxFiles": 10 } }, "_redirects": {