feat: schedule backup

This commit is contained in:
Methapon2001 2024-07-17 14:02:52 +07:00
parent d070d46525
commit ca12d80aa8
2 changed files with 202 additions and 3 deletions

View file

@ -1,6 +1,7 @@
import { Body, Controller, Delete, Get, Post, Route, Security } from "tsoa"; import { Body, Controller, Delete, Get, Path, Post, Put, Route, Security } from "tsoa";
import HttpError from "../interfaces/http-error"; import HttpError from "../interfaces/http-error";
import HttpStatus from "../interfaces/http-status"; import HttpStatus from "../interfaces/http-status";
import { randomUUID } from "crypto";
function getEnvVar(environmentName: string) { function getEnvVar(environmentName: string) {
const environmentValue = process.env[environmentName]; const environmentValue = process.env[environmentName];
@ -241,4 +242,171 @@ export class BackupController extends Controller {
return data; return data;
}); });
} }
@Get("schedule")
async listSchedule() {
const result = await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/list?path=${WINDMILL_BACKUP_FLOW_PATH}`,
{
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
},
},
).then(async (r) => {
const data = await r.json();
if (typeof data === "object" && "error" in data) {
console.error(data);
throw new Error("Cannot get status.");
}
return data as Record<string, any>[];
});
return result.map((v) => ({
id: v.path.replace("f/backup_schedule/", ""),
name: v.summary,
schedule: v.schedule,
}));
}
@Post("schedule")
async createSchedule(@Body() body: { name: string; schedule: string; timezone?: string }) {
if (!/^[a-zA-Z0-9\-]+$/.test(body.name)) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid name.");
}
return await fetch(`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/create`, {
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
schedule: body.schedule,
summary: body.name,
path: `f/backup_schedule/${randomUUID()}`,
enabled: true,
is_flow: true,
script_path: WINDMILL_BACKUP_FLOW_PATH,
timezone: body.timezone || "Asia/Bangkok",
args: {
backup_name: body.name,
prefix_timestamp: true,
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,
db_list: DB_LIST?.replace(",", " "),
},
},
}),
}).then(async (r) => {
const data = await r.text();
if (data.includes("error")) {
console.error(data);
throw new Error("Error create schedule.");
}
console.log(data);
return data;
});
}
@Put("schedule/{id}")
async updateSchedule(
@Path() id: string,
@Body() body: { name: string; schedule: string; timezone?: string },
) {
if (!/^[a-zA-Z0-9\-]+$/.test(body.name)) {
throw new HttpError(HttpStatus.BAD_REQUEST, "Invalid name.");
}
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/update/f/backup_schedule/${id}`,
{
method: "POST",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
body: JSON.stringify({
summary: body.name,
schedule: body.schedule,
timezone: body.timezone || "Asia/Bangkok",
args: {
backup_name: body.name,
prefix_timestamp: true,
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,
db_list: DB_LIST?.replace(",", " "),
},
},
}),
},
).then(async (r) => {
if (r.status >= 400) {
console.log(await r.json());
throw new Error("Error trying to run script/flow.");
}
const data = await r.text();
if (data.includes("error")) {
console.error(data);
throw new Error("Error create schedule.");
}
return data;
});
}
@Delete("schedule/{id}")
async deleteSchedule(@Path() id: string) {
return await fetch(
`${WINDMILL_URL}/api/w/${WINDMILL_WORKSPACE}/schedules/delete/f/backup_schedule/${id}`,
{
method: "DELETE",
headers: {
Authorization: `Bearer ${WINDMILL_API_KEY}`,
"Content-Type": "application/json",
},
},
).then(async (r) => {
if (r.status === 404) {
throw new HttpError(HttpStatus.NOT_FOUND, "Schedule not found.");
}
const data = await r.text();
if (data.includes("error")) {
console.error(data);
throw new Error("Error create schedule.");
}
});
}
} }

View file

@ -174,6 +174,7 @@ value:
modules: modules:
- id: c - id: c
value: value:
tag: ""
lock: |- lock: |-
{ {
"dependencies": {} "dependencies": {}
@ -195,6 +196,32 @@ value:
databaseBackupBucket: databaseBackupBucket:
expr: "`${flow_input.database.s3_bucket}`" expr: "`${flow_input.database.s3_bucket}`"
type: javascript type: javascript
summary: Conflict Backup Database Bucket Check
- id: d
value:
tag: ""
lock: |-
{
"dependencies": {}
}
//bun.lockb
<empty>
type: rawscript
content: >
export async function main(prefixTimestamp: boolean, bucketName:
string) {
if (prefixTimestamp) return `${Math.round(Date.now() / 1000)}-${bucketName}`;
return bucketName;
}
language: bun
input_transforms:
bucketName:
expr: flow_input.backup_name
type: javascript
prefixTimestamp:
expr: flow_input.prefix_timestamp
type: javascript
summary: Prefix backup name with timestamp
- id: a - id: a
value: value:
path: f/storage/backup_s3 path: f/storage/backup_s3
@ -204,7 +231,7 @@ value:
expr: "`${flow_input.storage.s3_dest_access}`" expr: "`${flow_input.storage.s3_dest_access}`"
type: javascript type: javascript
s3_dest_bucket: s3_dest_bucket:
expr: "`${flow_input.backup_name}`" expr: "`${results.d}`"
type: javascript type: javascript
s3_dest_secret: s3_dest_secret:
expr: "`${flow_input.storage.s3_dest_secret}`" expr: "`${flow_input.storage.s3_dest_secret}`"
@ -261,7 +288,7 @@ value:
expr: "`${flow_input.database.s3_endpoint}`" expr: "`${flow_input.database.s3_endpoint}`"
type: javascript type: javascript
backup_filename: backup_filename:
expr: "`${flow_input.backup_name}`" expr: "`${results.d}`"
type: javascript type: javascript
schema: schema:
$schema: https://json-schema.org/draft/2020-12/schema $schema: https://json-schema.org/draft/2020-12/schema
@ -495,6 +522,9 @@ schema:
- s3_dest_access - s3_dest_access
- s3_dest_secret - s3_dest_secret
description: "" description: ""
prefix_timestamp:
default: false
type: boolean
required: required:
- database - database
- storage - storage
@ -504,6 +534,7 @@ schema:
- backup_name - backup_name
- database - database
- storage - storage
- prefix_timestamp
``` ```
```yaml ```yaml