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
backupMail ,
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' ) ,
2021-07-14 11:07:19 -07:00
backups = require ( './backups.js' ) ,
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' ) ,
2024-10-14 19:10:31 +02:00
shell = require ( './shell.js' ) ( 'backuptask' ) ,
2022-04-30 16:42:14 -07:00
storage = require ( './storage.js' ) ;
2021-07-14 11:07:19 -07:00
const BACKUP _UPLOAD _CMD = path . join ( _ _dirname , 'scripts/backupupload.js' ) ;
2022-10-02 17:22:44 +02:00
async function checkPreconditions ( backupConfig , dataLayout ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
// check mount status before uploading
2024-09-09 17:39:17 +02:00
const status = await backups . ensureMounted ( ) ;
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
2024-07-08 10:46:20 +02:00
const available = await storage . api ( backupConfig . provider ) . getAvailableSize ( backupConfig ) ;
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)
2022-04-28 21:29:11 -07:00
async function upload ( remotePath , format , dataLayoutString , progressCallback ) {
2022-04-04 14:13:27 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof format , 'string' ) ;
assert . strictEqual ( typeof dataLayoutString , 'string' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2022-04-04 14:13:27 -07:00
debug ( ` upload: path ${ remotePath } format ${ format } dataLayout ${ dataLayoutString } ` ) ;
2021-07-14 11:07:19 -07:00
const dataLayout = DataLayout . fromString ( dataLayoutString ) ;
2023-08-04 11:24:28 +05:30
const backupConfig = await backups . getConfig ( ) ;
2022-10-02 17:22:44 +02:00
await checkPreconditions ( backupConfig , dataLayout ) ;
2021-07-14 11:07:19 -07:00
2022-04-30 16:42:14 -07:00
await backupFormat . api ( format ) . upload ( backupConfig , remotePath , dataLayout , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2022-04-28 21:29:11 -07:00
async function download ( backupConfig , remotePath , format , dataLayout , 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 format , 'string' ) ;
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-07-14 15:01:30 +02:00
debug ( ` download: Downloading ${ remotePath } of format ${ format } (encrypted: ${ ! ! backupConfig . encryption } ) to ${ dataLayout . toString ( ) } ` ) ;
2021-07-14 11:07:19 -07:00
2022-04-30 16:42:14 -07:00
await backupFormat . api ( format ) . download ( backupConfig , 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 ( ) ;
2023-08-04 11:24:28 +05:30
const backupConfig = restoreConfig . backupConfig || await backups . getConfig ( ) ;
2021-07-14 11:07:19 -07:00
2022-04-28 21:29:11 -07:00
await download ( backupConfig , restoreConfig . remotePath , restoreConfig . backupFormat , 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' ) ;
2022-04-04 14:13:27 -07:00
const { remotePath , backupConfig , dataLayout , progressTag } = uploadConfig ;
assert . strictEqual ( typeof remotePath , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
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 ) ;
2023-07-13 11:50:57 +05:30
if ( backupConfig . limits ? . memoryLimit >= 2 * 1024 * 1024 * 1024 ) {
const heapSize = Math . min ( ( backupConfig . 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-16 21:53:22 +02:00
const [ error ] = await safe ( shell . sudo ( [ BACKUP _UPLOAD _CMD , remotePath , backupConfig . format , 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
}
2021-09-16 13:59:03 -07:00
async function uploadBoxSnapshot ( backupConfig , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
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' ,
2021-09-16 13:59:03 -07:00
backupConfig ,
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
2021-09-16 13:59:03 -07:00
await backups . setSnapshotInfo ( 'box' , { timestamp : new Date ( ) . toISOString ( ) , format : backupConfig . format } ) ;
2021-07-14 11:07:19 -07:00
}
2022-04-04 14:13:27 -07:00
async function copy ( backupConfig , srcRemotePath , destRemotePath , progressCallback ) {
2021-09-26 18:37:04 -07:00
assert . strictEqual ( typeof backupConfig , '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' ) ;
2022-04-04 14:13:27 -07:00
const { provider , format } = backupConfig ;
2022-04-28 18:43:14 -07:00
const oldFilePath = backupFormat . api ( format ) . getBackupFilePath ( backupConfig , srcRemotePath ) ;
const newFilePath = backupFormat . api ( format ) . getBackupFilePath ( backupConfig , destRemotePath ) ;
2021-09-26 18:37:04 -07:00
2022-04-30 16:01:42 -07:00
const startTime = new Date ( ) ;
2023-01-17 10:43:17 +01:00
const [ copyError ] = await safe ( storage . api ( provider ) . copy ( backupConfig , oldFilePath , newFilePath , progressCallback ) ) ;
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
}
async function rotateBoxBackup ( backupConfig , tag , options , dependsOn , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
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
const format = backupConfig . format ;
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 ,
2021-07-14 11:07:19 -07:00
encryptionVersion : backupConfig . encryption ? 2 : null ,
packageVersion : constants . VERSION ,
type : backups . BACKUP _TYPE _BOX ,
state : backups . BACKUP _STATE _CREATING ,
2021-09-26 18:37:04 -07:00
identifier : backups . BACKUP _IDENTIFIER _BOX ,
dependsOn ,
2021-07-14 11:07:19 -07:00
manifest : null ,
2022-04-04 21:23:59 -07:00
format ,
2024-12-10 20:52:29 +01:00
preserveSecs : options . preserveSecs || 0 ,
appConfig : null
2021-07-14 11:07:19 -07:00
} ;
2022-04-04 14:13:27 -07:00
const id = await backups . add ( data ) ;
2022-04-05 13:11:30 +02:00
const [ error ] = await safe ( copy ( backupConfig , 'snapshot/box' , remotePath , progressCallback ) ) ;
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
2022-04-04 21:23:59 -07:00
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
}
2021-09-26 18:37:04 -07:00
async function backupBox ( dependsOn , tag , options , progressCallback ) {
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' ) ;
2023-08-04 11:24:28 +05:30
const backupConfig = await backups . getConfig ( ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
await uploadBoxSnapshot ( backupConfig , progressCallback ) ;
2022-04-04 14:13:27 -07:00
return await rotateBoxBackup ( backupConfig , tag , options , dependsOn , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
async function rotateAppBackup ( backupConfig , app , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const snapshotInfo = backups . getSnapshotInfo ( app . id ) ;
const manifest = snapshotInfo . restoreConfig ? snapshotInfo . restoreConfig . manifest : snapshotInfo . manifest ; // compat
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
const format = backupConfig . format ;
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 ,
2021-07-14 11:07:19 -07:00
encryptionVersion : backupConfig . encryption ? 2 : null ,
packageVersion : manifest . version ,
type : backups . BACKUP _TYPE _APP ,
state : backups . BACKUP _STATE _CREATING ,
identifier : app . id ,
2022-04-04 21:23:59 -07:00
dependsOn : [ ] ,
2021-07-14 11:07:19 -07:00
manifest ,
2022-04-04 21:23:59 -07:00
format ,
2024-12-09 23:20:44 +01:00
preserveSecs : options . preserveSecs || 0 ,
appConfig : app
2021-07-14 11:07:19 -07:00
} ;
2022-04-04 14:13:27 -07:00
const id = await backups . add ( data ) ;
2022-04-05 13:11:30 +02:00
const [ error ] = await safe ( copy ( backupConfig , ` snapshot/app_ ${ app . id } ` , remotePath , progressCallback ) ) ;
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
2022-04-04 21:23:59 -07:00
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-18 17:43:53 +02:00
async function backupApp ( app , options , progressCallback ) {
assert . strictEqual ( typeof app , 'object' ) ;
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 , '' ) ;
backupId = await backupAppWithTag ( app , tag , options , progressCallback ) ;
}
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
}
2021-09-16 13:59:03 -07:00
async function uploadAppSnapshot ( backupConfig , app , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof backupConfig , 'object' ) ;
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 ,
2021-09-16 13:59:03 -07:00
backupConfig ,
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
2021-09-16 13:59:03 -07:00
await backups . setSnapshotInfo ( app . id , { timestamp : new Date ( ) . toISOString ( ) , manifest : app . manifest , format : backupConfig . format } ) ;
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function backupAppWithTag ( app , tag , options , progressCallback ) {
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-18 10:56:52 +02:00
if ( ! apps . canBackupApp ( app ) ) { // if we cannot backup, reuse it's most recent backup
2021-09-16 13:59:03 -07:00
const results = await backups . getByIdentifierAndStatePaged ( app . id , backups . BACKUP _STATE _NORMAL , 1 , 1 ) ;
if ( results . length === 0 ) return null ; // no backup to re-use
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
return results [ 0 ] . id ;
2021-07-14 11:07:19 -07:00
}
2023-08-04 11:24:28 +05:30
const backupConfig = await backups . getConfig ( ) ;
2021-07-14 11:07:19 -07:00
2021-09-16 13:59:03 -07:00
await uploadAppSnapshot ( backupConfig , app , progressCallback ) ;
2022-04-04 14:13:27 -07:00
return await rotateAppBackup ( backupConfig , app , tag , options , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2021-09-26 18:37:04 -07:00
async function uploadMailSnapshot ( backupConfig , progressCallback ) {
assert . strictEqual ( typeof backupConfig , '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 uploadConfig = {
2022-04-04 14:13:27 -07:00
remotePath : 'snapshot/mail' ,
2021-09-26 18:37:04 -07:00
backupConfig ,
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 ` ) ;
await backups . setSnapshotInfo ( 'mail' , { timestamp : new Date ( ) . toISOString ( ) , format : backupConfig . format } ) ;
}
async function rotateMailBackup ( backupConfig , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupConfig , 'object' ) ;
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
const format = backupConfig . format ;
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 ,
2021-09-26 18:37:04 -07:00
encryptionVersion : backupConfig . encryption ? 2 : null ,
packageVersion : constants . VERSION ,
type : backups . BACKUP _TYPE _MAIL ,
state : backups . BACKUP _STATE _CREATING ,
identifier : backups . BACKUP _IDENTIFIER _MAIL ,
dependsOn : [ ] ,
manifest : null ,
2022-04-04 21:23:59 -07:00
format ,
2024-12-10 20:52:29 +01:00
preserveSecs : options . preserveSecs || 0 ,
appConfig : null
2021-09-26 18:37:04 -07:00
} ;
2022-04-04 14:13:27 -07:00
const id = await backups . add ( data ) ;
2022-04-05 13:11:30 +02:00
const [ error ] = await safe ( copy ( backupConfig , 'snapshot/mail' , remotePath , progressCallback ) ) ;
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
2022-04-04 21:23:59 -07:00
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
}
async function backupMailWithTag ( tag , options , progressCallback ) {
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
debug ( ` backupMailWithTag: backing up mail with tag ${ tag } ` ) ;
2023-08-04 11:24:28 +05:30
const backupConfig = await backups . getConfig ( ) ;
2021-09-26 18:37:04 -07:00
await uploadMailSnapshot ( backupConfig , progressCallback ) ;
2022-04-04 14:13:27 -07:00
return await rotateMailBackup ( backupConfig , tag , options , progressCallback ) ;
2021-09-26 18:37:04 -07:00
}
async function backupMail ( options , progressCallback ) {
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const tag = ( new Date ( ) ) . toISOString ( ) . replace ( /[T.]/g , '-' ) . replace ( /[:Z]/g , '' ) ;
debug ( ` backupMail: backing up mail with tag ${ tag } ` ) ;
return await backupMailWithTag ( tag , options , progressCallback ) ;
}
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
async function fullBackup ( options , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
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 ( ) ;
2024-12-17 19:08:43 +01:00
const [ appBackupError , appBackupId ] = await safe ( backupAppWithTag ( app , 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 ;
2022-04-04 14:13:27 -07:00
const mailBackupId = await backupMailWithTag ( 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 ) ;
2022-04-04 14:13:27 -07:00
const backupId = await backupBox ( 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
async function appBackup ( appId , options , progressCallback ) {
assert . strictEqual ( typeof appId , 'string' ) ;
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' ) ;
await progressCallback ( { percent : 1 , message : ` Backing up ${ app . fqdn } . Waiting for lock ` } ) ;
const startTime = new Date ( ) ;
2025-07-18 17:43:53 +02:00
const backupId = await backupApp ( app , 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 ;
}