2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2025-07-24 19:02:02 +02:00
get ,
list ,
2025-09-22 17:59:26 +02:00
listByContentForUpdates ,
2025-07-24 19:02:02 +02:00
add ,
2025-08-06 10:51:46 +02:00
addDefault ,
2025-07-24 19:02:02 +02:00
del ,
2016-04-10 19:17:44 -07:00
2025-07-24 19:02:02 +02:00
setConfig ,
setLimits ,
setSchedule ,
setRetention ,
2025-08-05 11:26:02 +02:00
setEncryption ,
2025-09-22 17:59:26 +02:00
setEnabledForUpdates ,
2025-08-04 14:19:49 +02:00
setName ,
2025-09-22 13:27:26 +02:00
setContents ,
2017-09-20 09:57:16 -07:00
2021-01-21 11:31:35 -08:00
removePrivateFields ,
2019-02-09 18:08:10 -08:00
2025-07-24 19:02:02 +02:00
startBackupTask ,
startCleanupTask ,
2021-07-14 11:07:19 -07:00
getSnapshotInfo ,
setSnapshotInfo ,
2025-09-22 17:59:26 +02:00
hasContent ,
2021-10-11 17:45:35 +02:00
remount ,
2025-08-04 10:47:00 +02:00
getStatus ,
2024-09-09 17:39:17 +02:00
ensureMounted ,
2025-08-01 14:54:32 +02:00
2025-08-01 20:49:11 +02:00
storageApi ,
2025-08-01 23:20:51 +02:00
createPseudo ,
2025-10-06 16:39:01 +02:00
reinitAll
2015-07-20 00:09:47 -07:00
} ;
2025-08-14 11:17:38 +05:30
const assert = require ( 'node:assert' ) ,
2025-08-15 16:09:58 +05:30
backupFormats = require ( './backupformats.js' ) ,
2019-10-22 20:36:20 -07:00
BoxError = require ( './boxerror.js' ) ,
2019-07-25 14:40:52 -07:00
constants = require ( './constants.js' ) ,
2023-08-04 11:24:28 +05:30
cron = require ( './cron.js' ) ,
2024-04-19 18:19:41 +02:00
{ CronTime } = require ( 'cron' ) ,
2025-08-14 11:17:38 +05:30
crypto = require ( 'node:crypto' ) ,
2017-11-22 10:58:07 -08:00
database = require ( './database.js' ) ,
2021-09-10 12:10:10 -07:00
debug = require ( 'debug' ) ( 'box:backups' ) ,
2018-12-09 03:20:00 -08:00
eventlog = require ( './eventlog.js' ) ,
2025-07-24 19:02:02 +02:00
hush = require ( './hush.js' ) ,
2024-12-07 14:35:45 +01:00
locks = require ( './locks.js' ) ,
2025-08-14 11:17:38 +05:30
path = require ( 'node:path' ) ,
2016-04-10 19:17:44 -07:00
paths = require ( './paths.js' ) ,
2017-04-21 14:07:10 -07:00
safe = require ( 'safetydance' ) ,
2025-07-28 12:53:27 +02:00
tasks = require ( './tasks.js' ) ;
2025-07-24 19:02:02 +02:00
2025-08-01 14:54:32 +02:00
// format: rsync or tgz
// provider: used to determine the api provider
// config: depends on the 'provider' field. 'provider' is not stored in config object. but it is injected when calling the api backends
// s3 providers - accessKeyId, secretAccessKey, bucket, prefix etc . see s3.js
// gcs - bucket, prefix, projectId, credentials . see gcs.js
// ext4/xfs/disk (managed providers) - mountOptions (diskPath), prefix, noHardlinks. disk is legacy.
// nfs/cifs/sshfs (managed providers) - mountOptions (host/username/password/seal/privateKey etc), prefix, noHardlinks
2025-08-02 10:37:37 +02:00
// filesystem - backupDir, noHardlinks
2025-08-01 14:54:32 +02:00
// mountpoint - mountPoint, prefix, noHardlinks
// encryption: 'encryptionPassword' and 'encryptedFilenames' is converted into an 'encryption' object using hush.js. Password is lost forever after conversion.
2025-09-23 16:42:57 +02:00
const BACKUP _TARGET _FIELDS = [ 'id' , 'name' , 'provider' , 'configJson' , 'limitsJson' , 'retentionJson' , 'schedule' , 'encryptionJson' , 'format' , 'enableForUpdates' , 'contentsJson' , 'creationTime' , 'ts' , 'integrityKeyPairJson' ] . join ( ',' ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
function storageApi ( backupSite ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-08-01 14:54:32 +02:00
2025-09-12 09:48:37 +02:00
switch ( backupSite . provider ) {
2025-08-01 14:54:32 +02:00
case 'nfs' : return require ( './storage/filesystem.js' ) ;
case 'cifs' : return require ( './storage/filesystem.js' ) ;
case 'sshfs' : return require ( './storage/filesystem.js' ) ;
case 'mountpoint' : return require ( './storage/filesystem.js' ) ;
case 'disk' : return require ( './storage/filesystem.js' ) ;
case 'ext4' : return require ( './storage/filesystem.js' ) ;
2025-09-23 17:30:09 +02:00
case 'xfs' : return require ( './storage/filesystem.js' ) ;
2025-08-01 14:54:32 +02:00
case 's3' : return require ( './storage/s3.js' ) ;
case 'gcs' : return require ( './storage/gcs.js' ) ;
case 'filesystem' : return require ( './storage/filesystem.js' ) ;
case 'minio' : return require ( './storage/s3.js' ) ;
case 's3-v4-compat' : return require ( './storage/s3.js' ) ;
case 'digitalocean-spaces' : return require ( './storage/s3.js' ) ;
case 'exoscale-sos' : return require ( './storage/s3.js' ) ;
case 'wasabi' : return require ( './storage/s3.js' ) ;
case 'scaleway-objectstorage' : return require ( './storage/s3.js' ) ;
case 'backblaze-b2' : return require ( './storage/s3.js' ) ;
case 'cloudflare-r2' : return require ( './storage/s3.js' ) ;
case 'linode-objectstorage' : return require ( './storage/s3.js' ) ;
case 'ovh-objectstorage' : return require ( './storage/s3.js' ) ;
case 'ionos-objectstorage' : return require ( './storage/s3.js' ) ;
case 'idrive-e2' : return require ( './storage/s3.js' ) ;
case 'vultr-objectstorage' : return require ( './storage/s3.js' ) ;
case 'upcloud-objectstorage' : return require ( './storage/s3.js' ) ;
case 'contabo-objectstorage' : return require ( './storage/s3.js' ) ;
case 'hetzner-objectstorage' : return require ( './storage/s3.js' ) ;
2026-01-06 16:34:04 +01:00
case 'synology-c2-objectstorage' : return require ( './storage/s3.js' ) ;
2025-09-12 09:48:37 +02:00
default : throw new BoxError ( BoxError . BAD _FIELD , ` Unknown provider: ${ backupSite . provider } ` ) ;
2025-08-01 14:54:32 +02:00
}
}
2021-07-14 11:07:19 -07:00
function postProcess ( result ) {
assert . strictEqual ( typeof result , 'object' ) ;
2016-04-10 19:17:44 -07:00
2025-07-24 13:19:27 +02:00
result . config = result . configJson ? safe . JSON . parse ( result . configJson ) : { } ;
delete result . configJson ;
2025-07-28 11:01:46 +02:00
result . limits = safe . JSON . parse ( result . limitsJson ) || { } ;
2025-07-24 13:19:27 +02:00
delete result . limitsJson ;
2025-07-28 11:01:46 +02:00
result . retention = safe . JSON . parse ( result . retentionJson ) || { } ;
2025-07-24 13:19:27 +02:00
delete result . retentionJson ;
result . encryption = result . encryptionJson ? safe . JSON . parse ( result . encryptionJson ) : null ;
delete result . encryptionJson ;
2025-08-11 19:30:22 +05:30
result . integrityKeyPair = result . integrityKeyPairJson ? safe . JSON . parse ( result . integrityKeyPairJson ) : null ;
delete result . integrityKeyPairJson ;
2025-09-23 16:42:57 +02:00
result . enableForUpdates = ! ! result . enableForUpdates ;
2025-07-24 13:19:27 +02:00
2025-09-22 13:27:26 +02:00
result . contents = safe . JSON . parse ( result . contentsJson ) || null ;
delete result . contentsJson ;
2025-07-24 13:19:27 +02:00
return result ;
}
2025-09-12 09:48:37 +02:00
function removePrivateFields ( site ) {
assert . strictEqual ( typeof site , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
site . encrypted = site . encryption !== null ;
site . encryptedFilenames = site . encryption ? . encryptedFilenames || false ;
site . encryptionPasswordHint = site . encryption ? . encryptionPasswordHint || null ;
delete site . encryption ;
2025-07-28 11:45:10 +02:00
2025-09-12 09:48:37 +02:00
delete site . integrityKeyPair . privateKey ;
2025-08-11 19:30:22 +05:30
2025-09-12 09:48:37 +02:00
site . config = storageApi ( site ) . removePrivateFields ( site . config ) ;
return site ;
2020-05-12 15:49:43 -07:00
}
2025-08-04 14:19:49 +02:00
function validateName ( name ) {
assert . strictEqual ( typeof name , 'string' ) ;
2023-07-12 10:01:53 +05:30
2025-08-04 15:00:21 +02:00
if ( name . length === 0 ) return new BoxError ( BoxError . BAD _FIELD , 'name cannot be empty' ) ;
2025-08-26 07:40:58 +02:00
if ( name . length > 100 ) return new BoxError ( BoxError . BAD _FIELD , 'name too long' ) ;
2025-07-24 19:02:02 +02:00
}
2025-09-22 13:27:26 +02:00
function validateContents ( contents ) {
assert . strictEqual ( typeof contents , 'object' ) ;
2025-09-30 19:23:44 +02:00
// if you change the structure of contents, look into app.js:del as well
2025-09-22 13:27:26 +02:00
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 ;
}
2025-07-24 19:02:02 +02:00
function validateSchedule ( schedule ) {
assert . strictEqual ( typeof schedule , 'string' ) ;
if ( schedule === constants . CRON _PATTERN _NEVER ) return null ;
const job = safe . safeCall ( function ( ) { return new CronTime ( schedule ) ; } ) ;
2023-07-12 10:01:53 +05:30
if ( ! job ) return new BoxError ( BoxError . BAD _FIELD , 'Invalid schedule pattern' ) ;
2025-07-24 19:02:02 +02:00
return null ;
}
function validateRetention ( retention ) {
assert . strictEqual ( typeof retention , 'object' ) ;
2023-07-12 10:01:53 +05:30
if ( ! retention ) return new BoxError ( BoxError . BAD _FIELD , 'retention is required' ) ;
if ( ! [ 'keepWithinSecs' , 'keepDaily' , 'keepWeekly' , 'keepMonthly' , 'keepYearly' ] . find ( k => ! ! retention [ k ] ) ) return new BoxError ( BoxError . BAD _FIELD , 'retention properties missing' ) ;
if ( 'keepWithinSecs' in retention && typeof retention . keepWithinSecs !== 'number' ) return new BoxError ( BoxError . BAD _FIELD , 'retention.keepWithinSecs must be a number' ) ;
if ( 'keepDaily' in retention && typeof retention . keepDaily !== 'number' ) return new BoxError ( BoxError . BAD _FIELD , 'retention.keepDaily must be a number' ) ;
if ( 'keepWeekly' in retention && typeof retention . keepWeekly !== 'number' ) return new BoxError ( BoxError . BAD _FIELD , 'retention.keepWeekly must be a number' ) ;
if ( 'keepMonthly' in retention && typeof retention . keepMonthly !== 'number' ) return new BoxError ( BoxError . BAD _FIELD , 'retention.keepMonthly must be a number' ) ;
if ( 'keepYearly' in retention && typeof retention . keepYearly !== 'number' ) return new BoxError ( BoxError . BAD _FIELD , 'retention.keepYearly must be a number' ) ;
2025-07-24 19:02:02 +02:00
return null ;
}
function validateEncryptionPassword ( password ) {
assert . strictEqual ( typeof password , 'string' ) ;
if ( password . length < 8 ) return new BoxError ( BoxError . BAD _FIELD , 'password must be atleast 8 characters' ) ;
2023-07-12 10:01:53 +05:30
}
2025-09-29 14:44:42 +02:00
async function list ( ) {
const results = await database . query ( ` SELECT ${ BACKUP _TARGET _FIELDS } FROM backupSites ORDER BY name DESC ` , [ ] ) ;
2025-07-24 19:02:02 +02:00
results . forEach ( function ( result ) { postProcess ( result ) ; } ) ;
return results ;
}
2025-09-22 17:59:26 +02:00
function hasContent ( { contents } , id ) {
2025-09-30 19:23:44 +02:00
// if you change the structure of contents, look into app.js:del as well
2025-09-22 17:59:26 +02:00
if ( ! contents ) return true ;
if ( contents . include && ! contents . include . includes ( id ) ) return false ;
if ( contents . exclude ? . includes ( id ) ) return false ;
return true ;
2025-07-24 19:02:02 +02:00
}
2025-09-22 17:59:26 +02:00
async function listByContentForUpdates ( id ) {
assert . strictEqual ( typeof id , 'string' ) ;
2025-09-23 16:42:57 +02:00
const results = await database . query ( ` SELECT ${ BACKUP _TARGET _FIELDS } FROM backupSites WHERE enableForUpdates=? ` , [ true ] ) ;
2025-09-22 17:59:26 +02:00
results . forEach ( function ( result ) { postProcess ( result ) ; } ) ;
return results . filter ( r => hasContent ( r , id ) ) ;
}
async function get ( id ) {
const results = await database . query ( ` SELECT ${ BACKUP _TARGET _FIELDS } FROM backupSites WHERE id=? ` , [ id ] ) ;
2025-07-25 12:55:14 +02:00
if ( results . length === 0 ) return null ;
return postProcess ( results [ 0 ] ) ;
}
2025-09-12 09:48:37 +02:00
async function update ( site , data ) {
assert . strictEqual ( typeof site , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert ( data && typeof data === 'object' ) ;
const args = [ ] ;
const fields = [ ] ;
for ( const k in data ) {
2025-09-23 16:42:57 +02:00
if ( k === 'name' || k === 'schedule' || k === 'enableForUpdates' ) { // format, provider cannot be updated
2025-07-24 19:02:02 +02:00
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
2025-09-22 13:27:26 +02:00
} else if ( k === 'config' || k === 'limits' || k === 'retention' || k === 'contents' ) { // encryption cannot be updated
2025-07-24 19:02:02 +02:00
fields . push ( ` ${ k } JSON = ? ` ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
}
}
2025-09-12 09:48:37 +02:00
args . push ( site . id ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
const [ updateError , result ] = await safe ( database . query ( 'UPDATE backupSites SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , args ) ) ;
2025-07-24 19:02:02 +02:00
if ( updateError ) throw updateError ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Target not found' ) ;
}
2025-09-12 09:48:37 +02:00
async function setSchedule ( backupSite , schedule , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof schedule , 'string' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-11-13 10:09:34 +01:00
const error = validateSchedule ( schedule ) ;
2025-07-24 19:02:02 +02:00
if ( error ) throw error ;
2025-09-12 09:48:37 +02:00
await update ( backupSite , { schedule } ) ;
await cron . handleBackupScheduleChanged ( Object . assign ( { } , backupSite , { schedule } ) ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , schedule } ) ;
2025-07-24 19:02:02 +02:00
}
2025-09-12 09:48:37 +02:00
async function setLimits ( backupSite , limits , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof limits , 'object' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
await update ( backupSite , { limits } ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , limits } ) ;
2025-07-24 19:02:02 +02:00
}
2025-09-12 09:48:37 +02:00
async function setRetention ( backupSite , retention , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof retention , 'object' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-11-13 10:09:34 +01:00
const error = validateRetention ( retention ) ;
2025-07-24 19:02:02 +02:00
if ( error ) throw error ;
2025-09-12 09:48:37 +02:00
await update ( backupSite , { retention } ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , retention } ) ;
2025-07-24 19:02:02 +02:00
}
2025-09-23 16:42:57 +02:00
async function setEnabledForUpdates ( backupSite , enableForUpdates , auditSource ) {
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-09-23 16:42:57 +02:00
assert . strictEqual ( typeof enableForUpdates , 'boolean' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-25 09:43:26 +02:00
2025-09-23 16:42:57 +02:00
await update ( backupSite , { enableForUpdates } ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , enableForUpdates } ) ;
2025-07-25 09:43:26 +02:00
}
2025-09-12 09:48:37 +02:00
async function setEncryption ( backupSite , data , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-08-05 11:26:02 +02:00
assert . strictEqual ( typeof data , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
let encryption = null ;
if ( data . encryptionPassword ) {
const encryptionPasswordError = validateEncryptionPassword ( data . encryptionPassword ) ;
if ( encryptionPasswordError ) throw encryptionPasswordError ;
2025-10-07 16:49:57 +02:00
if ( data . encryptionPassword === data . encryptionPasswordHint ) throw new BoxError ( BoxError . BAD _FIELD , 'password hint cannot be the same as password' ) ;
2025-08-05 11:26:02 +02:00
encryption = hush . generateEncryptionKeysSync ( data . encryptionPassword ) ;
encryption . encryptedFilenames = ! ! data . encryptedFilenames ;
encryption . encryptionPasswordHint = data . encryptionPasswordHint || '' ;
}
const queries = [
2025-09-12 09:48:37 +02:00
{ query : 'DELETE FROM backups WHERE siteId=?' , args : [ backupSite . id ] } ,
{ query : 'UPDATE backupSites SET encryptionJson=? WHERE id=?' , args : [ encryption ? JSON . stringify ( encryption ) : null , backupSite . id ] } ,
2025-08-05 11:26:02 +02:00
] ;
const [ error , result ] = await safe ( database . transaction ( queries ) ) ;
if ( error ) throw error ;
if ( result [ 1 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Target not found' ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , encryption : ! ! encryption } ) ;
2025-08-05 11:26:02 +02:00
}
2025-09-12 09:48:37 +02:00
async function setName ( backupSite , name , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-08-04 14:19:49 +02:00
assert . strictEqual ( typeof name , 'string' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
const nameError = validateName ( name ) ;
if ( nameError ) throw nameError ;
2025-09-12 09:48:37 +02:00
await update ( backupSite , { name } ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name , oldName : backupSite . name } ) ;
2025-08-04 14:19:49 +02:00
}
2025-09-22 13:27:26 +02:00
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 } ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , contents } ) ;
2025-09-22 13:27:26 +02:00
}
2025-09-12 09:48:37 +02:00
async function del ( backupSite , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
await safe ( storageApi ( backupSite ) . teardown ( backupSite . config ) , { debug } ) ; // ignore error
2025-08-01 14:54:32 +02:00
2025-07-25 09:43:26 +02:00
const queries = [
2025-09-12 09:48:37 +02:00
{ query : 'DELETE FROM archives WHERE backupId IN (SELECT id FROM backups WHERE siteId=?)' , args : [ backupSite . id ] } ,
{ query : 'DELETE FROM backups WHERE siteId=?' , args : [ backupSite . id ] } ,
2025-09-22 20:02:13 +02:00
{ query : 'DELETE FROM backupSites WHERE id=?' , args : [ backupSite . id ] }
2025-07-25 09:43:26 +02:00
] ;
2025-07-24 19:02:02 +02:00
const [ error , result ] = await safe ( database . transaction ( queries ) ) ;
2025-09-29 11:55:15 +02:00
if ( error && error . sqlCode === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , error ) ;
2025-07-24 19:02:02 +02:00
if ( error ) throw error ;
2025-08-04 10:16:19 +02:00
if ( result [ 2 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Target not found' ) ;
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _REMOVE , auditSource , { name : backupSite . name , backupSite : removePrivateFields ( backupSite ) } ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
backupSite . schedule = constants . CRON _PATTERN _NEVER ;
await cron . handleBackupScheduleChanged ( backupSite ) ;
2025-07-25 08:36:09 +02:00
2025-09-12 09:48:37 +02:00
const infoDir = path . join ( paths . BACKUP _INFO _DIR , backupSite . id ) ;
2025-08-14 13:43:41 +05:30
safe . fs . rmSync ( infoDir , { recursive : true } ) ;
2025-07-24 19:02:02 +02:00
}
2025-09-12 09:48:37 +02:00
async function startBackupTask ( site , auditSource ) {
assert . strictEqual ( typeof site , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
const [ error ] = await safe ( locks . acquire ( ` ${ locks . TYPE _FULL _BACKUP _TASK _PREFIX } ${ site . id } ` ) ) ;
2024-12-07 14:35:45 +01:00
if ( error ) throw new BoxError ( BoxError . BAD _STATE , ` Another backup task is in progress: ${ error . message } ` ) ;
2020-05-10 21:40:25 -07:00
2025-09-12 09:48:37 +02:00
const memoryLimit = site . limits ? . memoryLimit ? Math . max ( site . limits . memoryLimit / 1024 / 1024 , 1024 ) : 1024 ;
2018-07-27 06:55:54 -07:00
2025-09-12 09:48:37 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _FULL _BACKUP _PREFIX } ${ site . id } ` , [ site . id , { /* options */ } ] ) ;
2018-07-27 06:55:54 -07:00
2025-10-09 17:24:43 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _START , auditSource , { taskId , siteId : site . id , siteName : site . name } ) ;
2020-09-30 20:03:46 -07:00
2025-06-17 18:54:12 +02:00
// background
tasks . startTask ( taskId , { timeout : 24 * 60 * 60 * 1000 /* 24 hours */ , nice : 15 , memoryLimit , oomScoreAdjust : - 999 } )
2025-09-22 13:27:26 +02:00
. then ( async ( result ) => { // this can be the an array or string depending on site.contents
2025-10-09 17:24:43 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _FINISH , auditSource , { taskId , result , siteId : site . id , siteName : site . name } ) ;
2025-06-17 18:54:12 +02:00
} )
. catch ( async ( error ) => {
const timedOut = error . code === tasks . ETIMEOUT ;
2025-10-09 17:24:43 +02:00
await safe ( eventlog . add ( eventlog . ACTION _BACKUP _FINISH , auditSource , { taskId , errorMessage : error . message , timedOut , siteId : site . id , siteName : site . name } ) ) ;
2025-06-17 18:54:12 +02:00
} )
. finally ( async ( ) => {
2025-09-12 09:48:37 +02:00
await locks . release ( ` ${ locks . TYPE _FULL _BACKUP _TASK _PREFIX } ${ site . id } ` ) ;
2025-06-17 18:54:12 +02:00
await locks . releaseByTaskId ( taskId ) ;
} ) ;
2021-09-10 12:10:10 -07:00
return taskId ;
2021-07-14 11:07:19 -07:00
}
2018-07-27 11:46:42 -07:00
2025-07-30 11:19:07 +02:00
// keeps track of contents of the snapshot directory. this provides a way to clean up backups of uninstalled apps
2025-09-12 09:48:37 +02:00
async function getSnapshotInfo ( backupSite ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2018-02-22 10:58:56 -08:00
2025-09-12 09:48:37 +02:00
const snapshotFilePath = path . join ( paths . BACKUP _INFO _DIR , backupSite . id , constants . SNAPSHOT _INFO _FILENAME ) ;
2025-07-30 11:19:07 +02:00
const contents = safe . fs . readFileSync ( snapshotFilePath , 'utf8' ) ;
2021-07-14 11:07:19 -07:00
const info = safe . JSON . parse ( contents ) ;
2025-07-30 11:19:07 +02:00
return info || { } ;
2017-09-22 14:40:37 -07:00
}
2021-09-26 18:37:04 -07:00
// keeps track of contents of the snapshot directory. this provides a way to clean up backups of uninstalled apps
2025-09-12 09:48:37 +02:00
async function setSnapshotInfo ( backupSite , id , info ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-07-30 11:19:07 +02:00
assert . strictEqual ( typeof id , 'string' ) ; // 'box', 'mail' or appId
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof info , 'object' ) ;
2017-09-22 14:40:37 -07:00
2025-09-12 09:48:37 +02:00
const infoDir = path . join ( paths . BACKUP _INFO _DIR , backupSite . id ) ;
2025-07-30 11:19:07 +02:00
const snapshotFilePath = path . join ( infoDir , constants . SNAPSHOT _INFO _FILENAME ) ;
const contents = safe . fs . readFileSync ( snapshotFilePath , 'utf8' ) ;
const data = safe . JSON . parse ( contents ) || { } ;
2021-07-14 11:07:19 -07:00
if ( info ) data [ id ] = info ; else delete data [ id ] ;
2025-07-30 11:19:07 +02:00
if ( ! safe . fs . writeFileSync ( snapshotFilePath , JSON . stringify ( data , null , 4 ) , 'utf8' ) ) {
2021-09-16 13:59:03 -07:00
throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
2019-01-17 09:53:51 -08:00
}
2025-07-30 11:19:07 +02:00
if ( ! info ) { // unlink the cache files
safe . fs . unlinkSync ( path . join ( infoDir , ` ${ id } .sync.cache ` ) ) ;
safe . fs . unlinkSync ( path . join ( infoDir , ` ${ id } .sync.cache.new ` ) ) ;
}
2017-09-22 14:40:37 -07:00
}
2025-09-12 09:48:37 +02:00
async function startCleanupTask ( backupSite , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2017-09-19 20:27:36 -07:00
2025-09-12 09:48:37 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _CLEAN _BACKUPS _PREFIX } ${ backupSite . id } ` , [ backupSite . id ] ) ;
2017-09-19 20:27:36 -07:00
2025-06-17 18:54:12 +02:00
// background
tasks . startTask ( taskId , { } )
. then ( async ( result ) => { // { removedBoxBackupPaths, removedAppBackupPaths, removedMailBackupPaths, missingBackupPaths }
await eventlog . add ( eventlog . ACTION _BACKUP _CLEANUP _FINISH , auditSource , { taskId , errorMessage : null , ... result } ) ;
} )
. catch ( async ( error ) => {
await eventlog . add ( eventlog . ACTION _BACKUP _CLEANUP _FINISH , auditSource , { taskId , errorMessage : error . message } ) ;
} ) ;
2018-12-20 11:41:38 -08:00
2021-07-14 11:07:19 -07:00
return taskId ;
2017-09-23 14:27:35 -07:00
}
2025-09-12 09:48:37 +02:00
async function remount ( site ) {
assert . strictEqual ( typeof site , 'object' ) ;
2021-10-11 17:45:35 +02:00
2025-09-12 09:48:37 +02:00
await storageApi ( site ) . setup ( site . config ) ;
2021-10-11 17:45:35 +02:00
}
2023-04-30 17:21:18 +02:00
2025-09-12 09:48:37 +02:00
async function getStatus ( site ) {
assert . strictEqual ( typeof site , 'object' ) ;
2023-04-30 17:21:18 +02:00
2025-11-05 16:38:11 +01:00
return await storageApi ( site ) . getStatus ( site . config ) ; // { state, message }
2023-04-30 17:21:18 +02:00
}
2023-08-04 11:24:28 +05:30
2025-09-12 09:48:37 +02:00
async function ensureMounted ( site ) {
assert . strictEqual ( typeof site , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
const status = await getStatus ( site ) ;
2024-09-09 17:39:17 +02:00
if ( status . state === 'active' ) return status ;
2025-10-06 11:10:08 +02:00
await remount ( site ) ;
2025-09-12 09:48:37 +02:00
return await getStatus ( site ) ;
2023-08-04 11:24:28 +05:30
}
2025-09-12 09:48:37 +02:00
async function setConfig ( backupSite , newConfig , auditSource ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof newConfig , 'object' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2023-08-04 11:24:28 +05:30
2025-07-24 19:02:02 +02:00
if ( constants . DEMO ) throw new BoxError ( BoxError . BAD _STATE , 'Not allowed in demo mode' ) ;
2023-09-29 06:49:55 +05:30
2025-09-12 09:48:37 +02:00
const oldConfig = backupSite . config ;
2023-09-29 06:49:55 +05:30
2025-10-13 09:39:01 +02:00
newConfig = structuredClone ( newConfig ) ; // make a copy
2025-09-12 09:48:37 +02:00
storageApi ( backupSite ) . injectPrivateFields ( newConfig , oldConfig ) ;
2024-06-11 14:32:31 +02:00
2025-07-24 19:02:02 +02:00
debug ( 'setConfig: validating new storage configuration' ) ;
2025-09-12 09:48:37 +02:00
const sanitizedConfig = await storageApi ( backupSite ) . verifyConfig ( { id : backupSite . id , provider : backupSite . provider , config : newConfig } ) ;
2023-09-29 06:49:55 +05:30
2025-09-12 09:48:37 +02:00
await update ( backupSite , { config : sanitizedConfig } ) ;
2025-08-01 18:55:04 +02:00
debug ( 'setConfig: setting up new storage configuration' ) ;
2025-09-12 09:48:37 +02:00
await storageApi ( backupSite ) . setup ( sanitizedConfig ) ;
2023-09-29 06:49:55 +05:30
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _UPDATE , auditSource , { name : backupSite . name , config : storageApi ( backupSite ) . removePrivateFields ( newConfig ) } ) ;
2023-09-29 06:49:55 +05:30
}
2025-07-25 12:08:33 +02:00
async function add ( data , auditSource ) {
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof data , 'object' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2023-08-15 08:14:35 +05:30
2024-06-06 11:32:15 +02:00
if ( constants . DEMO ) throw new BoxError ( BoxError . BAD _STATE , 'Not allowed in demo mode' ) ;
2025-09-23 10:21:43 +02:00
const { provider , name , config , format , contents , retention , schedule , enableForUpdates } = data ; // required
2025-07-24 19:02:02 +02:00
const limits = data . limits || null ,
encryptionPassword = data . encryptionPassword || null ,
2025-08-05 11:26:02 +02:00
encryptedFilenames = data . encryptedFilenames || false ,
2025-10-07 16:49:57 +02:00
encryptionPasswordHint = data . encryptionPasswordHint || '' ;
2023-08-04 11:24:28 +05:30
2025-09-22 16:33:51 +02:00
const formatError = backupFormats . validateFormat ( format ) ;
2024-04-09 12:23:43 +02:00
if ( formatError ) throw formatError ;
2025-08-04 14:19:49 +02:00
const nameError = validateName ( name ) ;
if ( nameError ) throw nameError ;
2024-04-09 12:23:43 +02:00
2025-09-22 13:27:26 +02:00
const contentsError = validateContents ( contents ) ;
if ( contentsError ) throw contentsError ;
2025-07-24 19:02:02 +02:00
let encryption = null ;
if ( encryptionPassword ) {
const encryptionPasswordError = validateEncryptionPassword ( encryptionPassword ) ;
if ( encryptionPasswordError ) throw encryptionPasswordError ;
2025-10-07 16:49:57 +02:00
if ( data . encryptionPassword === data . encryptionPasswordHint ) throw new BoxError ( BoxError . BAD _FIELD , 'Password hint cannot be the same as password' ) ;
2025-07-24 19:02:02 +02:00
encryption = hush . generateEncryptionKeysSync ( encryptionPassword ) ;
encryption . encryptedFilenames = ! ! encryptedFilenames ;
2025-08-05 11:26:02 +02:00
encryption . encryptionPasswordHint = encryptionPasswordHint ;
2024-04-09 12:23:43 +02:00
}
2023-08-15 18:37:21 +05:30
2025-08-11 19:30:22 +05:30
const integrityKeyPair = crypto . generateKeyPairSync ( 'ed25519' , {
publicKeyEncoding : { type : 'spki' , format : 'pem' } ,
privateKeyEncoding : { type : 'pkcs8' , format : 'pem' }
} ) ;
2025-10-01 10:27:14 +02:00
const id = crypto . randomUUID ( ) ;
2025-07-30 11:19:07 +02:00
if ( ! safe . fs . mkdirSync ( ` ${ paths . BACKUP _INFO _DIR } / ${ id } ` ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to create info dir: ${ safe . error . message } ` ) ;
2025-08-01 18:55:04 +02:00
debug ( 'add: validating new storage configuration' ) ;
const sanitizedConfig = await storageApi ( { provider } ) . verifyConfig ( { id , provider , config } ) ;
2025-09-23 16:42:57 +02:00
await database . query ( 'INSERT INTO backupSites (id, name, provider, configJson, contentsJson, limitsJson, integrityKeyPairJson, retentionJson, schedule, encryptionJson, format, enableForUpdates) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
2025-09-23 10:21:43 +02:00
[ id , name , provider , JSON . stringify ( sanitizedConfig ) , JSON . stringify ( contents ) , JSON . stringify ( limits ) , JSON . stringify ( integrityKeyPair ) , JSON . stringify ( retention ) , schedule , JSON . stringify ( encryption ) , format , enableForUpdates ] ) ;
2025-07-25 12:08:33 +02:00
2025-08-01 14:54:32 +02:00
debug ( 'add: setting up new storage configuration' ) ;
2025-08-01 18:55:04 +02:00
await storageApi ( { provider } ) . setup ( sanitizedConfig ) ;
2025-08-01 14:54:32 +02:00
2025-10-09 17:11:06 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _SITE _ADD , auditSource , { id , name , provider , contents , schedule , format } ) ;
2025-07-25 12:08:33 +02:00
2025-07-24 19:02:02 +02:00
return id ;
2023-08-15 20:24:54 +05:30
}
2025-08-01 23:20:51 +02:00
2025-08-06 10:51:46 +02:00
async function addDefault ( auditSource ) {
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-09-12 09:48:37 +02:00
debug ( 'addDefault: adding default backup site' ) ;
const defaultBackupSite = {
2025-08-06 10:51:46 +02:00
name : 'Default' ,
provider : 'filesystem' ,
config : { backupDir : paths . DEFAULT _BACKUP _DIR } ,
retention : { keepWithinSecs : 2 * 24 * 60 * 60 } ,
schedule : '00 00 23 * * *' ,
2025-09-22 13:27:26 +02:00
format : 'tgz' ,
2025-09-22 20:02:13 +02:00
contents : null ,
2025-09-23 16:42:57 +02:00
enableForUpdates : true
2025-08-06 10:51:46 +02:00
} ;
2025-09-22 13:27:26 +02:00
return await add ( defaultBackupSite , auditSource ) ;
2025-08-06 10:51:46 +02:00
}
2025-09-12 09:48:37 +02:00
// creates a backup site object that is not in the database
2025-08-01 23:20:51 +02:00
async function createPseudo ( data ) {
assert . strictEqual ( typeof data , 'object' ) ;
const { id , provider , config , format } = data ; // required
2025-08-05 14:13:39 +02:00
const encryptionPassword = data . encryptionPassword ? ? null ,
encryptedFilenames = ! ! data . encryptedFilenames ;
2025-08-01 23:20:51 +02:00
2025-09-22 16:33:51 +02:00
const formatError = backupFormats . validateFormat ( format ) ;
2025-08-01 23:20:51 +02:00
if ( formatError ) throw formatError ;
let encryption = null ;
2025-10-07 19:57:20 +02:00
if ( encryptionPassword ) { // intentionally do not validate password!
2025-08-01 23:20:51 +02:00
encryption = hush . generateEncryptionKeysSync ( encryptionPassword ) ;
encryption . encryptedFilenames = ! ! encryptedFilenames ;
2025-08-05 11:26:02 +02:00
encryption . encryptionPasswordHint = '' ;
2025-08-01 23:20:51 +02:00
}
debug ( 'add: validating new storage configuration' ) ;
const sanitizedConfig = await storageApi ( { provider } ) . verifyConfig ( { id , provider , config } ) ;
return { id , format , provider , config : sanitizedConfig , encryption } ;
}
2025-10-06 16:39:01 +02:00
// after a restore, this recreates the working directories of the sites
async function reinitAll ( ) {
for ( const site of await list ( ) ) {
if ( ! safe . fs . mkdirSync ( ` ${ paths . BACKUP _INFO _DIR } / ${ site . id } ` , { recursive : true } ) ) throw new BoxError ( BoxError . FS _ERROR , ` Failed to create info dir: ${ safe . error . message } ` ) ;
2025-11-26 15:36:33 +01:00
const status = await getStatus ( site ) ;
if ( status . state === 'active' ) continue ;
safe ( remount ( site ) , { debug } ) ; // background
2025-10-06 16:39:01 +02:00
}
}