This commit is contained in:
Linrador 2025-08-14 15:06:59 +02:00
parent ae11e8e17d
commit 64faf5ced6
7 changed files with 180 additions and 99 deletions

View File

@ -12,6 +12,8 @@ const stream_1 = require("stream");
const util_1 = require("util"); const util_1 = require("util");
const unbzip2_stream_1 = __importDefault(require("unbzip2-stream")); const unbzip2_stream_1 = __importDefault(require("unbzip2-stream"));
const pipe = (0, util_1.promisify)(stream_1.pipeline); 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 * Entpackt eine .bz2-Datei mithilfe von Streams nach .dem
*/ */
@ -114,14 +116,34 @@ async function downloadDemoFile(match, outputBaseDir = 'demos', onProgress) {
const isPremier = !!lastRound?.b_switched_teams; const isPremier = !!lastRound?.b_switched_teams;
const matchType = isPremier ? 'premier' : 'competitive'; const matchType = isPremier ? 'premier' : 'competitive';
const tempDir = path_1.default.join(outputBaseDir, 'temp'); 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 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); const finalFileName = path_1.default.basename(finalFile);
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(tempDir, { recursive: true });
fs_1.default.mkdirSync(finalDir, { 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}...`); console.log(`📥 Lade Demo von ${demoUrl}...`);
try { try {
const success = await downloadWithHttps(demoUrl, tempFile, onProgress); const success = await downloadWithHttps(demoUrl, tempFile, onProgress);
@ -131,9 +153,7 @@ async function downloadDemoFile(match, outputBaseDir = 'demos', onProgress) {
if (fs_1.default.existsSync(tempFile)) if (fs_1.default.existsSync(tempFile))
fs_1.default.unlinkSync(tempFile); fs_1.default.unlinkSync(tempFile);
} }
catch { catch { }
console.warn(`⚠️ Konnte leere Datei nicht löschen: ${tempFileName}`);
}
return ''; return '';
} }
} }
@ -141,20 +161,39 @@ async function downloadDemoFile(match, outputBaseDir = 'demos', onProgress) {
throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : 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}...`; const entpackZeile = `🗜️ Entpacke nach ${path_1.default.basename(partialFile)}...`;
process.stdout.write(entpackZeile); process.stdout.write(entpackZeile);
await extractBz2Safe(tempFile, finalFile); 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 { }
throw e;
}
// Prüfen & atomar nach .dem verschieben
const successMessage = `✅ Entpackt: ${finalFileName}`; const successMessage = `✅ Entpackt: ${finalFileName}`;
const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`; const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`;
// Max-Zeichenlänge bestimmen
const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length); const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length);
if (!fs_1.default.existsSync(finalFile)) { if (!fs_1.default.existsSync(partialFile)) {
const paddedFail = failMessage.padEnd(maxLength, ' '); process.stdout.write(`\r${failMessage.padEnd(maxLength, ' ')}\n`);
process.stdout.write(`\r${paddedFail}\n`); try {
throw new Error(failMessage); if (fs_1.default.existsSync(tempFile))
fs_1.default.unlinkSync(tempFile);
} }
const paddedSuccess = successMessage.padEnd(maxLength, ' '); catch { }
process.stdout.write(`\r${paddedSuccess}\n`); throw new Error('Entpackung fehlgeschlagen');
}
fs_1.default.renameSync(partialFile, finalFile);
process.stdout.write(`\r${successMessage.padEnd(maxLength, ' ')}\n`);
// Aufräumen // Aufräumen
try { try {
fs_1.default.unlinkSync(tempFile); fs_1.default.unlinkSync(tempFile);
@ -164,4 +203,12 @@ async function downloadDemoFile(match, outputBaseDir = 'demos', onProgress) {
console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`); console.log(`⚠️ Konnte temporäre Datei nicht löschen: ${tempFileName}`);
} }
return finalFile; return finalFile;
})();
inflight.set(finalFile, job);
try {
return await job;
}
finally {
inflight.delete(finalFile);
}
} }

Binary file not shown.

Binary file not shown.

Binary file not shown.

8
dist/main.js vendored
View File

@ -49,12 +49,16 @@ async function start() {
console.log('✅ Download abgeschlossen'); console.log('✅ Download abgeschlossen');
} }
}); });
// JSON-Dateipfad erstellen // JSON nur erstellen, wenn die .dem existiert
if (demoFilePath && fs_1.default.existsSync(demoFilePath)) {
const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json'); const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json');
const jsonFileName = path_1.default.basename(jsonFilePath); 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'); await fs_1.default.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8');
console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`);
}
else {
console.warn('⚠️ Keine Demo-Datei vorhanden JSON wird nicht erstellt.');
}
// Antwort an den Client // Antwort an den Client
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });
res.end(JSON.stringify({ success: true, path: demoFilePath })); res.end(JSON.stringify({ success: true, path: demoFilePath }));

View File

