diff --git a/rippy-windows/src/fenster/kernverbindung.ts b/rippy-windows/src/fenster/kernverbindung.ts
index 4a71a28..f933502 100644
--- a/rippy-windows/src/fenster/kernverbindung.ts
+++ b/rippy-windows/src/fenster/kernverbindung.ts
@@ -381,6 +381,11 @@ export function schluesselPruefen(): void {
senden({ art: 'schluessel-pruefen' })
}
+/** 5.7.0 (F16): Ja/Nein auf eine Frage der MakeMKV-Beschaffung. */
+export function beschaffungAntworten(ja: boolean): void {
+ anKern({ art: 'beschaffung-antwort', ja }, 'Antwort an die Beschaffung')
+}
+
export function einstellungSetzen(schluessel: string, wert: string): void {
anKern({ art: 'einstellung-setzen', schluessel, wert }, `Einstellung ${schluessel}`)
}
diff --git a/rippy-windows/src/gemeinsam/nachrichten.ts b/rippy-windows/src/gemeinsam/nachrichten.ts
index 6ebb971..02abd51 100644
--- a/rippy-windows/src/gemeinsam/nachrichten.ts
+++ b/rippy-windows/src/gemeinsam/nachrichten.ts
@@ -257,6 +257,9 @@ export interface BeschaffungStand {
/** 0–1 während des Downloads; null wenn gerade nicht bezifferbar. */
anteil: number | null
fehler: boolean
+ /** 5.7.0 (F16): Rippy wartet auf ein Ja/Nein — z. B. „Archiv-Kopie laden?".
+ * Fehlt: keine Frage offen. */
+ frage?: string
}
// ── Vorgänge und Kompressions-Warteschlange (5.4.0) ─────────────────────
@@ -450,6 +453,8 @@ export type FensterNachricht =
| { art: 'makemkv-update' }
/** Schlüsselkette sofort neu laufen lassen (z. B. nach einer Installation). */
| { art: 'schluessel-pruefen' }
+ /** 5.7.0 (F16): Antwort auf eine Frage der MakeMKV-Beschaffung. */
+ | { art: 'beschaffung-antwort'; ja: boolean }
// ── 5.4.0: Vorgänge, Roh-Dateien, Warteschlange, Laufwerks-Gesundheit ──
| { art: 'vorgaenge-laden' }
/** Den Roh-Ordner EINES Vorgangs jetzt löschen (von Hand). */
@@ -712,6 +717,9 @@ export function istFensterNachricht(wert: unknown): wert is FensterNachricht {
if (wert.art === 'kompression-pause') {
return typeof wert.an === 'boolean'
}
+ if (wert.art === 'beschaffung-antwort') {
+ return typeof wert.ja === 'boolean'
+ }
if (wert.art === 'einstellung-setzen') {
return istKennung(wert.schluessel) && typeof wert.wert === 'string'
}
diff --git a/rippy-windows/src/kern/dienste/werkzeuge.ts b/rippy-windows/src/kern/dienste/werkzeuge.ts
index 3004a1f..4386c51 100644
--- a/rippy-windows/src/kern/dienste/werkzeuge.ts
+++ b/rippy-windows/src/kern/dienste/werkzeuge.ts
@@ -15,6 +15,9 @@ export class WerkzeugDienst {
schluesselStand: SchluesselStand | null = null
private beschaffungLaeuft = false
private installerAntwort: ((fehler: string | null) => void) | null = null
+ /** 5.7.0 (F16): Die offene Ja/Nein-Frage der Beschaffung — null, wenn keine. */
+ private frageAntwort: ((ja: boolean) => void) | null = null
+ private letzterBeschaffungsText = ''
constructor(
private readonly kontext: KernKontext,
@@ -115,9 +118,40 @@ export class WerkzeugDienst {
this.installerAntwort = null
}
- private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false): void {
- this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler } })
- this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}`)
+ private beschaffungMelden(text: string, anteil: number | null, laeuft: boolean, fehler = false, frage?: string): void {
+ this.letzterBeschaffungsText = text
+ this.kontext.anFenster({ art: 'beschaffung-status', stand: { werkzeug: 'makemkv', laeuft, text, anteil, fehler, ...(frage !== undefined ? { frage } : {}) } })
+ this.kontext.protokoll.schreiben(`MakeMKV-Update: ${text}${frage !== undefined ? ` — FRAGE: ${frage}` : ''}`)
+ }
+
+ /** Antwort des Fensters auf eine offene Frage (beschaffung-antwort). */
+ beschaffungAntwort(ja: boolean): void {
+ const antwort = this.frageAntwort
+ this.frageAntwort = null
+ if (antwort === null) {
+ this.kontext.hinweis('Es ist keine Frage der MakeMKV-Beschaffung offen.')
+ return
+ }
+ antwort(ja)
+ }
+
+ /** Eine Ja/Nein-Frage ans Fenster; ohne Antwort in zehn Minuten gilt Nein. */
+ private frage(text: string): Promise
{
+ return new Promise((fertig) => {
+ this.frageAntwort = (ja) => {
+ clearTimeout(zeiger)
+ this.beschaffungMelden(ja ? 'Freigegeben — die Archiv-Kopie wird geladen.' : 'Nicht freigegeben.', null, true)
+ fertig(ja)
+ }
+ const zeiger = setTimeout(() => {
+ if (this.frageAntwort === null) return
+ this.frageAntwort = null
+ this.beschaffungMelden('Keine Antwort — die Archiv-Kopie wird nicht geladen.', null, true)
+ fertig(false)
+ }, 10 * 60_000)
+ zeiger.unref()
+ this.beschaffungMelden(this.letzterBeschaffungsText, null, true, false, text)
+ })
}
// ── MakeMKV-Update (§ 9): holen, prüfen, installieren lassen ────────────
@@ -168,8 +202,25 @@ export class WerkzeugDienst {
const alteExe = this.makemkvcon()
const alteVersion = alteExe.length > 0 ? versionsangaben(alteExe).version : ''
- const ergebnis = await makemkvInstallerHolen(neueste, (text, anteil) => this.beschaffungMelden(text, anteil, true))
- this.beschaffungMelden(`Geprüft (Quelle: ${ergebnis.quelle}) — der Installer wird gestartet …`, null, true)
+ const ergebnis = await makemkvInstallerHolen(
+ neueste,
+ (text, anteil) => this.beschaffungMelden(text, anteil, true),
+ '',
+ undefined,
+ undefined,
+ undefined,
+ // F16: Das Archiv liefert eine unsignierte EXE, die gleich mit
+ // Adminrechten läuft — das entscheidet der Nutzer, nicht Rippy.
+ (quelle) =>
+ this.frage(
+ `makemkv.com antwortet nicht. Soll Rippy den MakeMKV-Installer ${neueste} aus dem ${quelle} laden? Das ist eine Kopie von fremder Hand, geprüft wird nur, dass sie ein MakeMKV-Programm ist — er läuft danach mit Adminrechten. Wer sicher gehen will, lädt später selbst von makemkv.com.`,
+ ),
+ )
+ this.beschaffungMelden(
+ `Geprüft (Quelle: ${ergebnis.quelle}, ${(ergebnis.bytes / 1024 ** 2).toFixed(1)} MB, SHA-256 ${ergebnis.sha256}) — der Installer wird gestartet …`,
+ null,
+ true,
+ )
const startFehler = await new Promise((fertig) => {
this.installerAntwort = fertig
diff --git a/rippy-windows/src/kern/index.ts b/rippy-windows/src/kern/index.ts
index 8a6f792..467f3a0 100644
--- a/rippy-windows/src/kern/index.ts
+++ b/rippy-windows/src/kern/index.ts
@@ -288,6 +288,9 @@ function fensterNachricht(frage: unknown): void {
case 'schluessel-pruefen':
void werkzeuge.schluesselLauf().then(() => werkzeuge.senden())
return
+ case 'beschaffung-antwort':
+ werkzeuge.beschaffungAntwort(frage.ja)
+ return
case 'vorgaenge-laden':
vorgaenge.neuMessen()
return
diff --git a/rippy-windows/src/kern/werkzeuge/beschaffen.ts b/rippy-windows/src/kern/werkzeuge/beschaffen.ts
index d4dae50..64aa683 100644
--- a/rippy-windows/src/kern/werkzeuge/beschaffen.ts
+++ b/rippy-windows/src/kern/werkzeuge/beschaffen.ts
@@ -22,7 +22,8 @@
// 3. NIE eine gute Datei durch eine halbe ersetzen: Geladen wird in
// `.neu`, geprüft, erst dann getauscht.
import { spawnSync } from 'node:child_process'
-import { createWriteStream, openSync, readSync, closeSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs'
+import { createHash } from 'node:crypto'
+import { createWriteStream, openSync, readFileSync, readSync, closeSync, mkdirSync, renameSync, rmSync, statSync } from 'node:fs'
import { win32 as pfad } from 'node:path'
import { werkzeugOrdner } from './katalog'
@@ -285,6 +286,22 @@ export interface BeschaffungErgebnis {
pfad: string
quelle: string
version: string
+ /** 5.7.0 (F16): Größe und SHA-256 der geladenen Datei — damit man sie
+ * gegen die Herstellerangabe halten kann, sobald die Seite wieder da ist. */
+ bytes: number
+ sha256: string
+}
+
+/** SHA-256 einer Datei als Hex — für die Anzeige neben dem Installer. */
+export function pruefsumme(dateiPfad: string): string {
+ return createHash('sha256').update(readFileSync(dateiPfad)).digest('hex')
+}
+
+/** Welche Quelle ein Rückfrage braucht: alles außer Hersteller und eigener
+ * Basis. Das Internet Archive liefert eine unsignierte EXE, die mit
+ * Adminrechten läuft — das entscheidet der Nutzer, nicht Rippy (F16). */
+export function brauchtFreigabe(quellenName: string): boolean {
+ return quellenName === 'Internet Archive'
}
/** Lädt den MakeMKV-Installer über die Quellenkette in Rippys
@@ -297,6 +314,9 @@ export async function makemkvInstallerHolen(
holen: typeof fetch = fetch,
ordner: string = pfad.join(werkzeugOrdner(), 'downloads'),
pruefen: typeof dateiPruefen = dateiPruefen,
+ /** 5.7.0 (F16): Darf eine Quelle mit Rückfrage benutzt werden? Ohne
+ * diese Funktion wird sie übersprungen — nie still geladen. */
+ freigabe?: (quellenName: string, url: string) => Promise,
): Promise {
mkdirSync(ordner, { recursive: true })
const ziel = pfad.join(ordner, `Setup_MakeMKV_v${version}.exe`)
@@ -304,6 +324,14 @@ export async function makemkvInstallerHolen(
const fehlschlaege: string[] = []
for (const [name, url] of makemkvQuellen(version, basis)) {
+ if (brauchtFreigabe(name)) {
+ const erlaubt = freigabe !== undefined && (await freigabe(name, url))
+ if (!erlaubt) {
+ fehlschlaege.push(`${name}: nicht freigegeben`)
+ melden(`${name} wurde nicht freigegeben.`, null)
+ continue
+ }
+ }
melden(`MakeMKV ${version} wird geladen (${name}) …`, 0)
try {
await dateiLaden(url, vorlaeufig, melden, holen)
@@ -312,7 +340,7 @@ export async function makemkvInstallerHolen(
// Erst tauschen, wenn geprüft — ein halber Download darf eine gute
// Datei nicht ersetzen.
renameSync(vorlaeufig, ziel)
- return { pfad: ziel, quelle: name, version }
+ return { pfad: ziel, quelle: name, version, bytes: statSync(ziel).size, sha256: pruefsumme(ziel) }
} catch (fehler) {
fehlschlaege.push(`${name}: ${fehler instanceof Error ? fehler.message : String(fehler)}`)
melden(`${name} hat nicht geliefert.`, null)
diff --git a/rippy-windows/test/beschaffung.test.ts b/rippy-windows/test/beschaffung.test.ts
index 9adb5de..f414422 100644
--- a/rippy-windows/test/beschaffung.test.ts
+++ b/rippy-windows/test/beschaffung.test.ts
@@ -136,10 +136,11 @@ describe('dateiPruefen', () => {
describe('makemkvInstallerHolen', () => {
const GROSS = Buffer.concat([Buffer.from('MZ'), Buffer.alloc(1024 * 1024 + 100, 7)])
- it('Fehlerseite mit HTTP 200 fällt durch die Mindestgröße — die nächste Quelle gewinnt', async () => {
+ it('Fehlerseite mit HTTP 200 fällt durch die Mindestgröße — die nächste Quelle gewinnt (mit Freigabe)', async () => {
const ordner = tempOrdner()
const holen = (async (url: string | URL | Request) =>
String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('kaputt')) as typeof fetch
+ const gefragt: string[] = []
const ergebnis = await makemkvInstallerHolen(
'1.18.4',
() => {},
@@ -147,16 +148,35 @@ describe('makemkvInstallerHolen', () => {
holen,
ordner,
(p, v) => dateiPruefen(p, v, () => ({ firma: 'GuinpinSoft inc', produkt: 'MakeMKV', version: 'v1.18.4' }), 'win32'),
+ async (quelle) => {
+ gefragt.push(quelle)
+ return true
+ },
)
+ expect(gefragt).toEqual(['Internet Archive'])
expect(ergebnis.quelle).toBe('Internet Archive')
expect(readFileSync(ergebnis.pfad).length).toBe(GROSS.length)
+ expect(ergebnis.bytes).toBe(GROSS.length)
+ expect(ergebnis.sha256).toMatch(/^[0-9a-f]{64}$/)
expect(existsSync(`${ergebnis.pfad}.neu`)).toBe(false)
})
+ // 5.7.0 (Durchsicht F16): Ohne Freigabe wird das Archiv NIE benutzt — es
+ // liefert eine unsignierte EXE, die mit Adminrechten läuft.
+ it('ohne Freigabe bleibt das Archiv aus — und der Bericht sagt es', async () => {
+ const ordner = tempOrdner()
+ const holen = (async (url: string | URL | Request) =>
+ String(url).includes('web.archive.org') ? antwort(GROSS) : antwort('kaputt')) as typeof fetch
+ await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner)).rejects.toThrow(/Internet Archive: nicht freigegeben/)
+ await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner, undefined, async () => false)).rejects.toThrow(
+ /Internet Archive: nicht freigegeben/,
+ )
+ })
+
it('liefert keine Quelle, steht im Wurf der Bericht ALLER Versuche', async () => {
const ordner = tempOrdner()
const holen = (async () => antwort('weg', 404)) as typeof fetch
- await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner)).rejects.toThrow(
+ await expect(makemkvInstallerHolen('1.18.4', () => {}, '', holen, ordner, undefined, async () => true)).rejects.toThrow(
/Hersteller: HTTP 404[\s\S]*Internet Archive: HTTP 404[\s\S]*makemkv\.com/,
)
})
@@ -166,6 +186,8 @@ describe('Nachrichten-Schema der Update-Strecke', () => {
it('Fenster → Kern: makemkv-update und schluessel-pruefen', () => {
expect(istFensterNachricht({ art: 'makemkv-update' })).toBe(true)
expect(istFensterNachricht({ art: 'schluessel-pruefen' })).toBe(true)
+ expect(istFensterNachricht({ art: 'beschaffung-antwort', ja: true })).toBe(true)
+ expect(istFensterNachricht({ art: 'beschaffung-antwort' })).toBe(false)
})
it('Haupt → Kern: installer-ergebnis', () => {
expect(istHauptNachricht({ art: 'installer-ergebnis', fehler: null })).toBe(true)