Skip to content
Merged
Show file tree
Hide file tree
Changes from 3 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
3 changes: 1 addition & 2 deletions app/client/packages/rts/src/ctl/backup.test.ts
Original file line number Diff line number Diff line change
Expand Up @@ -230,9 +230,8 @@ describe("Backup Tests", () => {

return password;
});
const password_res = backup.getEncryptionPasswordFromUser();

expect(password_res).toEqual(-1);
expect(() => backup.getEncryptionPasswordFromUser()).toThrow();
});

test("Get encrypted archive path", async () => {
Expand Down
176 changes: 107 additions & 69 deletions app/client/packages/rts/src/ctl/backup.ts
Original file line number Diff line number Diff line change
Expand Up @@ -10,26 +10,49 @@ import readlineSync from "readline-sync";

const command_args = process.argv.slice(3);

export async function run() {
const timestamp = getTimeStampInISO();
let errorCode = 0;
let backupRootPath, archivePath, encryptionPassword;
let encryptArchive = false;
class BackupState {
readonly initAt: string = getTimeStampInISO();
readonly errors: string[] = [];

backupRootPath: string;
archivePath: string;

encryptionPassword: string;

isEncryptionEnabled() {
return !!this.encryptionPassword;
}
}
Comment thread
sharat87 marked this conversation as resolved.

export async function run() {
await utils.ensureSupervisorIsRunning();

const state: BackupState = new BackupState();

try {
// PRE-BACKUP
console.log("Available free space at /appsmith-stacks");
const availSpaceInBytes =
getAvailableBackupSpaceInBytes("/appsmith-stacks");
const availSpaceInBytes: number =
await getAvailableBackupSpaceInBytes("/appsmith-stacks");

console.log("\n");

checkAvailableBackupSpace(availSpaceInBytes);

backupRootPath = await generateBackupRootPath();
const backupContentsPath = getBackupContentsPath(backupRootPath, timestamp);
if (
!command_args.includes("--non-interactive") &&
tty.isatty((process.stdout as any).fd)
) {
state.encryptionPassword = getEncryptionPasswordFromUser();
}

state.backupRootPath = await generateBackupRootPath();
const backupContentsPath: string = getBackupContentsPath(
state.backupRootPath,
state.initAt,
);

// BACKUP
await fsPromises.mkdir(backupContentsPath);

await exportDatabase(backupContentsPath);
Expand All @@ -38,43 +61,34 @@ export async function run() {

await createManifestFile(backupContentsPath);

if (
!command_args.includes("--non-interactive") &&
tty.isatty((process.stdout as any).fd)
) {
encryptionPassword = getEncryptionPasswordFromUser();

if (encryptionPassword == -1) {
throw new Error(
"Backup process aborted because a valid enctyption password could not be obtained from the user",
);
}

encryptArchive = true;
}

await exportDockerEnvFile(backupContentsPath, encryptArchive);
await exportDockerEnvFile(backupContentsPath, state.isEncryptionEnabled());

archivePath = await createFinalArchive(backupRootPath, timestamp);
state.archivePath = await createFinalArchive(
state.backupRootPath,
state.initAt,
);

// shell.exec("openssl enc -aes-256-cbc -pbkdf2 -iter 100000 -in " + archivePath + " -out " + archivePath + ".enc");
if (encryptArchive) {
// POST-BACKUP
if (state.isEncryptionEnabled()) {
const encryptedArchivePath = await encryptBackupArchive(
archivePath,
encryptionPassword,
state.archivePath,
state.encryptionPassword,
);

await logger.backup_info(
"Finished creating an encrypted a backup archive at " +
encryptedArchivePath,
);

if (archivePath != null) {
await fsPromises.rm(archivePath, { recursive: true, force: true });
if (state.archivePath != null) {
await fsPromises.rm(state.archivePath, {
recursive: true,
force: true,
});
}
} else {
await logger.backup_info(
"Finished creating a backup archive at " + archivePath,
"Finished creating a backup archive at " + state.archivePath,
);
console.log(
"********************************************************* IMPORTANT!!! *************************************************************",
Expand All @@ -90,11 +104,13 @@ export async function run() {
);
}

await fsPromises.rm(backupRootPath, { recursive: true, force: true });
await fsPromises.rm(state.backupRootPath, { recursive: true, force: true });

await logger.backup_info("Finished taking a backup at " + archivePath);
await logger.backup_info(
"Finished taking a backup at " + state.archivePath,
);
} catch (err) {
errorCode = 1;
process.exitCode = 1;
await logger.backup_error(err.stack);

if (command_args.includes("--error-mail")) {
Expand All @@ -106,27 +122,36 @@ export async function run() {
Constants.DURATION_BETWEEN_BACKUP_ERROR_MAILS_IN_MILLI_SEC <
currentTS
) {
await mailer.sendBackupErrorToAdmins(err, timestamp);
await mailer.sendBackupErrorToAdmins(err, state.initAt);
await utils.updateLastBackupErrorMailSentInMilliSec(currentTS);
}
}
} finally {
if (backupRootPath != null) {
await fsPromises.rm(backupRootPath, { recursive: true, force: true });
if (state.backupRootPath != null) {
await fsPromises.rm(state.backupRootPath, {
recursive: true,
force: true,
});
}

if (encryptArchive) {
if (archivePath != null) {
await fsPromises.rm(archivePath, { recursive: true, force: true });
if (state.isEncryptionEnabled()) {
if (state.archivePath != null) {
await fsPromises.rm(state.archivePath, {
recursive: true,
force: true,
});
}
}

await postBackupCleanup();
process.exit(errorCode);
process.exit();
Comment thread
sharat87 marked this conversation as resolved.
}
}

export async function encryptBackupArchive(archivePath, encryptionPassword) {
export async function encryptBackupArchive(
archivePath: string,
encryptionPassword: string,
) {
const encryptedArchivePath = archivePath + ".enc";

await utils.execCommand([
Expand All @@ -135,7 +160,7 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) {
"-aes-256-cbc",
"-pbkdf2",
"-iter",
100000,
"100000",
"-in",
archivePath,
"-out",
Expand All @@ -147,17 +172,17 @@ export async function encryptBackupArchive(archivePath, encryptionPassword) {
return encryptedArchivePath;
}

export function getEncryptionPasswordFromUser() {
export function getEncryptionPasswordFromUser(): string {
for (const attempt of [1, 2, 3]) {
if (attempt > 1) {
console.log("Retry attempt", attempt);
}

const encryptionPwd1 = readlineSync.question(
const encryptionPwd1: string = readlineSync.question(
"Enter a password to encrypt the backup archive: ",
{ hideEchoBack: true },
);
const encryptionPwd2 = readlineSync.question(
const encryptionPwd2: string = readlineSync.question(
"Enter the above password again: ",
{ hideEchoBack: true },
);
Expand All @@ -179,16 +204,18 @@ export function getEncryptionPasswordFromUser() {
"Aborting backup process, failed to obtain valid encryption password.",
);

return -1;
throw new Error(
"Backup process aborted because a valid encryption password could not be obtained from the user",
);
}

async function exportDatabase(destFolder) {
async function exportDatabase(destFolder: string) {
console.log("Exporting database");
await executeMongoDumpCMD(destFolder, utils.getDburl());
console.log("Exporting database done.");
}

async function createGitStorageArchive(destFolder) {
async function createGitStorageArchive(destFolder: string) {
console.log("Creating git-storage archive");

const gitRoot = getGitRoot(process.env.APPSMITH_GIT_ROOT);
Expand All @@ -198,7 +225,7 @@ async function createGitStorageArchive(destFolder) {
console.log("Created git-storage archive");
}

async function createManifestFile(path) {
async function createManifestFile(path: string) {
const version = await utils.getCurrentAppsmithVersion();
const manifest_data = {
appsmithVersion: version,
Expand All @@ -211,7 +238,10 @@ async function createManifestFile(path) {
);
}

async function exportDockerEnvFile(destFolder, encryptArchive) {
async function exportDockerEnvFile(
destFolder: string,
encryptArchive: boolean,
) {
console.log("Exporting docker environment file");
const content = await fsPromises.readFile(
"/appsmith-stacks/configuration/docker.env",
Expand All @@ -231,7 +261,10 @@ async function exportDockerEnvFile(destFolder, encryptArchive) {
console.log("Exporting docker environment file done.");
}

export async function executeMongoDumpCMD(destFolder, appsmithMongoURI) {
export async function executeMongoDumpCMD(
destFolder: string,
appsmithMongoURI: string,
) {
Comment thread
sharat87 marked this conversation as resolved.
return await utils.execCommand([
"mongodump",
`--uri=${appsmithMongoURI}`,
Expand All @@ -240,7 +273,7 @@ export async function executeMongoDumpCMD(destFolder, appsmithMongoURI) {
]); // generate cmd
}

async function createFinalArchive(destFolder, timestamp) {
async function createFinalArchive(destFolder: string, timestamp: string) {
console.log("Creating final archive");

const archive = `${Constants.BACKUP_PATH}/appsmith-backup-${timestamp}.tar.gz`;
Expand All @@ -263,7 +296,7 @@ async function createFinalArchive(destFolder, timestamp) {
async function postBackupCleanup() {
console.log("Starting the cleanup task after taking a backup.");
const backupArchivesLimit = getBackupArchiveLimit(
process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT,
parseInt(process.env.APPSMITH_BACKUP_ARCHIVE_LIMIT, 10),
);
const backupFiles = await utils.listLocalBackupFiles();

Expand All @@ -276,16 +309,16 @@ async function postBackupCleanup() {
console.log("Cleanup task completed.");
}

export async function executeCopyCMD(srcFolder, destFolder) {
export async function executeCopyCMD(srcFolder: string, destFolder: string) {
return await utils.execCommand([
"ln",
"-s",
srcFolder,
destFolder + "/git-storage",
path.join(destFolder, "git-storage"),
]);
}

export function getGitRoot(gitRoot?) {
export function getGitRoot(gitRoot?: string | undefined) {
if (gitRoot == null || gitRoot === "") {
gitRoot = "/appsmith-stacks/git-storage";
}
Expand All @@ -297,11 +330,14 @@ export async function generateBackupRootPath() {
return fsPromises.mkdtemp(path.join(os.tmpdir(), "appsmithctl-backup-"));
}

export function getBackupContentsPath(backupRootPath, timestamp) {
export function getBackupContentsPath(
backupRootPath: string,
timestamp: string,
): string {
return backupRootPath + "/appsmith-backup-" + timestamp;
}

export function removeSensitiveEnvData(content) {
export function removeSensitiveEnvData(content: string): string {
// Remove encryption and Mongodb data from docker.env
const output_lines = [];

Expand All @@ -318,14 +354,14 @@ export function removeSensitiveEnvData(content) {
return output_lines.join("\n");
}

export function getBackupArchiveLimit(backupArchivesLimit?) {
if (!backupArchivesLimit)
backupArchivesLimit = Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT;

return backupArchivesLimit;
export function getBackupArchiveLimit(backupArchivesLimit?: number): number {
return backupArchivesLimit || Constants.APPSMITH_DEFAULT_BACKUP_ARCHIVE_LIMIT;
}

export async function removeOldBackups(backupFiles, backupArchivesLimit) {
export async function removeOldBackups(
backupFiles: string[],
backupArchivesLimit: number,
) {
while (backupFiles.length > backupArchivesLimit) {
const fileName = backupFiles.shift();

Expand All @@ -339,13 +375,15 @@ export function getTimeStampInISO() {
return new Date().toISOString().replace(/:/g, "-");
}

export async function getAvailableBackupSpaceInBytes(path) {
export async function getAvailableBackupSpaceInBytes(
path: string,
): Promise<number> {
Comment thread
sharat87 marked this conversation as resolved.
const stat = await fsPromises.statfs(path);

return stat.bsize * stat.bfree;
}

export function checkAvailableBackupSpace(availSpaceInBytes) {
export function checkAvailableBackupSpace(availSpaceInBytes: number) {
if (availSpaceInBytes < Constants.MIN_REQUIRED_DISK_SPACE_IN_BYTES) {
throw new Error(
"Not enough space available at /appsmith-stacks. Please ensure availability of at least 2GB to backup successfully.",
Expand Down
Loading