@ -8,6 +8,9 @@ import bz2 from 'unbzip2-stream';
const pipe = promisify(pipeline); const pipe = promisify(pipeline);
// In-Flight-Lock pro Ziel-.dem
const inflight: Map<string, Promise<string>> = new Map();
/** /**
* Entpackt eine .bz2-Datei mithilfe von Streams nach .dem * 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. * Hauptfunktion: lädt und entpackt eine CS2-Demo (.bz2), mit Fortschrittsanzeige.
*/ */
@ -146,58 +147,76 @@ export async function downloadDemoFile(
const matchType = isPremier ? 'premier' : 'competitive'; const matchType = isPremier ? 'premier' : 'competitive';
const tempDir = path.join(outputBaseDir, 'temp'); const tempDir = path.join(outputBaseDir, 'temp');
const tempFileName = `match${appId}_${mapName}_${matchId}_${matchType}.bz2`; const finalDir = path.join(outputBaseDir, matchDate);
const baseName = path.parse(tempFileName).name;
// 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 tempFile = path.join(tempDir, tempFileName);
const finalDir = path.join(outputBaseDir, matchDate); const job = (async () => {
const finalFile = path.join(finalDir, `${baseName}.dem`);
const finalFileName = path.basename(finalFile)
fs.mkdirSync(tempDir, { recursive: true }); fs.mkdirSync(tempDir, { recursive: true });
fs.mkdirSync(finalDir, { 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 { try {
const success = await downloadWithHttps(demoUrl, tempFile, onProgress); const success = await downloadWithHttps(demoUrl, tempFile, onProgress);
if (!success || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) { if (!success || !fs.existsSync(tempFile) || fs.statSync(tempFile).size === 0) {
console.warn(`⚠️ Download fehlgeschlagen oder Datei leer lösche ${tempFileName}`); console.warn(`⚠️ Download fehlgeschlagen oder Datei leer lösche ${tempFileName}`);
try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {}
try {
if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile);
} catch {
console.warn(`⚠️ Konnte leere Datei nicht löschen: ${tempFileName}`);
}
return ''; return '';
} }
} catch (err) { } catch (err) {
throw new Error(`❌ Fehler beim Download: ${err instanceof Error ? err.message : 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}...`; const entpackZeile = `🗜️ Entpacke nach ${path.basename(partialFile)}...`;
process.stdout.write(entpackZeile); process.stdout.write(entpackZeile);
await extractBz2Safe(tempFile, finalFile); try {
await extractBz2Safe(tempFile, partialFile);
const successMessage = `✅ Entpackt: ${finalFileName}`; } catch (e) {
const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`; try { if (fs.existsSync(partialFile)) fs.unlinkSync(partialFile); } catch {}
try { if (fs.existsSync(tempFile)) fs.unlinkSync(tempFile); } catch {}
// Max-Zeichenlänge bestimmen throw e;
const maxLength = Math.max(entpackZeile.length, successMessage.length, failMessage.length);
if (!fs.existsSync(finalFile)) {
const paddedFail = failMessage.padEnd(maxLength, ' ');
process.stdout.write(`\r${paddedFail}\n`);
throw new Error(failMessage);
} }
const paddedSuccess = successMessage.padEnd(maxLength, ' '); // Prüfen & atomar nach .dem verschieben
process.stdout.write(`\r${paddedSuccess}\n`); const successMessage = `✅ Entpackt: ${finalFileName}`;
const failMessage = `❌ Entpackung fehlgeschlagen Datei nicht vorhanden`;
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');
}
fs.renameSync(partialFile, finalFile);
process.stdout.write(`\r${successMessage.padEnd(maxLength, ' ')}\n`);
// Aufräumen // Aufräumen
try { try {
@ -208,4 +227,12 @@ export async function downloadDemoFile(
} }
return finalFile; return finalFile;
})();
inflight.set(finalFile, job);
try {
return await job;
} finally {
inflight.delete(finalFile);
}
} }

View File

@ -56,13 +56,16 @@ async function start() {
} }
); );
// JSON-Dateipfad erstellen // JSON nur erstellen, wenn die .dem existiert
if (demoFilePath && fs.existsSync(demoFilePath)) {
const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json'); const jsonFilePath = demoFilePath.replace(/\.dem$/, '.json');
const jsonFileName = path.basename(jsonFilePath) const jsonFileName = path.basename(jsonFilePath);
// Match-Daten als JSON schreiben
await fs.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8'); await fs.promises.writeFile(jsonFilePath, JSON.stringify(match, null, 2), 'utf-8');
console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`); console.log(`📝 Match-Daten gespeichert unter: ${jsonFileName}`);
} else {
console.warn('⚠️ Keine Demo-Datei vorhanden JSON wird nicht erstellt.');
}
// Antwort an den Client // Antwort an den Client
res.writeHead(200, { 'Content-Type': 'application/json' }); res.writeHead(200, { 'Content-Type': 'application/json' });