Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 46s
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.
166 lines
5.6 KiB
JavaScript
166 lines
5.6 KiB
JavaScript
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'
|
||
})
|
||
}
|
||
})
|