import { execFile } from "node:child_process" import folderMap from "../config/backup-folders.json" const REMOTE_HOST = process.env.BACKUPS_REMOTE_HOST || "malio-b" const REMOTE_ROOT = process.env.BACKUPS_REMOTE_ROOT || "/home/malio-b/backups" const MAX_FILES_PER_FOLDER = Number(process.env.BACKUPS_MAX_FILES || "200") const isSafeFolder = (value: string) => /^[a-zA-Z0-9._-]+$/.test(value) const shellQuote = (value: string) => `'${value.replace(/'/g, `'\\''`)}'` const FOLDER_MAP = folderMap as Record function runSsh(command: string): Promise { return new Promise((resolve, reject) => { execFile( "ssh", ["-o", "BatchMode=yes", "-o", "ConnectTimeout=5", REMOTE_HOST, command], { maxBuffer: 10 * 1024 * 1024 }, (error, stdout, stderr) => { if (error) { reject(stderr || error.message) return } resolve(stdout) } ) }) } function isMissingPathError(error: unknown): boolean { const message = String(error || "").toLowerCase() return message.includes("no such file or directory") || message.includes("cannot access") } function toServerError(error: unknown) { return createError({ statusCode: 500, statusMessage: `Erreur SSH backups: ${String(error)}` }) } function parseLines(output: string): string[] { return output .split("\n") .map((line) => line.trim()) .filter(Boolean) } function quoteDir(pathValue: string) { return shellQuote(pathValue) } async function listRemoteFiles(remoteDir: string): Promise { const output = await runSsh( `cd ${quoteDir(remoteDir)} && ls -1A | sort -r | head -n ${MAX_FILES_PER_FOLDER}` ) return parseLines(output) } async function listRemoteDirs(remoteRoot: string): Promise { const output = await runSsh( `cd ${quoteDir(remoteRoot)} && for d in */; do [ -d "$d" ] && printf '%s\n' "\${d%/}"; done` ) return parseLines(output) } async function getLatestRemoteFile(remoteDir: string): Promise { const output = await runSsh( `cd ${quoteDir(remoteDir)} && ls -1A | sort -r | head -n 1` ) const files = parseLines(output) return files[0] || null } async function remoteDirExists(remoteDir: string): Promise { const output = await runSsh(`[ -d ${quoteDir(remoteDir)} ] && echo yes || echo no`) return output.trim() === "yes" } async function resolveFolderRemoteDir(folderName: string): Promise { const mapped = FOLDER_MAP[folderName] if (mapped) { return `${REMOTE_ROOT}/${mapped}` } const direct = `${REMOTE_ROOT}/${folderName}` if (await remoteDirExists(direct)) { return direct } const nested = `${REMOTE_ROOT}/bdd_recette/${folderName}` if (await remoteDirExists(nested)) { return nested } return null } export default defineEventHandler(async (event) => { const { folder } = getQuery(event) const folderName = typeof folder === "string" ? folder : null // Si un dossier est demandé, on retourne sa liste de fichiers. if (folderName) { if (!isSafeFolder(folderName)) { throw createError({ statusCode: 400, statusMessage: "Paramètre folder invalide" }) } const remoteDir = await resolveFolderRemoteDir(folderName) if (!remoteDir) return [] try { return await listRemoteFiles(remoteDir) } catch (error) { if (isMissingPathError(error)) return [] throw toServerError(error) } } // Sinon on récupère le dernier backup de chaque dossier distant. let dirs: string[] = [] try { dirs = await listRemoteDirs(REMOTE_ROOT) } catch (error) { throw toServerError(error) } const result: Array<{ folder: string; last: string | null }> = [] for (const dirName of dirs) { const remoteDir = `${REMOTE_ROOT}/${dirName}` try { result.push({ folder: dirName, last: await getLatestRemoteFile(remoteDir) }) } catch (error) { if (isMissingPathError(error)) { result.push({ folder: dirName, last: null }) continue } throw toServerError(error) } } return result })