WebDAV: update to v5 (#6780)

* Convert to async-await * use require instead of import * Use pipeline instead of pipe * Revert "use require instead of import" This reverts commit b5635e89cc71e47e0b7a078bce223321dc519e51. * Sanitize webdav foldername, move to setup defaults * Check for webdav config

PTR committed Jun 9, 2025 at 19:38 UTC ec3e06e37f81ab8337175d364802487d499d2530
2 files changed +46 -74
db.js
+39 -73
@@ -3780,84 +3780,50 @@ module.exports.CreateDB = function (parent, func) {
3780 }
3781 }
3782
3783 - // Perform cloud backup
3784 - obj.performCloudBackup = function (filename, func) {
3785 - // WebDAV Backup
3786 - if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
3787 - parent.debug( 'backup', 'Entering WebDAV backup');
3788 - if (func) { func('Entering WebDAV backup.'); }
3789 -
3790 - const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
3791 - // Fetch the folder name
3792 - var webdavfolderName = 'MeshCentral-Backups';
3793 - if (typeof parent.config.settings.autobackup.webdav.foldername == 'string') { webdavfolderName = parent.config.settings.autobackup.webdav.foldername; }
3794 -
3795 - // Clean up our WebDAV folder
3796 - function performWebDavCleanup(client) {
3797 - if ((typeof parent.config.settings.autobackup.webdav.maxfiles == 'number') && (parent.config.settings.autobackup.webdav.maxfiles > 1)) {
3798 - let fileName = parent.config.settings.autobackup.backupname;
3783 + async function webDAVBackup(filename, func) {
3784 + try {
3785 + const webDAV = await import ('webdav');
3786 + const wdConfig = parent.config.settings.autobackup.webdav;
3787 + const client = webDAV.createClient(wdConfig.url, {
3788 + username: wdConfig.username,
3789 + password: wdConfig.password,
3790 + maxContentLength: Infinity,
3791 + maxBodyLength: Infinity
3792 + });
3793 + if (await client.exists(wdConfig.foldername) === false) {
3794 + await client.createDirectory(wdConfig.foldername, { recursive: true});
3795 + } else {
3796 + // Clean up our WebDAV folder
3797 + if ((typeof wdConfig.maxfiles == 'number') && (wdConfig.maxfiles > 1)) {
3798 + const fileName = parent.config.settings.autobackup.backupname;
3799 //only files matching our backupfilename
3800 - let directoryItems = client.getDirectoryContents(webdavfolderName, { deep: false, glob: "/**/" + fileName + "*.zip" });
3801 - directoryItems.then(
3802 - function (files) {
3803 - for (var i in files) { files[i].xdate = new Date(files[i].lastmod); }
3804 - files.sort(xdateTimeSort);
3805 - parent.debug('backup','WebDAV filtered directory contents: ' + JSON.stringify(files, null, 4));
3806 - while (files.length >= parent.config.settings.autobackup.webdav.maxfiles) {
3807 - let delFile = files.shift().filename;
3808 - client.deleteFile(delFile).then(function (state) {
3809 - parent.debug('backup','WebDAV file deleted: ' + delFile);
3810 - if (func) { func('WebDAV file deleted: ' + delFile); }
3811 - }).catch(function (err) {
3812 - console.error(err);
3813 - if (func) { func('WebDAV (deleteFile) error: ' + err.message); }
3814 - });
3815 - }
3816 - }
3817 - ).catch(function (err) {
3818 - console.error(err);
3819 - if (func) { func('WebDAV (getDirectoryContents) error: ' + err.message); }
3820 - });
3800 + let files = await client.getDirectoryContents(wdConfig.foldername, { deep: false, glob: "/**/" + fileName + "*.zip" });
3801 + const xdateTimeSort = function (a, b) { if (a.xdate > b.xdate) return 1; if (a.xdate < b.xdate) return -1; return 0; }
3802 + for (const i in files) { files[i].xdate = new Date(files[i].lastmod); }
3803 + files.sort(xdateTimeSort);
3804 + while (files.length >= wdConfig.maxfiles) {
3805 + let delFile = files.shift().filename;
3806 + await client.deleteFile(delFile);
3807 + console.log('WebDAV file deleted: ' + delFile); if (func) { func('WebDAV file deleted: ' + delFile); }
3808 + }
3809 }
3810 }
3823 -
3811 // Upload to the WebDAV folder
3825 - function performWebDavUpload(client, filepath) {
3826 - require('fs').stat(filepath, function(err,stat){
3827 - var fileStream = require('fs').createReadStream(filepath);
3828 - fileStream.on('close', function () { console.log('WebDAV upload completed: ' + webdavfolderName + '/' + require('path').basename(filepath)); if (func) { func('WebDAV upload completed: ' + webdavfolderName + '/' + require('path').basename(filepath)); } })
3829 - fileStream.on('error', function (err) { console.error(err); if (func) { func('WebDAV (fileUpload) error: ' + err.message); } })
3830 - fileStream.pipe(client.createWriteStream('/' + webdavfolderName + '/' + require('path').basename(filepath), { headers: { "Content-Length": stat.size } }));
3831 - parent.debug('backup', 'Uploading using WebDAV to: ' + parent.config.settings.autobackup.webdav.url);
3832 - if (func) { func('Uploading using WebDAV to: ' + parent.config.settings.autobackup.webdav.url); }
3833 - });
3834 - }
3812 + const { pipeline } = require('stream/promises');
3813 + await pipeline(fs.createReadStream(filename), client.createWriteStream( wdConfig.foldername + path.basename(filename)));
3814 + console.log('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); if (func) { func('WebDAV upload completed: ' + wdConfig.foldername + path.basename(filename)); }
3815 + }
3816 + catch(err) {
3817 + console.error('WebDAV error: ' + err.message); if (func) { func('WebDAV error: ' + err.message);}
3818 + }
3819 + }
3820
3836 - const { createClient } = require('webdav');
3837 - const client = createClient(parent.config.settings.autobackup.webdav.url, {
3838 - username: parent.config.settings.autobackup.webdav.username,
3839 - password: parent.config.settings.autobackup.webdav.password,
3840 - maxContentLength: Infinity,
3841 - maxBodyLength: Infinity
3842 - });
3843 - client.exists(webdavfolderName).then(function(a){
3844 - if(a){
3845 - performWebDavCleanup(client);
3846 - performWebDavUpload(client, filename);
3847 - }else{
3848 - client.createDirectory(webdavfolderName, {recursive: true}).then(function (a) {
3849 - console.log('backup','WebDAV folder created: ' + webdavfolderName);
3850 - if (func) { func('WebDAV folder created: ' + webdavfolderName); }
3851 - performWebDavUpload(client, filename);
3852 - }).catch(function (err) {
3853 - console.error(err);
3854 - if (func) { func('WebDAV (createDirectory) error: ' + err.message); }
3855 - });
3856 - }
3857 - }).catch(function (err) {
3858 - console.error(err);
3859 - if (func) { func('WebDAV (exists) error: ' + err.message); }
3860 - });
3821 + // Perform cloud backup
3822 + obj.performCloudBackup = function (filename, func) {
3823 + // WebDAV Backup
3824 + if ((typeof parent.config.settings.autobackup == 'object') && (typeof parent.config.settings.autobackup.webdav == 'object')) {
3825 + parent.debug( 'backup', 'Entering WebDAV backup'); if (func) { func('Entering WebDAV backup.'); }
3826 + webDAVBackup(filename, func);
3827 }
3828
3829 // Google Drive Backup
meshcentral.js
+7 -1
@@ -2123,6 +2123,12 @@ function CreateMeshCentralServer(config, args) {
2123 else if (typeof obj.config.settings.autobackup.backupskipfoldersglob == 'string') { obj.config.settings.autobackup.backupskipfoldersglob = obj.config.settings.autobackup.backupskipfoldersglob.replaceAll(', ', ',').split(','); };
2124 if (typeof obj.config.settings.autobackup.backuppath == 'string') { obj.backuppath = (obj.config.settings.autobackup.backuppath = (obj.path.resolve(obj.config.settings.autobackup.backuppath))) } else { obj.config.settings.autobackup.backuppath = obj.backuppath };
2125 if (typeof obj.config.settings.autobackup.backupname != 'string') { obj.config.settings.autobackup.backupname = 'meshcentral-autobackup-'};
2126 + if (typeof obj.config.settings.autobackup.webdav == 'object') {
2127 + //make webdav compliant: http://www.webdav.org/specs/rfc4918.html#rfc.section.5.2, http://www.webdav.org/specs/rfc2518.html#METHOD_MKCOL
2128 + // So with leading and trailing slash in the foldername, and no double and backslashes
2129 + if (typeof obj.config.settings.autobackup.webdav.foldername != 'string') {obj.config.settings.autobackup.webdav.foldername = '/MeshCentral-Backups/'}
2130 + else {obj.config.settings.autobackup.webdav.foldername = ('/' + obj.config.settings.autobackup.webdav.foldername + '/').replaceAll("\\", "/").replaceAll("//", "/").replaceAll("//", "/")};
2131 + }
2132 }
2133
2134 // Check if the database is capable of performing a backup
@@ -4321,7 +4327,7 @@ function mainStart() {
4327 if (typeof config.settings.autobackup.googledrive == 'object') { modules.push('googleapis@128.0.0'); }
4328 // Enable WebDAV Support
4329 if (typeof config.settings.autobackup.webdav == 'object') {
4324 - 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'); }
4330 + 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@5.8.0'); }
4331 }
4332 // Enable S3 Support
4333 if (typeof config.settings.autobackup.s3 == 'object') { modules.push('minio@8.0.2'); }