diff --git a/app/client/packages/rts/src/ctl/backup.test.ts b/app/client/packages/rts/src/ctl/backup.test.ts index 2f360bfd8fee..4dddd337b53c 100644 --- a/app/client/packages/rts/src/ctl/backup.test.ts +++ b/app/client/packages/rts/src/ctl/backup.test.ts @@ -230,9 +230,8 @@ describe("Backup Tests", () => { return password; }); - const password_res = backup.getEncryptionPasswordFromUser(); - expect(password_res).toEqual(-1); + expect(() => backup.getEncryptionPasswordFromUser()).toThrow(); }); test("Get encrypted archive path", async () => { diff --git a/app/client/packages/rts/src/ctl/backup.ts b/app/client/packages/rts/src/ctl/backup.ts index 39a36294f921..772ef3cd5869 100644 --- a/app/client/packages/rts/src/ctl/backup.ts +++ b/app/client/packages/rts/src/ctl/backup.ts @@ -10,26 +10,49 @@ import readlineSync from "readline-sync"; const command_args = process.argv.slice(3); -export async function run() { - const timestamp = getTimeStampInISO(); - let errorCode = 0; - let backupRootPath, archivePath, encryptionPassword; - let encryptArchive = false; +class BackupState { + readonly initAt: string = getTimeStampInISO(); + readonly errors: string[] = []; + + backupRootPath: string = ""; + archivePath: string = ""; + encryptionPassword: string = ""; + + isEncryptionEnabled() { + return !!this.encryptionPassword; + } +} + +export async function run() { await utils.ensureSupervisorIsRunning(); + const state: BackupState = new BackupState(); + try { + // PRE-BACKUP console.log("Available free space at /appsmith-stacks"); - const availSpaceInBytes = - getAvailableBackupSpaceInBytes("/appsmith-stacks"); + const availSpaceInBytes: number = + await getAvailableBackupSpaceInBytes("/appsmith-stacks"); console.log("\n"); checkAvailableBackupSpace(availSpaceInBytes); - backupRootPath = await generateBackupRootPath(); - const backupContentsPath = getBackupContentsPath(backupRootPath, timestamp); + if ( + !command_args.includes("--non-interactive") && + tty.isatty((process.stdout as any).fd) + ) { + state.encryptionPassword = getEncryptionPasswordFromUser(); + } + + state.backupRootPath = await generateBackupRootPath(); + const backupContentsPath: string = getBackupContentsPath( + state.backupRootPath, + state.initAt, + ); + // BACKUP await fsPromises.mkdir(backupContentsPath); await exportDatabase(backupContentsPath); @@ -38,30 +61,18 @@ export async function run() { await createManifestFile(backupContentsPath); - if ( - !command_args.includes("--non-interactive") && - tty.isatty((process.stdout as any).fd) - ) { - encryptionPassword = getEncryptionPasswordFromUser(); - - if (encryptionPassword == -1) { - throw new Error( - "Backup process aborted because a valid enctyption password could not be obtained from the user", - ); - } - - encryptArchive = true; - } - - await exportDockerEnvFile(backupContentsPath, encryptArchive); + await exportDockerEnvFile(backupContentsPath, state.isEncryptionEnabled()); - archivePath = await createFinalArchive(backupRootPath, timestamp); + state.archivePath = await createFinalArchive( + state.backupRootPath, + state.initAt, + ); - // shell.exec("openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -in " + archivePath + " -out " + archivePath + ".enc"); - if (encryptArchive) { + // POST-BACKUP + if (state.isEncryptionEnabled()) { const encryptedArchivePath = await encryptBackupArchive( - archivePath, - encryptionPassword, + state.archivePath, + state.encryptionPassword, ); await logger.backup_info( @@ -69,12 +80,15 @@ export async function run() { encryptedArchivePath, ); - if (archivePath != null) { - await fsPromises.rm(archivePath, { recursive: true, force: true }); + if (state.archivePath != null) { + await fsPromises.rm(state.archivePath, { + recursive: true, + force: true, + }); } } else { await logger.backup_info( - "Finished creating a backup archive at " + archivePath, + "Finished creating a backup archive at " + state.archivePath, ); console.log( "********************************************************* IMPORTANT!!! *************************************************************", @@ -90,11 +104,13 @@ export async function run() { ); } - await fsPromises.rm(backupRootPath, { recursive: true, force: true }); + await fsPromises.rm(state.backupRootPath, { recursive: true, force: true }); - await logger.backup_info("Finished taking a backup at " + archivePath); + await logger.backup_info( + "Finished taking a backup at " + state.archivePath, + ); } catch (err) { - errorCode = 1; + process.exitCode = 1; await logger.backup_error(err.stack); if (command_args.includes("--error-mail")) { @@ -106,27 +122,36 @@ export async function run() { Constants.DURATION_BETWEEN_BACKUP_ERROR_MAILS_IN_MILLI_SEC < currentTS ) { - await mailer.sendBackupErrorToAdmins(err, timestamp); + await mailer.sendBackupErrorToAdmins(err, state.initAt); await utils.updateLastBackupErrorMailSentInMilliSec(currentTS); } } } finally { - if (backupRootPath != null) { - await fsPromises.rm(backupRootPath, { recursive: true, force: true }); + if (state.backupRootPath != null) { + await fsPromises.rm(state.backupRootPath, { + recursive: true, + force: true, + }); } - if (encryptArchive) { - if (archivePath != null) { - await fsPromises.rm(archivePath, { recursive: true, force: true }); + if (state.isEncryptionEnabled()) { + if (state.archivePath != null) { + await fsPromises.rm(state.archivePath, { + recursive: true, + force: true, + }); } } await postBackupCleanup(); - process.exit(errorCode); + process.exit(); } } -export async function encryptBackupArchive(archivePath, encryptionPassword) { +export async function encryptBackupArchive( + archivePath: string, + encryptionPassword: string, +) { const encryptedArchivePath = archivePath + ".enc"; await utils.execCommand([ @@ -135,7 +160,7 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) { "-aes-256-cbc", "-pbkdf2", "-iter", - 100000, + "100000", "-in", archivePath, "-out", @@ -147,17 +172,17 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) { return encryptedArchivePath; } -export function getEncryptionPasswordFromUser() { +export function getEncryptionPasswordFromUser(): string { for (const attempt of [1, 2, 3]) { if (attempt > 1) { console.log("Retry attempt", attempt); } - const encryptionPwd1 = readlineSync.question( + const encryptionPwd1: string = readlineSync.question( "Enter a password to encrypt the backup archive: ", { hideEchoBack: true }, ); - const encryptionPwd2 = readlineSync.question( + const encryptionPwd2: string = readlineSync.question( "Enter the above password again: ", { hideEchoBack: true }, ); @@ -179,16 +204,18 @@ export function getEncryptionPasswordFromUser() { "Aborting backup process, failed to obtain valid encryption password.", ); - return -1; + throw new Error( + "Backup process aborted because a valid encryption password could not be obtained from the user", + ); } -async function exportDatabase(destFolder) { +async function exportDatabase(destFolder: string) { console.log("Exporting database"); await executeMongoDumpCMD(destFolder, utils.getDburl()); console.log("Exporting database done."); } -async function createGitStorageArchive(destFolder) { +async function createGitStorageArchive(destFolder: string) { console.log("Creating git-storage archive"); const gitRoot = getGitRoot(process.env.APPSMITH_GIT_ROOT); @@ -198,7 +225,7 @@ async function createGitStorageArchive(destFolder) { console.log("Created git-storage archive"); } -async function createManifestFile(path) { +async function createManifestFile(path: string) { const version = await utils.getCurrentAppsmithVersion(); const manifest_data = { appsmithVersion: version, @@ -211,7 +238,10 @@ async function createManifestFile(path) { ); } -async function exportDockerEnvFile(destFolder, encryptArchive) { +async function exportDockerEnvFile( + destFolder: string, + encryptArchive: boolean, +) { console.log("Exporting docker environment file"); const content = await fsPromises.readFile( "/appsmith-stacks/configuration/docker.env", @@ -231,7 +261,10 @@ async function exportDockerEnvFile(destFolder, encryptArchive) { console.log("Exporting docker environment file done."); } -export async function executeMongoDumpCMD(destFolder, appsmithMongoURI) { +export async function executeMongoDumpCMD( + destFolder: string, + appsmithMongoURI: string, +) { return await utils.execCommand([ "mongodump", `--uri=${appsmithMongoURI}`, @@ -240,7 +273,7 @@ export async function executeMongoDumpCMD(destFolder, appsmithMongoURI) { ]); // generate cmd } -async function createFinalArchive(destFolder, timestamp) { +async function createFinalArchive(destFolder: string, timestamp: string) { console.log("Creating final archive"); const archive = `${Constants.BACKUP_PATH}/appsmith-backup-${timestamp}.tar.gz`; @@ -263,7 +296,7 @@ async function createFinalArchive(destFolder, timestamp) { async function postBackupCleanup() { console.log("Starting the cleanup task after taking a backup."); const backupArchivesLimit = getBackupArchiveLimit( - process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT, + parseInt(process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT, 10), ); const backupFiles = await utils.listLocalBackupFiles(); @@ -276,16 +309,16 @@ async function postBackupCleanup() { console.log("Cleanup task completed."); } -export async function executeCopyCMD(srcFolder, destFolder) { +export async function executeCopyCMD(srcFolder: string, destFolder: string) { return await utils.execCommand([ "ln", "-s", srcFolder, - destFolder + "/git-storage", + path.join(destFolder, "git-storage"), ]); } -export function getGitRoot(gitRoot?) { +export function getGitRoot(gitRoot?: string | undefined) { if (gitRoot == null || gitRoot === "") { gitRoot = "/appsmith-stacks/git-storage"; } @@ -297,11 +330,14 @@ export async function generateBackupRootPath() { return fsPromises.mkdtemp(path.join(os.tmpdir(), "appsmithctl-backup-")); } -export function getBackupContentsPath(backupRootPath, timestamp) { +export function getBackupContentsPath( + backupRootPath: string, + timestamp: string, +): string { return backupRootPath + "/appsmith-backup-" + timestamp; } -export function removeSensitiveEnvData(content) { +export function removeSensitiveEnvData(content: string): string { // Remove encryption and Mongodb data from docker.env const output_lines = []; @@ -318,14 +354,14 @@ export function removeSensitiveEnvData(content) { return output_lines.join("\n"); } -export function getBackupArchiveLimit(backupArchivesLimit?) { - if (!backupArchivesLimit) - backupArchivesLimit = Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT; - - return backupArchivesLimit; +export function getBackupArchiveLimit(backupArchivesLimit?: number): number { + return backupArchivesLimit || Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT; } -export async function removeOldBackups(backupFiles, backupArchivesLimit) { +export async function removeOldBackups( + backupFiles: string[], + backupArchivesLimit: number, +) { while (backupFiles.length > backupArchivesLimit) { const fileName = backupFiles.shift(); @@ -339,13 +375,15 @@ export function getTimeStampInISO() { return new Date().toISOString().replace(/:/g, "-"); } -export async function getAvailableBackupSpaceInBytes(path) { +export async function getAvailableBackupSpaceInBytes( + path: string, +): Promise { const stat = await fsPromises.statfs(path); return stat.bsize * stat.bfree; } -export function checkAvailableBackupSpace(availSpaceInBytes) { +export function checkAvailableBackupSpace(availSpaceInBytes: number) { if (availSpaceInBytes < Constants.MIN_REQUIRED_DISK_SPACE_IN_BYTES) { throw new Error( "Not enough space available at /appsmith-stacks. Please ensure availability of at least 2GB to backup successfully.", diff --git a/app/client/packages/rts/src/ctl/restore.ts b/app/client/packages/rts/src/ctl/restore.ts index adf93898efab..42e9692d6439 100644 --- a/app/client/packages/rts/src/ctl/restore.ts +++ b/app/client/packages/rts/src/ctl/restore.ts @@ -57,7 +57,10 @@ async function getBackupFileName() { } } -async function decryptArchive(encryptedFilePath, backupFilePath) { +async function decryptArchive( + encryptedFilePath: string, + backupFilePath: string, +) { console.log("Enter the password to decrypt the backup archive:"); for (const attempt of [1, 2, 3]) { @@ -75,7 +78,7 @@ async function decryptArchive(encryptedFilePath, backupFilePath) { "-aes-256-cbc", "-pbkdf2", "-iter", - 100000, + "100000", "-in", encryptedFilePath, "-out", @@ -93,7 +96,7 @@ async function decryptArchive(encryptedFilePath, backupFilePath) { return false; } -async function extractArchive(backupFilePath, restoreRootPath) { +async function extractArchive(backupFilePath: string, restoreRootPath: string) { console.log("Extracting the Appsmith backup archive at " + backupFilePath); await utils.execCommand([ "tar", @@ -105,7 +108,7 @@ async function extractArchive(backupFilePath, restoreRootPath) { console.log("Extracting the backup archive completed"); } -async function restoreDatabase(restoreContentsPath, dbUrl) { +async function restoreDatabase(restoreContentsPath: string, dbUrl: string) { console.log("Restoring database..."); const cmd = [ "mongorestore", @@ -136,9 +139,9 @@ async function restoreDatabase(restoreContentsPath, dbUrl) { } async function restoreDockerEnvFile( - restoreContentsPath, - backupName, - overwriteEncryptionKeys, + restoreContentsPath: string, + backupName: string, + overwriteEncryptionKeys: boolean, ) { console.log("Restoring docker environment file"); const dockerEnvFile = "/appsmith-stacks/configuration/docker.env"; @@ -227,9 +230,11 @@ async function restoreDockerEnvFile( console.log("Restoring docker environment file completed"); } -async function restoreGitStorageArchive(restoreContentsPath, backupName) { +async function restoreGitStorageArchive( + restoreContentsPath: string, + backupName: string, +) { console.log("Restoring git-storage archive"); - // TODO: Consider APPSMITH_GIT_ROOT env for later iterations const gitRoot = "/appsmith-stacks/git-storage"; await utils.execCommand(["mv", gitRoot, gitRoot + "-" + backupName]); @@ -241,7 +246,7 @@ async function restoreGitStorageArchive(restoreContentsPath, backupName) { console.log("Restoring git-storage archive completed"); } -async function checkRestoreVersionCompatability(restoreContentsPath) { +async function checkRestoreVersionCompatability(restoreContentsPath: string) { const currentVersion = await utils.getCurrentAppsmithVersion(); const manifest_data = await fsPromises.readFile( restoreContentsPath + "/manifest.json", @@ -280,7 +285,7 @@ async function checkRestoreVersionCompatability(restoreContentsPath) { } } -async function getBackupDatabaseName(restoreContentsPath) { +async function getBackupDatabaseName(restoreContentsPath: string) { let db_name = "appsmith"; if (command_args.includes("--backup-db-name")) { @@ -307,10 +312,9 @@ async function getBackupDatabaseName(restoreContentsPath) { } export async function run() { - let errorCode = 0; let cleanupArchive = false; let overwriteEncryptionKeys = true; - let backupFilePath; + let backupFilePath: string; await utils.ensureSupervisorIsRunning(); @@ -318,7 +322,7 @@ export async function run() { let backupFileName = await getBackupFileName(); if (backupFileName == null) { - process.exit(errorCode); + process.exit(); } else { backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName); @@ -342,7 +346,7 @@ export async function run() { "You have entered the incorrect password multiple times. Aborting the restore process.", ); await fsPromises.rm(backupFilePath, { force: true }); - process.exit(errorCode); + process.exit(); } } @@ -372,17 +376,17 @@ export async function run() { } } catch (err) { console.log(err); - errorCode = 1; + process.exitCode = 1; } finally { if (cleanupArchive) { await fsPromises.rm(backupFilePath, { force: true }); } await utils.start(["backend", "rts"]); - process.exit(errorCode); + process.exit(); } } -function isArchiveEncrypted(backupFilePath) { +function isArchiveEncrypted(backupFilePath: string) { return backupFilePath.endsWith(".enc"); }