// ISO-Backup für Datendiscs (KONZEPT § 6.4) — die einzige echte Lücke // gegenüber ARM: Wird eine Disc weder als Film noch als Musik erkannt, // wird sie 1:1 gesichert statt abgelehnt. Sequenziell aus \\.\G: lesen; // die Länge kommt aus IOCTL_DISK_GET_LENGTH_INFO (§ 3.2 gemessen: exakte // Bytes). Kein Fremdwerkzeug nötig. // // Async gelesen: Ein 33-GB-Abbild darf den Kern nicht blockieren — die // Disc-Wache und der Ping laufen währenddessen weiter. Windows verlangt // auf Geräte-Handles SEKTOR-AUSGERICHTETE Zugriffe — der Blockpuffer ist // ein Vielfaches von 2048 (1 MiB = 512 Sektoren), gelesen wird strikt // sequenziell ab 0. import { open } from 'node:fs/promises' const BLOCK_BYTES = 1024 * 1024 export type IsoErgebnis = | { status: 'success'; bytes: number } | { status: 'cancelled' } | { status: 'error'; fehler: string; bytes: number } export async function isoSichern( geraetepfad: string, zielDatei: string, erwarteteBytes: number, aufFortschritt?: (prozent: number) => void, abbruch?: AbortSignal, ): Promise { if (erwarteteBytes <= 0) { return { status: 'error', fehler: 'Die Größe der Disc ist unbekannt — ohne sie gibt es kein 1:1-Abbild.', bytes: 0 } } let quelle try { quelle = await open(geraetepfad, 'r') } catch (fehler) { return { status: 'error', fehler: `Gerät nicht lesbar: ${String(fehler)}`, bytes: 0 } } let ziel try { ziel = await open(zielDatei, 'w') } catch (fehler) { await quelle.close() return { status: 'error', fehler: `Zieldatei nicht anlegbar: ${String(fehler)}`, bytes: 0 } } const puffer = Buffer.alloc(BLOCK_BYTES) let gelesen = 0 let letzterProzent = -1 try { while (gelesen < erwarteteBytes) { if (abbruch?.aborted === true) return { status: 'cancelled' } const rest = erwarteteBytes - gelesen const anfordern = rest >= BLOCK_BYTES ? BLOCK_BYTES : rest const { bytesRead } = await quelle.read(puffer, 0, anfordern, gelesen) if (bytesRead <= 0) break await ziel.write(puffer, 0, bytesRead) gelesen += bytesRead const prozent = Math.floor((gelesen * 100) / erwarteteBytes) if (prozent !== letzterProzent) { letzterProzent = prozent aufFortschritt?.(prozent) } } } catch (fehler) { return { status: 'error', fehler: `Lesen brach bei ${gelesen} Bytes ab: ${String(fehler)}`, bytes: gelesen } } finally { await quelle.close() await ziel.close() } if (gelesen !== erwarteteBytes) { // R4/„Rückgabewert ist kein Beweis": Das Abbild ist NUR vollständig, // wenn die Bytes stimmen — alles andere wird gesagt, nicht beschönigt. return { status: 'error', fehler: `Abbild unvollständig: ${gelesen} von ${erwarteteBytes} Bytes gelesen.`, bytes: gelesen, } } return { status: 'success', bytes: gelesen } }