Files
harheimertc/server/api/cms/save-csv.post.js
Torsten Schulz (local) ac7a57347a
Some checks failed
Code Analysis (JS/Vue) / analyze (push) Failing after 47s
Enhance CSV file saving mechanism in CMS with atomic write and verification
This commit improves the CSV file handling in the CMS by implementing an atomic write function that ensures data integrity during file saves. It introduces a verification step to check file size after writing, preventing issues with incomplete or corrupted files. Additionally, it refines the logic for determining target paths, prioritizing preferred directories and providing better error handling for write operations. These changes enhance the reliability and robustness of data management in the application.
2026-01-18 23:50:25 +01:00

158 lines
5.2 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}`)
}
} 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'
})
}
})