import { runSsh, shellQuote, resolveFolderRemoteDir, REMOTE_HOST, isSafeFolder, isSafeFile } from "../utils/ssh.ts" import { spawn } from "node:child_process" function buildContentDisposition(fileName: string) { const asciiName = fileName.replace(/[^\x20-\x7E]/g, "_").replace(/["\\]/g, "_") return `attachment; filename="${asciiName}"; filename*=UTF-8''${encodeURIComponent(fileName)}` } export default defineEventHandler(async (event) => { const { folder, file } = getQuery(event) const folderName = typeof folder === "string" ? folder : null const fileName = typeof file === "string" ? file : null if (!folderName || !fileName) { throw createError({ statusCode: 400, statusMessage: "Paramètres manquants" }) } if (!isSafeFolder(folderName) || !isSafeFile(fileName)) { throw createError({ statusCode: 400, statusMessage: "Paramètres invalides" }) } const remoteDir = await resolveFolderRemoteDir(folderName) if (!remoteDir) { throw createError({ statusCode: 404, statusMessage: "Dossier introuvable" }) } const remotePath = `${remoteDir}/${fileName}` const existsOutput = await runSsh(`[ -f ${shellQuote(remotePath)} ] && echo yes || echo no`) if (existsOutput.trim() !== "yes") { throw createError({ statusCode: 404, statusMessage: "Fichier introuvable" }) } setHeader(event, "Content-Type", "application/octet-stream") setHeader(event, "Content-Disposition", buildContentDisposition(fileName)) const child = spawn("ssh", [ "-o", "BatchMode=yes", "-o", "ConnectTimeout=5", REMOTE_HOST, `cat ${shellQuote(remotePath)}` ]) let stderr = "" child.stderr.on("data", (chunk) => { stderr += chunk.toString() }) child.on("close", (code) => { if (code !== 0) { console.error(`Erreur téléchargement SSH (${code}): ${stderr}`) } }) event.node.res.on("close", () => child.kill()) return sendStream(event, child.stdout) })