backup site: add contents

it is a json that can be one of the three:

* null - include everything
* include - only include these ids
* exclude - everything except these ids
This commit is contained in:
Girish Ramakrishnan
2025-09-22 13:27:26 +02:00
parent 0d5c1b99df
commit 4c3a8e1fd7
8 changed files with 146 additions and 20 deletions

View File

@@ -12,9 +12,10 @@ exports = module.exports = {
setLimits,
setSchedule,
setRetention,
setPrimary,
setEncryption,
setPrimary,
setName,
setContents,
removePrivateFields,
@@ -36,7 +37,6 @@ exports = module.exports = {
const assert = require('node:assert'),
backupFormats = require('./backupformats.js'),
backups = require('./backups.js'),
BoxError = require('./boxerror.js'),
constants = require('./constants.js'),
cron = require('./cron.js'),
@@ -62,7 +62,7 @@ const assert = require('node:assert'),
// filesystem - backupDir, noHardlinks
// mountpoint - mountPoint, prefix, noHardlinks
// encryption: 'encryptionPassword' and 'encryptedFilenames' is converted into an 'encryption' object using hush.js. Password is lost forever after conversion.
const BACKUP_TARGET_FIELDS = [ 'id', 'name', 'provider', 'configJson', 'limitsJson', 'retentionJson', 'schedule', 'encryptionJson', 'format', 'main', 'creationTime', 'ts', 'integrityKeyPairJson' ].join(',');
const BACKUP_TARGET_FIELDS = [ 'id', 'name', 'provider', 'configJson', 'limitsJson', 'retentionJson', 'schedule', 'encryptionJson', 'format', 'main', 'contentsJson', 'creationTime', 'ts', 'integrityKeyPairJson' ].join(',');
function storageApi(backupSite) {
assert.strictEqual(typeof backupSite, 'object');
@@ -119,6 +119,9 @@ function postProcess(result) {
result.primary = !!result.main; // primary is a reserved keyword in mysql
delete result.main;
result.contents = safe.JSON.parse(result.contentsJson) || null;
delete result.contentsJson;
return result;
}
@@ -143,6 +146,22 @@ function validateName(name) {
if (name.length > 100) return new BoxError(BoxError.BAD_FIELD, 'name too long');
}
function validateContents(contents) {
assert.strictEqual(typeof contents, 'object');
if (contents === null) return null;
if ('exclude' in contents) {
if (!Array.isArray(contents.exclude)) return new BoxError(BoxError.BAD_FIELD, 'exclude should be an array of strings');
if (!contents.exclude.every(item => typeof item === 'string')) return new BoxError(BoxError.BAD_FIELD, 'exclude should be an array of strings');
} else if ('include' in contents) {
if (!Array.isArray(contents.include)) return new BoxError(BoxError.BAD_FIELD, 'include should be an array of strings');
if (!contents.include.every(item => typeof item === 'string')) return new BoxError(BoxError.BAD_FIELD, 'include should be an array of strings');
}
return null;
}
function validateSchedule(schedule) {
assert.strictEqual(typeof schedule, 'string');
@@ -207,7 +226,7 @@ async function update(site, data) {
if (k === 'name' || k === 'schedule' || k === 'main') { // format, provider cannot be updated
fields.push(k + ' = ?');
args.push(data[k]);
} else if (k === 'config' || k === 'limits' || k === 'retention') { // encryption cannot be updated
} else if (k === 'config' || k === 'limits' || k === 'retention' || k === 'contents') { // encryption cannot be updated
fields.push(`${k}JSON = ?`);
args.push(JSON.stringify(data[k]));
}
@@ -308,6 +327,18 @@ async function setName(backupSite, name, auditSource) {
await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupSite, name });
}
async function setContents(backupSite, contents, auditSource) {
assert.strictEqual(typeof backupSite, 'object');
assert.strictEqual(typeof contents, 'object');
assert.strictEqual(typeof auditSource, 'object');
const contentsError = validateContents(contents);
if (contentsError) throw contentsError;
await update(backupSite, { contents });
await eventlog.add(eventlog.ACTION_BACKUP_TARGET_UPDATE, auditSource, { backupSite, contents });
}
async function del(backupSite, auditSource) {
assert.strictEqual(typeof backupSite, 'object');
assert.strictEqual(typeof auditSource, 'object');
@@ -326,7 +357,7 @@ async function del(backupSite, auditSource) {
if (error && error.code === 'ER_NO_REFERENCED_ROW_2') throw new BoxError(BoxError.NOT_FOUND, error);
if (error) throw error;
if (result[2].affectedRows !== 1) throw new BoxError(BoxError.NOT_FOUND, 'Target not found');
await eventlog.add(eventlog.ACTION_BACKUP_TARGET_REMOVE, auditSource, { backupSite: backupSite });
await eventlog.add(eventlog.ACTION_BACKUP_TARGET_REMOVE, auditSource, { backupSite });
backupSite.schedule = constants.CRON_PATTERN_NEVER;
await cron.handleBackupScheduleChanged(backupSite);
@@ -345,13 +376,12 @@ async function startBackupTask(site, auditSource) {
const taskId = await tasks.add(`${tasks.TASK_FULL_BACKUP_PREFIX}${site.id}`, [ site.id, { /* options */ } ]);
await eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId });
await eventlog.add(eventlog.ACTION_BACKUP_START, auditSource, { taskId, siteId: site });
// background
tasks.startTask(taskId, { timeout: 24 * 60 * 60 * 1000 /* 24 hours */, nice: 15, memoryLimit, oomScoreAdjust: -999 })
.then(async (backupId) => {
const backup = await backups.get(backupId);
await eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, backupId, remotePath: backup.remotePath });
.then(async (result) => { // this can be the an array or string depending on site.contents
await eventlog.add(eventlog.ACTION_BACKUP_FINISH, auditSource, { taskId, result, site });
})
.catch(async (error) => {
const timedOut = error.code === tasks.ETIMEOUT;
@@ -481,7 +511,7 @@ async function add(data, auditSource) {
if (constants.DEMO) throw new BoxError(BoxError.BAD_STATE, 'Not allowed in demo mode');
const { provider, name, config, format, retention, schedule } = data; // required
const { provider, name, config, format, contents, retention, schedule } = data; // required
const limits = data.limits || null,
encryptionPassword = data.encryptionPassword || null,
encryptedFilenames = data.encryptedFilenames || false,
@@ -493,6 +523,9 @@ async function add(data, auditSource) {
const nameError = validateName(name);
if (nameError) throw nameError;
const contentsError = validateContents(contents);
if (contentsError) throw contentsError;
let encryption = null;
if (encryptionPassword) {
const encryptionPasswordError = validateEncryptionPassword(encryptionPassword);
@@ -513,13 +546,13 @@ async function add(data, auditSource) {
debug('add: validating new storage configuration');
const sanitizedConfig = await storageApi({ provider }).verifyConfig({id, provider, config });
await database.query('INSERT INTO backupSites (id, name, provider, configJson, limitsJson, integrityKeyPairJson, retentionJson, schedule, encryptionJson, format, main) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, name, provider, JSON.stringify(sanitizedConfig), JSON.stringify(limits), JSON.stringify(integrityKeyPair), JSON.stringify(retention), schedule, JSON.stringify(encryption), format, false ]);
await database.query('INSERT INTO backupSites (id, name, provider, configJson, contentsJson, limitsJson, integrityKeyPairJson, retentionJson, schedule, encryptionJson, format) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)',
[ id, name, provider, JSON.stringify(sanitizedConfig), JSON.stringify(contents), JSON.stringify(limits), JSON.stringify(integrityKeyPair), JSON.stringify(retention), schedule, JSON.stringify(encryption), format ]);
debug('add: setting up new storage configuration');
await storageApi({ provider }).setup(sanitizedConfig);
await eventlog.add(eventlog.ACTION_BACKUP_TARGET_ADD, auditSource, { id, name, provider, config, schedule, format });
await eventlog.add(eventlog.ACTION_BACKUP_TARGET_ADD, auditSource, { id, name, provider, config, contents, schedule, format });
return id;
}
@@ -534,10 +567,10 @@ async function addDefault(auditSource) {
config: { backupDir: paths.DEFAULT_BACKUP_DIR },
retention: { keepWithinSecs: 2 * 24 * 60 * 60 },
schedule: '00 00 23 * * *',
format: 'tgz'
format: 'tgz',
contents: null
};
defaultBackupSite.id = await add(defaultBackupSite, auditSource);
await setPrimary(defaultBackupSite, auditSource);
return await add(defaultBackupSite, auditSource);
}
// creates a backup site object that is not in the database