2024-07-10 17:53:27 +07:00
|
|
|
import { Body, Controller, Delete, Get, Post, Route, Security } from "tsoa";
|
|
|
|
|
import { Client as MinioClient, BucketItem } from "minio";
|
2024-07-15 10:50:45 +07:00
|
|
|
import HttpError from "../interfaces/http-error";
|
|
|
|
|
import HttpStatus from "../interfaces/http-status";
|
2024-07-10 17:53:27 +07:00
|
|
|
|
2024-07-15 10:50:45 +07:00
|
|
|
function getEnvVar(environmentName: string) {
|
|
|
|
|
const environmentValue = process.env[environmentName];
|
|
|
|
|
if (!environmentValue) throw new Error(`${environmentName} is required.`);
|
|
|
|
|
return environmentValue;
|
2024-07-10 17:53:27 +07:00
|
|
|
}
|
|
|
|
|
|
|
|
|
|
const WINDMILL_URL = getEnvVar("WINDMILL_URL");
|
|
|
|
|
const WINDMILL_WORKSPACE = getEnvVar("WINDMILL_WORKSPACE");
|
2024-07-15 10:50:45 +07:00
|
|
|
const WINDMILL_BACKUP_FLOW_PATH = getEnvVar("WINDMILL_BACKUP_FLOW_PATH");
|
|
|
|
|
const WINDMILL_RESTORE_FLOW_PATH = getEnvVar("WINDMILL_RESTORE_FLOW_PATH");
|
2024-07-16 13:24:27 +07:00
|
|
|
const WINDMILL_BACKUP_DELETE_SCRIPT_PATH = getEnvVar("WINDMILL_BACKUP_DELETE_SCRIPT_PATH");
|
2024-07-10 17:53:27 +07:00
|
|
|
const WINDMILL_API_KEY = getEnvVar("WINDMILL_API_KEY");
|
|
|
|
|
const DB_HOST = getEnvVar("DB_HOST");
|
|
|
|
|
const DB_PORT = process.env.DB_PORT;
|
|
|
|
|
const DB_USERNAME = getEnvVar("DB_USERNAME");
|
|
|
|
|
const DB_PASSWORD = getEnvVar("DB_PASSWORD");
|
2024-07-15 10:50:45 +07:00
|
|
|
const MAIN_MINIO_USE_SSL = getEnvVar("MAIN_MINIO_USE_SSL");
|
|
|
|
|
const MAIN_MINIO_HOST = getEnvVar("MAIN_MINIO_HOST");
|
|
|
|
|
const MAIN_MINIO_PORT = process.env.MAIN_MINIO_PORT;
|
|
|
|
|
const MAIN_MINIO_ACCESS_KEY = getEnvVar("MAIN_MINIO_ACCESS_KEY");
|
|
|
|
|
const MAIN_MINIO_SECRET_KEY = getEnvVar("MAIN_MINIO_SECRET_KEY");
|
|
|
|
|
const MAIN_MINIO_BUCKET = getEnvVar("MAIN_MINIO_BUCKET");
|
|
|
|
|
const BACKUP_MINIO_USE_SSL = getEnvVar("BACKUP_MINIO_USE_SSL");
|
|
|
|
|
const BACKUP_MINIO_HOST = getEnvVar("BACKUP_MINIO_HOST");
|
|
|
|
|
const BACKUP_MINIO_PORT = process.env.BACKUP_MINIO_PORT;
|
|
|
|
|
const BACKUP_MINIO_ACCESS_KEY = getEnvVar("BACKUP_MINIO_ACCESS_KEY");
|
|
|
|
|
const BACKUP_MINIO_SECRET_KEY = getEnvVar("BACKUP_MINIO_SECRET_KEY");
|
|
|
|
|
const BACKUP_MINIO_BUCKET = getEnvVar("BACKUP_MINIO_BUCKET");
|
2024-07-10 17:53:27 +07:00
|
|
|
|
|
|
|
|
const minio = new MinioClient({
|
2024-07-15 10:50:45 +07:00
|
|
|
useSSL: BACKUP_MINIO_USE_SSL === "true",
|
|
|
|
|
endPoint: BACKUP_MINIO_HOST,
|
|
|
|
|
port: +(BACKUP_MINIO_PORT || "9000"),
|
|
|
|
|
accessKey: BACKUP_MINIO_ACCESS_KEY,
|
|
|
|
|
secretKey: BACKUP_MINIO_SECRET_KEY,
|
2024-07-10 17:53:27 +07:00
|
|
|
});
|
|
|
|
|
|
|
|
|
|
@Route("/api/v1/backup")
|
2024-07-11 10:41:36 +07:00
|
|
|
@Security("keycloak")
|
2024-07-10 17:53:27 +07:00
|
|
|
export class BackupController extends Controller {
|
|
|
|
|
@Get()
|
|
|
|
|
async listBackup() {
|
|
|
|
|
return await new Promise((resolve, reject) => {
|
|
|
|
|
const data: BucketItem[] = [];
|
2024-07-15 10:50:45 +07:00
|
|
|
const stream = minio.listObjectsV2(BACKUP_MINIO_BUCKET);
|
2024-07-11 15:37:25 +07:00
|
|
|
stream.on("data", (obj) => data.unshift(obj));
|
2024-07-10 17:53:27 +07:00
|
|
|
stream.on("end", () =>
|
|
|
|
|
resolve(
|
2024-07-11 09:06:35 +07:00
|
|
|
data.flatMap((v) =>
|
|
|
|
|
"prefix" in v
|
|
|
|
|
? []
|
|
|
|
|
: {
|
2024-07-15 10:50:45 +07:00
|
|
|
name: v.name.replace(".sql.gz", ""),
|
2024-07-11 09:06:35 +07:00
|
|
|
timestamp: v.lastModified,
|
|
|
|
|
},
|
|
|
|
|
),
|
2024-07-10 17:53:27 +07:00
|
|
|
),
|
|
|
|
|
);
|
|
|
|
|
stream.on("error", (err) => reject(err));
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-11 11:45:23 +07:00
|
|
|
@Get("backup-running-list")
|
|
|
|
|
async runningBackupStatus() {
|
|
|
|
|
return await fetch(
|
2024-07-15 10:50:45 +07:00
|
|
|
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/list?running=true&script_path_exact=${WINDMILL_BACKUP_FLOW_PATH}`,
|
2024-07-11 11:45:23 +07:00
|
|
|
{
|
|
|
|
|
headers: { Authorization: `Bearer ${WINDMILL_API_KEY}` },
|
|
|
|
|
},
|
|
|
|
|
).then(async (r) => {
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
if (typeof data === "object" && "error" in data) {
|
|
|
|
|
console.error(data);
|
2024-07-15 10:50:45 +07:00
|
|
|
throw new Error("Cannot get status.");
|
2024-07-11 11:45:23 +07:00
|
|
|
}
|
2024-07-15 10:50:45 +07:00
|
|
|
return data as Record<string, any>[];
|
2024-07-11 11:45:23 +07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Get("restore-running-list")
|
|
|
|
|
async runningRestoreStatus() {
|
|
|
|
|
return await fetch(
|
2024-07-15 10:50:45 +07:00
|
|
|
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/list?running=true&script_path_exact=${WINDMILL_RESTORE_FLOW_PATH}`,
|
2024-07-11 11:45:23 +07:00
|
|
|
{
|
|
|
|
|
headers: { Authorization: `Bearer ${WINDMILL_API_KEY}` },
|
|
|
|
|
},
|
|
|
|
|
).then(async (r) => {
|
|
|
|
|
const data = await r.json();
|
|
|
|
|
if (typeof data === "object" && "error" in data) {
|
|
|
|
|
console.error(data);
|
2024-07-15 10:50:45 +07:00
|
|
|
throw new Error("Cannot get status.");
|
2024-07-11 11:45:23 +07:00
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 17:53:27 +07:00
|
|
|
@Post("create")
|
2024-07-15 10:50:45 +07:00
|
|
|
async runBackup(@Body() body?: { name?: string }) {
|
|
|
|
|
const timestamp = Math.round(Date.now() / 1000);
|
|
|
|
|
const name =
|
|
|
|
|
body?.name && body.name !== "auto-backup"
|
|
|
|
|
? `${timestamp}-${body.name}`
|
|
|
|
|
: `${timestamp}-manual`;
|
|
|
|
|
|
|
|
|
|
const listRunning = await this.runningBackupStatus();
|
|
|
|
|
|
|
|
|
|
if (!listRunning || listRunning.length > 0) {
|
|
|
|
|
throw new HttpError(HttpStatus.NOT_ACCEPTABLE, "Cannot create two backup at the same time.");
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 17:53:27 +07:00
|
|
|
return await fetch(
|
2024-07-15 10:50:45 +07:00
|
|
|
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run/f/${WINDMILL_BACKUP_FLOW_PATH}`,
|
2024-07-10 17:53:27 +07:00
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${WINDMILL_API_KEY}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
2024-07-15 10:50:45 +07:00
|
|
|
backup_name: name,
|
|
|
|
|
storage: {
|
|
|
|
|
s3_source_endpoint: `${MAIN_MINIO_USE_SSL === "true" ? "https://" : "http://"}${MAIN_MINIO_HOST}${(MAIN_MINIO_PORT && ":" + MAIN_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_source_access: MAIN_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_source_secret: MAIN_MINIO_SECRET_KEY,
|
|
|
|
|
s3_source_bucket: MAIN_MINIO_BUCKET,
|
|
|
|
|
s3_dest_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_dest_access: BACKUP_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_dest_secret: BACKUP_MINIO_SECRET_KEY,
|
|
|
|
|
},
|
|
|
|
|
database: {
|
|
|
|
|
s3_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_access: BACKUP_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_secret: BACKUP_MINIO_SECRET_KEY,
|
|
|
|
|
s3_bucket: BACKUP_MINIO_BUCKET,
|
|
|
|
|
db_host: DB_HOST,
|
|
|
|
|
db_port: DB_PORT,
|
|
|
|
|
db_user: DB_USERNAME,
|
|
|
|
|
db_password: DB_PASSWORD,
|
|
|
|
|
},
|
2024-07-10 17:53:27 +07:00
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
).then(async (r) => {
|
2024-07-11 13:25:22 +07:00
|
|
|
const data = await r.text();
|
|
|
|
|
if (data.includes("error")) {
|
2024-07-10 17:53:27 +07:00
|
|
|
console.error(data);
|
|
|
|
|
throw new Error("Backup Error");
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Post("restore")
|
2024-07-15 10:50:45 +07:00
|
|
|
async restoreBackup(@Body() body: { name: string }) {
|
|
|
|
|
const listRunning = await this.runningRestoreStatus();
|
|
|
|
|
|
|
|
|
|
if (!listRunning || listRunning.length > 0) {
|
|
|
|
|
throw new HttpError(HttpStatus.NOT_ACCEPTABLE, "Cannot restore two backup at the same time.");
|
|
|
|
|
}
|
|
|
|
|
|
2024-07-10 17:53:27 +07:00
|
|
|
return await fetch(
|
2024-07-15 10:50:45 +07:00
|
|
|
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run/f/${WINDMILL_RESTORE_FLOW_PATH}`,
|
2024-07-10 17:53:27 +07:00
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${WINDMILL_API_KEY}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
2024-07-15 10:50:45 +07:00
|
|
|
backup_name: body.name,
|
|
|
|
|
storage: {
|
|
|
|
|
s3_restore_endpoint: `${MAIN_MINIO_USE_SSL === "true" ? "https://" : "http://"}${MAIN_MINIO_HOST}${(MAIN_MINIO_PORT && ":" + MAIN_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_restore_access: MAIN_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_restore_secret: MAIN_MINIO_SECRET_KEY,
|
|
|
|
|
s3_restore_bucket: MAIN_MINIO_BUCKET,
|
|
|
|
|
s3_backup_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_backup_access: BACKUP_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_backup_secret: BACKUP_MINIO_SECRET_KEY,
|
|
|
|
|
},
|
|
|
|
|
database: {
|
|
|
|
|
s3_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_access: BACKUP_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_secret: BACKUP_MINIO_SECRET_KEY,
|
|
|
|
|
s3_bucket: BACKUP_MINIO_BUCKET,
|
|
|
|
|
db_host: DB_HOST,
|
|
|
|
|
db_port: DB_PORT,
|
|
|
|
|
db_user: DB_USERNAME,
|
|
|
|
|
db_password: DB_PASSWORD,
|
|
|
|
|
},
|
2024-07-10 17:53:27 +07:00
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
).then(async (r) => {
|
2024-07-11 14:53:01 +07:00
|
|
|
const data = await r.text();
|
|
|
|
|
if (data.includes("error")) {
|
2024-07-10 17:53:27 +07:00
|
|
|
console.error(data);
|
|
|
|
|
throw new Error("Backup Error");
|
|
|
|
|
}
|
|
|
|
|
return data;
|
|
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
|
|
|
|
|
@Delete("delete")
|
2024-07-15 10:50:45 +07:00
|
|
|
async deleteBackup(@Body() body: { name: string }) {
|
|
|
|
|
await fetch(
|
2024-07-16 13:24:27 +07:00
|
|
|
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run_wait_result/p/${WINDMILL_BACKUP_DELETE_SCRIPT_PATH}`,
|
2024-07-15 10:50:45 +07:00
|
|
|
{
|
|
|
|
|
method: "POST",
|
|
|
|
|
headers: {
|
|
|
|
|
Authorization: `Bearer ${WINDMILL_API_KEY}`,
|
|
|
|
|
"Content-Type": "application/json",
|
|
|
|
|
},
|
|
|
|
|
body: JSON.stringify({
|
|
|
|
|
backup_name: body.name,
|
|
|
|
|
s3_backup_endpoint: `${BACKUP_MINIO_USE_SSL === "true" ? "https://" : "http://"}${BACKUP_MINIO_HOST}${(BACKUP_MINIO_PORT && ":" + BACKUP_MINIO_PORT) || ""}`,
|
|
|
|
|
s3_backup_access: BACKUP_MINIO_ACCESS_KEY,
|
|
|
|
|
s3_backup_secret: BACKUP_MINIO_SECRET_KEY,
|
|
|
|
|
s3_backup_bucket: BACKUP_MINIO_BUCKET,
|
|
|
|
|
}),
|
|
|
|
|
},
|
|
|
|
|
).then(async (r) => {
|
|
|
|
|
const data = await r.text();
|
|
|
|
|
if (data.includes("error")) {
|
|
|
|
|
console.error(data);
|
|
|
|
|
throw new Error("Error delete backup.");
|
|
|
|
|
}
|
|
|
|
|
return data;
|
2024-07-10 17:53:27 +07:00
|
|
|
});
|
|
|
|
|
}
|
|
|
|
|
}
|