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 assert from 'assert';
|
|
|
|
|
import backups from './backups.js';
|
2026-02-14 15:43:24 +01:00
|
|
|
import backupFormats from './backupformats.js';
|
|
|
|
|
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 consumers from 'node:stream/consumers';
|
|
|
|
|
import crypto from 'node:crypto';
|
|
|
|
|
import debugModule from 'debug';
|
|
|
|
|
import safe from 'safetydance';
|
2025-08-15 16:09:58 +05:30
|
|
|
|
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 debug = debugModule('box:backupintegrity');
|
|
|
|
|
|
2025-10-08 20:11:55 +02:00
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
async function downloadBackupInfo(backupSite, backup) {
|
2025-09-12 09:48:37 +02:00
|
|
|
const stream = await backupSites.storageApi(backupSite).download(backupSite.config, `${backup.remotePath}.backupinfo`);
|
2025-08-15 16:09:58 +05:30
|
|
|
const buffer = await consumers.buffer(stream);
|
2026-02-08 11:17:27 +01:00
|
|
|
return buffer;
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
async function verify(backup, backupSite, progressCallback) {
|
|
|
|
|
assert.strictEqual(typeof backup, 'object');
|
|
|
|
|
assert.strictEqual(typeof backupSite, 'object');
|
|
|
|
|
assert.strictEqual(typeof progressCallback, 'function');
|
2025-08-15 16:09:58 +05:30
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
if (backup === null) return [`${backup.id} is missing in database`];
|
|
|
|
|
|
2026-02-15 14:31:09 +01:00
|
|
|
const stats = { startTime: Date.now(), duration: null };
|
|
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
const [downloadError, backupInfoBuffer] = await safe(downloadBackupInfo(backupSite, backup));
|
|
|
|
|
if (downloadError) {
|
|
|
|
|
const messages = [`Failed to download ${backup.remotePath}.backupinfo: ${downloadError.message}`];
|
2026-02-15 14:31:09 +01:00
|
|
|
stats.duration = Date.now() - stats.startTime;
|
|
|
|
|
return { stats, messages };
|
2026-02-08 11:17:27 +01:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const validSignature = crypto.verify(null /* algo */, backupInfoBuffer, backupSite.integrityKeyPair.publicKey, Buffer.from(backup.integrity.signature, 'hex'));
|
2026-02-15 21:50:01 +01:00
|
|
|
progressCallback({ message: `${backup.remotePath}.backupinfo has ${validSignature ? 'valid': 'invalid' } signature`});
|
2025-08-15 16:09:58 +05:30
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
const backupInfo = JSON.parse(backupInfoBuffer.toString('utf8'));
|
2025-08-15 16:09:58 +05:30
|
|
|
const integrityMap = new Map(Object.entries(backupInfo));
|
|
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
const [verifyError, verifyMessages] = await safe(backupFormats.api(backupSite.format).verify(backupSite, backup.remotePath, integrityMap, progressCallback));
|
2026-02-15 21:50:01 +01:00
|
|
|
progressCallback({ message: `Verification of ${backup.remotePath} done` });
|
2025-08-15 16:09:58 +05:30
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
const messages = [];
|
|
|
|
|
if (!validSignature) messages.push(`${backup.remotePath}.backupinfo has invalid signature`);
|
|
|
|
|
if (verifyError) messages.push(`Failed to verify ${backup.remotePath}: ${verifyError.message}`);
|
|
|
|
|
if (verifyMessages) messages.push(...verifyMessages);
|
|
|
|
|
|
2026-03-03 16:13:44 +05:30
|
|
|
debug(`verified: ${backup.remotePath} ${JSON.stringify(messages, null, 4)}`);
|
2026-02-08 11:17:27 +01:00
|
|
|
|
2026-02-15 14:31:09 +01:00
|
|
|
stats.duration = Date.now() - stats.startTime;
|
2026-02-15 21:50:01 +01:00
|
|
|
return { stats, messages: messages.slice(0, 50) }; // keep rsync fails to 50 to not overflow db
|
2025-08-15 16:09:58 +05:30
|
|
|
}
|
2025-10-07 18:42:51 +02:00
|
|
|
|
2026-02-08 11:17:27 +01:00
|
|
|
async function check(backupId, progressCallback) {
|
|
|
|
|
const backup = await backups.get(backupId);
|
|
|
|
|
if (!backup) throw new BoxError(BoxError.BAD_FIELD, 'Backup not found');
|
|
|
|
|
|
|
|
|
|
const backupSite = await backupSites.get(backup.siteId);
|
|
|
|
|
if (!backupSite) throw new BoxError(BoxError.BAD_FIELD, 'Backup site not found');
|
|
|
|
|
|
2026-03-04 18:30:16 +05:30
|
|
|
const total = backup.dependsOn.length + 1;
|
|
|
|
|
let completed = 0;
|
|
|
|
|
|
2026-02-15 14:31:09 +01:00
|
|
|
const aggregatedStats = { startTime: Date.now(), duration: null };
|
2026-02-08 11:17:27 +01:00
|
|
|
const aggregatedMessages = [];
|
|
|
|
|
for (const depId of backup.dependsOn) {
|
|
|
|
|
const depBackup = await backups.get(depId);
|
2026-03-04 18:30:16 +05:30
|
|
|
progressCallback({ percent: Math.round(completed / total * 100), message: `Verifying ${depBackup.remotePath}` });
|
2026-02-15 14:31:09 +01:00
|
|
|
const result = await verify(depBackup, backupSite, progressCallback); // { stats, messages }
|
2026-03-04 18:30:16 +05:30
|
|
|
completed++;
|
2026-02-08 11:17:27 +01:00
|
|
|
|
2026-02-15 21:50:01 +01:00
|
|
|
await backups.setIntegrityResult(depBackup, result.messages.length === 0 ? 'passed' : 'failed', result);
|
2026-02-15 14:31:09 +01:00
|
|
|
if (result.messages.length) aggregatedMessages.push(`Integrity check of dependent backup ${depBackup.remotePath} failed`);
|
2026-02-08 11:17:27 +01:00
|
|
|
}
|
|
|
|
|
|
2026-03-04 18:30:16 +05:30
|
|
|
progressCallback({ percent: Math.round(completed / total * 100), message: `Verifying ${backup.remotePath}` });
|
2026-02-15 14:31:09 +01:00
|
|
|
const result = await verify(backup, backupSite, progressCallback);
|
|
|
|
|
aggregatedStats.duration = Date.now() - aggregatedStats.startTime;
|
|
|
|
|
aggregatedMessages.push(...result.messages);
|
2026-02-15 23:38:05 +01:00
|
|
|
const status = aggregatedMessages.length === 0 ? 'passed' : 'failed';
|
|
|
|
|
await backups.setIntegrityResult(backup, status, { stats: aggregatedStats, messages: aggregatedMessages });
|
|
|
|
|
return status;
|
2026-02-08 11:17:27 +01:00
|
|
|
}
|
2026-02-14 15:43:24 +01:00
|
|
|
|
|
|
|
|
export default {
|
|
|
|
|
check
|
|
|
|
|
};
|