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' ) ,
2025-08-14 11:17:38 +05:30
assert = require ( 'node: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' ) ,
2024-12-07 14:35:45 +01:00
locks = require ( './locks.js' ) ,
2025-08-14 11:17:38 +05:30
path = require ( 'node:path' ) ,
2021-07-14 11:07:19 -07:00
paths = require ( './paths.js' ) ,
2025-08-15 14:33:31 +05:30
{ Readable } = require ( 'node:stream' ) ,
2021-07-14 11:07:19 -07:00
safe = require ( 'safetydance' ) ,
services = require ( './services.js' ) ,
2025-08-15 14:33:31 +05:30
shell = require ( './shell.js' ) ( 'backuptask' ) ,
stream = require ( 'stream/promises' ) ;
2021-07-14 11:07:19 -07:00
const BACKUP _UPLOAD _CMD = path . join ( _ _dirname , 'scripts/backupupload.js' ) ;
2025-08-01 22:58:19 +02:00
function addFileExtension ( backupTarget , remotePath ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
assert . strictEqual ( typeof remotePath , 'string' ) ;
const ext = backupFormat . api ( backupTarget . format ) . getFileExtension ( ! ! backupTarget . encyption ) ;
return remotePath + ext ;
}
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
}
2025-08-15 14:33:31 +05:30
async function uploadBackupInfo ( backupTarget , remotePath , integrityMap ) {
const sortedIntegrityMap = [ ... integrityMap . entries ( ) ] . sort ( ( [ a ] , [ b ] ) => a < b ) ; // for readability, order the entries
const integrityDataJsonString = JSON . stringify ( Object . fromEntries ( sortedIntegrityMap ) , null , 2 ) ;
const integrityDataStream = Readable . from ( integrityDataJsonString ) ;
const integrityUploader = await backupTargets . storageApi ( backupTarget ) . upload ( backupTarget . config , ` ${ remotePath } .backupinfo ` ) ;
await stream . pipeline ( integrityDataStream , integrityUploader . stream ) ;
await integrityUploader . finish ( ) ;
return await crypto . sign ( null /* algorithm */ , integrityDataJsonString , backupTarget . integrityKeyPair . privateKey ) ;
}
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-08-15 14:33:31 +05:30
const { stats , integrityMap } = await backupFormat . api ( backupTarget . format ) . upload ( backupTarget , remotePath , dataLayout , progressCallback ) ;
const signature = await uploadBackupInfo ( backupTarget , remotePath , integrityMap ) ;
return { stats , integrity : { signature } } ;
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
}
2025-08-01 23:20:51 +02:00
async function restore ( backupTarget , remotePath , 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 . 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 , [ ] ) ;
2025-08-01 23:20:51 +02:00
await download ( backupTarget , remotePath , 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' ) ;
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-08-02 19:09:21 +02:00
let { backupTarget , remotePath } = restoreConfig ; // set when importing
if ( ! remotePath ) {
const backup = await backups . get ( restoreConfig . backupId ) ;
if ( ! backup ) throw new BoxError ( BoxError . BAD _FIELD , 'No such backup' ) ;
remotePath = backup . remotePath ;
backupTarget = await backupTargets . get ( backup . targetId ) ;
}
2021-07-14 11:07:19 -07:00
2025-08-02 19:09:21 +02:00
await download ( backupTarget , 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 } ` ;
}
2025-08-11 19:30:22 +05:30
let lastMessage = null ; // the script communicates error result as a string
2022-04-28 21:29:11 -07:00
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 ) } ` ) ;
2025-08-11 19:30:22 +05:30
lastMessage = progress ;
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
2025-08-11 19:30:22 +05:30
throw new BoxError ( BoxError . EXTERNAL _ERROR , lastMessage . errorMessage ) ;
2022-04-28 21:29:11 -07:00
}
2025-08-11 19:30:22 +05:30
return lastMessage . 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
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension ( backupTarget , ` snapshot/box ` ) ;
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 = {
2025-08-01 22:58:19 +02:00
remotePath ,
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
2025-08-12 19:41:50 +05:30
const { stats , integrity } = 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 ( ) } ) ;
2025-08-11 19:30:22 +05:30
2025-08-12 19:41:50 +05:30
return { stats , integrity } ;
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' ) ;
2022-04-30 16:01:42 -07:00
const startTime = new Date ( ) ;
2025-08-02 01:46:29 +02:00
const [ copyError ] = await safe ( backupTargets . storageApi ( backupTarget ) . copy ( backupTarget . config , srcRemotePath , destRemotePath , 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 ` ) ;
2025-08-11 19:30:22 +05:30
2025-08-13 21:29:34 +05:30
const [ copyChecksumError ] = await safe ( backupTargets . storageApi ( backupTarget ) . copy ( backupTarget . config , ` ${ srcRemotePath } .backupinfo ` , ` ${ destRemotePath } .backupinfo ` , progressCallback ) ) ;
2025-08-11 19:30:22 +05:30
if ( copyChecksumError ) {
debug ( ` copy: copied to ${ destRemotePath } errored. error: ${ copyChecksumError . message } ` ) ;
throw copyChecksumError ;
}
2025-08-13 21:29:34 +05:30
debug ( ` copy: copied backupinfo successfully to ${ destRemotePath } .backupinfo ` ) ;
2021-09-26 18:37:04 -07:00
}
2025-08-12 19:41:50 +05:30
async function backupBox ( backupTarget , dependsOn , tag , options , progressCallback ) {
2025-07-24 19:02:02 +02:00
assert . strictEqual ( typeof backupTarget , 'object' ) ;
2025-08-12 19:41:50 +05:30
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-08-12 19:41:50 +05:30
const { stats , integrity } = await uploadBoxSnapshot ( backupTarget , progressCallback ) ;
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension ( backupTarget , ` ${ tag } /box_v ${ constants . VERSION } ` ) ;
2021-07-14 11:07:19 -07:00
2025-08-12 19:41:50 +05:30
debug ( ` backupBox: 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 ,
2025-08-12 19:41:50 +05:30
targetId : backupTarget . id ,
stats ,
integrity
2021-07-14 11:07:19 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-08-01 22:58:19 +02:00
const snapshotPath = addFileExtension ( backupTarget , 'snapshot/box' ) ;
const [ error ] = await safe ( copy ( backupTarget , snapshotPath , 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
}
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
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension ( backupTarget , ` 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
2025-08-12 19:41:50 +05:30
const { stats , integrity } = 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 } ) ;
2025-08-11 19:30:22 +05:30
2025-08-12 19:41:50 +05:30
return { stats , integrity } ;
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-08-12 19:41:50 +05:30
const { stats , integrity } = await uploadAppSnapshot ( backupTarget , app , progressCallback ) ;
const manifest = app . manifest ;
const remotePath = addFileExtension ( backupTarget , ` ${ tag } /app_ ${ app . fqdn } _v ${ manifest . version } ` ) ;
debug ( ` backupAppWithTag: rotating ${ app . fqdn } to path ${ remotePath } ` ) ;
const data = {
remotePath ,
encryptionVersion : backupTarget . encryption ? 2 : null ,
packageVersion : manifest . version ,
type : backups . BACKUP _TYPE _APP ,
state : backups . BACKUP _STATE _CREATING ,
identifier : app . id ,
dependsOn : [ ] ,
manifest ,
preserveSecs : options . preserveSecs || 0 ,
appConfig : app ,
targetId : backupTarget . id ,
stats ,
integrity
} ;
const id = await backups . add ( data ) ;
const snapshotPath = addFileExtension ( backupTarget , ` snapshot/app_ ${ app . id } ` ) ;
const [ error ] = await safe ( copy ( backupTarget , snapshotPath , remotePath , progressCallback ) ) ;
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
await backups . setState ( id , state ) ;
if ( error ) throw error ;
2025-08-11 19:30:22 +05:30
return id ;
2021-07-14 11:07:19 -07:00
}
2025-08-12 19:41:50 +05:30
async function backupApp ( app , backupTarget , options , progressCallback ) {
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof backupTarget , '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 , backupTarget , tag , options , progressCallback ) ;
}
await locks . release ( ` ${ locks . TYPE _APP _BACKUP _PREFIX } ${ app . id } ` ) ;
return backupId ;
}
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' ) ;
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension ( backupTarget , 'snapshot/mail' ) ;
2021-09-26 18:37:04 -07:00
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 = {
2025-08-01 22:58:19 +02:00
remotePath ,
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 ( ) ;
2025-08-12 19:41:50 +05:30
const { stats , integrity } = 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 ( ) } ) ;
2025-08-11 19:30:22 +05:30
2025-08-12 19:41:50 +05:30
return { stats , integrity } ;
2021-09-26 18:37:04 -07:00
}
2025-08-12 19:41:50 +05:30
async function backupMailWithTag ( backupTarget , tag , options , progressCallback ) {
2025-07-25 07:44:25 +02:00
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' ) ;
2025-08-12 19:41:50 +05:30
debug ( ` backupMailWithTag: backing up mail with tag ${ tag } ` ) ;
const { stats , integrity } = await uploadMailSnapshot ( backupTarget , progressCallback ) ;
2025-08-01 22:58:19 +02:00
const remotePath = addFileExtension ( backupTarget , ` ${ tag } /mail_v ${ constants . VERSION } ` ) ;
2021-09-26 18:37:04 -07:00
2025-08-12 19:41:50 +05:30
debug ( ` backupMailWithTag: 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 ,
2025-08-12 19:41:50 +05:30
targetId : backupTarget . id ,
stats ,
integrity
2021-09-26 18:37:04 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-08-01 22:58:19 +02:00
const snapshotPath = addFileExtension ( backupTarget , 'snapshot/mail' ) ;
const [ error ] = await safe ( copy ( backupTarget , snapshotPath , 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-08-01 23:20:51 +02:00
async function downloadMail ( backupTarget , remotePath , progressCallback ) {
assert . strictEqual ( typeof backupTarget , 'object' ) ;
assert . strictEqual ( typeof remotePath , 'string' ) ;
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 dataLayout = new DataLayout ( mailDataDir , [ ] ) ;
const startTime = new Date ( ) ;
2025-08-01 23:20:51 +02:00
await download ( backupTarget , remotePath , 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 ;
}