Files
abdulive/packages/core/src/zip.ts
T
ZeasyAlexandClaude Opus 5 5f6301268a Native Windows-Auslieferung statt Container
Docker faellt weg. Das Projekt hat keine npm-Abhaengigkeiten und Node fuehrt
TypeScript direkt aus - der Container schrieb nur noch drei Programme fest,
und dafuer braucht es keine Virtualisierung, sondern einen Ordner.

Der Anlass war, dass ADR-0004 zwar `docker compose pull` versprach, es aber
nie ein veroeffentlichtes Image gab. Wer installieren wollte, musste
whisper.cpp aus dem Quelltext uebersetzen.

Plattformschicht (packages/core/src/platform.ts):
- Programme werden neben der Anwendung gesucht, nicht im PATH
- Daten unter %LOCALAPPDATA%, damit Installation und Update ohne
  Administratorrechte auskommen
- Schriften mitgeliefert: ohne fontsdir setzt libass wortlos eine
  Ersatzschrift ein - gemessen 720 statt 826 Pixel Zeilenbreite

Windows-Eigenheiten, an echtem ffmpeg 8.1 durchprobiert:
- Pfade im Filtergraphen brauchen alle drei Schritte (Vorwaertsschraegstriche,
  escapter Doppelpunkt, Anfuehrungszeichen); je zwei davon reichen nicht
- `nice` gibt es nicht, ersetzt durch os.setPriority

Auslieferung:
- scripts/build-package.ts baut das Paket, laeuft auch unter Linux
- install.ps1 ohne Administratorrechte, Autostart ueber den Aufgabenplaner
- update legt neue Fassungen bereit; eingespielt wird beim naechsten Start,
  nie mitten im Stream
- ZIP lesen und schreiben ohne Abhaengigkeiten (packages/core/src/zip.ts)

Kein `irm ... | iex`-Einzeiler: git.zeasy.dev antwortete instanzweit mit einer
Umleitung auf /user/login, und `iex` fuehrte die Anmeldeseite als PowerShell
aus. Der Updater verfolgt Umleitungen jetzt nicht und hat keine erfundene
Vorgabeadresse mehr.

Ausserdem: backup.ps1 war an zwei Stellen dokumentiert, aber nie geschrieben.

ADR-0007 ersetzt ADR-0004. 84 Tests, Typpruefung sauber.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01AhVSfMFdWnXKCv5JfLnArZ
2026-09-09 17:39:26 +02:00

165 lines
5.6 KiB
TypeScript

