chore: Reduce lint exceptions in ctl (#37643)
Fix linting exceptions in `ctl`.
## Automation
/test sanity
### 🔍 Cypress test results
<!-- This is an auto-generated comment: Cypress test results -->
> [!WARNING]
> Tests have not run on the HEAD
1f2242abcfac193fb321dee8d64cb194dea0f803 yet
> <hr>Fri, 22 Nov 2024 10:06:23 UTC
<!-- end of auto-generated comment: Cypress test results -->
## Communication
Should the DevRel and Marketing teams inform users about this change?
- [ ] Yes
- [x] No
<!-- This is an auto-generated comment: release notes by coderabbit.ai
-->
## Summary by CodeRabbit
## Release Notes
- **New Features**
- Enhanced backup and restore processes with improved user prompts and
error handling.
- Added support for optional command-line flags during database imports.
- **Bug Fixes**
- Improved error handling for various operations, including database
exports and imports.
- Enhanced logging for backup errors to provide more context.
- **Documentation**
- Updated user prompts and error messages for clarity during backup and
restore operations.
- **Tests**
- Expanded test coverage for backup functionalities and utility
functions to ensure robust error handling and output validation.
- **Chores**
- Updated dependencies to enhance TypeScript development experience.
<!-- end of auto-generated comment: release notes by coderabbit.ai -->
This commit is contained in:
parent
d87f7ccd62
commit
0685d335ea
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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"
|
||||
}
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
});
|
||||
|
|
|
|||
|
|
@ -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;
|
||||
}
|
||||
|
||||
|
|
|
|||
|
|
@ -26,6 +26,7 @@ export async function exec() {
|
|||
|
||||
async function checkReplicaSet(client: MongoClient) {
|
||||
await client.connect();
|
||||
|
||||
return await new Promise<boolean>((resolve) => {
|
||||
try {
|
||||
const changeStream = client
|
||||
|
|
@ -43,6 +44,7 @@ async function checkReplicaSet(client: MongoClient) {
|
|||
} else {
|
||||
console.error("Error even from changeStream", err);
|
||||
}
|
||||
|
||||
resolve(false);
|
||||
});
|
||||
|
||||
|
|
|
|||
|
|
@ -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",
|
||||
|
|
|
|||
|
|
@ -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.`,
|
||||
);
|
||||
|
|
|
|||
|
|
@ -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");
|
||||
|
|
|
|||
|
|
@ -1,4 +1,3 @@
|
|||
// @ts-ignore
|
||||
import fsPromises from "fs/promises";
|
||||
import * as Constants from "./constants";
|
||||
|
||||
|
|
|
|||
|
|
@ -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({
|
||||
|
|
|
|||
|
|
@ -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,
|
||||
|
|
|
|||
|
|
@ -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);
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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();
|
||||
|
|
|
|||
|
|
@ -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<void>((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<string>((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<void>((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];
|
||||
}
|
||||
|
|
|
|||
|
|
@ -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 {
|
||||
|
|
|
|||
|
|
@ -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
|
||||
|
|
|
|||
Loading…
Reference in New Issue
Block a user