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
165 lines
5.6 KiB
TypeScript
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;
|
|
}
|