Files
dashy/services/save-config.js
Alicia Sykes c43712550a Adds option to disable config backups on save
By default, everything functions the same. But if the `DISABLE_CONFIG_BACKUPS` env var is set, then when the user saves the config to disk (via UI, calling the save-config endpoint), then the previous config will not get coppied/backedup to the BACKUP_DIR (user-data/config-backups) anymore. Also added docs for this change. This option might be useful on file systems with restricted permissions (if /app/user-data/config-backups doesn't have create/write access), or when the user just doesn't want a ton of backup files for every single litttle config change.
2026-04-29 07:51:10 +01:00

79 lines
2.8 KiB
JavaScript

/**
* This file exports a function, used by the write config endpoint.
* It will make a backup of the users conf.yml file
* and then write their new config into the main conf.yml file.
* Finally, it will call a function with the status message
*/
const fsPromises = require('fs').promises;
const path = require('path');
const MAX_CONFIG_BYTES = 256 * 1024;
// Disallow paths having path separators, control chars (NUL/CR/LF), or ..
const SAFE_FILENAME = /^(?!\.+$)[^\\/\0\r\n]+\.ya?ml$/i;
module.exports = async (newConfig, render) => {
const respond = (success, message) => render(JSON.stringify({ success, message }));
// Validate request body
if (!newConfig || typeof newConfig.config !== 'string' || newConfig.config.length === 0) {
respond(false, "Request body is missing or has an invalid 'config' field");
return;
}
if (newConfig.config.length > MAX_CONFIG_BYTES) {
respond(false, `Config exceeds maximum size of ${MAX_CONFIG_BYTES / 1024} KB`);
return;
}
// If `filename` (for sub-pages) is specified validate and set it
let usersFileName;
if (typeof newConfig.filename === 'string' && newConfig.filename) {
const base = path.basename(newConfig.filename);
if (!SAFE_FILENAME.test(base)) {
respond(false, 'Invalid filename: must be a basename ending in .yml or .yaml');
return;
}
usersFileName = base;
}
// Resolve paths
const userDataDirectory = process.env.USER_DATA_DIR || './user-data/';
const backupLocation = process.env.BACKUP_DIR || path.join(userDataDirectory, 'config-backups');
const backupsEnabled = process.env.DISABLE_CONFIG_BACKUPS !== 'true';
const targetFile = usersFileName || 'conf.yml';
const targetFilePath = path.join(userDataDirectory, targetFile);
const backupBase = targetFile.replace(/\.ya?ml$/i, '');
const backupFilePath = path.join(backupLocation, `${backupBase}-${Date.now()}.backup.yml`);
// Backup current config before proceeding (unless disabled via DISABLE_CONFIG_BACKUPS)
let backedUp = false;
if (backupsEnabled) {
try {
await fsPromises.mkdir(backupLocation, { recursive: true });
await fsPromises.copyFile(targetFilePath, backupFilePath);
backedUp = true;
} catch (error) {
if (error.code !== 'ENOENT') {
respond(false, `Unable to backup ${targetFile}: ${error}`);
return;
}
}
}
// Write the new config
try {
await fsPromises.writeFile(targetFilePath, newConfig.config, { encoding: 'utf8' });
} catch (error) {
respond(false, `Unable to write to ${targetFile}: ${error}`);
return;
}
// If successful, then render hasn't yet been called- call it
let responseMsg = `Config saved successfully in ${targetFilePath}.`;
if (backedUp) {
responseMsg += ` Previous ${targetFile} was backed up to ${backupFilePath}.`;
}
respond(true, responseMsg);
};