diff --git a/app/client/packages/rts/package.json b/app/client/packages/rts/package.json index 8f27c36d6bef..07d9ffec6985 100644 --- a/app/client/packages/rts/package.json +++ b/app/client/packages/rts/package.json @@ -36,6 +36,7 @@ "devDependencies": { "@types/express": "^4.17.14", "@types/jest": "^29.2.3", + "@types/node": "*", "@types/nodemailer": "^6.4.17", "@types/readline-sync": "^1.4.8", "jest": "^29.3.1", diff --git a/app/client/packages/rts/src/ctl/.eslintrc.json b/app/client/packages/rts/src/ctl/.eslintrc.json index 89c79ccd3a38..1d9fa7e2b880 100644 --- a/app/client/packages/rts/src/ctl/.eslintrc.json +++ b/app/client/packages/rts/src/ctl/.eslintrc.json @@ -1,16 +1,6 @@ { - "extends": ["../../../../.eslintrc.base.json"], + "extends": ["../../.eslintrc.json"], "rules": { - "@typescript-eslint/ban-ts-comment": "off", - "@typescript-eslint/prefer-nullish-coalescing": "off", - "@typescript-eslint/strict-boolean-expressions": "off", - "@typescript-eslint/no-explicit-any": "off", - "testing-library/no-debugging-utils": "off", - "@typescript-eslint/no-var-requires": "off", - "padding-line-between-statements": "off", - "no-console": "off", - "@typescript-eslint/promise-function-async": "off", - "@typescript-eslint/no-unused-vars": "off", - "sort-destructure-keys/sort-destructure-keys": "off" + "no-console": "off" } } diff --git a/app/client/packages/rts/src/ctl/backup.test.ts b/app/client/packages/rts/src/ctl/backup.test.ts index 30749eea2ce8..2f360bfd8fee 100644 --- a/app/client/packages/rts/src/ctl/backup.test.ts +++ b/app/client/packages/rts/src/ctl/backup.test.ts @@ -6,7 +6,6 @@ jest.mock("./utils", () => ({ import * as backup from "./backup"; import * as Constants from "./constants"; import os from "os"; -// @ts-ignore import fsPromises from "fs/promises"; import * as utils from "./utils"; import readlineSync from "readline-sync"; @@ -21,16 +20,19 @@ describe("Backup Tests", () => { test("Available Space in /appsmith-stacks volume in Bytes", async () => { const res = expect(await backup.getAvailableBackupSpaceInBytes("/")); + res.toBeGreaterThan(1024 * 1024); }); it("Check the constant is 2 GB", () => { const size = 2 * 1024 * 1024 * 1024; + expect(Constants.MIN_REQUIRED_DISK_SPACE_IN_BYTES).toBe(size); }); it("Should throw Error when the available size is below MIN_REQUIRED_DISK_SPACE_IN_BYTES", () => { const size = Constants.MIN_REQUIRED_DISK_SPACE_IN_BYTES - 1; + expect(() => backup.checkAvailableBackupSpace(size)).toThrow(); }); @@ -48,12 +50,14 @@ describe("Backup Tests", () => { os.tmpdir = jest.fn().mockReturnValue("temp/dir"); fsPromises.mkdtemp = jest.fn().mockImplementation((a) => a); const res = await backup.generateBackupRootPath(); + expect(res).toBe("temp/dir/appsmithctl-backup-"); }); test("Test backup contents path generation", () => { const root = "/rootDir"; const timestamp = "0000-00-0T00-00-00.00Z"; + expect(backup.getBackupContentsPath(root, timestamp)).toBe( "/rootDir/appsmith-backup-0000-00-0T00-00-00.00Z", ); @@ -65,6 +69,7 @@ describe("Backup Tests", () => { const cmd = "mongodump --uri=mongodb://username:password@host/appsmith --archive=/dest/mongodb-data.gz --gzip"; const res = await backup.executeMongoDumpCMD(dest, appsmithMongoURI); + expect(res).toBe(cmd); console.log(res); }); @@ -86,6 +91,7 @@ describe("Backup Tests", () => { const dest = "/destdir"; const cmd = "ln -s /appsmith-stacks/git-storage /destdir/git-storage"; const res = await backup.executeCopyCMD(gitRoot, dest); + expect(res).toBe(cmd); console.log(res); }); @@ -99,6 +105,7 @@ describe("Backup Tests", () => { } }); const res = await utils.getCurrentAppsmithVersion(); + expect(res).toBe("v0.0.0-SNAPSHOT"); }); @@ -130,10 +137,12 @@ describe("Backup Tests", () => { test("Cleanup Backups when limit is 4 and there are 5 files", async () => { const backupArchivesLimit = 4; + fsPromises.rm = jest.fn().mockImplementation(async (a) => console.log(a)); const backupFiles = ["file1", "file2", "file3", "file4", "file5"]; const expectedBackupFiles = ["file2", "file3", "file4", "file5"]; const res = await backup.removeOldBackups(backupFiles, backupArchivesLimit); + console.log(res); expect(res).toEqual(expectedBackupFiles); @@ -141,10 +150,12 @@ describe("Backup Tests", () => { test("Cleanup Backups when limit is 2 and there are 5 files", async () => { const backupArchivesLimit = 2; + fsPromises.rm = jest.fn().mockImplementation(async (a) => console.log(a)); const backupFiles = ["file1", "file2", "file3", "file4", "file5"]; const expectedBackupFiles = ["file4", "file5"]; const res = await backup.removeOldBackups(backupFiles, backupArchivesLimit); + console.log(res); expect(res).toEqual(expectedBackupFiles); @@ -152,10 +163,12 @@ describe("Backup Tests", () => { test("Cleanup Backups when limit is 4 and there are 4 files", async () => { const backupArchivesLimit = 4; + fsPromises.rm = jest.fn().mockImplementation(async (a) => console.log(a)); const backupFiles = ["file1", "file2", "file3", "file4"]; const expectedBackupFiles = ["file1", "file2", "file3", "file4"]; const res = await backup.removeOldBackups(backupFiles, backupArchivesLimit); + console.log(res); expect(res).toEqual(expectedBackupFiles); @@ -163,10 +176,12 @@ describe("Backup Tests", () => { test("Cleanup Backups when limit is 4 and there are 2 files", async () => { const backupArchivesLimit = 4; + fsPromises.rm = jest.fn().mockImplementation(async (a) => console.log(a)); const backupFiles = ["file1", "file2"]; const expectedBackupFiles = ["file1", "file2"]; const res = await backup.removeOldBackups(backupFiles, backupArchivesLimit); + console.log(res); expect(res).toEqual(expectedBackupFiles); @@ -174,26 +189,31 @@ describe("Backup Tests", () => { test("Cleanup Backups when limit is 2 and there is 1 file", async () => { const backupArchivesLimit = 4; + fsPromises.rm = jest.fn().mockImplementation(async (a) => console.log(a)); const backupFiles = ["file1"]; const expectedBackupFiles = ["file1"]; const res = await backup.removeOldBackups(backupFiles, backupArchivesLimit); + console.log(res); expect(res).toEqual(expectedBackupFiles); }); test("Cleanup Backups when limit is 2 and there is no file", async () => { const backupArchivesLimit = 4; + fsPromises.rm = jest.fn().mockImplementation(async (a) => console.log(a)); const backupFiles = []; const expectedBackupFiles = []; const res = await backup.removeOldBackups(backupFiles, backupArchivesLimit); + console.log(res); expect(res).toEqual(expectedBackupFiles); }); test("Test get encryption password from user prompt when both passwords are the same", async () => { const password = "password#4321"; + readlineSync.question = jest.fn().mockImplementation(() => password); const password_res = backup.getEncryptionPasswordFromUser(); @@ -202,10 +222,12 @@ describe("Backup Tests", () => { test("Test get encryption password from user prompt when both passwords are the different", async () => { const password = "password#4321"; + readlineSync.question = jest.fn().mockImplementation((a) => { if (a == "Enter the above password again: ") { return "pass"; } + return password; }); const password_res = backup.getEncryptionPasswordFromUser(); @@ -233,6 +255,7 @@ describe("Backup Tests", () => { archivePath, encryptionPassword, ); + console.log(res); expect(res).toEqual("/rootDir/appsmith-backup-0000-00-0T00-00-00.00Z.enc"); }); @@ -243,6 +266,7 @@ test("Get DB name from Mongo URI 1", async () => { "mongodb+srv://admin:password@test.cluster.mongodb.net/my_db_name?retryWrites=true&minPoolSize=1&maxPoolSize=10&maxIdleTimeMS=900000&authSource=admin"; const expectedDBName = "my_db_name"; const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri); + expect(dbName).toEqual(expectedDBName); }); @@ -251,6 +275,7 @@ test("Get DB name from Mongo URI 2", async () => { "mongodb+srv://admin:password@test.cluster.mongodb.net/test123?retryWrites=true&minPoolSize=1&maxPoolSize=10&maxIdleTimeMS=900000&authSource=admin"; const expectedDBName = "test123"; const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri); + expect(dbName).toEqual(expectedDBName); }); @@ -259,6 +284,7 @@ test("Get DB name from Mongo URI 3", async () => { "mongodb+srv://admin:password@test.cluster.mongodb.net/test123"; const expectedDBName = "test123"; const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri); + expect(dbName).toEqual(expectedDBName); }); @@ -266,5 +292,6 @@ test("Get DB name from Mongo URI 4", async () => { const mongodb_uri = "mongodb://appsmith:pAssW0rd!@localhost:27017/appsmith"; const expectedDBName = "appsmith"; const dbName = utils.getDatabaseNameFromMongoURI(mongodb_uri); + expect(dbName).toEqual(expectedDBName); }); diff --git a/app/client/packages/rts/src/ctl/backup.ts b/app/client/packages/rts/src/ctl/backup.ts index 4961053f304b..39a36294f921 100644 --- a/app/client/packages/rts/src/ctl/backup.ts +++ b/app/client/packages/rts/src/ctl/backup.ts @@ -1,4 +1,3 @@ -// @ts-ignore import fsPromises from "fs/promises"; import path from "path"; import os from "os"; @@ -23,6 +22,7 @@ export async function run() { console.log("Available free space at /appsmith-stacks"); const availSpaceInBytes = getAvailableBackupSpaceInBytes("/appsmith-stacks"); + console.log("\n"); checkAvailableBackupSpace(availSpaceInBytes); @@ -43,26 +43,32 @@ export async function run() { 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); archivePath = await createFinalArchive(backupRootPath, timestamp); + // shell.exec("openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -in " + archivePath + " -out " + archivePath + ".enc"); if (encryptArchive) { const encryptedArchivePath = await encryptBackupArchive( archivePath, encryptionPassword, ); + await logger.backup_info( "Finished creating an encrypted a backup archive at " + encryptedArchivePath, ); + if (archivePath != null) { await fsPromises.rm(archivePath, { recursive: true, force: true }); } @@ -94,6 +100,7 @@ export async function run() { if (command_args.includes("--error-mail")) { const currentTS = new Date().getTime(); const lastMailTS = await utils.getLastBackupErrorMailSentInMilliSec(); + if ( lastMailTS + Constants.DURATION_BETWEEN_BACKUP_ERROR_MAILS_IN_MILLI_SEC < @@ -107,11 +114,13 @@ export async function run() { if (backupRootPath != null) { await fsPromises.rm(backupRootPath, { recursive: true, force: true }); } + if (encryptArchive) { if (archivePath != null) { await fsPromises.rm(archivePath, { recursive: true, force: true }); } } + await postBackupCleanup(); process.exit(errorCode); } @@ -119,6 +128,7 @@ export async function run() { export async function encryptBackupArchive(archivePath, encryptionPassword) { const encryptedArchivePath = archivePath + ".enc"; + await utils.execCommand([ "openssl", "enc", @@ -133,11 +143,16 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) { "-k", encryptionPassword, ]); + return encryptedArchivePath; } export function getEncryptionPasswordFromUser() { - for (const _ of [1, 2, 3]) { + for (const attempt of [1, 2, 3]) { + if (attempt > 1) { + console.log("Retry attempt", attempt); + } + const encryptionPwd1 = readlineSync.question( "Enter a password to encrypt the backup archive: ", { hideEchoBack: true }, @@ -146,10 +161,12 @@ export function getEncryptionPasswordFromUser() { "Enter the above password again: ", { hideEchoBack: true }, ); + if (encryptionPwd1 === encryptionPwd2) { if (encryptionPwd1) { return encryptionPwd1; } + console.error( "Invalid input. Empty password is not allowed, please try again.", ); @@ -157,9 +174,11 @@ export function getEncryptionPasswordFromUser() { console.error("The passwords do not match, please try again."); } } + console.error( "Aborting backup process, failed to obtain valid encryption password.", ); + return -1; } @@ -185,6 +204,7 @@ async function createManifestFile(path) { appsmithVersion: version, dbName: utils.getDatabaseNameFromMongoURI(utils.getDburl()), }; + await fsPromises.writeFile( path + "/manifest.json", JSON.stringify(manifest_data), @@ -198,6 +218,7 @@ async function exportDockerEnvFile(destFolder, encryptArchive) { { encoding: "utf8" }, ); let cleaned_content = removeSensitiveEnvData(content); + if (encryptArchive) { cleaned_content += "\nAPPSMITH_ENCRYPTION_SALT=" + @@ -205,6 +226,7 @@ async function exportDockerEnvFile(destFolder, encryptArchive) { "\nAPPSMITH_ENCRYPTION_PASSWORD=" + process.env.APPSMITH_ENCRYPTION_PASSWORD; } + await fsPromises.writeFile(destFolder + "/docker.env", cleaned_content); console.log("Exporting docker environment file done."); } @@ -222,6 +244,7 @@ async function createFinalArchive(destFolder, timestamp) { console.log("Creating final archive"); const archive = `${Constants.BACKUP_PATH}/appsmith-backup-${timestamp}.tar.gz`; + await utils.execCommand([ "tar", "-cah", @@ -243,10 +266,13 @@ async function postBackupCleanup() { process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT, ); const backupFiles = await utils.listLocalBackupFiles(); + while (backupFiles.length > backupArchivesLimit) { const fileName = backupFiles.shift(); + await fsPromises.rm(Constants.BACKUP_PATH + "/" + fileName); } + console.log("Cleanup task completed."); } @@ -263,10 +289,11 @@ export function getGitRoot(gitRoot?) { if (gitRoot == null || gitRoot === "") { gitRoot = "/appsmith-stacks/git-storage"; } + return gitRoot; } -export function generateBackupRootPath() { +export async function generateBackupRootPath() { return fsPromises.mkdtemp(path.join(os.tmpdir(), "appsmithctl-backup-")); } @@ -277,6 +304,7 @@ export function getBackupContentsPath(backupRootPath, timestamp) { export function removeSensitiveEnvData(content) { // Remove encryption and Mongodb data from docker.env const output_lines = []; + content.split(/\r?\n/).forEach((line) => { if ( !line.startsWith("APPSMITH_ENCRYPTION") && @@ -286,20 +314,24 @@ export function removeSensitiveEnvData(content) { output_lines.push(line); } }); + return output_lines.join("\n"); } export function getBackupArchiveLimit(backupArchivesLimit?) { if (!backupArchivesLimit) backupArchivesLimit = Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT; + return backupArchivesLimit; } export async function removeOldBackups(backupFiles, backupArchivesLimit) { while (backupFiles.length > backupArchivesLimit) { const fileName = backupFiles.shift(); + await fsPromises.rm(Constants.BACKUP_PATH + "/" + fileName); } + return backupFiles; } @@ -309,6 +341,7 @@ export function getTimeStampInISO() { export async function getAvailableBackupSpaceInBytes(path) { const stat = await fsPromises.statfs(path); + return stat.bsize * stat.bfree; } diff --git a/app/client/packages/rts/src/ctl/check_replica_set.ts b/app/client/packages/rts/src/ctl/check_replica_set.ts index d9b00594357c..2e4d139de1ef 100644 --- a/app/client/packages/rts/src/ctl/check_replica_set.ts +++ b/app/client/packages/rts/src/ctl/check_replica_set.ts @@ -26,6 +26,7 @@ export async function exec() { async function checkReplicaSet(client: MongoClient) { await client.connect(); + return await new Promise((resolve) => { try { const changeStream = client @@ -43,6 +44,7 @@ async function checkReplicaSet(client: MongoClient) { } else { console.error("Error even from changeStream", err); } + resolve(false); }); diff --git a/app/client/packages/rts/src/ctl/export_db.ts b/app/client/packages/rts/src/ctl/export_db.ts index 5661c409c2f8..d8d4980ca0c9 100644 --- a/app/client/packages/rts/src/ctl/export_db.ts +++ b/app/client/packages/rts/src/ctl/export_db.ts @@ -1,4 +1,3 @@ -// @ts-ignore import fsPromises from "fs/promises"; import * as Constants from "./constants"; import * as utils from "./utils"; @@ -6,6 +5,7 @@ import * as utils from "./utils"; export async function exportDatabase() { console.log("export_database ...."); const dbUrl = utils.getDburl(); + await fsPromises.mkdir(Constants.BACKUP_PATH, { recursive: true }); await utils.execCommand([ "mongodump", diff --git a/app/client/packages/rts/src/ctl/import_db.ts b/app/client/packages/rts/src/ctl/import_db.ts index d00f16d1885c..81b178e10d61 100644 --- a/app/client/packages/rts/src/ctl/import_db.ts +++ b/app/client/packages/rts/src/ctl/import_db.ts @@ -30,6 +30,7 @@ export async function run(forceOption) { console.log("stop backend & rts application before import database"); await utils.stop(["backend", "rts"]); let shellCmdResult: string; + try { shellCmdResult = await utils.execCommandReturningOutput([ "mongo", @@ -43,11 +44,14 @@ export async function run(forceOption) { throw error; } const collectionsLen = parseInt(shellCmdResult.trimEnd()); + if (collectionsLen > 0) { if (forceOption) { await importDatabase(); + return; } + console.log(); console.log( "**************************** WARNING ****************************", @@ -59,12 +63,15 @@ export async function run(forceOption) { "Importing this DB will erase this data. Are you sure you want to proceed?[Yes/No] ", ); const answer = input && input.toLocaleUpperCase(); + if (answer === "Y" || answer === "YES") { await importDatabase(); + return; } else if (answer === "N" || answer === "NO") { return; } + console.log( `Your input is invalid. Please try to run import command again.`, ); diff --git a/app/client/packages/rts/src/ctl/index.ts b/app/client/packages/rts/src/ctl/index.ts index 0cec2e5924bb..9d25417aacd7 100755 --- a/app/client/packages/rts/src/ctl/index.ts +++ b/app/client/packages/rts/src/ctl/index.ts @@ -38,6 +38,7 @@ if (["export-db", "export_db", "ex"].includes(command)) { console.log("Importing database"); // Get Force option flag to run import DB immediately const forceOption = process.argv[3] === "-f"; + try { import_db.run(forceOption); console.log("Importing database done"); diff --git a/app/client/packages/rts/src/ctl/logger.ts b/app/client/packages/rts/src/ctl/logger.ts index 0c08a1362042..742fb6ea6262 100644 --- a/app/client/packages/rts/src/ctl/logger.ts +++ b/app/client/packages/rts/src/ctl/logger.ts @@ -1,4 +1,3 @@ -// @ts-ignore import fsPromises from "fs/promises"; import * as Constants from "./constants"; diff --git a/app/client/packages/rts/src/ctl/mailer.ts b/app/client/packages/rts/src/ctl/mailer.ts index 44fde47d9edb..41c1719c98b5 100644 --- a/app/client/packages/rts/src/ctl/mailer.ts +++ b/app/client/packages/rts/src/ctl/mailer.ts @@ -59,6 +59,7 @@ export async function sendBackupErrorToAdmins(err, backupTimestamp) { if (instanceName) { text = text + "Appsmith instance name: " + instanceName + "\n"; } + if (domainName) { text = text + @@ -68,6 +69,7 @@ export async function sendBackupErrorToAdmins(err, backupTimestamp) { "/settings/general" + "\n"; } + text = text + "\n" + err.stack; const transporter = nodemailer.createTransport({ diff --git a/app/client/packages/rts/src/ctl/mongo_shell_utils.ts b/app/client/packages/rts/src/ctl/mongo_shell_utils.ts index b6c44cb2fff5..996925d7def0 100644 --- a/app/client/packages/rts/src/ctl/mongo_shell_utils.ts +++ b/app/client/packages/rts/src/ctl/mongo_shell_utils.ts @@ -4,6 +4,7 @@ const command_args = process.argv.slice(3); export async function exec() { let errorCode = 0; + try { await execMongoEval(command_args[0], process.env.APPSMITH_DB_URL); } catch (err) { @@ -16,9 +17,11 @@ export async function exec() { async function execMongoEval(queryExpression, appsmithMongoURI) { queryExpression = queryExpression.trim(); + if (command_args.includes("--pretty")) { queryExpression += ".pretty()"; } + return await utils.execCommand([ "mongosh", appsmithMongoURI, diff --git a/app/client/packages/rts/src/ctl/restore.ts b/app/client/packages/rts/src/ctl/restore.ts index e40c44b4b9b3..adf93898efab 100644 --- a/app/client/packages/rts/src/ctl/restore.ts +++ b/app/client/packages/rts/src/ctl/restore.ts @@ -1,4 +1,3 @@ -// @ts-ignore import fsPromises from "fs/promises"; import path from "path"; import os from "os"; @@ -10,14 +9,17 @@ const command_args = process.argv.slice(3); async function getBackupFileName() { const backupFiles = await utils.listLocalBackupFiles(); + console.log( "\n" + backupFiles.length + " Appsmith backup file(s) found: [Sorted in ascending/chronological order]", ); + if (backupFiles.length == 0) { return; } + console.log( "----------------------------------------------------------------", ); @@ -25,11 +27,13 @@ async function getBackupFileName() { console.log( "----------------------------------------------------------------", ); + for (let i = 0; i < backupFiles.length; i++) { if (i === backupFiles.length - 1) console.log(i + "\t|\t" + backupFiles[i] + " <--Most recent backup"); else console.log(i + "\t|\t" + backupFiles[i]); } + console.log( "----------------------------------------------------------------", ); @@ -38,6 +42,7 @@ async function getBackupFileName() { readlineSync.question("Please enter the backup file index: "), 10, ); + if ( !isNaN(backupFileIndex) && Number.isInteger(backupFileIndex) && @@ -54,8 +59,14 @@ async function getBackupFileName() { async function decryptArchive(encryptedFilePath, backupFilePath) { console.log("Enter the password to decrypt the backup archive:"); - for (const _ of [1, 2, 3]) { + + for (const attempt of [1, 2, 3]) { + if (attempt > 1) { + console.log("Retry attempt", attempt); + } + const decryptionPwd = readlineSync.question("", { hideEchoBack: true }); + try { await utils.execCommandSilent([ "openssl", @@ -72,11 +83,13 @@ async function decryptArchive(encryptedFilePath, backupFilePath) { "-k", decryptionPwd, ]); + return true; } catch (error) { console.log("Invalid password. Please try again:"); } } + return false; } @@ -101,9 +114,11 @@ async function restoreDatabase(restoreContentsPath, dbUrl) { `--archive=${restoreContentsPath}/mongodb-data.gz`, "--gzip", ]; + try { const fromDbName = await getBackupDatabaseName(restoreContentsPath); const toDbName = utils.getDatabaseNameFromMongoURI(dbUrl); + console.log("Restoring database from " + fromDbName + " to " + toDbName); cmd.push( "--nsInclude=*", @@ -130,6 +145,7 @@ async function restoreDockerEnvFile( const updatedbUrl = utils.getDburl(); let encryptionPwd = process.env.APPSMITH_ENCRYPTION_PASSWORD; let encryptionSalt = process.env.APPSMITH_ENCRYPTION_SALT; + await utils.execCommand([ "mv", dockerEnvFile, @@ -140,6 +156,7 @@ async function restoreDockerEnvFile( restoreContentsPath + "/docker.env", dockerEnvFile, ]); + if (overwriteEncryptionKeys) { if (encryptionPwd && encryptionSalt) { const input = readlineSync.question( @@ -148,6 +165,7 @@ async function restoreDockerEnvFile( Or Type "n"/"No" to provide encryption key & password corresponding to the original Appsmith instance that is being restored.\n', ); const answer = input && input.toLocaleUpperCase(); + if (answer === "N" || answer === "NO") { encryptionPwd = readlineSync.question( "Enter the APPSMITH_ENCRYPTION_PASSWORD: ", @@ -180,6 +198,7 @@ async function restoreDockerEnvFile( }, ); } + await fsPromises.appendFile( dockerEnvFile, "\nAPPSMITH_ENCRYPTION_PASSWORD=" + @@ -204,6 +223,7 @@ async function restoreDockerEnvFile( process.env.APPSMITH_MONGODB_PASSWORD, ); } + console.log("Restoring docker environment file completed"); } @@ -211,6 +231,7 @@ async function restoreGitStorageArchive(restoreContentsPath, backupName) { 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]); await utils.execCommand([ "mv", @@ -228,6 +249,7 @@ async function checkRestoreVersionCompatability(restoreContentsPath) { ); const manifest_json = JSON.parse(manifest_data); const restoreVersion = manifest_json["appsmithVersion"]; + console.log("Current Appsmith Version: " + currentVersion); console.log("Restore Appsmith Version: " + restoreVersion); @@ -251,6 +273,7 @@ async function checkRestoreVersionCompatability(restoreContentsPath) { const confirm = readlineSync.question( 'Press Enter to continue \nOr Type "c" to cancel the restore process.\n', ); + if (confirm.toLowerCase() === "c") { process.exit(0); } @@ -259,6 +282,7 @@ async function checkRestoreVersionCompatability(restoreContentsPath) { async function getBackupDatabaseName(restoreContentsPath) { let db_name = "appsmith"; + if (command_args.includes("--backup-db-name")) { for (let i = 0; i < command_args.length; i++) { if (command_args[i].startsWith("--backup-db-name")) { @@ -271,12 +295,14 @@ async function getBackupDatabaseName(restoreContentsPath) { { encoding: "utf8" }, ); const manifest_json = JSON.parse(manifest_data); + if ("dbName" in manifest_json) { db_name = manifest_json["dbName"]; } } console.log("Backup Database Name: " + db_name); + return db_name; } @@ -290,15 +316,18 @@ export async function run() { try { let backupFileName = await getBackupFileName(); + if (backupFileName == null) { process.exit(errorCode); } else { backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName); + if (isArchiveEncrypted(backupFileName)) { const encryptedBackupFilePath = path.join( Constants.BACKUP_PATH, backupFileName, ); + backupFileName = backupFileName.replace(".enc", ""); backupFilePath = path.join(Constants.BACKUP_PATH, backupFileName); cleanupArchive = true; @@ -307,6 +336,7 @@ export async function run() { encryptedBackupFilePath, backupFilePath, ); + if (!decryptSuccess) { console.log( "You have entered the incorrect password multiple times. Aborting the restore process.", @@ -315,6 +345,7 @@ export async function run() { process.exit(errorCode); } } + const backupName = backupFileName.replace(/\.tar\.gz$/, ""); const restoreRootPath = await fsPromises.mkdtemp(os.tmpdir()); const restoreContentsPath = path.join(restoreRootPath, backupName); @@ -346,6 +377,7 @@ export async function run() { if (cleanupArchive) { await fsPromises.rm(backupFilePath, { force: true }); } + await utils.start(["backend", "rts"]); process.exit(errorCode); } diff --git a/app/client/packages/rts/src/ctl/utils.test.ts b/app/client/packages/rts/src/ctl/utils.test.ts index fce3dd9cf388..921c66c46704 100644 --- a/app/client/packages/rts/src/ctl/utils.test.ts +++ b/app/client/packages/rts/src/ctl/utils.test.ts @@ -9,6 +9,7 @@ describe("execCommandReturningOutput", () => { "hello", "world", ]); + expect(result).toBe("hello world"); }); @@ -18,6 +19,7 @@ describe("execCommandReturningOutput", () => { "--eval", "console.log('to out')", ]); + expect(result).toBe("to out"); }); @@ -27,6 +29,7 @@ describe("execCommandReturningOutput", () => { "--eval", "console.error('to err')", ]); + expect(result).toBe("to err"); }); @@ -36,6 +39,7 @@ describe("execCommandReturningOutput", () => { "--eval", "console.log('to out'); console.error('to err')", ]); + expect(result).toBe("to out\nto err"); }); @@ -45,6 +49,7 @@ describe("execCommandReturningOutput", () => { "--eval", "console.error('to err'); console.log('to out')", ]); + expect(result).toBe("to out\nto err"); }); }); @@ -56,6 +61,7 @@ describe("execCommandSilent", () => { test("silences stdout and stderr", async () => { const consoleSpy = jest.spyOn(console, "log"); + await utils.execCommandSilent(["node", "--eval", "console.log('test')"]); expect(consoleSpy).not.toHaveBeenCalled(); consoleSpy.mockRestore(); diff --git a/app/client/packages/rts/src/ctl/utils.ts b/app/client/packages/rts/src/ctl/utils.ts index e0f9e8c74c9d..62d0ec878871 100644 --- a/app/client/packages/rts/src/ctl/utils.ts +++ b/app/client/packages/rts/src/ctl/utils.ts @@ -1,8 +1,6 @@ -// @ts-ignore import fsPromises from "fs/promises"; import * as Constants from "./constants"; import childProcess from "child_process"; -// @ts-ignore import fs from "node:fs"; import { ConnectionString } from "mongodb-connection-string-url"; @@ -42,11 +40,13 @@ export async function start(apps) { export function getDburl() { let dbUrl = ""; + try { const env_array = fs .readFileSync(Constants.ENV_PATH, "utf8") .toString() .split("\n"); + for (const i in env_array) { if ( env_array[i].startsWith("APPSMITH_MONGODB_URI") || @@ -61,14 +61,16 @@ export function getDburl() { } const dbEnvUrl = process.env.APPSMITH_DB_URL || process.env.APPSMITH_MONGO_DB_URI; + // Make sure dbEnvUrl takes precedence over dbUrl if (dbEnvUrl && dbEnvUrl !== "undefined") { dbUrl = dbEnvUrl.trim(); } + return dbUrl; } -export function execCommand(cmd: string[], options?) { +export async function execCommand(cmd: string[], options?) { return new Promise((resolve, reject) => { let isPromiseDone = false; @@ -81,7 +83,9 @@ export function execCommand(cmd: string[], options?) { if (isPromiseDone) { return; } + isPromiseDone = true; + if (code === 0) { resolve(); } else { @@ -93,6 +97,7 @@ export function execCommand(cmd: string[], options?) { if (isPromiseDone) { return; } + isPromiseDone = true; console.error("Error running command", err); reject(); @@ -100,7 +105,7 @@ export function execCommand(cmd: string[], options?) { }); } -export function execCommandReturningOutput(cmd, options?) { +export async function execCommandReturningOutput(cmd, options?) { return new Promise((resolve, reject) => { const p = childProcess.spawn(cmd[0], cmd.slice(1), options); @@ -125,6 +130,7 @@ export function execCommandReturningOutput(cmd, options?) { "\n" + errChunks.join("").trim() ).trim(); + if (code === 0) { resolve(output); } else { @@ -137,6 +143,7 @@ export function execCommandReturningOutput(cmd, options?) { export async function listLocalBackupFiles() { // Ascending order const backupFiles = []; + await fsPromises .readdir(Constants.BACKUP_PATH) .then((filenames) => { @@ -149,6 +156,7 @@ export async function listLocalBackupFiles() { .catch((err) => { console.log(err); }); + return backupFiles; } @@ -160,6 +168,7 @@ export async function updateLastBackupErrorMailSentInMilliSec(ts) { export async function getLastBackupErrorMailSentInMilliSec() { try { const ts = await fsPromises.readFile(Constants.LAST_ERROR_MAIL_TS, "utf8"); + return parseInt(ts, 10); } catch (error) { return 0; @@ -179,6 +188,7 @@ export function preprocessMongoDBURI(uri /* string */) { const cs = new ConnectionString(uri); const params = cs.searchParams; + params.set("appName", "appsmithctl"); if ( @@ -205,7 +215,7 @@ export function preprocessMongoDBURI(uri /* string */) { return cs.toString(); } -export function execCommandSilent(cmd, options?) { +export async function execCommandSilent(cmd, options?) { return new Promise((resolve, reject) => { let isPromiseDone = false; @@ -218,7 +228,9 @@ export function execCommandSilent(cmd, options?) { if (isPromiseDone) { return; } + isPromiseDone = true; + if (code === 0) { resolve(); } else { @@ -230,6 +242,7 @@ export function execCommandSilent(cmd, options?) { if (isPromiseDone) { return; } + isPromiseDone = true; reject(err); }); @@ -238,5 +251,6 @@ export function execCommandSilent(cmd, options?) { export function getDatabaseNameFromMongoURI(uri) { const uriParts = uri.split("/"); + return uriParts[uriParts.length - 1].split("?")[0]; } diff --git a/app/client/packages/rts/src/ctl/version.ts b/app/client/packages/rts/src/ctl/version.ts index 79d34be03cbc..7d82c4fb2283 100644 --- a/app/client/packages/rts/src/ctl/version.ts +++ b/app/client/packages/rts/src/ctl/version.ts @@ -2,12 +2,14 @@ import * as utils from "./utils"; export async function exec() { let version = null; + try { version = await utils.getCurrentAppsmithVersion(); } catch (err) { console.error("Error fetching current Appsmith version", err); process.exit(1); } + if (version) { console.log(version); } else { diff --git a/app/client/yarn.lock b/app/client/yarn.lock index 8ff2894d557a..fc5e58b76623 100644 --- a/app/client/yarn.lock +++ b/app/client/yarn.lock @@ -12820,6 +12820,7 @@ __metadata: "@shared/ast": "workspace:^" "@types/express": ^4.17.14 "@types/jest": ^29.2.3 + "@types/node": "*" "@types/nodemailer": ^6.4.17 "@types/readline-sync": ^1.4.8 axios: ^1.7.4