From 98bce1b0da675715b2e5482c0ad61ba21135f62f Mon Sep 17 00:00:00 2001
From: Server-Bibliothek <server-bibliothek@lokal>
Date: Di, 04 Aug 2026 07:00:04 +0200
Subject: [PATCH] Add a desktop client that knows what is installed locally
---
packages/bibliothek-server/src/zip.ts | 107 +++++++++++++++++++++++++++++++++++++++++++++++++++++
1 files changed, 107 insertions(+), 0 deletions(-)
diff --git a/packages/bibliothek-server/src/zip.ts b/packages/bibliothek-server/src/zip.ts
new file mode 100644
index 0000000..0ed22aa
--- /dev/null
+++ b/packages/bibliothek-server/src/zip.ts
@@ -0,0 +1,107 @@
+/**
+ * Minimaler ZIP-Schreiber (Speicher-Methode, ohne Komprimierung).
+ *
+ * Bewusst ohne Fremdbibliothek: Installer und Archive sind bereits komprimiert,
+ * erneutes Packen brächte nichts und kostet nur Zeit. Damit bleibt der Server
+ * ohne Zusatz-Abhängigkeit.
+ *
+ * Grenze: Das klassische ZIP-Format adressiert mit 32 Bit, ein Archiv (bzw. eine
+ * Einzeldatei darin) darf also 4 GB nicht überschreiten — ZIP64 ist nicht umgesetzt.
+ */
+
+const CRC_TABELLE = erzeugeCrcTabelle();
+
+function erzeugeCrcTabelle(): Uint32Array {
+ const tabelle = new Uint32Array(256);
+ for (let index = 0; index < 256; index++) {
+ let wert = index;
+ for (let bit = 0; bit < 8; bit++) {
+ wert = wert & 1 ? 0xedb88320 ^ (wert >>> 1) : wert >>> 1;
+ }
+ tabelle[index] = wert >>> 0;
+ }
+ return tabelle;
+}
+
+function crc32(daten: Buffer): number {
+ let pruefsumme = 0xffffffff;
+ for (const byte of daten) {
+ pruefsumme = CRC_TABELLE[(pruefsumme ^ byte) & 0xff] ^ (pruefsumme >>> 8);
+ }
+ return (pruefsumme ^ 0xffffffff) >>> 0;
+}
+
+/** Wandelt einen Zeitpunkt in das von ZIP verwendete DOS-Format (Datum + Uhrzeit). */
+function dosZeitstempel(zeitpunkt: Date): { zeit: number; datum: number } {
+ const zeit = (zeitpunkt.getHours() << 11) | (zeitpunkt.getMinutes() << 5) | (Math.floor(zeitpunkt.getSeconds() / 2) & 0x1f);
+ const datum = ((zeitpunkt.getFullYear() - 1980) << 9) | ((zeitpunkt.getMonth() + 1) << 5) | zeitpunkt.getDate();
+ return { zeit, datum };
+}
+
+export interface ZipEintrag {
+ dateiname: string;
+ inhalt: Buffer;
+}
+
+/** Packt die Einträge zu einem ZIP-Archiv (ohne Komprimierung). */
+export function erzeugeZip(eintraege: ZipEintrag[], zeitpunkt: Date = new Date()): Buffer {
+ const { zeit, datum } = dosZeitstempel(zeitpunkt);
+ const lokaleBloecke: Buffer[] = [];
+ const zentralBloecke: Buffer[] = [];
+ let versatz = 0;
+
+ for (const eintrag of eintraege) {
+ const nameBytes = Buffer.from(eintrag.dateiname, 'utf8');
+ const pruefsumme = crc32(eintrag.inhalt);
+ const groesse = eintrag.inhalt.length;
+
+ const lokalerKopf = Buffer.alloc(30);
+ lokalerKopf.writeUInt32LE(0x04034b50, 0); // Signatur
+ lokalerKopf.writeUInt16LE(20, 4); // benötigte Version
+ lokalerKopf.writeUInt16LE(0x0800, 6); // Flags: Dateiname ist UTF-8
+ lokalerKopf.writeUInt16LE(0, 8); // Methode 0 = gespeichert
+ lokalerKopf.writeUInt16LE(zeit, 10);
+ lokalerKopf.writeUInt16LE(datum, 12);
+ lokalerKopf.writeUInt32LE(pruefsumme, 14);
+ lokalerKopf.writeUInt32LE(groesse, 18); // komprimierte Größe
+ lokalerKopf.writeUInt32LE(groesse, 22); // Originalgröße
+ lokalerKopf.writeUInt16LE(nameBytes.length, 26);
+ lokalerKopf.writeUInt16LE(0, 28); // keine Extra-Felder
+ lokaleBloecke.push(lokalerKopf, nameBytes, eintrag.inhalt);
+
+ const zentralKopf = Buffer.alloc(46);
+ zentralKopf.writeUInt32LE(0x02014b50, 0); // Signatur
+ zentralKopf.writeUInt16LE(20, 4); // erstellt mit Version
+ zentralKopf.writeUInt16LE(20, 6); // benötigte Version
+ zentralKopf.writeUInt16LE(0x0800, 8);
+ zentralKopf.writeUInt16LE(0, 10);
+ zentralKopf.writeUInt16LE(zeit, 12);
+ zentralKopf.writeUInt16LE(datum, 14);
+ zentralKopf.writeUInt32LE(pruefsumme, 16);
+ zentralKopf.writeUInt32LE(groesse, 20);
+ zentralKopf.writeUInt32LE(groesse, 24);
+ zentralKopf.writeUInt16LE(nameBytes.length, 28);
+ zentralKopf.writeUInt16LE(0, 30); // Extra
+ zentralKopf.writeUInt16LE(0, 32); // Kommentar
+ zentralKopf.writeUInt16LE(0, 34); // Datenträger
+ zentralKopf.writeUInt16LE(0, 36); // interne Attribute
+ zentralKopf.writeUInt32LE(0, 38); // externe Attribute
+ zentralKopf.writeUInt32LE(versatz, 42); // Position des lokalen Kopfes
+ zentralBloecke.push(zentralKopf, nameBytes);
+
+ versatz += lokalerKopf.length + nameBytes.length + groesse;
+ }
+
+ const zentralVerzeichnis = Buffer.concat(zentralBloecke);
+ const abschluss = Buffer.alloc(22);
+ abschluss.writeUInt32LE(0x06054b50, 0); // Signatur
+ abschluss.writeUInt16LE(0, 4); // Datenträger
+ abschluss.writeUInt16LE(0, 6); // Datenträger mit Verzeichnis
+ abschluss.writeUInt16LE(eintraege.length, 8);
+ abschluss.writeUInt16LE(eintraege.length, 10);
+ abschluss.writeUInt32LE(zentralVerzeichnis.length, 12);
+ abschluss.writeUInt32LE(versatz, 16); // Beginn des Zentralverzeichnisses
+ abschluss.writeUInt16LE(0, 20); // kein Kommentar
+
+ return Buffer.concat([...lokaleBloecke, zentralVerzeichnis, abschluss]);
+}
--
Gitblit v1.9.3