2015-07-20 00:09:47 -07:00
'use strict' ;
exports = module . exports = {
2025-07-24 19:02:02 +02:00
get ,
2025-07-25 12:55:14 +02:00
getPrimary ,
2025-07-24 19:02:02 +02:00
list ,
add ,
del ,
2016-04-10 19:17:44 -07:00
2025-07-24 19:02:02 +02:00
setConfig ,
setLimits ,
setSchedule ,
setRetention ,
2025-07-25 09:43:26 +02:00
setPrimary ,
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 ,
2023-08-15 20:24:54 +05:30
validateFormat ,
2020-05-12 14:00:05 -07:00
2023-08-15 20:24:54 +05:30
getRootPath ,
2023-08-15 20:24:54 +05:30
2021-10-11 17:45:35 +02:00
remount ,
2023-04-30 17:21:18 +02:00
getMountStatus ,
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 ,
getBackupFilePath
2015-07-20 00:09:47 -07:00
} ;
2021-07-14 11:07:19 -07:00
const assert = require ( 'assert' ) ,
2025-07-25 01:34:29 +02:00
backups = require ( './backups.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-07-28 12:53:27 +02:00
crypto = require ( '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' ) ,
2023-04-30 17:21:18 +02:00
mounts = require ( './mounts.js' ) ,
2016-04-10 19:17:44 -07:00
path = require ( 'path' ) ,
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
// filesystem - backupFolder, noHardlinks
// mountpoint - mountPoint, prefix, noHardlinks
// encryption: 'encryptionPassword' and 'encryptedFilenames' is converted into an 'encryption' object using hush.js. Password is lost forever after conversion.
2025-07-25 09:43:26 +02:00
const BACKUP _TARGET _FIELDS = [ 'id' , 'label' , 'provider' , 'configJson' , 'limitsJson' , 'retentionJson' , 'schedule' , 'encryptionJson' , 'format' , 'main' , 'creationTime' , 'ts' ] . join ( ',' ) ;
2025-07-24 19:02:02 +02:00
2025-08-01 14:54:32 +02:00
function storageApi ( backupTarget ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
switch ( backupTarget . provider ) {
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' ) ;
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' ) ;
case 'noop' : return require ( './storage/noop.js' ) ;
default : throw new BoxError ( BoxError . BAD _FIELD , ` Unknown provider: ${ backupTarget . provider } ` ) ;
}
}
2025-08-01 20:49:11 +02:00
function getBackupFilePath ( backupTarget , remotePath ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
assert . strictEqual ( typeof remotePath , 'string' ) ;
// we don't have a rootPath for noop
if ( backupTarget . provider === 'noop' ) return remotePath ;
return path . join ( backupTarget . config . rootPath , remotePath ) ;
}
2025-07-24 19:02:02 +02:00
function getRootPath ( provider , config , mountPath ) {
assert . strictEqual ( typeof config , 'object' ) ;
assert . strictEqual ( typeof mountPath , 'string' ) ;
2021-07-14 11:07:19 -07:00
2025-07-24 19:02:02 +02:00
if ( mounts . isManagedProvider ( provider ) ) {
return path . join ( mountPath , config . prefix ) ;
} else if ( provider === 'mountpoint' ) {
return path . join ( config . mountPoint , config . prefix ) ;
} else if ( provider === 'filesystem' ) {
return config . backupFolder ;
} else {
return config . prefix ;
}
}
2016-04-10 19:17:44 -07: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-24 19:02:02 +02:00
// note: rootPath will be dynamic for managed mount providers during app import . since it's used in api backends it has to be inside config
result . config . rootPath = getRootPath ( result . provider , result . config , paths . MANAGED _BACKUP _MOUNT _DIR ) ;
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-07-25 09:43:26 +02:00
result . primary = ! ! result . main ; // primary is a reserved keyword in mysql
delete result . main ;
2025-07-24 13:19:27 +02:00
return result ;
}
2025-07-24 19:02:02 +02:00
function removePrivateFields ( target ) {
assert . strictEqual ( typeof target , 'object' ) ;
2025-07-28 11:45:10 +02:00
target . encrypted = target . encryption !== null ;
target . encryptedFilenames = target . encryption ? . encryptedFilenames || false ;
delete target . encryption ;
2025-07-25 11:29:00 +02:00
delete target . config . rootPath ;
2025-08-01 14:54:32 +02:00
target . config = storageApi ( target ) . removePrivateFields ( target . config ) ;
2025-07-25 11:29:00 +02:00
return target ;
2020-05-12 15:49:43 -07:00
}
2025-07-24 19:02:02 +02:00
function validateFormat ( format ) {
assert . strictEqual ( typeof format , 'string' ) ;
2020-05-12 14:00:05 -07:00
2025-07-24 19:02:02 +02:00
if ( format === 'tgz' || format == 'rsync' ) return null ;
return new BoxError ( BoxError . BAD _FIELD , 'Invalid backup format' ) ;
2020-05-12 14:00:05 -07:00
}
2019-12-05 11:55:51 -08:00
2025-07-24 19:02:02 +02:00
function validateLabel ( label ) {
assert . strictEqual ( typeof label , 'string' ) ;
2023-07-12 10:01:53 +05:30
2025-07-24 19:02:02 +02:00
if ( label . length > 48 ) return new BoxError ( BoxError . BAD _FIELD , 'Label too long' ) ;
}
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-07-24 19:02:02 +02:00
async function list ( page , perPage ) {
assert ( typeof page === 'number' && page > 0 ) ;
assert ( typeof perPage === 'number' && perPage > 0 ) ;
2025-07-25 11:29:00 +02:00
const results = await database . query ( ` SELECT ${ BACKUP _TARGET _FIELDS } FROM backupTargets ORDER BY main DESC LIMIT ?,? ` , [ ( page - 1 ) * perPage , perPage ] ) ;
2025-07-24 19:02:02 +02:00
results . forEach ( function ( result ) { postProcess ( result ) ; } ) ;
return results ;
}
async function get ( id ) {
const results = await database . query ( ` SELECT ${ BACKUP _TARGET _FIELDS } FROM backupTargets WHERE id=? ` , [ id ] ) ;
if ( results . length === 0 ) return null ;
return postProcess ( results [ 0 ] ) ;
}
2025-07-25 12:55:14 +02:00
async function getPrimary ( ) {
const results = await database . query ( ` SELECT ${ BACKUP _TARGET _FIELDS } FROM backupTargets WHERE main=? ` , [ true ] ) ;
if ( results . length === 0 ) return null ;
return postProcess ( results [ 0 ] ) ;
}
2025-07-24 19:02:02 +02:00
async function update ( target , data ) {
assert . strictEqual ( typeof target , 'object' ) ;
assert ( data && typeof data === 'object' ) ;
const args = [ ] ;
const fields = [ ] ;
for ( const k in data ) {
2025-07-25 09:43:26 +02:00
if ( k === 'label' || k === 'schedule' || k === 'main' ) { // format, provider cannot be updated
2025-07-24 19:02:02 +02:00
fields . push ( k + ' = ?' ) ;
args . push ( data [ k ] ) ;
} else if ( k === 'config' || k === 'limits' || k === 'retention' ) { // encryption cannot be updated
fields . push ( ` ${ k } JSON = ? ` ) ;
args . push ( JSON . stringify ( data [ k ] ) ) ;
}
}
args . push ( target . id ) ;
const [ updateError , result ] = await safe ( database . query ( 'UPDATE backupTargets SET ' + fields . join ( ', ' ) + ' WHERE id = ?' , args ) ) ;
if ( updateError ) throw updateError ;
if ( result . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Target not found' ) ;
}
2025-07-25 12:08:33 +02:00
async function setSchedule ( backupTarget , schedule , auditSource ) {
assert . strictEqual ( typeof backupTarget , '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
const error = await validateSchedule ( schedule ) ;
if ( error ) throw error ;
2025-07-25 12:08:33 +02:00
await update ( backupTarget , { schedule } ) ;
2025-07-24 19:02:02 +02:00
2025-07-25 12:08:33 +02:00
await cron . handleBackupScheduleChanged ( backupTarget ) ;
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _UPDATE , auditSource , { backupTarget , schedule } ) ;
2025-07-24 19:02:02 +02:00
}
2025-07-25 12:08:33 +02:00
async function setLimits ( backupTarget , limits , auditSource ) {
assert . strictEqual ( typeof backupTarget , '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-07-25 12:08:33 +02:00
await update ( backupTarget , { limits } ) ;
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _UPDATE , auditSource , { backupTarget , limits } ) ;
2025-07-24 19:02:02 +02:00
}
2025-07-25 12:08:33 +02:00
async function setRetention ( backupTarget , retention , auditSource ) {
assert . strictEqual ( typeof backupTarget , '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
const error = await validateRetention ( retention ) ;
if ( error ) throw error ;
2025-07-25 12:08:33 +02:00
await update ( backupTarget , { retention } ) ;
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _UPDATE , auditSource , { backupTarget , retention } ) ;
2025-07-24 19:02:02 +02:00
}
2025-07-25 12:08:33 +02:00
async function setPrimary ( backupTarget , auditSource ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-25 09:43:26 +02:00
const queries = [
2025-07-25 12:08:33 +02:00
{ query : 'SELECT 1 FROM backupTargets WHERE id=? FOR UPDATE' , args : [ backupTarget . id ] } , // ensure this exists!
2025-07-25 09:43:26 +02:00
{ query : 'UPDATE backupTargets SET main=?' , args : [ false ] } ,
2025-07-25 12:08:33 +02:00
{ query : 'UPDATE backupTargets SET main=? WHERE id=?' , args : [ true , backupTarget . id ] }
2025-07-25 09:43:26 +02:00
] ;
const [ error , result ] = await safe ( database . transaction ( queries ) ) ;
if ( error ) throw error ;
if ( result [ 2 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Target not found' ) ;
2025-07-25 12:08:33 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _UPDATE , auditSource , { backupTarget , primary : true } ) ;
2025-07-25 09:43:26 +02:00
}
2025-07-30 11:19:07 +02:00
async function del ( backupTarget , auditSource ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2025-07-25 12:08:33 +02:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2025-07-24 19:02:02 +02:00
2025-08-01 14:54:32 +02:00
await safe ( storageApi ( backupTarget ) . teardown ( backupTarget . config ) , { debug } ) ; // ignore error
2025-07-30 11:19:07 +02:00
if ( backupTarget . primary ) throw new BoxError ( BoxError . CONFLICT , 'Cannot delete the primary backup target' ) ;
2025-07-24 19:02:02 +02:00
2025-07-25 09:43:26 +02:00
const queries = [
2025-07-30 11:19:07 +02:00
{ query : 'DELETE FROM backups WHERE targetId = ?' , args : [ backupTarget . id ] } ,
{ query : 'DELETE FROM backupTargets WHERE id=? AND main=?' , args : [ backupTarget . id , false ] } , // cannot delete primary
2025-07-25 09:43:26 +02:00
] ;
2025-07-24 19:02:02 +02:00
const [ error , result ] = await safe ( database . transaction ( queries ) ) ;
if ( error && error . code === 'ER_NO_REFERENCED_ROW_2' ) throw new BoxError ( BoxError . NOT _FOUND , error ) ;
if ( error ) throw error ;
if ( result [ 1 ] . affectedRows !== 1 ) throw new BoxError ( BoxError . NOT _FOUND , 'Target not found' ) ;
2025-07-30 11:19:07 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _REMOVE , auditSource , { backupTarget : backupTarget } ) ;
2025-07-24 19:02:02 +02:00
2025-07-30 11:19:07 +02:00
backupTarget . schedule = constants . CRON _PATTERN _NEVER ;
await cron . handleBackupScheduleChanged ( backupTarget ) ;
2025-07-25 08:36:09 +02:00
2025-07-30 11:19:07 +02:00
const infoDir = path . join ( paths . BACKUP _INFO _DIR , backupTarget . id ) ;
safe . fs . rmdirSync ( infoDir , { recursive : true } ) ;
2025-07-24 19:02:02 +02:00
}
async function startBackupTask ( target , auditSource ) {
assert . strictEqual ( typeof target , 'object' ) ;
2025-07-25 14:46:55 +02:00
const [ error ] = await safe ( locks . acquire ( ` ${ locks . TYPE _FULL _BACKUP _TASK _PREFIX } ${ target . 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-07-24 19:02:02 +02:00
const memoryLimit = target . limits ? . memoryLimit ? Math . max ( target . limits . memoryLimit / 1024 / 1024 , 1024 ) : 1024 ;
2018-07-27 06:55:54 -07:00
2025-07-24 19:02:02 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _FULL _BACKUP _PREFIX } ${ target . id } ` , [ target . id , { /* options */ } ] ) ;
2018-07-27 06:55:54 -07:00
2021-09-10 12:10:10 -07:00
await eventlog . add ( eventlog . ACTION _BACKUP _START , auditSource , { taskId } ) ;
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 } )
. then ( async ( backupId ) => {
2025-07-25 01:34:29 +02:00
const backup = await backups . get ( backupId ) ;
2025-06-17 18:54:12 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _FINISH , auditSource , { taskId , backupId , remotePath : backup . remotePath } ) ;
} )
. catch ( async ( error ) => {
const timedOut = error . code === tasks . ETIMEOUT ;
await safe ( eventlog . add ( eventlog . ACTION _BACKUP _FINISH , auditSource , { taskId , errorMessage : error . message , timedOut } ) ) ;
} )
. finally ( async ( ) => {
2025-07-25 14:46:55 +02:00
await locks . release ( ` ${ locks . TYPE _FULL _BACKUP _TASK _PREFIX } ${ target . 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
async function removeCacheFiles ( backupTarget ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2017-09-20 09:57:16 -07:00
2025-07-30 11:19:07 +02:00
const infoDir = path . join ( paths . BACKUP _INFO _DIR , backupTarget . id ) ;
const files = safe . fs . readdirSync ( infoDir ) ;
if ( ! files ) throw new BoxError ( BoxError . FS _ERROR , ` Unable to access ${ infoDir } : ${ safe . error . message } ` ) ;
for ( const f of files ) {
if ( ! f . endsWith ( '.sync.cache' ) ) continue ;
safe . fs . unlinkSync ( path . join ( infoDir , f ) ) ;
}
2020-05-10 21:40:25 -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
async function getSnapshotInfo ( backupTarget ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2018-02-22 10:58:56 -08:00
2025-07-30 11:19:07 +02:00
const snapshotFilePath = path . join ( paths . BACKUP _INFO _DIR , backupTarget . id , constants . SNAPSHOT _INFO _FILENAME ) ;
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-07-30 11:19:07 +02:00
async function setSnapshotInfo ( backupTarget , id , info ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
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-07-30 11:19:07 +02:00
const infoDir = path . join ( paths . BACKUP _INFO _DIR , backupTarget . id ) ;
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-07-25 14:54:51 +02:00
async function startCleanupTask ( backupTarget , auditSource ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof auditSource , 'object' ) ;
2017-09-19 20:27:36 -07:00
2025-07-25 14:54:51 +02:00
const taskId = await tasks . add ( ` ${ tasks . TASK _CLEAN _BACKUPS _PREFIX } ${ backupTarget . id } ` , [ backupTarget . 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-07-24 19:02:02 +02:00
async function remount ( target ) {
assert . strictEqual ( typeof target , 'object' ) ;
2021-10-11 17:45:35 +02:00
2025-08-01 14:54:32 +02:00
await storageApi ( target ) . setup ( target . config ) ;
2021-10-11 17:45:35 +02:00
}
2023-04-30 17:21:18 +02:00
2025-07-24 19:02:02 +02:00
async function getMountStatus ( target ) {
assert . strictEqual ( typeof target , 'object' ) ;
2023-04-30 17:21:18 +02:00
let hostPath ;
2025-07-24 19:02:02 +02:00
if ( mounts . isManagedProvider ( target . provider ) ) {
2023-04-30 17:21:18 +02:00
hostPath = paths . MANAGED _BACKUP _MOUNT _DIR ;
2025-07-24 19:02:02 +02:00
} else if ( target . provider === 'mountpoint' ) {
hostPath = target . config . mountPoint ;
} else if ( target . provider === 'filesystem' ) {
hostPath = target . config . backupFolder ;
2023-05-15 10:32:39 +02:00
} else {
2024-09-09 16:44:19 +02:00
return { state : 'active' } ;
2023-04-30 17:21:18 +02:00
}
2025-07-24 19:02:02 +02:00
return await mounts . getStatus ( target . provider , hostPath ) ; // { state, message }
2023-04-30 17:21:18 +02:00
}
2023-08-04 11:24:28 +05:30
2025-07-24 19:02:02 +02:00
async function ensureMounted ( target ) {
assert . strictEqual ( typeof target , 'object' ) ;
const status = await getMountStatus ( target ) ;
2024-09-09 17:39:17 +02:00
if ( status . state === 'active' ) return status ;
await remount ( ) ;
2025-07-24 19:02:02 +02:00
return await getMountStatus ( target ) ;
2023-08-04 11:24:28 +05:30
}
2025-07-25 12:08:33 +02:00
async function setConfig ( backupTarget , newConfig , auditSource ) {
assert . strictEqual ( typeof backupTarget , '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-07-25 12:08:33 +02:00
const oldConfig = backupTarget . config ;
2023-09-29 06:49:55 +05:30
2025-08-01 14:54:32 +02:00
storageApi ( backupTarget ) . 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-08-01 18:55:04 +02:00
const sanitizedConfig = await storageApi ( backupTarget ) . verifyConfig ( { id : backupTarget . id , provider : backupTarget . provider , config : newConfig } ) ;
2023-09-29 06:49:55 +05:30
2025-07-24 19:02:02 +02:00
debug ( 'setConfig: clearing backup cache' ) ;
2025-07-30 11:19:07 +02:00
// FIXME: this cleans up the cache files in case the bucket or the prefix changes and the destination already has something there
// however, this will also resync if just the credentials change
await removeCacheFiles ( backupTarget ) ;
2025-07-25 12:08:33 +02:00
2025-08-01 18:55:04 +02:00
await update ( backupTarget , { config : sanitizedConfig } ) ;
debug ( 'setConfig: setting up new storage configuration' ) ;
await storageApi ( backupTarget ) . setup ( sanitizedConfig ) ;
2023-09-29 06:49:55 +05:30
2025-07-25 12:08:33 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _UPDATE , auditSource , { backupTarget , 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-07-24 19:02:02 +02:00
const { provider , label , config , format , retention , schedule } = data ; // required
const limits = data . limits || null ,
encryptionPassword = data . encryptionPassword || null ,
encryptedFilenames = data . encryptedFilenames || false ;
2023-08-04 11:24:28 +05:30
2025-07-24 19:02:02 +02:00
const formatError = validateFormat ( format ) ;
2024-04-09 12:23:43 +02:00
if ( formatError ) throw formatError ;
2025-07-24 19:02:02 +02:00
const labelError = validateLabel ( label ) ;
if ( labelError ) throw labelError ;
2024-04-09 12:23:43 +02:00
2025-07-24 19:02:02 +02:00
let encryption = null ;
if ( encryptionPassword ) {
const encryptionPasswordError = validateEncryptionPassword ( encryptionPassword ) ;
if ( encryptionPasswordError ) throw encryptionPasswordError ;
encryption = hush . generateEncryptionKeysSync ( encryptionPassword ) ;
encryption . encryptedFilenames = ! ! encryptedFilenames ;
2024-04-09 12:23:43 +02:00
}
2023-08-15 18:37:21 +05:30
2025-07-28 12:53:27 +02:00
const id = ` bc- ${ 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-07-25 09:43:26 +02:00
await database . query ( 'INSERT INTO backupTargets (id, label, provider, configJson, limitsJson, retentionJson, schedule, encryptionJson, format, main) VALUES (?, ?, ?, ?, ?, ?, ?, ?, ?, ?)' ,
2025-08-01 18:55:04 +02:00
[ id , label , provider , JSON . stringify ( sanitizedConfig ) , JSON . stringify ( limits ) , JSON . stringify ( retention ) , schedule , JSON . stringify ( encryption ) , format , false ] ) ;
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-07-25 12:08:33 +02:00
await eventlog . add ( eventlog . ACTION _BACKUP _TARGET _ADD , auditSource , { id , label , provider , config , schedule , format } ) ;
2025-07-24 19:02:02 +02:00
return id ;
2023-08-15 20:24:54 +05:30
}