// Build-Protokolle je Projekt: upload-data/preview-logs//, maskiert, letzte 5 import crypto from 'node:crypto' import fs from 'node:fs/promises' import path from 'node:path' import { config } from '../config.js' import { maskLog } from './previewEnv.js' const KEEP = 5 const MAX_LINES = 300 const PROJECT_RE = /^[A-Za-z0-9][A-Za-z0-9._-]{0,35}$/ const FILE_RE = /^\d{4}-\d{2}-\d{2}T\d{2}-\d{2}-\d{2}-\d{3}Z-[0-9a-f]{6}\.log$/ const projectDir = (projectId) => { if (!PROJECT_RE.test(String(projectId || '')) || String(projectId).includes('..')) { throw new Error('Ungültige Projekt-ID') } return path.join(config.previewRunner.logDir, projectId) } export async function saveRunLog(projectId, lines, env) { const dir = projectDir(projectId) await fs.mkdir(dir, { recursive: true }) const stamp = new Date().toISOString().replace(/[:.]/g, '-') const runFile = `${stamp}-${crypto.randomBytes(3).toString('hex')}.log` const text = maskLog((Array.isArray(lines) ? lines : []).slice(-MAX_LINES).join('\n'), env) await fs.writeFile(path.join(dir, runFile), text + '\n', 'utf8') const all = (await fs.readdir(dir)).filter((f) => FILE_RE.test(f)).sort() for (const old of all.slice(0, Math.max(0, all.length - KEEP))) { await fs.rm(path.join(dir, old), { force: true }) } return runFile } export const isRunFile = (name) => FILE_RE.test(String(name || '')) // Neueste zuerst export async function listRunLogs(projectId) { let files try { files = await fs.readdir(projectDir(projectId)) } catch { return [] } return files .filter((f) => FILE_RE.test(f)) .sort() .reverse() .map((runFile) => ({ runFile, at: runFile.slice(0, 24).replace(/^(.{13})-(.{2})-(.{2})-/, '$1:$2:$3.'), })) } export async function readRunLog(projectId, runFile) { if (!FILE_RE.test(String(runFile || ''))) return null try { return await fs.readFile(path.join(projectDir(projectId), runFile), 'utf8') } catch { return null } }