Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import apps from './apps.js' ;
import assert from 'node:assert' ;
2026-02-14 15:43:24 +01:00
import backupFormats from './backupformats.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import backups from './backups.js' ;
2026-02-14 15:43:24 +01:00
import backupSites from './backupsites.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import BoxError from './boxerror.js' ;
import constants from './constants.js' ;
import crypto from 'node:crypto' ;
import DataLayout from './datalayout.js' ;
2026-02-14 15:43:24 +01:00
import database from './database.js' ;
2026-03-12 22:55:28 +05:30
import logger from './logger.js' ;
2026-02-14 15:43:24 +01:00
import df from './df.js' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import locks from './locks.js' ;
import path from 'node:path' ;
import paths from './paths.js' ;
import { Readable } from 'node:stream' ;
2026-04-01 09:40:28 +02:00
import safe from '@cloudron/safetydance' ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
import services from './services.js' ;
import shellModule from './shell.js' ;
import stream from 'stream/promises' ;
import util from 'util' ;
2026-03-12 23:23:23 +05:30
const { log } = logger ( 'backuptask' ) ;
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const shell = shellModule ( 'backuptask' ) ;
2025-10-08 20:11:55 +02:00
Migrate codebase from CommonJS to ES Modules
- Convert all require()/module.exports to import/export across 260+ files
- Add "type": "module" to package.json to enable ESM by default
- Add migrations/package.json with "type": "commonjs" to keep db-migrate compatible
- Convert eslint.config.js to ESM with sourceType: "module"
- Replace __dirname/__filename with import.meta.dirname/import.meta.filename
- Replace require.main === module with process.argv[1] === import.meta.filename
- Remove 'use strict' directives (implicit in ESM)
- Convert dynamic require() in switch statements to static import lookup maps
(dns.js, domains.js, backupformats.js, backupsites.js, network.js)
- Extract self-referencing exports.CONSTANT patterns into standalone const
declarations (apps.js, services.js, locks.js, users.js, mail.js, etc.)
- Lazify SERVICES object in services.js to avoid circular dependency TDZ issues
- Add clearMailQueue() to mailer.js for ESM-safe queue clearing in tests
- Add _setMockApp() to ldapserver.js for ESM-safe test mocking
- Add _setMockResolve() wrapper to dig.js for ESM-safe DNS mocking in tests
- Convert backupupload.js to use dynamic imports so --check exits before
loading the module graph (which requires BOX_ENV)
- Update check-install to use ESM import for infra_version.js
- Convert scripts/ (hotfix, release, remote_hotfix.js, find-unused-translations)
- All 1315 tests passing
Migration stats (AI-assisted using Cursor with Claude):
- Wall clock time: ~3-4 hours
- Assistant completions: ~80-100
- Estimated token usage: ~1-2M tokens
Co-authored-by: Cursor <cursoragent@cursor.com>
2026-02-14 09:53:14 +01:00
const BACKUP _UPLOAD _CMD = path . join ( import . meta . dirname , 'scripts/backupupload.js' ) ;
2021-07-14 11:07:19 -07:00
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 ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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 ( ) ) {
2026-03-12 22:55:28 +05:30
log ( ` 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' } ) ) ;
2026-03-12 22:55:28 +05:30
if ( duError ) log ( ` checkPreconditions: du error for ${ localPath } . code: ${ duError . code } stderror: ${ duError . stderr } ` ) ;
2024-11-06 14:53:41 +01:00
used += parseInt ( duError ? duError . stdout : result , 10 ) ;
2022-10-02 17:22:44 +02:00
}
2026-03-12 22:55:28 +05:30
log ( ` 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 ` ) ;
2026-01-21 08:58:30 +01:00
await stream . pipeline ( integrityDataStream , integrityUploader . createStream ( ) ) ;
2025-08-15 14:33:31 +05:30
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' ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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 ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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' ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( '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 ` ) ;
2026-03-12 22:55:28 +05:30
log ( 'restore: database imported' ) ;
remove global lock
Currently, the update/apptask/fullbackup/platformstart take a
global lock and cannot run in parallel. This causes situations
where when a user tries to trigger an apptask, it says "waiting for
backup to finish..." etc
The solution is to let them run in parallel. We need a lock at the
app level as app operations running in parallel would be bad (tm).
In addition, the update task needs a lock just for the update part.
We also need multi-process locks. Running tasks as processes is core
to our "kill" strategy.
Various inter process locks were explored:
* node's IPC mechanism with process.send(). But this only works for direct node.js
children. taskworker is run via sudo and the IPC does not work.
* File lock using O_EXCL. Basic ideas to create lock files. While file creation
can be done atomically, it becomes complicated to clean up lock files when
the tasks crash. We need a way to know what locks were held by the crashing task.
flock and friends are not built-into node.js
* sqlite/redis were options but introduce additional deps
* Settled on MySQL based locking. Initial plan was to have row locks
or table locks. Each row is a kind of lock. While implementing, it was found that
we need many types of locks (and not just update lock and app locks). For example,
we need locks for each task type, so that only one task type is active at a time.
* Instead of rows, we can just lock table and have a json blob in it. This hit a road
block that LOCK TABLE is per session and our db layer cannot handle this easily! i.e
when issing two db.query() it might use two different connections from the pool. We have to
expose the connection, release connection etc.
* Next idea was atomic blob update of the blob checking if old blob was same. This approach,
was finally refined into a version field.
Phew!
2024-12-07 14:35:45 +01:00
2025-07-14 15:01:30 +02:00
await locks . releaseAll ( ) ; // clear the locks table in database
2021-07-14 11:07:19 -07:00
}
2021-09-16 13:59:03 -07:00
async function downloadApp ( app , restoreConfig , progressCallback ) {
2021-07-14 11:07:19 -07:00
assert . strictEqual ( typeof app , 'object' ) ;
assert . strictEqual ( typeof restoreConfig , 'object' ) ;
assert . strictEqual ( typeof progressCallback , 'function' ) ;
const appDataDir = safe . fs . realpathSync ( path . join ( paths . APPS _DATA _DIR , app . id ) ) ;
2021-09-16 13:59:03 -07:00
if ( ! appDataDir ) throw new BoxError ( BoxError . FS _ERROR , safe . error . message ) ;
2022-06-01 22:44:52 -07:00
const dataLayout = new DataLayout ( appDataDir , app . storageVolumeId ? [ { localDir : await apps . getStorageDir ( app ) , remoteDir : 'data' } ] : [ ] ) ;
2021-07-14 11:07:19 -07:00
const startTime = new Date ( ) ;
2025-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 ) ;
2026-03-12 22:55:28 +05:30
log ( '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 ) ;
2026-03-12 22:55:28 +05:30
log ( ` runBackupUpload: adjusting heap size to ${ heapSize } M ` ) ;
2021-07-14 11:07:19 -07:00
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 } ) ` } ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( ` 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 ` ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( ` 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 ) {
2026-03-12 22:55:28 +05:30
log ( ` copy: copy to ${ destRemotePath } errored. error: ${ copyError . message } ` ) ;
2023-01-17 10:43:17 +01:00
throw copyError ;
}
2026-03-12 22:55:28 +05:30
log ( ` 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 ) {
2026-03-12 22:55:28 +05:30
log ( ` copy: copy to ${ destRemotePath } errored. error: ${ copyChecksumError . message } ` ) ;
2025-08-11 19:30:22 +05:30
throw copyChecksumError ;
}
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( ` backupBox: rotating box snapshot of ${ backupSite . id } to id ${ remotePath } . ${ JSON . stringify ( stats ) } ` ) ;
2025-10-20 10:13:08 +02:00
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 ) ;
2026-02-21 19:42:52 +01:00
await services . runBackupCommand ( app ) ;
2021-09-16 13:59:03 -07:00
await services . backupAddons ( app , app . manifest . addons ) ;
2021-07-14 11:07:19 -07:00
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( ` 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
2026-03-12 22:55:28 +05:30
log ( ` uploadMailSnapshot: took ${ ( new Date ( ) - startTime ) / 1000 } seconds ` ) ;
2021-09-26 18:37:04 -07:00
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' ) ;
2026-03-12 22:55:28 +05:30
log ( ` backupMailWithTag: backing up mail with tag ${ tag } ` ) ;
2025-08-12 19:41:50 +05:30
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
2026-03-12 22:55:28 +05:30
log ( ` 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 ) ;
2026-03-12 22:55:28 +05:30
log ( 'downloadMail: time: %s' , ( new Date ( ) - startTime ) / 1000 ) ;
2021-09-26 18:37:04 -07:00
}
// 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 ) {
2026-03-12 22:55:28 +05:30
log ( ` 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 ) ) {
2026-03-12 22:55:28 +05:30
log ( ` fullBackup: skipped backup ${ app . fqdn } ( ${ i + 1 } / ${ allApps . length } ) as it is not in site contents ` ) ;
2025-09-22 17:59:26 +02:00
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 ( ) ;
2026-02-18 08:18:37 +01:00
const [ appBackupError , appBackupResult ] = await safe ( backupAppWithTag ( app , backupSite , tag , options , ( progress ) => progressCallback ( { percent , message : progress . message } ) ) ) ;
2026-03-12 22:55:28 +05:30
log ( ` 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 ;
2026-02-18 08:18:37 +01:00
if ( appBackupResult ) appBackupsMap . set ( appBackupResult . id , appBackupResult . 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
}
2026-02-14 15:43:24 +01:00
export default {
fullBackup ,
appBackup ,
restore ,
downloadApp ,
backupApp ,
downloadMail ,
upload ,
} ;