// ZIP lesen. Node bringt zlib mit, aber kein Archivformat, und das Projekt
// kommt ohne Abhaengigkeiten aus (ADR-0001) - wie beim YAML-Parser und beim
// obs-websocket-Client also selbst gebaut.
//
// Gebraucht wird das an zwei Stellen: beim Paketbau kommen ffmpeg,
// whisper.cpp und Node als ZIP von ihren Herausgebern, und der Updater auf
// Abdus Rechner packt damit das neue Paket aus - ohne PowerShell, ohne
// externes Programm.
import zlib from 'node:zlib';
const EOCD = 0x06054b50; // End of Central Directory
const CENTRAL = 0x02014b50; // Eintrag im Inhaltsverzeichnis
const LOCAL = 0x04034b50; // Kopf vor den eigentlichen Daten
export type ZipEntry = {
name: string;
/** Unkomprimierte Groesse in Bytes. */
size: number;
/** Verzeichniseintraege haben keine Daten. */
isDirectory: boolean;
data: () => Buffer;
};
/**
* Inhaltsverzeichnis eines ZIP lesen.
*
* Die Daten werden erst beim Aufruf von `data()` entpackt - beim Paketbau
* werden von 800 Eintraegen keine 30 gebraucht.
*/
export function readZip(buf: Buffer): ZipEntry[] {
const eocd = findEocd(buf);
const count = buf.readUInt16LE(eocd + 10);
const dirSize = buf.readUInt32LE(eocd + 12);
const dirStart = buf.readUInt32LE(eocd + 16);
if (dirStart === 0xffffffff || dirSize === 0xffffffff || count === 0xffff) {
throw new Error('ZIP64 wird nicht unterstuetzt - das Archiv ist zu gross.');
}
const entries: ZipEntry[] = [];
let p = dirStart;
for (let i = 0; i < count; i++) {
if (buf.readUInt32LE(p) !== CENTRAL) {
throw new Error(`Beschaedigtes Inhaltsverzeichnis bei Eintrag ${i}`);
}
const method = buf.readUInt16LE(p + 10);
const compressed = buf.readUInt32LE(p + 20);
const size = buf.readUInt32LE(p + 24);
const nameLen = buf.readUInt16LE(p + 28);
const extraLen = buf.readUInt16LE(p + 30);
const commentLen = buf.readUInt16LE(p + 32);
const localOffset = buf.readUInt32LE(p + 42);
const name = buf.toString('utf8', p + 46, p + 46 + nameLen);
entries.push({
name,
size,
isDirectory: name.endsWith('/'),
data: () => extract(buf, localOffset, method, compressed, size, name),
});
p += 46 + nameLen + extraLen + commentLen;
}
return entries;
}
function extract(
buf: Buffer, localOffset: number, method: number,
compressed: number, size: number, name: string
): Buffer {
if (buf.readUInt32LE(localOffset) !== LOCAL) {
throw new Error(`Kein lokaler Dateikopf fuer ${name}`);
}
// Der lokale Kopf hat eigene Laengen fuer Name und Extra-Feld; sie koennen
// von denen im Inhaltsverzeichnis abweichen und muessen von hier kommen.
const nameLen = buf.readUInt16LE(localOffset + 26);
const extraLen = buf.readUInt16LE(localOffset + 28);
const start = localOffset + 30 + nameLen + extraLen;
const raw = buf.subarray(start, start + compressed);
if (method === 0) return Buffer.from(raw);
if (method === 8) {
const out = zlib.inflateRawSync(raw);
if (out.length !== size) {
throw new Error(`${name}: ${out.length} Bytes entpackt, ${size} erwartet`);
}
return out;
}
throw new Error(`${name}: unbekanntes Kompressionsverfahren ${method}`);
}
/**
* Das Inhaltsverzeichnis steht am Dateiende, hinter einem Kommentar
* beliebiger Laenge. Also von hinten suchen - hoechstens 64 KB weit, mehr
* darf der Kommentar nicht sein.
*/
function findEocd(buf: Buffer): number {
const min = Math.max(0, buf.length - 0x10000 - 22);
for (let i = buf.length - 22; i >= min; i--) {
if (buf.readUInt32LE(i) === EOCD) return i;
}
throw new Error('Kein ZIP-Inhaltsverzeichnis gefunden.');
}
/**
* ZIP schreiben. Nur so viel Format, wie der Windows-Explorer und
* `Expand-Archive` verlangen - insbesondere die CRC32-Pruefsumme, sonst
* halten beide das Archiv fuer beschaedigt.
*/
export function writeZip(files: Array<{ name: string; data: Buffer }>): Buffer {
const locals: Buffer[] = [];
const centrals: Buffer[] = [];
let offset = 0;
for (const f of files) {
const name = Buffer.from(f.name.replace(/\\/g, '/'), 'utf8');
const packed = zlib.deflateRawSync(f.data, { level: 9 });
// Groesser als das Original kommt bei schon komprimierten Dateien vor -
// dann unveraendert ablegen.
const useDeflate = packed.length < f.data.length;
const body = useDeflate ? packed : f.data;
const crc = zlib.crc32(f.data);
const local = Buffer.alloc(30 + name.length);
local.writeUInt32LE(LOCAL, 0);
local.writeUInt16LE(20, 4); // benoetigte Version: 2.0
local.writeUInt16LE(useDeflate ? 8 : 0, 8);
local.writeUInt32LE(crc, 14);
local.writeUInt32LE(body.length, 18);
local.writeUInt32LE(f.data.length, 22);
local.writeUInt16LE(name.length, 26);
name.copy(local, 30);
locals.push(local, body);
const central = Buffer.alloc(46 + name.length);
central.writeUInt32LE(CENTRAL, 0);
central.writeUInt16LE(20, 4);
central.writeUInt16LE(20, 6);
central.writeUInt16LE(useDeflate ? 8 : 0, 10);
central.writeUInt32LE(crc, 16);
central.writeUInt32LE(body.length, 20);
central.writeUInt32LE(f.data.length, 24);
central.writeUInt16LE(name.length, 28);
central.writeUInt32LE(offset, 42);
name.copy(central, 46);
centrals.push(central);
offset += local.length + body.length;
}
const dir = Buffer.concat(centrals);
const eocd = Buffer.alloc(22);
eocd.writeUInt32LE(EOCD, 0);
eocd.writeUInt16LE(files.length, 8);
eocd.writeUInt16LE(files.length, 10);
eocd.writeUInt32LE(dir.length, 12);
eocd.writeUInt32LE(offset, 16);
const out = Buffer.concat([...locals, dir, eocd]);
if (out.length > 0xffffffff) throw new Error('Ueber 4 GB braeuchte ZIP64.');
return out;
}