import { Body, Controller, Delete, Get, Post, Route, Security } from "tsoa"; import { Client as MinioClient, BucketItem } from "minio"; function getEnvVar(name: string) { const value = process.env[name]; if (!value) throw new Error(`${name} is required.`); return value; } const WINDMILL_URL = getEnvVar("WINDMILL_URL"); const WINDMILL_WORKSPACE = getEnvVar("WINDMILL_WORKSPACE"); const WINDMILL_BACKUP_SCRIPT_PATH = getEnvVar("WINDMILL_BACKUP_SCRIPT_PATH"); const WINDMILL_RESTORE_SCRIPT_PATH = getEnvVar("WINDMILL_RESTORE_SCRIPT_PATH"); 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"); const MINIO_USE_SSL = getEnvVar("MINIO_USE_SSL"); const MINIO_HOST = getEnvVar("MINIO_HOST"); const MINIO_PORT = process.env.MINIO_PORT; const MINIO_ACCESS_KEY = getEnvVar("MINIO_ACCESS_KEY"); const MINIO_SECRET_KEY = getEnvVar("MINIO_SECRET_KEY"); const MINIO_BUCKET = getEnvVar("MINIO_BUCKET"); const MINIO_BACKUP_FILE_PREFIX = process.env.MINIO_BACKUP_FILE_PREFIX?.split("/").filter(Boolean).join("/").concat("/") || ""; const minio = new MinioClient({ useSSL: MINIO_USE_SSL === "true", endPoint: MINIO_HOST, port: +(MINIO_PORT || "9000"), accessKey: MINIO_ACCESS_KEY, secretKey: MINIO_SECRET_KEY, }); @Route("/api/v1/backup") @Security("keycloak") export class BackupController extends Controller { @Get() async listBackup() { return await new Promise((resolve, reject) => { const data: BucketItem[] = []; const stream = minio.listObjectsV2(MINIO_BUCKET, MINIO_BACKUP_FILE_PREFIX); stream.on("data", (obj) => data.push(obj)); stream.on("end", () => resolve( data.flatMap((v) => "prefix" in v ? [] : { name: v.name.replace(MINIO_BACKUP_FILE_PREFIX || "", ""), timestamp: v.lastModified, }, ), ), ); stream.on("error", (err) => reject(err)); }); } @Post("create") async runBackup() { return await fetch( `${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run_wait_result/p/${WINDMILL_BACKUP_SCRIPT_PATH}`, { method: "POST", headers: { Authorization: `Bearer ${WINDMILL_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ s3_endpoint: `${MINIO_USE_SSL === "true" ? "https" : "http"}://${MINIO_HOST}${(MINIO_PORT && ":" + MINIO_PORT) || ""}`, s3_access: MINIO_ACCESS_KEY, s3_secret: MINIO_SECRET_KEY, s3_bucket: MINIO_BUCKET, s3_prefix: MINIO_BACKUP_FILE_PREFIX || "/", db_host: DB_HOST, db_port: DB_PORT, db_user: DB_USERNAME, db_password: DB_PASSWORD, backup_filename: "manual", num_versions_to_keep: 0, }), }, ).then(async (r) => { const data = await r.json(); if (typeof data === "object" && "error" in data) { console.error(data); throw new Error("Backup Error"); } return data; }); } @Post("restore") async restoreBackup(@Body() body: { filename: string }) { return await fetch( `${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/jobs/run_wait_result/p/${WINDMILL_RESTORE_SCRIPT_PATH}`, { method: "POST", headers: { Authorization: `Bearer ${WINDMILL_API_KEY}`, "Content-Type": "application/json", }, body: JSON.stringify({ s3_endpoint: `${MINIO_USE_SSL === "true" ? "https" : "http"}://${MINIO_HOST}${(MINIO_PORT && ":" + MINIO_PORT) || ""}`, s3_access: MINIO_ACCESS_KEY, s3_secret: MINIO_SECRET_KEY, s3_bucket: MINIO_BUCKET, s3_prefix: MINIO_BACKUP_FILE_PREFIX || "/", db_host: DB_HOST, db_port: DB_PORT, db_user: DB_USERNAME, db_password: DB_PASSWORD, restore_filename: body.filename, }), }, ).then(async (r) => { const data = await r.json(); if (typeof data === "object" && "error" in data) { console.error(data); throw new Error("Backup Error"); } return data; }); } @Delete("delete") async deleteBackup(@Body() body: { filename: string }) { await minio.removeObject(MINIO_BUCKET, MINIO_BACKUP_FILE_PREFIX + body.filename, { forceDelete: true, }); } }