Files
harheimertc/server/api/cms/save-csv.post.js
Torsten Schulz (local) 0849c625cb
Some checks failed
Code Analysis and Production Deploy / analyze (push) Has been skipped
Code Analysis and Production Deploy / deploy-production (push) Has been skipped
Code Analysis and Production Deploy / deploy-test (push) Successful in 2m2s
Code Analysis and Production Deploy / analyze (pull_request) Failing after 33s
Code Analysis and Production Deploy / deploy-production (pull_request) Has been skipped
Code Analysis and Production Deploy / deploy-test (pull_request) Has been skipped
Require Package Version Change / check (pull_request) Failing after 10s
Add script for importing match schedule and logging
- Created `import-spielplan.js` to fetch and parse the match schedule from the specified URL, saving the output as JSON.
- Added `run-spielplan-import.sh` to automate the execution of the import script and log output.
- Introduced `spielplan.html` file to store the downloaded HTML content for further processing.
2026-05-19 16:23:28 +02:00

158 lines
5.6 KiB
JavaScript

import fs from 'fs/promises'
import path from 'path'
import { getUserFromToken, hasAnyRole } from '../../utils/auth.js'
import {
createSpielplanJsonFromCsv,
getCurrentSeasonSlug,
getSpielplanSeasonJsonPathForCsvPath,
inferSeasonSlugFromRows
} from '../../utils/spielplan-data.js'
export default defineEventHandler(async (event) => {
try {
let token = getCookie(event, 'auth_token')
if (!token) {
const authHeader = getHeader(event, 'authorization')
if (authHeader && authHeader.startsWith('Bearer ')) {
token = authHeader.substring(7).trim()
}
}
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'
})
}
// Neuer Ablauf (Option B): Schreibe CSVs ausschließlich in internes Datenverzeichnis,
// damit keine direkten Schreibzugriffe auf `public/` stattfinden.
// Später kann ein kontrollierter Deploy-/Sync-Prozess die Daten aus `server/data/public-data`
// in die öffentlich ausgelieferte `public/`-Location übernehmen.
const cwd = process.cwd()
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) { /* no-op */ }
}
} catch (e) {
// best-effort cleanup
try { await fs.unlink(tmpPath) } catch (_e2) { /* no-op */ }
throw e
}
}
// Ziel: internes Datenverzeichnis unter `server/data/public-data` (persistente, interne Quelle)
const dataTargetsByFile = {
'vereinsmeisterschaften.csv': [`${cwd}/server/data/public-data/vereinsmeisterschaften.csv`, `${cwd}/../server/data/public-data/vereinsmeisterschaften.csv`],
'mannschaften.csv': [`${cwd}/server/data/public-data/mannschaften.csv`, `${cwd}/../server/data/public-data/mannschaften.csv`],
'termine.csv': [`${cwd}/server/data/public-data/termine.csv`, `${cwd}/../server/data/public-data/termine.csv`],
'spielplan.csv': [`${cwd}/server/data/public-data/spielplan.csv`, `${cwd}/../server/data/public-data/spielplan.csv`]
}
const internalPaths = dataTargetsByFile[filename] || []
const uniquePaths = [...new Set([...internalPaths])]
const writeResults = []
const writeErrors = []
const jsonWriteResults = []
for (const targetPath of uniquePaths) {
try {
await writeFileAtomicAndVerify(targetPath, content)
writeResults.push(targetPath)
if (filename === 'spielplan.csv') {
const spielplanJson = createSpielplanJsonFromCsv(content)
const seasonSlug = inferSeasonSlugFromRows(spielplanJson.data) || getCurrentSeasonSlug()
const jsonContent = `${JSON.stringify(spielplanJson, null, 2)}\n`
const jsonPath = getSpielplanSeasonJsonPathForCsvPath(targetPath, seasonSlug)
await writeFileAtomicAndVerify(jsonPath, jsonContent)
jsonWriteResults.push(jsonPath)
}
} catch (e) {
writeErrors.push({ targetPath, error: e?.message || String(e) })
}
}
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,
jsonWrittenTo: jsonWriteResults
}
} 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'
})
}
})