2021-07-14 11:07:19 -07:00
'use strict' ;
exports = module . exports = {
2021-09-26 18:37:04 -07:00
fullBackup ,
2025-07-18 10:56:52 +02:00
appBackup ,
2021-07-14 11:07:19 -07:00
restore ,
downloadApp ,
2025-07-18 17:43:53 +02:00
backupApp ,
2021-07-14 11:07:19 -07:00
2021-09-26 18:37:04 -07:00
downloadMail ,
2021-07-14 11:07:19 -07:00
upload ,
} ;
const apps = require ( './apps.js' ) ,
assert = require ( 'assert' ) ,
2022-04-28 18:43:14 -07:00
backupFormat = require ( './backupformat.js' ) ,
2025-07-25 01:34:29 +02:00
backups = require ( './backups.js' ) ,
2025-07-24 18:46:21 +02:00
backupTargets = require ( './backuptargets.js' ) ,
2021-07-14 11:07:19 -07:00
BoxError = require ( './boxerror.js' ) ,
constants = require ( './constants.js' ) ,
DataLayout = require ( './datalayout.js' ) ,
database = require ( './database.js' ) ,
debug = require ( 'debug' ) ( 'box:backuptask' ) ,
2024-07-08 10:46:20 +02:00
df = require ( './df.js' ) ,
remove global lock
Currently, the update/apptask/fullbackup/platformstart take a
global lock and cannot run in parallel. This causes situations
where when a user tries to trigger an apptask, it says "waiting for
backup to finish..." etc
The solution is to let them run in parallel. We need a lock at the
app level as app operations running in parallel would be bad (tm).
In addition, the update task needs a lock just for the update part.
We also need multi-process locks. Running tasks as processes is core
to our "kill" strategy.
Various inter process locks were explored:
* node's IPC mechanism with process.send(). But this only works for direct node.js
children. taskworker is run via sudo and the IPC does not work.
* File lock using O_EXCL. Basic ideas to create lock files. While file creation
can be done atomically, it becomes complicated to clean up lock files when
the tasks crash. We need a way to know what locks were held by the crashing task.
flock and friends are not built-into node.js
* sqlite/redis were options but introduce additional deps
* Settled on MySQL based locking. Initial plan was to have row locks
or table locks. Each row is a kind of lock. While implementing, it was found that
we need many types of locks (and not just update lock and app locks). For example,
we need locks for each task type, so that only one task type is active at a time.
* Instead of rows, we can just lock table and have a json blob in it. This hit a road
block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e
when issing two db.query() it might use two different connections from the pool. We have to
expose the connection, release connection etc.
* Next idea was atomic blob update of the blob checking if old blob was same. This approach,
was finally refined into a version field.
Phew!
2024-12-07 14:35:45 +01:00
locks = require ( './locks.js' ) ,
2021-07-14 11:07:19 -07:00
path = require ( 'path' ) ,
paths = require ( './paths.js' ) ,
safe = require ( 'safetydance' ) ,
services = require ( './services.js' ) ,
2025-08-01 14:54:32 +02:00
shell = require ( './shell.js' ) ( 'backuptask' ) ;
2021-07-14 11:07:19 -07:00
const BACKUP _UPLOAD _CMD = path . join ( _ _dirname , 'scripts/backupupload.js' ) ;
2025-07-24 19:02:02 +02:00
async function checkPreconditions ( backupTarget , dataLayout ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2022-10-02 17:22:44 +02:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
// check mount status before uploading
2025-07-24 19:02:02 +02:00
const status = await backupTargets . ensureMounted ( backupTarget ) ;
2024-11-06 14:53:41 +01:00
debug ( ` checkPreconditions: mount point status is ${ JSON . stringify ( status ) } ` ) ;
2022-11-05 08:43:02 +01:00
if ( status . state !== 'active' ) throw new BoxError ( BoxError . MOUNT _ERROR , ` Backup endpoint is not active: ${ status . message } ` ) ;
2022-10-02 17:22:44 +02:00
// check availabe size. this requires root for df to work
2025-08-01 14:54:32 +02:00
const available = await backupTargets . storageApi ( backupTarget ) . getAvailableSize ( backupTarget . config ) ;
2022-10-02 17:22:44 +02:00
let used = 0 ;
for ( const localPath of dataLayout . localPaths ( ) ) {
debug ( ` checkPreconditions: getting disk usage of ${ localPath } ` ) ;
2024-11-06 14:53:41 +01:00
// du can error when files go missing as it is computing the size. it still prints some size anyway
// to match df output in getAvailableSize() we must use disk usage size here and not apparent size
const [ duError , result ] = await safe ( shell . spawn ( 'du' , [ '--dereference-args' , '--summarize' , '--block-size=1' , '--exclude=*.lock' , '--exclude=dovecot.list.index.log.*' , localPath ] , { encoding : 'utf8' } ) ) ;
if ( duError ) debug ( ` checkPreconditions: du error for ${ localPath } . code: ${ duError . code } stderror: ${ duError . stderr } ` ) ;
used += parseInt ( duError ? duError . stdout : result , 10 ) ;
2022-10-02 17:22:44 +02:00
}
2024-07-08 10:46:20 +02:00
debug ( ` checkPreconditions: total required= ${ used } available= ${ available } ` ) ;
2022-10-02 17:22:44 +02:00
const needed = 0.6 * used + ( 1024 * 1024 * 1024 ) ; // check if there is atleast 1GB left afterwards. aim for 60% because rsync/tgz won't need full 100%
2024-07-08 10:46:20 +02:00
if ( available <= needed ) throw new BoxError ( BoxError . FS _ERROR , ` Not enough disk space for backup. Needed: ${ df . prettyBytes ( needed ) } Available: ${ df . prettyBytes ( available ) } ` ) ;
2022-10-02 17:22:44 +02:00
}
2021-07-14 11:07:19 -07:00
// this function is called via backupupload (since it needs root to traverse app's directory)
2025-07-24 19:02:02 +02:00
async function upload ( remotePath , targetId , dataLayoutString , progressCallback ) {
2022-04-04 14:13:27 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof targetId , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof dataLayoutString , 'string' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-24 19:02:02 +02:00
debug ( ` upload: path ${ remotePath } target ${ targetId } dataLayout ${ dataLayoutString } ` ) ;
const backupTarget = await backupTargets . get ( targetId ) ;
if ( ! backupTarget ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup target not found' ) ;
2021-07-14 11:07:19 -07:00
const dataLayout = DataLayout . fromString ( dataLayoutString ) ;
2022-10-02 17:22:44 +02:00
2025-07-24 19:02:02 +02:00
await checkPreconditions ( backupTarget , dataLayout ) ;
2021-07-14 11:07:19 -07:00
2025-07-24 19:02:02 +02:00
await backupFormat . api ( backupTarget . format ) . upload ( backupTarget , remotePath , dataLayout , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-25 12:55:14 +02:00
async function download ( backupTarget , remotePath , dataLayout , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2022-04-05 09:28:30 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-25 12:55:14 +02:00
debug ( ` download: Downloading ${ remotePath } of format ${ backupTarget . format } (encrypted: ${ ! ! backupTarget . encryption } ) to ${ dataLayout . toString ( ) } ` ) ;
2021-07-14 11:07:19 -07:00
2025-07-25 12:55:14 +02:00
await backupFormat . api ( backupTarget . format ) . download ( backupTarget , remotePath , dataLayout , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2022-04-05 09:28:30 -07:00
async function restore ( backupConfig , remotePath , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
2022-04-05 09:28:30 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const boxDataDir = safe . fs . realpathSync ( paths . BOX _DATA _DIR ) ;
2021-09-16 13:59:03 -07:00
if ( ! boxDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Error resolving boxdata: ${ safe . error . message } ` ) ;
2021-07-14 11:07:19 -07:00
const dataLayout = new DataLayout ( boxDataDir , [ ] ) ;
2022-04-28 21:29:11 -07:00
await download ( backupConfig , remotePath , backupConfig . format , dataLayout , progressCallback ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
debug ( 'restore: download completed, importing database' ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
await database . importFromFile ( ` ${ dataLayout . localRoot ( ) } /box.mysqldump ` ) ;
debug ( 'restore: database imported' ) ;
remove global lock
Currently, the update/apptask/fullbackup/platformstart take a
global lock and cannot run in parallel. This causes situations
where when a user tries to trigger an apptask, it says "waiting for
backup to finish..." etc
The solution is to let them run in parallel. We need a lock at the
app level as app operations running in parallel would be bad (tm).
In addition, the update task needs a lock just for the update part.
We also need multi-process locks. Running tasks as processes is core
to our "kill" strategy.
Various inter process locks were explored:
* node's IPC mechanism with process.send(). But this only works for direct node.js
children. taskworker is run via sudo and the IPC does not work.
* File lock using O_EXCL. Basic ideas to create lock files. While file creation
can be done atomically, it becomes complicated to clean up lock files when
the tasks crash. We need a way to know what locks were held by the crashing task.
flock and friends are not built-into node.js
* sqlite/redis were options but introduce additional deps
* Settled on MySQL based locking. Initial plan was to have row locks
or table locks. Each row is a kind of lock. While implementing, it was found that
we need many types of locks (and not just update lock and app locks). For example,
we need locks for each task type, so that only one task type is active at a time.
* Instead of rows, we can just lock table and have a json blob in it. This hit a road
block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e
when issing two db.query() it might use two different connections from the pool. We have to
expose the connection, release connection etc.
* Next idea was atomic blob update of the blob checking if old blob was same. This approach,
was finally refined into a version field.
Phew!
2024-12-07 14:35:45 +01:00
2025-07-14 15:01:30 +02:00
await locks . releaseAll ( ) ; // clear the locks table in database
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function downloadApp ( app , restoreConfig , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof restoreConfig , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const appDataDir = safe . fs . realpathSync ( path . join ( paths . APPS _DATA _DIR , app . id ) ) ;
2021-09-16 13:59:03 -07:00
if ( ! appDataDir ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
2022-06-01 22:44:52 -07:00
const dataLayout = new DataLayout ( appDataDir , app . storageVolumeId ? [ { localDir : await apps . getStorageDir ( app ) , remoteDir : 'data' } ] : [ ] ) ;
2021-07-14 11:07:19 -07:00
const startTime = new Date ( ) ;
2025-07-25 12:55:14 +02:00
const backup = await backups . get ( restoreConfig . backupId ) ;
const backupTarget = await backupTargets . get ( backup . targetId ) ;
2021-07-14 11:07:19 -07:00
2025-07-25 12:55:14 +02:00
await download ( backupTarget , backup . remotePath , dataLayout , progressCallback ) ;
2021-09-16 13:59:03 -07:00
debug ( 'downloadApp: time: %s' , ( new Date ( ) - startTime ) / 1000 ) ;
2021-07-14 11:07:19 -07:00
}
2022-04-28 21:29:11 -07:00
async function runBackupUpload ( uploadConfig , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof uploadConfig , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-24 19:02:02 +02:00
const { remotePath , backupTarget , dataLayout , progressTag } = uploadConfig ;
2022-04-04 14:13:27 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof progressTag , 'string' ) ;
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
// https://stackoverflow.com/questions/48387040/node-js-recommended-max-old-space-size
const envCopy = Object . assign ( { } , process . env ) ;
2025-07-24 19:02:02 +02:00
if ( backupTarget . limits ? . memoryLimit >= 2 * 1024 * 1024 * 1024 ) {
const heapSize = Math . min ( ( backupTarget . limits . memoryLimit / 1024 / 1024 ) - 256 , 8192 ) ;
2021-07-14 11:07:19 -07:00
debug ( ` runBackupUpload: adjusting heap size to ${ heapSize } M ` ) ;
envCopy . NODE _OPTIONS = ` --max-old-space-size= ${ heapSize } ` ;
}
2022-04-28 21:29:11 -07:00
let result = '' ; // the script communicates error result as a string
function onMessage ( progress ) { // this is { message } or { result }
2021-07-14 11:07:19 -07:00
if ( 'message' in progress ) return progressCallback ( { message : ` ${ progress . message } ( ${ progressTag } ) ` } ) ;
debug ( ` runBackupUpload: result - ${ JSON . stringify ( progress ) } ` ) ;
result = progress . result ;
2022-04-28 21:29:11 -07:00
}
2025-07-16 21:32:27 +02:00
// do not use debug for logging child output because it already has timestamps via it's own debug
2025-07-24 19:02:02 +02:00
const [ error ] = await safe ( shell . sudo ( [ BACKUP _UPLOAD _CMD , remotePath , backupTarget . id , dataLayout . toString ( ) ] , { env : envCopy , preserveEnv : true , onMessage , logger : process . stdout . write } ) ) ;
2022-04-28 21:29:11 -07:00
if ( error && ( error . code === null /* signal */ || ( error . code !== 0 && error . code !== 50 ) ) ) { // backuptask crashed
2024-10-14 18:26:01 +02:00
debug ( ` runBackupUpload: backuptask crashed ` , error ) ;
2022-04-28 21:29:11 -07:00
throw new BoxError ( BoxError . INTERNAL _ERROR , 'Backuptask crashed' ) ;
} else if ( error && error . code === 50 ) { // exited with error
throw new BoxError ( BoxError . EXTERNAL _ERROR , result ) ;
}
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function snapshotBox ( progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
progressCallback ( { message : 'Snapshotting box' } ) ;
const startTime = new Date ( ) ;
2021-09-16 13:59:03 -07:00
await database . exportToFile ( ` ${ paths . BOX _DATA _DIR } /box.mysqldump ` ) ;
debug ( ` snapshotBox: took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function uploadBoxSnapshot ( backupTarget , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2021-09-16 13:59:03 -07:00
await snapshotBox ( progressCallback ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const boxDataDir = safe . fs . realpathSync ( paths . BOX _DATA _DIR ) ;
if ( ! boxDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Error resolving boxdata: ${ safe . error . message } ` ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const uploadConfig = {
2022-04-04 14:13:27 -07:00
remotePath : 'snapshot/box' ,
2025-07-24 19:02:02 +02:00
backupTarget ,
2021-09-16 13:59:03 -07:00
dataLayout : new DataLayout ( boxDataDir , [ ] ) ,
progressTag : 'box'
} ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
progressCallback ( { message : 'Uploading box snapshot' } ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const startTime = new Date ( ) ;
2021-07-14 11:07:19 -07:00
2022-04-28 21:29:11 -07:00
await runBackupUpload ( uploadConfig , progressCallback ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
debug ( ` uploadBoxSnapshot: took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2021-07-14 11:07:19 -07:00
2025-07-30 11:19:07 +02:00
await backupTargets . setSnapshotInfo ( backupTarget , 'box' , { timestamp : new Date ( ) . toISOString ( ) } ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function copy ( backupTarget , srcRemotePath , destRemotePath , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2022-04-04 14:13:27 -07:00
assert . strictEqual ( typeof srcRemotePath , 'string' ) ;
assert . strictEqual ( typeof destRemotePath , 'string' ) ;
2021-09-26 18:37:04 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-08-01 20:49:11 +02:00
const oldFilePath = backupTargets . getBackupFilePath ( backupTarget , srcRemotePath ) ;
const newFilePath = backupTargets . getBackupFilePath ( backupTarget , destRemotePath ) ;
2021-09-26 18:37:04 -07:00
2022-04-30 16:01:42 -07:00
const startTime = new Date ( ) ;
2025-08-01 20:49:11 +02:00
const [ copyError ] = await safe ( backupTargets . storageApi ( backupTarget ) . copy ( backupTarget . config , oldFilePath , newFilePath , progressCallback ) ) ;
2023-01-17 10:43:17 +01:00
if ( copyError ) {
debug ( ` copy: copied to ${ destRemotePath } errored. error: ${ copyError . message } ` ) ;
throw copyError ;
}
2022-04-30 16:01:42 -07:00
debug ( ` copy: copied successfully to ${ destRemotePath } . Took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2021-09-26 18:37:04 -07:00
}
2025-07-24 19:02:02 +02:00
async function rotateBoxBackup ( backupTarget , tag , options , dependsOn , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
2021-09-26 18:37:04 -07:00
assert ( Array . isArray ( dependsOn ) ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2022-04-04 14:13:27 -07:00
const remotePath = ` ${ tag } /box_v ${ constants . VERSION } ` ;
2021-07-14 11:07:19 -07:00
2022-04-04 14:13:27 -07:00
debug ( ` rotateBoxBackup: rotating to id ${ remotePath } ` ) ;
2021-07-14 11:07:19 -07:00
const data = {
2022-04-04 14:13:27 -07:00
remotePath ,
2025-07-24 19:02:02 +02:00
encryptionVersion : backupTarget . encryption ? 2 : null ,
2021-07-14 11:07:19 -07:00
packageVersion : constants . VERSION ,
2025-07-25 01:34:29 +02:00
type : backups . BACKUP _TYPE _BOX ,
state : backups . BACKUP _STATE _CREATING ,
identifier : backups . BACKUP _IDENTIFIER _BOX ,
2021-09-26 18:37:04 -07:00
dependsOn ,
2021-07-14 11:07:19 -07:00
manifest : null ,
2024-12-10 20:52:29 +01:00
preserveSecs : options . preserveSecs || 0 ,
2025-07-25 07:44:25 +02:00
appConfig : null ,
targetId : backupTarget . id
2021-07-14 11:07:19 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-07-24 19:02:02 +02:00
const [ error ] = await safe ( copy ( backupTarget , 'snapshot/box' , remotePath , progressCallback ) ) ;
2025-07-25 01:34:29 +02:00
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
await backups . setState ( id , state ) ;
2022-04-05 13:11:30 +02:00
if ( error ) throw error ;
2022-04-04 14:13:27 -07:00
return id ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function backupBox ( backupTarget , dependsOn , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-09-26 18:37:04 -07:00
assert ( Array . isArray ( dependsOn ) ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-24 19:02:02 +02:00
await uploadBoxSnapshot ( backupTarget , progressCallback ) ;
return await rotateBoxBackup ( backupTarget , tag , options , dependsOn , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function rotateAppBackup ( backupTarget , app , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-30 11:19:07 +02:00
const manifest = app . manifest ;
2022-04-04 14:13:27 -07:00
const remotePath = ` ${ tag } /app_ ${ app . fqdn } _v ${ manifest . version } ` ;
2021-07-14 11:07:19 -07:00
2022-04-04 14:13:27 -07:00
debug ( ` rotateAppBackup: rotating ${ app . fqdn } to path ${ remotePath } ` ) ;
2021-07-14 11:07:19 -07:00
const data = {
2022-04-04 14:13:27 -07:00
remotePath ,
2025-07-24 19:02:02 +02:00
encryptionVersion : backupTarget . encryption ? 2 : null ,
2021-07-14 11:07:19 -07:00
packageVersion : manifest . version ,
2025-07-25 01:34:29 +02:00
type : backups . BACKUP _TYPE _APP ,
state : backups . BACKUP _STATE _CREATING ,
2021-07-14 11:07:19 -07:00
identifier : app . id ,
2022-04-04 21:23:59 -07:00
dependsOn : [ ] ,
2021-07-14 11:07:19 -07:00
manifest ,
2024-12-09 23:20:44 +01:00
preserveSecs : options . preserveSecs || 0 ,
2025-07-25 07:44:25 +02:00
appConfig : app ,
targetId : backupTarget . id
2021-07-14 11:07:19 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-07-24 19:02:02 +02:00
const [ error ] = await safe ( copy ( backupTarget , ` snapshot/app_ ${ app . id } ` , remotePath , progressCallback ) ) ;
2025-07-25 01:34:29 +02:00
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
await backups . setState ( id , state ) ;
2022-04-05 13:11:30 +02:00
if ( error ) throw error ;
2022-04-04 14:13:27 -07:00
return id ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function backupApp ( app , backupTarget , options , progressCallback ) {
2025-07-18 17:43:53 +02:00
assert . strictEqual ( typeof app , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2025-07-18 17:43:53 +02:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
let backupId = null ;
await locks . wait ( ` ${ locks . TYPE _APP _BACKUP _PREFIX } ${ app . id } ` ) ;
if ( options . snapshotOnly ) {
await snapshotApp ( app , progressCallback ) ;
} else {
const tag = ( new Date ( ) ) . toISOString ( ) . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ;
2025-07-24 19:02:02 +02:00
backupId = await backupAppWithTag ( app , backupTarget , tag , options , progressCallback ) ;
2025-07-18 17:43:53 +02:00
}
await locks . release ( ` ${ locks . TYPE _APP _BACKUP _PREFIX } ${ app . id } ` ) ;
return backupId ;
}
2021-09-16 13:59:03 -07:00
async function snapshotApp ( app , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const startTime = new Date ( ) ;
progressCallback ( { message : ` Snapshotting app ${ app . fqdn } ` } ) ;
2024-02-10 11:53:25 +01:00
await apps . writeConfig ( app ) ;
2021-09-16 13:59:03 -07:00
await services . backupAddons ( app , app . manifest . addons ) ;
2021-07-14 11:07:19 -07:00
2021-09-26 18:45:23 -07:00
debug ( ` snapshotApp: ${ app . fqdn } took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function uploadAppSnapshot ( backupTarget , app , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2021-09-16 13:59:03 -07:00
await snapshotApp ( app , progressCallback ) ;
2021-07-14 11:07:19 -07:00
2022-04-26 18:53:07 -07:00
const remotePath = ` snapshot/app_ ${ app . id } ` ;
2021-09-16 13:59:03 -07:00
const appDataDir = safe . fs . realpathSync ( path . join ( paths . APPS _DATA _DIR , app . id ) ) ;
if ( ! appDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Error resolving appsdata: ${ safe . error . message } ` ) ;
2021-07-14 11:07:19 -07:00
2022-06-01 22:44:52 -07:00
const dataLayout = new DataLayout ( appDataDir , app . storageVolumeId ? [ { localDir : await apps . getStorageDir ( app ) , remoteDir : 'data' } ] : [ ] ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
progressCallback ( { message : ` Uploading app snapshot ${ app . fqdn } ` } ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const uploadConfig = {
2022-04-04 14:13:27 -07:00
remotePath ,
2025-07-24 19:02:02 +02:00
backupTarget ,
2021-09-16 13:59:03 -07:00
dataLayout ,
progressTag : app . fqdn
} ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const startTime = new Date ( ) ;
2021-07-14 11:07:19 -07:00
2022-04-28 21:29:11 -07:00
await runBackupUpload ( uploadConfig , progressCallback ) ;
2021-07-14 11:07:19 -07:00
2022-04-26 18:53:07 -07:00
debug ( ` uploadAppSnapshot: ${ app . fqdn } uploaded to ${ remotePath } . ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2021-07-14 11:07:19 -07:00
2025-07-30 11:19:07 +02:00
await backupTargets . setSnapshotInfo ( backupTarget , app . id , { timestamp : new Date ( ) . toISOString ( ) , manifest : app . manifest } ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function backupAppWithTag ( app , backupTarget , tag , options , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-18 10:56:52 +02:00
if ( ! apps . canBackupApp ( app ) ) { // if we cannot backup, reuse it's most recent backup
2025-07-25 14:03:31 +02:00
const lastKnownGoodAppBackup = await backups . getLatestInTargetByIdentifier ( app . id , backupTarget . id ) ;
if ( lastKnownGoodAppBackup === null ) return null ; // no backup to re-use
return lastKnownGoodAppBackup . id ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
await uploadAppSnapshot ( backupTarget , app , progressCallback ) ;
return await rotateAppBackup ( backupTarget , app , tag , options , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2025-07-24 19:02:02 +02:00
async function uploadMailSnapshot ( backupTarget , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-09-26 18:37:04 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const mailDataDir = safe . fs . realpathSync ( paths . MAIL _DATA _DIR ) ;
if ( ! mailDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Error resolving maildata: ${ safe . error . message } ` ) ;
const uploadConfig = {
2022-04-04 14:13:27 -07:00
remotePath : 'snapshot/mail' ,
2025-07-24 19:02:02 +02:00
backupTarget ,
2021-09-26 18:37:04 -07:00
dataLayout : new DataLayout ( mailDataDir , [ ] ) ,
progressTag : 'mail'
} ;
progressCallback ( { message : 'Uploading mail snapshot' } ) ;
const startTime = new Date ( ) ;
2022-04-28 21:29:11 -07:00
await runBackupUpload ( uploadConfig , progressCallback ) ;
2021-09-26 18:37:04 -07:00
debug ( ` uploadMailSnapshot: took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2025-07-30 11:19:07 +02:00
await backupTargets . setSnapshotInfo ( backupTarget , 'mail' , { timestamp : new Date ( ) . toISOString ( ) } ) ;
2021-09-26 18:37:04 -07:00
}
2025-07-25 07:44:25 +02:00
async function rotateMailBackup ( backupTarget , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-09-26 18:37:04 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2022-04-04 14:13:27 -07:00
const remotePath = ` ${ tag } /mail_v ${ constants . VERSION } ` ;
2021-09-26 18:37:04 -07:00
2022-04-04 14:13:27 -07:00
debug ( ` rotateMailBackup: rotating to ${ remotePath } ` ) ;
2021-09-26 18:37:04 -07:00
const data = {
2022-04-04 14:13:27 -07:00
remotePath ,
2025-07-25 07:44:25 +02:00
encryptionVersion : backupTarget . encryption ? 2 : null ,
2021-09-26 18:37:04 -07:00
packageVersion : constants . VERSION ,
2025-07-25 01:34:29 +02:00
type : backups . BACKUP _TYPE _MAIL ,
state : backups . BACKUP _STATE _CREATING ,
identifier : backups . BACKUP _IDENTIFIER _MAIL ,
2021-09-26 18:37:04 -07:00
dependsOn : [ ] ,
manifest : null ,
2024-12-10 20:52:29 +01:00
preserveSecs : options . preserveSecs || 0 ,
2025-07-25 07:44:25 +02:00
appConfig : null ,
targetId : backupTarget . id
2021-09-26 18:37:04 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-07-25 07:44:25 +02:00
const [ error ] = await safe ( copy ( backupTarget , 'snapshot/mail' , remotePath , progressCallback ) ) ;
2025-07-25 01:34:29 +02:00
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
await backups . setState ( id , state ) ;
2022-04-05 13:11:30 +02:00
if ( error ) throw error ;
2022-04-04 14:13:27 -07:00
return id ;
2021-09-26 18:37:04 -07:00
}
2025-07-25 07:44:25 +02:00
async function backupMailWithTag ( backupTarget , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2021-09-26 18:37:04 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
debug ( ` backupMailWithTag: backing up mail with tag ${ tag } ` ) ;
2025-07-25 07:44:25 +02:00
await uploadMailSnapshot ( backupTarget , progressCallback ) ;
return await rotateMailBackup ( backupTarget , tag , options , progressCallback ) ;
2021-09-26 18:37:04 -07:00
}
async function downloadMail ( restoreConfig , progressCallback ) {
assert . strictEqual ( typeof restoreConfig , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const mailDataDir = safe . fs . realpathSync ( paths . MAIL _DATA _DIR ) ;
if ( ! mailDataDir ) throw new BoxError ( BoxError . FS _ERROR , ` Error resolving maildata: ${ safe . error . message } ` ) ;
const dataLayout = new DataLayout ( mailDataDir , [ ] ) ;
const startTime = new Date ( ) ;
2022-04-28 21:29:11 -07:00
await download ( restoreConfig . backupConfig , restoreConfig . remotePath , restoreConfig . backupFormat , dataLayout , progressCallback ) ;
2021-09-26 18:37:04 -07:00
debug ( 'downloadMail: time: %s' , ( new Date ( ) - startTime ) / 1000 ) ;
}
// this function is called from external process. calling process is expected to have a lock
2025-07-24 19:02:02 +02:00
async function fullBackup ( backupTargetId , options , progressCallback ) {
assert . strictEqual ( typeof backupTargetId , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-24 19:02:02 +02:00
const backupTarget = await backupTargets . get ( backupTargetId ) ;
if ( ! backupTarget ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Backup target not found' ) ;
2022-04-04 14:13:27 -07:00
const tag = ( new Date ( ) ) . toISOString ( ) . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ; // unique tag under which all apps/mail/box backs up
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const allApps = await apps . list ( ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
let percent = 1 ;
2024-07-08 10:47:00 +02:00
const step = 100 / ( allApps . length + 3 ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
const appBackupIds = [ ] ;
2021-11-02 18:07:19 -07:00
for ( let i = 0 ; i < allApps . length ; i ++ ) {
const app = allApps [ i ] ;
2021-09-16 13:59:03 -07:00
percent += step ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
if ( ! app . enableBackup ) {
2022-02-28 11:04:44 -08:00
debug ( ` fullBackup: skipped backup ${ app . fqdn } ( ${ i + 1 } / ${ allApps . length } ) since automatic backup disabled ` ) ;
2021-11-02 17:59:08 -07:00
continue ; // nothing to backup
2021-09-16 13:59:03 -07:00
}
2021-07-14 11:07:19 -07:00
2024-12-09 08:38:23 +01:00
progressCallback ( { percent , message : ` Backing up ${ app . fqdn } ( ${ i + 1 } / ${ allApps . length } ). Waiting for lock ` } ) ;
2025-07-18 10:56:52 +02:00
await locks . wait ( ` ${ locks . TYPE _APP _BACKUP _PREFIX } ${ app . id } ` ) ;
2021-09-16 13:59:03 -07:00
const startTime = new Date ( ) ;
2025-07-24 19:02:02 +02:00
const [ appBackupError , appBackupId ] = await safe ( backupAppWithTag ( app , backupTarget , tag , options , ( progress ) => progressCallback ( { percent , message : progress . message } ) ) ) ;
2021-09-26 18:37:04 -07:00
debug ( ` fullBackup: app ${ app . fqdn } backup finished. Took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2025-07-18 10:56:52 +02:00
await locks . release ( ` ${ locks . TYPE _APP _BACKUP _PREFIX } ${ app . id } ` ) ;
2024-12-17 19:08:43 +01:00
if ( appBackupError ) throw appBackupError ;
2021-09-26 18:37:04 -07:00
if ( appBackupId ) appBackupIds . push ( appBackupId ) ; // backupId can be null if in BAD_STATE and never backed up
2021-09-16 13:59:03 -07:00
}
2021-07-14 11:07:19 -07:00
2022-04-04 14:13:27 -07:00
progressCallback ( { percent , message : 'Backing up mail' } ) ;
2021-09-26 18:37:04 -07:00
percent += step ;
2025-07-24 19:02:02 +02:00
const mailBackupId = await backupMailWithTag ( backupTarget , tag , options , ( progress ) => progressCallback ( { percent , message : progress . message } ) ) ;
2021-09-26 18:37:04 -07:00
2022-04-04 14:13:27 -07:00
progressCallback ( { percent , message : 'Backing up system data' } ) ;
2021-09-16 13:59:03 -07:00
percent += step ;
2021-07-14 11:07:19 -07:00
2021-09-26 18:37:04 -07:00
const dependsOn = appBackupIds . concat ( mailBackupId ) ;
2025-07-24 19:02:02 +02:00
const backupId = await backupBox ( backupTarget , dependsOn , tag , options , ( progress ) => progressCallback ( { percent , message : progress . message } ) ) ;
2021-09-16 13:59:03 -07:00
return backupId ;
2021-07-14 11:07:19 -07:00
}
2025-07-18 10:56:52 +02:00
// this function is called from external process
2025-07-24 19:02:02 +02:00
async function appBackup ( appId , backupTargetId , options , progressCallback ) {
2025-07-18 10:56:52 +02:00
assert . strictEqual ( typeof appId , 'string' ) ;
2025-07-25 12:55:14 +02:00
assert . strictEqual ( typeof backupTargetId , 'string' ) ;
2025-07-18 10:56:52 +02:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const app = await apps . get ( appId ) ;
if ( ! app ) throw new BoxError ( BoxError . BAD _FIELD , 'App not found' ) ;
2025-07-24 19:02:02 +02:00
const backupTarget = await backupTargets . get ( backupTargetId ) ;
if ( ! backupTarget ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Backup target not found' ) ;
2025-07-18 10:56:52 +02:00
await progressCallback ( { percent : 1 , message : ` Backing up ${ app . fqdn } . Waiting for lock ` } ) ;
const startTime = new Date ( ) ;
2025-07-24 19:02:02 +02:00
const backupId = await backupApp ( app , backupTarget , options , progressCallback ) ;
2025-07-18 10:56:52 +02:00
await progressCallback ( { percent : 100 , message : ` app ${ app . fqdn } backup finished. Took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` } ) ;
return backupId ;
}