From e5cd26ef1657868fc7e6908c5805207f51ffd9d4 Mon Sep 17 00:00:00 2001 From: Timo Behrendt Date: Mon, 1 Sep 2025 19:53:31 +0200 Subject: [PATCH] sync --- .gitignore | 34 ++++++++++++++ bun.lock | 90 +++++++++++++++++++++++++++++++++++++ package.json | 17 +++++++ src/backup.ts | 33 ++++++++++++++ src/backupContext.ts | 33 ++++++++++++++ src/backupUtils.ts | 100 +++++++++++++++++++++++++++++++++++++++++ src/directoryBackup.ts | 13 ++++++ src/env.ts | 61 +++++++++++++++++++++++++ src/gotify.ts | 23 ++++++++++ src/main.ts | 15 +++++++ src/postgresBackup.ts | 78 ++++++++++++++++++++++++++++++++ tsconfig.json | 29 ++++++++++++ 12 files changed, 526 insertions(+) create mode 100644 .gitignore create mode 100644 bun.lock create mode 100644 package.json create mode 100644 src/backup.ts create mode 100644 src/backupContext.ts create mode 100644 src/backupUtils.ts create mode 100644 src/directoryBackup.ts create mode 100644 src/env.ts create mode 100644 src/gotify.ts create mode 100644 src/main.ts create mode 100644 src/postgresBackup.ts create mode 100644 tsconfig.json diff --git a/.gitignore b/.gitignore new file mode 100644 index 0000000..a14702c --- /dev/null +++ b/.gitignore @@ -0,0 +1,34 @@ +# dependencies (bun install) +node_modules + +# output +out +dist +*.tgz + +# code coverage +coverage +*.lcov + +# logs +logs +_.log +report.[0-9]_.[0-9]_.[0-9]_.[0-9]_.json + +# dotenv environment variable files +.env +.env.development.local +.env.test.local +.env.production.local +.env.local + +# caches +.eslintcache +.cache +*.tsbuildinfo + +# IntelliJ based IDEs +.idea + +# Finder (MacOS) folder config +.DS_Store diff --git a/bun.lock b/bun.lock new file mode 100644 index 0000000..eb7d367 --- /dev/null +++ b/bun.lock @@ -0,0 +1,90 @@ +{ + "lockfileVersion": 1, + "workspaces": { + "": { + "name": "backupsidecar", + "dependencies": { + "env-var": "^7.5.0", + "pino": "^9.9.0", + "pino-pretty": "^13.1.1", + }, + "devDependencies": { + "@types/bun": "latest", + }, + "peerDependencies": { + "typescript": "^5", + }, + }, + }, + "packages": { + "@types/bun": ["@types/bun@1.2.21", "", { "dependencies": { "bun-types": "1.2.21" } }, "sha512-NiDnvEqmbfQ6dmZ3EeUO577s4P5bf4HCTXtI6trMc6f6RzirY5IrF3aIookuSpyslFzrnvv2lmEWv5HyC1X79A=="], + + "@types/node": ["@types/node@24.3.0", "", { "dependencies": { "undici-types": "~7.10.0" } }, "sha512-aPTXCrfwnDLj4VvXrm+UUCQjNEvJgNA8s5F1cvwQU+3KNltTOkBm1j30uNLyqqPNe7gE3KFzImYoZEfLhp4Yow=="], + + "@types/react": ["@types/react@19.1.12", "", { "dependencies": { "csstype": "^3.0.2" } }, "sha512-cMoR+FoAf/Jyq6+Df2/Z41jISvGZZ2eTlnsaJRptmZ76Caldwy1odD4xTr/gNV9VLj0AWgg/nmkevIyUfIIq5w=="], + + "atomic-sleep": ["atomic-sleep@1.0.0", "", {}, "sha512-kNOjDqAh7px0XWNI+4QbzoiR/nTkHAWNud2uvnJquD1/x5a7EQZMJT0AczqK0Qn67oY/TTQ1LbUKajZpp3I9tQ=="], + + "bun-types": ["bun-types@1.2.21", "", { "dependencies": { "@types/node": "*" }, "peerDependencies": { "@types/react": "^19" } }, "sha512-sa2Tj77Ijc/NTLS0/Odjq/qngmEPZfbfnOERi0KRUYhT9R8M4VBioWVmMWE5GrYbKMc+5lVybXygLdibHaqVqw=="], + + "colorette": ["colorette@2.0.20", "", {}, "sha512-IfEDxwoWIjkeXL1eXcDiow4UbKjhLdq6/EuSVR9GMN7KVH3r9gQ83e73hsz1Nd1T3ijd5xv1wcWRYO+D6kCI2w=="], + + "csstype": ["csstype@3.1.3", "", {}, "sha512-M1uQkMl8rQK/szD0LNhtqxIPLpimGm8sOBwU7lLnCpSbTyY3yeU1Vc7l4KT5zT4s/yOxHH5O7tIuuLOCnLADRw=="], + + "dateformat": ["dateformat@4.6.3", "", {}, "sha512-2P0p0pFGzHS5EMnhdxQi7aJN+iMheud0UhG4dlE1DLAlvL8JHjJJTX/CSm4JXwV0Ka5nGk3zC5mcb5bUQUxxMA=="], + + "end-of-stream": ["end-of-stream@1.4.5", "", { "dependencies": { "once": "^1.4.0" } }, "sha512-ooEGc6HP26xXq/N+GCGOT0JKCLDGrq2bQUZrQ7gyrJiZANJ/8YDTxTpQBXGMn+WbIQXNVpyWymm7KYVICQnyOg=="], + + "env-var": ["env-var@7.5.0", "", {}, "sha512-mKZOzLRN0ETzau2W2QXefbFjo5EF4yWq28OyKb9ICdeNhHJlOE/pHHnz4hdYJ9cNZXcJHo5xN4OT4pzuSHSNvA=="], + + "fast-copy": ["fast-copy@3.0.2", "", {}, "sha512-dl0O9Vhju8IrcLndv2eU4ldt1ftXMqqfgN4H1cpmGV7P6jeB9FwpN9a2c8DPGE1Ys88rNUJVYDHq73CGAGOPfQ=="], + + "fast-redact": ["fast-redact@3.5.0", "", {}, "sha512-dwsoQlS7h9hMeYUq1W++23NDcBLV4KqONnITDV9DjfS3q1SgDGVrBdvvTLUotWtPSD7asWDV9/CmsZPy8Hf70A=="], + + "fast-safe-stringify": ["fast-safe-stringify@2.1.1", "", {}, "sha512-W+KJc2dmILlPplD/H4K9l9LcAHAfPtP6BY84uVLXQ6Evcz9Lcg33Y2z1IVblT6xdY54PXYVHEv+0Wpq8Io6zkA=="], + + "help-me": ["help-me@5.0.0", "", {}, "sha512-7xgomUX6ADmcYzFik0HzAxh/73YlKR9bmFzf51CZwR+b6YtzU2m0u49hQCqV6SvlqIqsaxovfwdvbnsw3b/zpg=="], + + "joycon": ["joycon@3.1.1", "", {}, "sha512-34wB/Y7MW7bzjKRjUKTa46I2Z7eV62Rkhva+KkopW7Qvv/OSWBqvkSY7vusOPrNuZcUG3tApvdVgNB8POj3SPw=="], + + "minimist": ["minimist@1.2.8", "", {}, "sha512-2yyAR8qBkN3YuheJanUpWC5U3bb5osDywNB8RzDVlDwDHbocAJveqqj1u8+SVD7jkWT4yvsHCpWqqWqAxb0zCA=="], + + "on-exit-leak-free": ["on-exit-leak-free@2.1.2", "", {}, "sha512-0eJJY6hXLGf1udHwfNftBqH+g73EU4B504nZeKpz1sYRKafAghwxEJunB2O7rDZkL4PGfsMVnTXZ2EjibbqcsA=="], + + "once": ["once@1.4.0", "", { "dependencies": { "wrappy": "1" } }, "sha512-lNaJgI+2Q5URQBkccEKHTQOPaXdUxnZZElQTZY0MFUAuaEqe1E+Nyvgdz/aIyNi6Z9MzO5dv1H8n58/GELp3+w=="], + + "pino": ["pino@9.9.0", "", { "dependencies": { "atomic-sleep": "^1.0.0", "fast-redact": "^3.1.1", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pino-std-serializers": "^7.0.0", "process-warning": "^5.0.0", "quick-format-unescaped": "^4.0.3", "real-require": "^0.2.0", "safe-stable-stringify": "^2.3.1", "sonic-boom": "^4.0.1", "thread-stream": "^3.0.0" }, "bin": { "pino": "bin.js" } }, "sha512-zxsRIQG9HzG+jEljmvmZupOMDUQ0Jpj0yAgE28jQvvrdYTlEaiGwelJpdndMl/MBuRr70heIj83QyqJUWaU8mQ=="], + + "pino-abstract-transport": ["pino-abstract-transport@2.0.0", "", { "dependencies": { "split2": "^4.0.0" } }, "sha512-F63x5tizV6WCh4R6RHyi2Ml+M70DNRXt/+HANowMflpgGFMAym/VKm6G7ZOQRjqN7XbGxK1Lg9t6ZrtzOaivMw=="], + + "pino-pretty": ["pino-pretty@13.1.1", "", { "dependencies": { "colorette": "^2.0.7", "dateformat": "^4.6.3", "fast-copy": "^3.0.2", "fast-safe-stringify": "^2.1.1", "help-me": "^5.0.0", "joycon": "^3.1.1", "minimist": "^1.2.6", "on-exit-leak-free": "^2.1.0", "pino-abstract-transport": "^2.0.0", "pump": "^3.0.0", "secure-json-parse": "^4.0.0", "sonic-boom": "^4.0.1", "strip-json-comments": "^5.0.2" }, "bin": { "pino-pretty": "bin.js" } }, "sha512-TNNEOg0eA0u+/WuqH0MH0Xui7uqVk9D74ESOpjtebSQYbNWJk/dIxCXIxFsNfeN53JmtWqYHP2OrIZjT/CBEnA=="], + + "pino-std-serializers": ["pino-std-serializers@7.0.0", "", {}, "sha512-e906FRY0+tV27iq4juKzSYPbUj2do2X2JX4EzSca1631EB2QJQUqGbDuERal7LCtOpxl6x3+nvo9NPZcmjkiFA=="], + + "process-warning": ["process-warning@5.0.0", "", {}, "sha512-a39t9ApHNx2L4+HBnQKqxxHNs1r7KF+Intd8Q/g1bUh6q0WIp9voPXJ/x0j+ZL45KF1pJd9+q2jLIRMfvEshkA=="], + + "pump": ["pump@3.0.3", "", { "dependencies": { "end-of-stream": "^1.1.0", "once": "^1.3.1" } }, "sha512-todwxLMY7/heScKmntwQG8CXVkWUOdYxIvY2s0VWAAMh/nd8SoYiRaKjlr7+iCs984f2P8zvrfWcDDYVb73NfA=="], + + "quick-format-unescaped": ["quick-format-unescaped@4.0.4", "", {}, "sha512-tYC1Q1hgyRuHgloV/YXs2w15unPVh8qfu/qCTfhTYamaw7fyhumKa2yGpdSo87vY32rIclj+4fWYQXUMs9EHvg=="], + + "real-require": ["real-require@0.2.0", "", {}, "sha512-57frrGM/OCTLqLOAh0mhVA9VBMHd+9U7Zb2THMGdBUoZVOtGbJzjxsYGDJ3A9AYYCP4hn6y1TVbaOfzWtm5GFg=="], + + "safe-stable-stringify": ["safe-stable-stringify@2.5.0", "", {}, "sha512-b3rppTKm9T+PsVCBEOUR46GWI7fdOs00VKZ1+9c1EWDaDMvjQc6tUwuFyIprgGgTcWoVHSKrU8H31ZHA2e0RHA=="], + + "secure-json-parse": ["secure-json-parse@4.0.0", "", {}, "sha512-dxtLJO6sc35jWidmLxo7ij+Eg48PM/kleBsxpC8QJE0qJICe+KawkDQmvCMZUr9u7WKVHgMW6vy3fQ7zMiFZMA=="], + + "sonic-boom": ["sonic-boom@4.2.0", "", { "dependencies": { "atomic-sleep": "^1.0.0" } }, "sha512-INb7TM37/mAcsGmc9hyyI6+QR3rR1zVRu36B0NeGXKnOOLiZOfER5SA+N7X7k3yUYRzLWafduTDvJAfDswwEww=="], + + "split2": ["split2@4.2.0", "", {}, "sha512-UcjcJOWknrNkF6PLX83qcHM6KHgVKNkV62Y8a5uYDVv9ydGQVwAHMKqHdJje1VTWpljG0WYpCDhrCdAOYH4TWg=="], + + "strip-json-comments": ["strip-json-comments@5.0.3", "", {}, "sha512-1tB5mhVo7U+ETBKNf92xT4hrQa3pm0MZ0PQvuDnWgAAGHDsfp4lPSpiS6psrSiet87wyGPh9ft6wmhOMQ0hDiw=="], + + "thread-stream": ["thread-stream@3.1.0", "", { "dependencies": { "real-require": "^0.2.0" } }, "sha512-OqyPZ9u96VohAyMfJykzmivOrY2wfMSf3C5TtFJVgN+Hm6aj+voFhlK+kZEIv2FBh1X6Xp3DlnCOfEQ3B2J86A=="], + + "typescript": ["typescript@5.9.2", "", { "bin": { "tsc": "bin/tsc", "tsserver": "bin/tsserver" } }, "sha512-CWBzXQrc/qOkhidw1OzBTQuYRbfyxDXJMVJ1XNwUHGROVmuaeiEm3OslpZ1RV96d7SKKjZKrSJu3+t/xlw3R9A=="], + + "undici-types": ["undici-types@7.10.0", "", {}, "sha512-t5Fy/nfn+14LuOc2KNYg75vZqClpAiqscVvMygNnlsHBFpSXdJaYtXMcdNLpl/Qvc3P2cB3s6lOV51nqsFq4ag=="], + + "wrappy": ["wrappy@1.0.2", "", {}, "sha512-l4Sp/DRseor9wL6EvV2+TuQn63dMkPjZ/sp9XkghTEbV9KlPS1xUsZ3u7/IQO4wxtcFB4bgpQPRcR3QCvezPcQ=="], + } +} diff --git a/package.json b/package.json new file mode 100644 index 0000000..1f60067 --- /dev/null +++ b/package.json @@ -0,0 +1,17 @@ +{ + "name": "backupsidecar", + "module": "src/main.ts", + "type": "module", + "private": true, + "devDependencies": { + "@types/bun": "latest" + }, + "peerDependencies": { + "typescript": "^5" + }, + "dependencies": { + "env-var": "^7.5.0", + "pino": "^9.9.0", + "pino-pretty": "^13.1.1" + } +} diff --git a/src/backup.ts b/src/backup.ts new file mode 100644 index 0000000..7ae1731 --- /dev/null +++ b/src/backup.ts @@ -0,0 +1,33 @@ +import { + BACKUP_MODE, + globalLogger, + GOTIFY_HOST, + GOTIFY_TOKEN, + GOTIFY_TOPIC, +} from "./env"; +import { directoryBackup } from "./directoryBackup"; +import { postgresBackup } from "./postgresBackup"; +import { gotifyClientFactory } from "./gotify"; +import { createBackupContext, reHomeContext } from "./backupContext"; + +export default async function backup() { + const context = createBackupContext( + "backup", + BACKUP_MODE, + globalLogger, + gotifyClientFactory(GOTIFY_HOST, GOTIFY_TOKEN, GOTIFY_TOPIC) + ); + context.logger.debug("Starting backup"); + + switch (BACKUP_MODE) { + case "directory": + context.logger.debug("Starting directory backup"); + return await directoryBackup(reHomeContext(context, "directoryBackup")); + case "postgres": + context.logger.debug("Starting postgres backup"); + return await postgresBackup(reHomeContext(context, "postgresBackup")); + default: + context.logger.error("Invalid backup mode"); + throw new Error("Invalid backup mode"); + } +} diff --git a/src/backupContext.ts b/src/backupContext.ts new file mode 100644 index 0000000..55d5c8e --- /dev/null +++ b/src/backupContext.ts @@ -0,0 +1,33 @@ +import type { Logger } from "pino"; +import { type NotificationClient } from "./gotify"; + +export interface BackupContext { + logger: Logger; + notificationClient: NotificationClient; + resticRepository: string; +} + +export function createBackupContext( + module: string, + resticRepository: string, + globalLogger: Logger, + notificationClient: NotificationClient +): BackupContext { + const logger = globalLogger.child({ module }); + + return { + logger, + notificationClient, + resticRepository, + }; +} + +export function reHomeContext( + context: BackupContext, + module: string +): BackupContext { + return { + ...context, + logger: context.logger.child({ module }), + }; +} diff --git a/src/backupUtils.ts b/src/backupUtils.ts new file mode 100644 index 0000000..bbf0661 --- /dev/null +++ b/src/backupUtils.ts @@ -0,0 +1,100 @@ +import type { BackupContext } from "./backupContext"; + +export function parseResticSummary(output: string): string | null { + try { + const lines = output.split("\n").filter((line) => line.trim()); + for (const line of lines) { + try { + const parsed = JSON.parse(line); + if (parsed.message_type === "summary") { + return `Snapshot ${parsed.snapshot_id || "none"}: files new: ${ + parsed.files_new || 0 + }, files changed: ${parsed.files_changed || 0}, data added: ${ + parsed.data_added || 0 + } bytes in ${parsed.total_duration || 0} sec`; + } + } catch { + continue; + } + } + } catch (error) { + console.warn(`Failed to parse restic output: ${error}`); + } + return null; +} + +export function runResticBackup( + sourceDir: string, + context: BackupContext +): { success: boolean; output: string; summary: string | null } { + const { logger, resticRepository } = context; + + logger.info( + `Starting backup of '${sourceDir}' to repository ${resticRepository}` + ); + + const result = Bun.spawnSync( + [ + "restic", + "-r", + resticRepository, + "backup", + "--no-cache", + "--json", + "--verbose", + ".", + ], + { + cwd: sourceDir, + stdio: ["pipe", "pipe", "pipe"], + } + ); + + const output = result.stdout?.toString() + result.stderr?.toString() || ""; + const success = result.success; + const summary = parseResticSummary(output); + + return { success, output, summary }; +} + +export async function executeBackup( + backupType: string, + backupFn: () => Promise<{ + success: boolean; + output: string; + summary: string | null; + }>, + context: BackupContext +): Promise { + const { logger, notificationClient } = context; + + try { + logger.info(`Starting ${backupType} backup process`); + + const { success, output, summary } = await backupFn(); + + console.log(output); + + if (success) { + const message = `${backupType} backup successful. ${ + summary || "No summary available" + }`; + logger.info(message); + await notificationClient.sendNotification(message); + } else { + const message = `${backupType} backup failed: ${ + summary || "Unknown error" + }`; + logger.error(message); + await notificationClient.sendNotification(message); + throw new Error(`${backupType} backup failed: ${message}`); + } + + logger.info(`${backupType} backup completed successfully`); + } catch (error) { + const errorMessage = `${backupType} backup failed: ${error}`; + logger.error(errorMessage); + await notificationClient.sendNotification(errorMessage); + throw error; + } +} diff --git a/src/directoryBackup.ts b/src/directoryBackup.ts new file mode 100644 index 0000000..6c602f4 --- /dev/null +++ b/src/directoryBackup.ts @@ -0,0 +1,13 @@ +import { SOURCEDIR } from "./env"; +import { executeBackup, runResticBackup } from "./backupUtils"; +import type { BackupContext } from "./backupContext"; + +export async function directoryBackup(context: BackupContext): Promise { + await executeBackup( + "Directory", + async () => { + return runResticBackup(SOURCEDIR, context); + }, + context + ); +} diff --git a/src/env.ts b/src/env.ts new file mode 100644 index 0000000..39c1ce0 --- /dev/null +++ b/src/env.ts @@ -0,0 +1,61 @@ +import { from } from "env-var"; +import pino from "pino"; + +const initialEnv = from(process.env, {}); +const LOG_LEVEL = initialEnv + .get("LOG_LEVEL") + .default("info") + .asEnum(["fatal", "error", "warn", "info", "debug", "trace"]); + +export const globalLogger = pino({ + level: LOG_LEVEL, + transport: { + target: "pino-pretty", + options: { + colorize: true, + translateTime: "SYS:standard", + ignore: "pid,hostname", + }, + }, +}); + +const env = from(process.env, {}, (msg: string) => globalLogger.info(msg)); + +export const BACKUP_MODE = env + .get("BACKUP_MODE") + .required() + .asEnum(["directory", "postgres"]); + +export const GOTIFY_HOST = env.get("GOTIFY_HOST").required().asUrlString(); +export const GOTIFY_TOKEN = env.get("GOTIFY_TOKEN").required().asString(); +export const GOTIFY_TOPIC = env.get("GOTIFY_TOPIC").required().asString(); + +export const RESTIC_PASSWORD = env.get("RESTIC_PASSWORD").required().asString(); +export const RESTIC_REPOSITORY = env + .get("RESTIC_REPOSITORY") + .required() + .asString(); +export const RESTIC_REST_USERNAME = env.get("RESTIC_REST_USERNAME").asString(); +export const RESTIC_REST_PASSWORD = env.get("RESTIC_REST_PASSWORD").asString(); + +export const SOURCEDIR = env + .get("SOURCEDIR") + .required(BACKUP_MODE === "directory") + .asString(); + +export const PGDATABASE = env + .get("PGDATABASE") + .required(BACKUP_MODE === "postgres") + .asString(); +export const PGHOST = env + .get("PGHOST") + .required(BACKUP_MODE === "postgres") + .asString(); +export const PGUSER = env + .get("PGUSER") + .required(BACKUP_MODE === "postgres") + .asString(); +export const PGPORT = env + .get("PGPORT") + .required(BACKUP_MODE === "postgres") + .asString(); diff --git a/src/gotify.ts b/src/gotify.ts new file mode 100644 index 0000000..1025dc3 --- /dev/null +++ b/src/gotify.ts @@ -0,0 +1,23 @@ +export interface NotificationClient { + sendNotification(message: string): Promise; +} + +export function gotifyClientFactory( + gotifyHost: string, + gotifyToken: string, + gotifyTopic: string +): NotificationClient { + const sendNotification = async (message: string) => { + await fetch(`${gotifyHost}/message?token=${gotifyToken}`, { + method: "POST", + body: JSON.stringify({ + title: gotifyTopic, + message: message, + }), + }); + }; + + return { + sendNotification, + }; +} diff --git a/src/main.ts b/src/main.ts new file mode 100644 index 0000000..fed9041 --- /dev/null +++ b/src/main.ts @@ -0,0 +1,15 @@ +import { globalLogger } from "./env"; +import backup from "./backup"; + +const logger = globalLogger.child({ module: "main" }); + +(async () => { + try { + logger.info("Starting backup application"); + await backup(); + logger.info("Backup application completed successfully"); + } catch (error) { + logger.error(`Backup application failed: ${error}`); + process.exit(1); + } +})(); diff --git a/src/postgresBackup.ts b/src/postgresBackup.ts new file mode 100644 index 0000000..4a8a0bb --- /dev/null +++ b/src/postgresBackup.ts @@ -0,0 +1,78 @@ +import { writeFileSync, mkdtempSync, rmSync } from "fs"; +import { join } from "path"; +import { tmpdir } from "os"; +import { PGHOST, PGDATABASE, PGUSER, PGPORT } from "./env"; +import { executeBackup, runResticBackup } from "./backupUtils"; +import type { BackupContext } from "./backupContext"; + +function dumpPostgresDatabase(context: BackupContext): { + success: boolean; + tempDir: string; + dumpFile: string; +} { + const { logger } = context; + + const tempDir = mkdtempSync(join(tmpdir(), "postgres-backup-")); + const dumpFile = join(tempDir, "dump.sql"); + + logger.info(`Created temporary directory: ${tempDir}`); + logger.info(`Dumping PostgreSQL database to ${dumpFile}...`); + + const result = Bun.spawnSync( + ["pg_dump", "-h", PGHOST, "-p", PGPORT, "-U", PGUSER, PGDATABASE], + { + stdio: ["pipe", "pipe", "pipe"], + } + ); + + if (result.success) { + writeFileSync(dumpFile, result.stdout?.toString() || ""); + logger.info("Database dump created successfully."); + return { success: true, tempDir, dumpFile }; + } else { + logger.error(`PostgreSQL dump failed`); + logger.error(`stderr: ${result.stderr?.toString() || ""}`); + return { success: false, tempDir, dumpFile }; + } +} + +export async function postgresBackup(context: BackupContext): Promise { + let tempDir: string | null = null; + + try { + context.logger.info( + `Starting PostgreSQL backup for database '${PGDATABASE}' on host '${PGHOST}'` + ); + + const { success, tempDir: dir, dumpFile } = dumpPostgresDatabase(context); + tempDir = dir; + + if (!success) { + throw new Error("PostgreSQL dump failed"); + } + + await executeBackup( + "PostgreSQL", + async () => { + if (!tempDir) { + throw new Error("Temporary directory not created"); + } + return runResticBackup(tempDir, context); + }, + context + ); + } catch (error) { + throw error; + } finally { + if (tempDir) { + try { + rmSync(tempDir, { recursive: true, force: true }); + context.logger.info(`Removed temporary directory ${tempDir}`); + } catch (cleanupError) { + context.logger.warn( + `Failed to cleanup temporary directory ${tempDir}: ${cleanupError}` + ); + } + } + } +} diff --git a/tsconfig.json b/tsconfig.json new file mode 100644 index 0000000..bfa0fea --- /dev/null +++ b/tsconfig.json @@ -0,0 +1,29 @@ +{ + "compilerOptions": { + // Environment setup & latest features + "lib": ["ESNext"], + "target": "ESNext", + "module": "Preserve", + "moduleDetection": "force", + "jsx": "react-jsx", + "allowJs": true, + + // Bundler mode + "moduleResolution": "bundler", + "allowImportingTsExtensions": true, + "verbatimModuleSyntax": true, + "noEmit": true, + + // Best practices + "strict": true, + "skipLibCheck": true, + "noFallthroughCasesInSwitch": true, + "noUncheckedIndexedAccess": true, + "noImplicitOverride": true, + + // Some stricter flags (disabled by default) + "noUnusedLocals": false, + "noUnusedParameters": false, + "noPropertyAccessFromIndexSignature": false + } +}