diff --git a/dist/app/downloadDemoFile.js b/dist/app/downloadDemoFile.js index fcea04c..1c4ac88 100644 --- a/dist/app/downloadDemoFile.js +++ b/dist/app/downloadDemoFile.js @@ -12,6 +12,8 @@ const stream_1 = require("stream"); const util_1 = require("util"); const unbzip2_stream_1 = __importDefault(require("unbzip2-stream")); const pipe = (0, util_1.promisify)(stream_1.pipeline); +// In-Flight-Lock pro Ziel-.dem +const inflight = new Map(); /** * Entpackt eine .bz2-Datei mithilfe von Streams nach .dem */ @@ -114,54 +116,99 @@ async function downloadDemoFile(match, outputBaseDir = 'demos', onProgress) { const isPremier = !!lastRound?.b_switched_teams; const matchType = isPremier ? 'premier' : 'competitive'; const tempDir = path_1.default.join(outputBaseDir, 'temp'); - const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`; - const baseName = path_1.default.parse(tempFileName).name; - const tempFile = path_1.default.join(tempDir, tempFileName); const finalDir = path_1.default.join(outputBaseDir, matchDate); - const finalFile = path_1.default.join(finalDir, `${baseName}.dem`); + // Stabiler finaler Basisname (wichtig fürs De-Duplizieren) + const baseFinalName = `match${appId}_${mapName}_${matchId}_${matchType}`; + const finalFile = path_1.default.join(finalDir, `${baseFinalName}.dem`); const finalFileName = path_1.default.basename(finalFile); - fs_1.default.mkdirSync(tempDir, { recursive: true }); - fs_1.default.mkdirSync(finalDir, { recursive: true }); - console.log(`📥 Lade Demo von ${demoUrl}...`); - try { - const success = await downloadWithHttps(demoUrl, tempFile, onProgress); - if (!success || !fs_1.default.existsSync(tempFile) || fs_1.default.statSync(tempFile).size === 0) { - console.warn(`⚠️ Download fehlgeschlagen oder Datei leer – lösche ${tempFileName}`); + const partialFile = `${finalFile}.part`; + // Bereits vorhanden? -> sofort zurück + if (fs_1.default.existsSync(finalFile)) { + console.log(`♻️ Demo existiert bereits: ${finalFileName}`); + return finalFile; + } + // Parallele Requests für dieselbe Zieldatei zusammenlegen + if (inflight.has(finalFile)) { + return await inflight.get(finalFile); + } + // Eindeutiger Temp-Dateiname im temp/ + const rand = Math.random().toString(36).slice(2, 8); + const tempFileName = `${baseFinalName}_${rand}.bz2`; + const tempFile = path_1.default.join(tempDir, tempFileName); + const job = (async () => { + fs_1.default.mkdirSync(tempDir, { recursive: true }); + fs_1.default.mkdirSync(finalDir, { recursive: true }); + // Stale .part von früheren Abbrüchen entfernen + try { + if (fs_1.default.existsSync(partialFile)) + fs_1.default.unlinkSync(partialFile); + } + catch { } + console.log(`📥 Lade Demo von ${demoUrl}...`); + try { + const success = await downloadWithHttps(demoUrl, tempFile, onProgress); + if (!success || !fs_1.default.existsSync(tempFile) || fs_1.default.statSync(tempFile).size === 0) { + console.warn(`⚠️ Download fehlgeschlagen oder Datei leer – lösche ${tempFileName}`); + try { + if (fs_1.default.existsSync(tempFile)) + fs_1.default.unlinkSync(tempFile); + } + catch { } + return ''; + } + } + catch (err) { + throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`); + } + console.log(`✅ Gespeichert als ${tempFileName}`); + const entpackZeile = `🗜️ Entpacke nach ${path_1.default.basename(partialFile)}...`; + process.stdout.write(entpackZeile); + try { + await extractBz2Safe(tempFile, partialFile); + } + catch (e) { + try { + if (fs_1.default.existsSync(partialFile)) + fs_1.default.unlinkSync(partialFile); + } + catch { } try { if (fs_1.default.existsSync(tempFile)) fs_1.default.unlinkSync(tempFile); } - catch { - console.warn(`⚠️ Konnte leere Datei nicht löschen: ${tempFileName}`); - } - return ''; + catch { } + throw e; } - } - catch (err) { - throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`); - } - console.log(`✅ Gespeichert als ${tempFileName}`); - const entpackZeile = `🗜️ Entpacke ${finalFileName}...`; - process.stdout.write(entpackZeile); - await extractBz2Safe(tempFile, finalFile); - const successMessage = `✅ Entpackt: ${finalFileName}`; - const failMessage = `❌ Entpackung fehlgeschlagen – Datei nicht vorhanden`; - // Max-Zeichenlänge bestimmen - const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length); - if (!fs_1.default.existsSync(finalFile)) { - const paddedFail = failMessage.padEnd(maxLength, ' '); - process.stdout.write(`\r${paddedFail}\n`); - throw new Error(failMessage); - } - const paddedSuccess = successMessage.padEnd(maxLength, ' '); - process.stdout.write(`\r${paddedSuccess}\n`); - // Aufräumen + // Prüfen & atomar nach .dem verschieben + const successMessage = `✅ Entpackt: ${finalFileName}`; + const failMessage = `❌ Entpackung fehlgeschlagen – Datei nicht vorhanden`; + const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length); + if (!fs_1.default.existsSync(partialFile)) { + process.stdout.write(`\r${failMessage.padEnd(maxLength, ' ')}\n`); + try { + if (fs_1.default.existsSync(tempFile)) + fs_1.default.unlinkSync(tempFile); + } + catch { } + throw new Error('Entpackung fehlgeschlagen'); + } + fs_1.default.renameSync(partialFile, finalFile); + process.stdout.write(`\r${successMessage.padEnd(maxLength, ' ')}\n`); + // Aufräumen + try { + fs_1.default.unlinkSync(tempFile); + console.log(`🧹 Gelöscht: ${tempFileName}`); + } + catch { + console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); + } + return finalFile; + })(); + inflight.set(finalFile, job); try { - fs_1.default.unlinkSync(tempFile); - console.log(`🧹 Gelöscht: ${tempFileName}`); + return await job; } - catch { - console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); + finally { + inflight.delete(finalFile); } - return finalFile; } diff --git a/dist/cs2-demo-downloader-linux b/dist/cs2-demo-downloader-linux index 168d093..e746056 100644 Binary files a/dist/cs2-demo-downloader-linux and b/dist/cs2-demo-downloader-linux differ diff --git a/dist/cs2-demo-downloader-macos b/dist/cs2-demo-downloader-macos index 5a13e32..e458635 100644 Binary files a/dist/cs2-demo-downloader-macos and b/dist/cs2-demo-downloader-macos differ diff --git a/dist/cs2-demo-downloader-win.exe b/dist/cs2-demo-downloader-win.exe index 2d79045..84fab23 100644 Binary files a/dist/cs2-demo-downloader-win.exe and b/dist/cs2-demo-downloader-win.exe differ diff --git a/dist/main.js b/dist/main.js index 2ac647c..cdd936b 100644 --- a/dist/main.js +++ b/dist/main.js @@ -49,12 +49,16 @@ async function start() { console.log('✅ Download abgeschlossen'); } }); - // JSON-Dateipfad erstellen - const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json'); - const jsonFileName = path_1.default.basename(jsonFilePath); - // Match-Daten als JSON schreiben - await fs_1.default.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8'); - console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); + // JSON nur erstellen, wenn die .dem existiert + if (demoFilePath && fs_1.default.existsSync(demoFilePath)) { + const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json'); + const jsonFileName = path_1.default.basename(jsonFilePath); + await fs_1.default.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8'); + console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); + } + else { + console.warn('⚠️ Keine Demo-Datei vorhanden – JSON wird nicht erstellt.'); + } // Antwort an den Client res.writeHead(200, { 'Content-Type': 'application/json' }); res.end(JSON.stringify({ success: true, path: demoFilePath })); diff --git a/src/app/downloadDemoFile.ts b/src/app/downloadDemoFile.ts index 6ea8f08..dd50811 100644 --- a/src/app/downloadDemoFile.ts +++ b/src/app/downloadDemoFile.ts @@ -8,6 +8,9 @@ import bz2 from 'unbzip2-stream'; const pipe = promisify(pipeline); +// In-Flight-Lock pro Ziel-.dem +const inflight: Map> = new Map(); + /** * Entpackt eine .bz2-Datei mithilfe von Streams nach .dem */ @@ -107,8 +110,6 @@ function downloadWithHttps( }); } - - /** * Hauptfunktion: lädt und entpackt eine CS2-Demo (.bz2), mit Fortschrittsanzeige. */ @@ -146,66 +147,92 @@ export async function downloadDemoFile( const matchType = isPremier ? 'premier' : 'competitive'; const tempDir = path.join(outputBaseDir, 'temp'); - const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`; - const baseName = path.parse(tempFileName).name; + const finalDir = path.join(outputBaseDir, matchDate); + + // Stabiler finaler Basisname (wichtig fürs De-Duplizieren) + const baseFinalName = `match${appId}_${mapName}_${matchId}_${matchType}`; + const finalFile = path.join(finalDir, `${baseFinalName}.dem`); + const finalFileName = path.basename(finalFile); + const partialFile = `${finalFile}.part`; + + // Bereits vorhanden? -> sofort zurück + if (fs.existsSync(finalFile)) { + console.log(`♻️ Demo existiert bereits: ${finalFileName}`); + return finalFile; + } + + // Parallele Requests für dieselbe Zieldatei zusammenlegen + if (inflight.has(finalFile)) { + return await inflight.get(finalFile)!; + } + + // Eindeutiger Temp-Dateiname im temp/ + const rand = Math.random().toString(36).slice(2, 8); + const tempFileName = `${baseFinalName}_${rand}.bz2`; const tempFile = path.join(tempDir, tempFileName); - const finalDir = path.join(outputBaseDir, matchDate); - const finalFile = path.join(finalDir, `${baseName}.dem`); - const finalFileName = path.basename(finalFile) + const job = (async () => { + fs.mkdirSync(tempDir, { recursive: true }); + fs.mkdirSync(finalDir, { recursive: true }); - fs.mkdirSync(tempDir, { recursive: true }); - fs.mkdirSync(finalDir, { recursive: true }); + // Stale .part von früheren Abbrüchen entfernen + try { if (fs.existsSync(partialFile)) fs.unlinkSync(partialFile); } catch {} - console.log(`📥 Lade Demo von ${demoUrl}...`); + console.log(`📥 Lade Demo von ${demoUrl}...`); - try { - const success = await downloadWithHttps(demoUrl, tempFile, onProgress); - if (!success || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) { - console.warn(`⚠️ Download fehlgeschlagen oder Datei leer – lösche ${tempFileName}`); - - try { - if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); - } catch { - console.warn(`⚠️ Konnte leere Datei nicht löschen: ${tempFileName}`); + try { + const success = await downloadWithHttps(demoUrl, tempFile, onProgress); + if (!success || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) { + console.warn(`⚠️ Download fehlgeschlagen oder Datei leer – lösche ${tempFileName}`); + try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {} + return ''; } - - return ''; + } catch (err) { + throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`); } - } catch (err) { - throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : err}`); - } - - console.log(`✅ Gespeichert als ${tempFileName}`); + console.log(`✅ Gespeichert als ${tempFileName}`); - const entpackZeile = `🗜️ Entpacke ${finalFileName}...`; - process.stdout.write(entpackZeile); + const entpackZeile = `🗜️ Entpacke nach ${path.basename(partialFile)}...`; + process.stdout.write(entpackZeile); - await extractBz2Safe(tempFile, finalFile); + try { + await extractBz2Safe(tempFile, partialFile); + } catch (e) { + try { if (fs.existsSync(partialFile)) fs.unlinkSync(partialFile); } catch {} + try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {} + throw e; + } - const successMessage = `✅ Entpackt: ${finalFileName}`; - const failMessage = `❌ Entpackung fehlgeschlagen – Datei nicht vorhanden`; + // Prüfen & atomar nach .dem verschieben + const successMessage = `✅ Entpackt: ${finalFileName}`; + const failMessage = `❌ Entpackung fehlgeschlagen – Datei nicht vorhanden`; + const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length); - // Max-Zeichenlänge bestimmen - const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length); + if (!fs.existsSync(partialFile)) { + process.stdout.write(`\r${failMessage.padEnd(maxLength, ' ')}\n`); + try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {} + throw new Error('Entpackung fehlgeschlagen'); + } - if (!fs.existsSync(finalFile)) { - const paddedFail = failMessage.padEnd(maxLength, ' '); - process.stdout.write(`\r${paddedFail}\n`); - throw new Error(failMessage); - } + fs.renameSync(partialFile, finalFile); + process.stdout.write(`\r${successMessage.padEnd(maxLength, ' ')}\n`); - const paddedSuccess = successMessage.padEnd(maxLength, ' '); - process.stdout.write(`\r${paddedSuccess}\n`); + // Aufräumen + try { + fs.unlinkSync(tempFile); + console.log(`🧹 Gelöscht: ${tempFileName}`); + } catch { + console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); + } - // Aufräumen + return finalFile; + })(); + + inflight.set(finalFile, job); try { - fs.unlinkSync(tempFile); - console.log(`🧹 Gelöscht: ${tempFileName}`); - } catch { - console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); + return await job; + } finally { + inflight.delete(finalFile); } - - return finalFile; } diff --git a/src/main.ts b/src/main.ts index 9dcd304..3641988 100644 --- a/src/main.ts +++ b/src/main.ts @@ -56,13 +56,16 @@ async function start() { } ); - // JSON-Dateipfad erstellen - const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json'); - const jsonFileName = path.basename(jsonFilePath) + // JSON nur erstellen, wenn die .dem existiert + if (demoFilePath && fs.existsSync(demoFilePath)) { + const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json'); + const jsonFileName = path.basename(jsonFilePath); - // Match-Daten als JSON schreiben - await fs.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8'); - console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); + await fs.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8'); + console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); + } else { + console.warn('⚠️ Keine Demo-Datei vorhanden – JSON wird nicht erstellt.'); + } // Antwort an den Client res.writeHead(200, { 'Content-Type': 'application/json' });