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