2021-07-14 11:07:19 -07:00
'use strict' ;
2025-10-08 20:11:55 +02:00
exports = module . exports = {
fullBackup ,
appBackup ,
restore ,
downloadApp ,
backupApp ,
downloadMail ,
upload ,
} ;
2021-07-14 11:07:19 -07:00
const apps = require ( './apps.js' ) ,
2025-08-14 11:17:38 +05:30
assert = require ( 'node:assert' ) ,
2025-08-15 16:09:58 +05:30
backupFormats = require ( './backupformats.js' ) ,
2025-07-25 01:34:29 +02:00
backups = require ( './backups.js' ) ,
2025-09-12 09:48:37 +02:00
backupSites = require ( './backupsites.js' ) ,
2021-07-14 11:07:19 -07:00
BoxError = require ( './boxerror.js' ) ,
constants = require ( './constants.js' ) ,
2025-08-15 16:01:59 +05:30
crypto = require ( 'node:crypto' ) ,
2021-07-14 11:07:19 -07:00
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' ) ,
2025-10-10 12:55:03 +02:00
stream = require ( 'stream/promises' ) ,
util = require ( 'util' ) ;
2021-07-14 11:07:19 -07:00
const BACKUP _UPLOAD _CMD = path . join ( _ _dirname , 'scripts/backupupload.js' ) ;
2025-09-12 09:48:37 +02:00
function addFileExtension ( backupSite , remotePath ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-08-01 22:58:19 +02:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2025-10-07 17:30:57 +02:00
const ext = backupFormats . api ( backupSite . format ) . getFileExtension ( ! ! backupSite . encryption ) ;
2025-08-01 22:58:19 +02:00
return remotePath + ext ;
}
2025-09-12 09:48:37 +02:00
async function checkPreconditions ( backupSite , dataLayout ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2022-10-02 17:22:44 +02:00
assert ( dataLayout instanceof DataLayout , 'dataLayout must be a DataLayout' ) ;
// check mount status before uploading
2025-09-12 09:48:37 +02:00
const status = await backupSites . ensureMounted ( backupSite ) ;
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-09-12 09:48:37 +02:00
const available = await backupSites . storageApi ( backupSite ) . getAvailableSize ( backupSite . 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-09-12 09:48:37 +02:00
async function uploadBackupInfo ( backupSite , remotePath , integrityMap ) {
2025-08-15 14:33:31 +05:30
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 ) ;
2025-10-09 09:04:22 +02:00
// unencrypted for easy verification without having to decrypt anything
2025-11-14 13:18:21 +01:00
const integrityUploader = await backupSites . storageApi ( backupSite ) . upload ( backupSite . config , backupSite . limits , ` ${ remotePath } .backupinfo ` ) ;
2025-08-15 14:33:31 +05:30
await stream . pipeline ( integrityDataStream , integrityUploader . stream ) ;
await integrityUploader . finish ( ) ;
2025-10-08 22:35:39 +02:00
const signatureBuffer = await crypto . sign ( null /* algorithm */ , integrityDataJsonString , backupSite . integrityKeyPair . privateKey ) ;
return signatureBuffer . toString ( 'hex' ) ;
2025-08-15 14:33:31 +05:30
}
2021-07-14 11:07:19 -07:00
// this function is called via backupupload (since it needs root to traverse app's directory)
2025-09-12 09:48:37 +02:00
async function upload ( remotePath , siteId , dataLayoutString , progressCallback ) {
2022-04-04 14:13:27 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof siteId , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof dataLayoutString , 'string' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-09-12 09:48:37 +02:00
debug ( ` upload: path ${ remotePath } site ${ siteId } dataLayout ${ dataLayoutString } ` ) ;
2025-07-24 19:02:02 +02:00
2025-09-12 09:48:37 +02:00
const backupSite = await backupSites . get ( siteId ) ;
if ( ! backupSite ) throw new BoxError ( BoxError . NOT _FOUND , 'Backup site not found' ) ;
2021-07-14 11:07:19 -07:00
const dataLayout = DataLayout . fromString ( dataLayoutString ) ;
2022-10-02 17:22:44 +02:00
2025-09-12 09:48:37 +02:00
await checkPreconditions ( backupSite , dataLayout ) ;
2021-07-14 11:07:19 -07:00
2025-10-08 23:01:13 +02:00
// integrityMap - { size, fileCount, sha256 } of each file. this is saved in .backupinfo file
// - tgz: only one entry named "." in the map. fileCount has the file count inside.
// - rsync: entry for each relative path.
2025-10-01 17:19:58 +02:00
// integrity - { signature } of the uploaded .backupinfo .
2025-10-20 13:22:51 +02:00
// stats - { fileCount, size, transferred }
2025-10-08 23:01:13 +02:00
// - tgz: size (backup size) and transferred is the same
// - rsync: size (final backup size) will be different from what was transferred (only changed files)
// stats.fileCount and stats.size are stored in db and should match up what is written into .backupinfo
2025-09-12 09:48:37 +02:00
const { stats , integrityMap } = await backupFormats . api ( backupSite . format ) . upload ( backupSite , remotePath , dataLayout , progressCallback ) ;
2025-10-20 10:13:08 +02:00
debug ( ` upload: path ${ remotePath } site ${ siteId } uploaded: ${ JSON . stringify ( stats ) } ` ) ;
2025-08-15 14:33:31 +05:30
2025-08-15 16:01:59 +05:30
progressCallback ( { message : ` Uploading integrity information to ${ remotePath } .backupinfo ` } ) ;
2025-09-12 09:48:37 +02:00
const signature = await uploadBackupInfo ( backupSite , remotePath , integrityMap ) ;
2025-10-08 22:35:39 +02:00
return { stats , integrity : { signature } } ;
2021-07-14 11:07:19 -07:00
}
2025-09-12 09:48:37 +02:00
async function download ( backupSite , remotePath , dataLayout , progressCallback ) {
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
debug ( ` download: Downloading ${ remotePath } of format ${ backupSite . format } (encrypted: ${ ! ! backupSite . encryption } ) to ${ dataLayout . toString ( ) } ` ) ;
2021-07-14 11:07:19 -07:00
2025-09-12 09:48:37 +02:00
await backupFormats . api ( backupSite . format ) . download ( backupSite , remotePath , dataLayout , progressCallback ) ;
2021-07-14 11:07:19 -07:00
}
2025-09-12 09:48:37 +02:00
async function restore ( backupSite , remotePath , progressCallback ) {
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
await download ( backupSite , 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-09-12 09:48:37 +02:00
let { backupSite , remotePath } = restoreConfig ; // set when importing
2025-08-02 19:09:21 +02:00
if ( ! remotePath ) {
const backup = await backups . get ( restoreConfig . backupId ) ;
if ( ! backup ) throw new BoxError ( BoxError . BAD _FIELD , 'No such backup' ) ;
remotePath = backup . remotePath ;
2025-09-12 09:48:37 +02:00
backupSite = await backupSites . get ( backup . siteId ) ;
2025-08-02 19:09:21 +02:00
}
2021-07-14 11:07:19 -07:00
2025-09-12 09:48:37 +02:00
await download ( backupSite , 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-09-12 09:48:37 +02:00
const { remotePath , backupSite , dataLayout , progressTag } = uploadConfig ;
2022-04-04 14:13:27 -07:00
assert . strictEqual ( typeof remotePath , 'string' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
if ( backupSite . limits ? . memoryLimit >= 2 * 1024 * 1024 * 1024 ) {
const heapSize = Math . min ( ( backupSite . 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-09-12 09:48:37 +02:00
const [ error ] = await safe ( shell . sudo ( [ BACKUP _UPLOAD _CMD , remotePath , backupSite . 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
2025-10-20 10:13:08 +02:00
return lastMessage . result ; // { stats, integrity }
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-09-12 09:48:37 +02:00
async function uploadBoxSnapshot ( backupSite , progressCallback ) {
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
const remotePath = addFileExtension ( backupSite , ` snapshot/box ` ) ;
2025-08-01 22:58:19 +02: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 = {
2025-08-01 22:58:19 +02:00
remotePath ,
2025-09-12 09:48:37 +02:00
backupSite ,
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-09-12 09:48:37 +02:00
await backupSites . setSnapshotInfo ( backupSite , '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-09-12 09:48:37 +02:00
async function copy ( backupSite , srcRemotePath , destRemotePath , progressCallback ) {
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
const [ copyError ] = await safe ( backupFormats . api ( backupSite . format ) . copy ( backupSite , srcRemotePath , destRemotePath , progressCallback ) ) ;
2023-01-17 10:43:17 +01:00
if ( copyError ) {
2025-08-25 23:45:14 +02:00
debug ( ` copy: copy to ${ destRemotePath } errored. error: ${ copyError . message } ` ) ;
2023-01-17 10:43:17 +01:00
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-09-12 09:48:37 +02:00
const [ copyChecksumError ] = await safe ( backupSites . storageApi ( backupSite ) . copy ( backupSite . config , ` ${ srcRemotePath } .backupinfo ` , ` ${ destRemotePath } .backupinfo ` , progressCallback ) ) ;
2025-08-11 19:30:22 +05:30
if ( copyChecksumError ) {
2025-08-25 23:45:14 +02:00
debug ( ` copy: copy to ${ destRemotePath } errored. error: ${ copyChecksumError . message } ` ) ;
2025-08-11 19:30:22 +05:30
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-10-10 12:55:03 +02:00
async function backupBox ( backupSite , appBackupsMap , tag , options , progressCallback ) {
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-10-20 13:22:51 +02:00
assert ( util . types . isMap ( appBackupsMap ) , 'appBackupsMap should be a Map' ) ; // id -> stats: { upload: { fileCount, size, startTime, duration, transferred } }
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof tag , 'string' ) ;
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-10-20 13:22:51 +02:00
const uploadStartTime = Date . now ( ) ;
const uploadResult = await uploadBoxSnapshot ( backupSite , progressCallback ) ; // { stats, integrity }
const stats = { upload : { ... uploadResult . stats , startTime : uploadStartTime , duration : Date . now ( ) - uploadStartTime } } ;
2025-08-12 19:41:50 +05:30
2025-09-12 09:48:37 +02:00
const remotePath = addFileExtension ( backupSite , ` ${ tag } /box_v ${ constants . VERSION } ` ) ;
2021-07-14 11:07:19 -07:00
2025-10-16 12:32:24 +02:00
// stats object might be null for stopped/errored apps from old versions
2025-10-27 08:48:24 +01:00
stats . aggregatedUpload = Array . from ( appBackupsMap . values ( ) ) . filter ( s => ! ! s ? . upload ) . reduce ( ( acc , cur ) => ( {
2025-10-20 13:22:51 +02:00
fileCount : acc . fileCount + cur . upload . fileCount ,
size : acc . size + cur . upload . size ,
transferred : acc . transferred + cur . upload . transferred ,
startTime : Math . min ( acc . startTime , cur . upload . startTime ) ,
duration : acc . duration + cur . upload . duration ,
} ) , stats . upload ) ;
2025-10-10 12:55:03 +02:00
2025-10-20 10:13:08 +02:00
debug ( ` backupBox: rotating box snapshot of ${ backupSite . id } to id ${ remotePath } . ${ JSON . stringify ( stats ) } ` ) ;
2021-07-14 11:07:19 -07:00
const data = {
2022-04-04 14:13:27 -07:00
remotePath ,
2025-09-12 09:48:37 +02:00
encryptionVersion : backupSite . 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 ,
2025-10-10 12:55:03 +02:00
dependsOn : [ ... appBackupsMap . keys ( ) ] ,
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-09-12 09:48:37 +02:00
siteId : backupSite . id ,
2025-08-12 19:41:50 +05:30
stats ,
2025-10-20 13:22:51 +02:00
integrity : uploadResult . integrity
2021-07-14 11:07:19 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-09-12 09:48:37 +02:00
const snapshotPath = addFileExtension ( backupSite , 'snapshot/box' ) ;
2025-10-20 13:22:51 +02:00
const copyStartTime = Date . now ( ) ;
2025-09-12 09:48:37 +02:00
const [ error ] = await safe ( copy ( backupSite , snapshotPath , remotePath , progressCallback ) ) ;
2025-07-25 01:34:29 +02:00
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
2025-10-20 13:22:51 +02:00
if ( ! error ) {
stats . copy = { startTime : copyStartTime , duration : Date . now ( ) - copyStartTime } ;
// stats object might be null for stopped/errored apps from old versions
2025-11-13 14:42:38 +01:00
stats . aggregatedCopy = Array . from ( appBackupsMap . values ( ) ) . filter ( s => ! ! s ? . copy ) . reduce ( ( acc , cur ) => ( {
2025-10-20 13:22:51 +02:00
startTime : Math . min ( acc . startTime , cur . copy . startTime ) ,
duration : acc . duration + cur . copy . duration ,
} ) , stats . copy ) ;
}
await backups . update ( { id } , { stats , 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-09-12 09:48:37 +02:00
async function uploadAppSnapshot ( backupSite , app , progressCallback ) {
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
const remotePath = addFileExtension ( backupSite , ` 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-09-12 09:48:37 +02:00
backupSite ,
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-09-12 09:48:37 +02:00
await backupSites . setSnapshotInfo ( backupSite , 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-09-12 09:48:37 +02:00
async function backupAppWithTag ( app , backupSite , tag , options , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof backupSite , '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-09-12 09:48:37 +02:00
const lastKnownGoodAppBackup = await backups . getLatestInTargetByIdentifier ( app . id , backupSite . id ) ;
2025-07-25 14:03:31 +02:00
if ( lastKnownGoodAppBackup === null ) return null ; // no backup to re-use
2025-10-16 12:32:24 +02:00
return { id : lastKnownGoodAppBackup . id , stats : lastKnownGoodAppBackup . stats } ;
2021-07-14 11:07:19 -07:00
}
2025-10-20 13:22:51 +02:00
const uploadStartTime = Date . now ( ) ;
const uploadResult = await uploadAppSnapshot ( backupSite , app , progressCallback ) ; // { stats, integrity }
const stats = { upload : { ... uploadResult . stats , startTime : uploadStartTime , duration : Date . now ( ) - uploadStartTime } } ;
2025-08-12 19:41:50 +05:30
const manifest = app . manifest ;
2025-09-12 09:48:37 +02:00
const remotePath = addFileExtension ( backupSite , ` ${ tag } /app_ ${ app . fqdn } _v ${ manifest . version } ` ) ;
2025-08-12 19:41:50 +05:30
2025-09-12 09:48:37 +02:00
debug ( ` backupAppWithTag: rotating ${ app . fqdn } snapshot of ${ backupSite . id } to path ${ remotePath } ` ) ;
2025-08-12 19:41:50 +05:30
const data = {
remotePath ,
2025-09-12 09:48:37 +02:00
encryptionVersion : backupSite . encryption ? 2 : null ,
2025-08-12 19:41:50 +05:30
packageVersion : manifest . version ,
type : backups . BACKUP _TYPE _APP ,
state : backups . BACKUP _STATE _CREATING ,
identifier : app . id ,
dependsOn : [ ] ,
manifest ,
preserveSecs : options . preserveSecs || 0 ,
appConfig : app ,
2025-09-12 09:48:37 +02:00
siteId : backupSite . id ,
2025-08-12 19:41:50 +05:30
stats ,
2025-10-20 13:22:51 +02:00
integrity : uploadResult . integrity
2025-08-12 19:41:50 +05:30
} ;
const id = await backups . add ( data ) ;
2025-09-12 09:48:37 +02:00
const snapshotPath = addFileExtension ( backupSite , ` snapshot/app_ ${ app . id } ` ) ;
2025-10-20 13:22:51 +02:00
const copyStartTime = Date . now ( ) ;
2025-09-12 09:48:37 +02:00
const [ error ] = await safe ( copy ( backupSite , snapshotPath , remotePath , progressCallback ) ) ;
2025-08-12 19:41:50 +05:30
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
2025-10-20 13:22:51 +02:00
if ( ! error ) stats . copy = { startTime : copyStartTime , duration : Date . now ( ) - copyStartTime } ;
await backups . update ( { id } , { stats , state } ) ;
2025-08-12 19:41:50 +05:30
if ( error ) throw error ;
2025-10-20 13:22:51 +02:00
return { id , stats } ;
2021-07-14 11:07:19 -07:00
}
2025-09-12 09:48:37 +02:00
async function backupApp ( app , backupSite , options , progressCallback ) {
2025-08-12 19:41:50 +05:30
assert . strictEqual ( typeof app , 'object' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-08-12 19:41:50 +05:30
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-10-10 12:55:03 +02:00
let backup = null ;
2025-08-12 19:41:50 +05:30
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-10-16 13:00:08 +02:00
backup = await backupAppWithTag ( app , backupSite , tag , options , progressCallback ) ; // { id, stats }
2025-08-12 19:41:50 +05:30
}
await locks . release ( ` ${ locks . TYPE _APP _BACKUP _PREFIX } ${ app . id } ` ) ;
2025-10-10 12:55:03 +02:00
return backup ;
2025-08-12 19:41:50 +05:30
}
2025-09-12 09:48:37 +02:00
async function uploadMailSnapshot ( backupSite , progressCallback ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2021-09-26 18:37:04 -07:00
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-09-12 09:48:37 +02:00
const remotePath = addFileExtension ( backupSite , 'snapshot/mail' ) ;
2025-08-01 22:58:19 +02:00
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-09-12 09:48:37 +02:00
backupSite ,
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-09-12 09:48:37 +02:00
await backupSites . setSnapshotInfo ( backupSite , '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-09-12 09:48:37 +02:00
async function backupMailWithTag ( backupSite , tag , options , progressCallback ) {
assert . strictEqual ( typeof backupSite , '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 } ` ) ;
2025-10-20 13:22:51 +02:00
const uploadStartTime = Date . now ( ) ;
const uploadResult = await uploadMailSnapshot ( backupSite , progressCallback ) ; // { stats, integrity }
const stats = { upload : { ... uploadResult . stats , startTime : uploadStartTime , duration : Date . now ( ) - uploadStartTime } } ;
2025-08-12 19:41:50 +05:30
2025-09-12 09:48:37 +02:00
const remotePath = addFileExtension ( backupSite , ` ${ tag } /mail_v ${ constants . VERSION } ` ) ;
2021-09-26 18:37:04 -07:00
2025-09-12 09:48:37 +02:00
debug ( ` backupMailWithTag: rotating mail snapshot of ${ backupSite . id } to ${ remotePath } ` ) ;
2021-09-26 18:37:04 -07:00
const data = {
2022-04-04 14:13:27 -07:00
remotePath ,
2025-09-12 09:48:37 +02:00
encryptionVersion : backupSite . 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-09-12 09:48:37 +02:00
siteId : backupSite . id ,
2025-08-12 19:41:50 +05:30
stats ,
2025-10-20 13:22:51 +02:00
integrity : uploadResult . integrity
2021-09-26 18:37:04 -07:00
} ;
2025-07-25 01:34:29 +02:00
const id = await backups . add ( data ) ;
2025-09-12 09:48:37 +02:00
const snapshotPath = addFileExtension ( backupSite , 'snapshot/mail' ) ;
2025-10-20 13:22:51 +02:00
const copyStartTime = Date . now ( ) ;
2025-09-12 09:48:37 +02:00
const [ error ] = await safe ( copy ( backupSite , snapshotPath , remotePath , progressCallback ) ) ;
2025-07-25 01:34:29 +02:00
const state = error ? backups . BACKUP _STATE _ERROR : backups . BACKUP _STATE _NORMAL ;
2025-10-20 13:22:51 +02:00
if ( ! error ) stats . copy = { startTime : copyStartTime , duration : Date . now ( ) - copyStartTime } ;
await backups . update ( { id } , { stats , state } ) ;
2022-04-05 13:11:30 +02:00
if ( error ) throw error ;
2022-04-04 14:13:27 -07:00
2025-10-20 13:22:51 +02:00
return { id , stats } ;
2021-09-26 18:37:04 -07:00
}
2025-09-12 09:48:37 +02:00
async function downloadMail ( backupSite , remotePath , progressCallback ) {
assert . strictEqual ( typeof backupSite , 'object' ) ;
2025-08-01 23:20:51 +02:00
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-09-12 09:48:37 +02:00
await download ( backupSite , 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-09-12 09:48:37 +02:00
async function fullBackup ( backupSiteId , options , progressCallback ) {
assert . strictEqual ( typeof backupSiteId , 'string' ) ;
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof options , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
2025-09-12 09:48:37 +02:00
const backupSite = await backupSites . get ( backupSiteId ) ;
if ( ! backupSite ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Backup site not found' ) ;
2025-07-24 19:02:02 +02:00
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
2025-10-10 12:55:03 +02:00
const appBackupsMap = new Map ( ) ; // id -> stats
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
}
2025-09-22 17:59:26 +02:00
if ( ! backupSites . hasContent ( backupSite , app . id ) ) {
debug ( ` fullBackup: skipped backup ${ app . fqdn } ( ${ i + 1 } / ${ allApps . length } ) as it is not in site contents ` ) ;
continue ;
}
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-10-10 12:55:03 +02:00
const [ appBackupError , appBackup ] = await safe ( backupAppWithTag ( app , backupSite , 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 ;
2025-10-10 12:55:03 +02:00
if ( appBackup ) appBackupsMap . set ( appBackup . id , appBackup . stats ) ; // 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
2025-10-10 12:55:03 +02:00
if ( ! backupSites . hasContent ( backupSite , 'box' ) ) return [ ... appBackupsMap . keys ( ) ] ;
2025-09-22 13:27:26 +02: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-10-10 12:55:03 +02:00
const mailBackup = await backupMailWithTag ( backupSite , tag , options , ( progress ) => progressCallback ( { percent , message : progress . message } ) ) ;
appBackupsMap . set ( mailBackup . id , mailBackup . stats ) ;
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
2025-10-10 12:55:03 +02:00
const backupId = await backupBox ( backupSite , appBackupsMap , 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-09-12 09:48:37 +02:00
async function appBackup ( appId , backupSiteId , options , progressCallback ) {
2025-07-18 10:56:52 +02:00
assert . strictEqual ( typeof appId , 'string' ) ;
2025-09-12 09:48:37 +02:00
assert . strictEqual ( typeof backupSiteId , '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-09-12 09:48:37 +02:00
const backupSite = await backupSites . get ( backupSiteId ) ;
if ( ! backupSite ) throw new BoxError ( BoxError . EXTERNAL _ERROR , 'Backup site not found' ) ;
2025-07-24 19:02:02 +02:00
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-10-16 13:00:08 +02:00
const backup = await backupApp ( app , backupSite , options , progressCallback ) ; // { id, stats }
2025-07-18 10:56:52 +02:00
await progressCallback ( { percent : 100 , message : ` app ${ app . fqdn } backup finished. Took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` } ) ;
2025-10-10 12:55:03 +02:00
return backup . id ;
2025-07-18 10:56:52 +02:00
}