Files
harheimertc/server/api/cms/save-csv.post.js
Torsten Schulz (local) 27312cc118
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 46s
Implement cleanup of old compressed CSV files after successful write in CMS
This commit adds logic to remove outdated `.gz` and `.br` files after a successful CSV write operation in the CMS. This ensures that users do not encounter inconsistent content due to leftover pre-compressed assets, enhancing data integrity and reliability in the application.
2026-01-19 08:13:02 +01:00

166 lines
5.6 KiB
JavaScript
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
export default defineEventHandler(async (event) => {
try {
const token = getCookie(event, 'auth_token')
const currentUser = token ? await getUserFromToken(token) : null
if (!currentUser) {
throw createError({
statusCode: 401,
statusMessage: 'Nicht authentifiziert'
})
}
if (!hasAnyRole(currentUser, 'admin', 'vorstand')) {
throw createError({
statusCode: 403,
statusMessage: 'Keine Berechtigung'
})
}
const { filename, content } = await readBody(event)
if (!filename || !content) {
throw createError({
statusCode: 400,
statusMessage: 'Filename und Content sind erforderlich'
})
}
// Sicherheitsprüfung: Nur bestimmte Dateien erlauben
const allowedFiles = [
'vereinsmeisterschaften.csv',
'mannschaften.csv',
'termine.csv',
'spielplan.csv'
]
if (!allowedFiles.includes(filename)) {
throw createError({
statusCode: 403,
statusMessage: 'Datei nicht erlaubt'
})
}
// Wichtig: In Production werden statische Dateien aus `.output/public` ausgeliefert.
// Wenn PM2 `cwd` auf das Repo-Root setzt, ist `process.cwd()` NICHT `.output`
// daher schreiben wir robust in alle sinnvollen Zielorte:
// - `.output/public/data/<file>` (damit die laufende Instanz sofort die neuen Daten liefert)
// - `public/data/<file>` (damit der nächste Build die Daten wieder übernimmt)
//
// nosemgrep: javascript.lang.security.audit.path-traversal.path-join-resolve-traversal.path-join-resolve-traversal
// filename is validated against allowlist above, path traversal prevented
const cwd = process.cwd()
const pathExists = async (p) => {
try {
await fs.access(p)
return true
} catch {
return false
}
}
const writeFileAtomicAndVerify = async (targetPath, data) => {
const dataDir = path.dirname(targetPath)
await fs.mkdir(dataDir, { recursive: true })
// Atomar schreiben: erst temp-Datei in *gleichem Verzeichnis*, dann rename.
// So vermeiden wir:
// - halb geschriebene Dateien (Reader sieht "Partial Transfer")
// - Erfolgsmeldungen, obwohl die Datei effektiv kaputt ist
const tmpPath = `${targetPath}.tmp-${process.pid}-${Date.now()}`
try {
await fs.writeFile(tmpPath, data, 'utf8')
await fs.rename(tmpPath, targetPath)
const expectedSize = Buffer.byteLength(data, 'utf8')
const st = await fs.stat(targetPath)
if (st.size !== expectedSize) {
throw new Error(`Size mismatch after write. expected=${expectedSize} actual=${st.size}`)
}
// Wenn beim Build pre-komprimierte Assets erzeugt wurden, können für CSVs
// noch alte `.gz`/`.br` Dateien liegen bleiben. Nach einem Update würden dann
// ggf. inkonsistente Inhalte ausgeliefert (Browser meldet Partial Transfer).
// Daher: nach erfolgreichem Schreiben alte Varianten entfernen.
for (const ext of ['.gz', '.br']) {
try { await fs.unlink(`${targetPath}${ext}`) } catch (_e3) {}
}
} catch (e) {
// best-effort cleanup
try { await fs.unlink(tmpPath) } catch (_e2) {}
throw e
}
}
// Preferred: das tatsächlich ausgelieferte Verzeichnis in Production
// (Nuxt/Nitro serve static aus `.output/public`)
const preferredPaths = []
if (await pathExists(path.join(cwd, '.output/public'))) {
preferredPaths.push(path.join(cwd, '.output/public/data', filename))
}
if (await pathExists(path.join(cwd, '../.output/public'))) {
preferredPaths.push(path.join(cwd, '../.output/public/data', filename))
}
// Fallbacks: Source-Public (für Persistenz bei nächstem Build) und diverse cwd-Layouts
const fallbackPaths = [
path.join(cwd, 'public/data', filename),
path.join(cwd, '../public/data', filename)
]
const uniquePaths = [...new Set([...preferredPaths, ...fallbackPaths])]
const writeResults = []
const writeErrors = []
let wrotePreferred = false
for (const targetPath of uniquePaths) {
try {
await writeFileAtomicAndVerify(targetPath, content)
writeResults.push(targetPath)
if (preferredPaths.includes(targetPath)) wrotePreferred = true
} catch (e) {
writeErrors.push({ targetPath, error: e?.message || String(e) })
}
}
// Wenn wir ein `.output/public` gefunden haben, MUSS auch dorthin geschrieben worden sein.
// Sonst melden wir nicht "Erfolg", weil die laufende Instanz dann weiterhin alte/defekte Daten ausliefert.
if (preferredPaths.length > 0 && !wrotePreferred) {
console.error('CSV wurde NICHT in .output/public geschrieben. Errors:', writeErrors)
throw createError({
statusCode: 500,
statusMessage: 'CSV konnte nicht in das ausgelieferte Verzeichnis geschrieben werden'
})
}
if (writeResults.length === 0) {
console.error('Konnte CSV-Datei in keinen Zielpfad schreiben:', writeErrors)
throw createError({
statusCode: 500,
statusMessage: 'Fehler beim Speichern der Datei'
})
}
return {
success: true,
message: 'Datei erfolgreich gespeichert',
writtenTo: writeResults
}
} catch (error) {
console.error('Fehler beim Speichern der CSV-Datei:', error)
if (error.statusCode) {
throw error
}
throw createError({
statusCode: 500,
statusMessage: 'Fehler beim Speichern der Datei'
})
}
})