import * as fs from 'fs';
|
import * as path from 'path';
|
|
/** Alles, was eine Bezugsquelle für einen Abgleich braucht. */
|
export interface Abgleichauftrag {
|
/** Quell-spezifische Angabe: URL, `besitzer/repo`, Paket-ID, Repo-URL … */
|
wert: string;
|
/** Bisher bekannte Versions-/Stand-Kennung. */
|
aktuelleVersion: string;
|
/** Ordner, in dem die bereitgestellte Datei liegen soll. */
|
zielordner: string;
|
/** Arbeitskopie für Quellen, die etwas auschecken/bauen müssen. */
|
arbeitsordner: string;
|
/** Befehl zum Bauen (nur für die Git-Quelle). */
|
bauBefehl: string;
|
/** Pfad des Bau-Ergebnisses relativ zur Arbeitskopie (nur für die Git-Quelle). */
|
bauErgebnis: string;
|
/** Größe der bisher bereitgestellten Datei — für die Plausibilitätsprüfung. */
|
vorherigeGroesse: number;
|
/** Hinterlegte Bau-Geheimnisse, die dem Bau als Umgebungsvariablen mitgegeben werden. */
|
geheimnisse: Record<string, string>;
|
/** SSH-Aufruf für Git-Zugriffe aus den Einstellungen (leer = SSH-Konfiguration entscheidet). */
|
gitSshBefehl: string;
|
}
|
|
/** Ein Geheimnis, das ein Repository für seinen Bau anfordert. */
|
export interface GeheimnisBedarf {
|
name: string;
|
beschreibung: string;
|
}
|
|
/** Ergebnis eines Quellen-Abgleichs. */
|
export interface QuellenAbgleich {
|
erfolg: boolean;
|
/** Wurde eine neue Datei geholt (true) oder war schon alles aktuell (false)? */
|
geaendert: boolean;
|
/** Neue Versions-/Stand-Kennung. */
|
version: string;
|
/** Name der aktuell vorliegenden Datei. */
|
dateiname: string;
|
meldung: string;
|
/** Veröffentlichungsdatum beim Anbieter (ISO), falls die Quelle es kennt. */
|
veroeffentlichtAm?: string;
|
/** Vom Repository angeforderte Bau-Geheimnisse (nur die Git-Quelle liefert das). */
|
benoetigteGeheimnisse?: GeheimnisBedarf[];
|
}
|
|
/**
|
* Eine Bezugsquelle prüft ihre Quelle und stellt bei Bedarf die aktuelle Datei bereit.
|
* Jede neue Quellenart implementiert nur dieses Interface.
|
*/
|
export interface Bezugsquelle {
|
gleicheAb(auftrag: Abgleichauftrag): Promise<QuellenAbgleich>;
|
}
|
|
export function abgleichMisserfolg(version: string, meldung: string): QuellenAbgleich {
|
return { erfolg: false, geaendert: false, version, dateiname: '', meldung };
|
}
|
|
/** Wandelt eine Datumsangabe der Quelle in ISO um (leer, wenn unbrauchbar). */
|
export function alsIsoDatum(rohwert: string | null | undefined): string {
|
if (!rohwert) {
|
return '';
|
}
|
const zeitpunkt = new Date(rohwert);
|
return Number.isNaN(zeitpunkt.getTime()) ? '' : zeitpunkt.toISOString();
|
}
|
|
// --- Plausibilitätsprüfung ---
|
|
/** Unterhalb dieser Größe gilt eine Datei als verdächtig, wenn sie zudem stark geschrumpft ist. */
|
const VERDAECHTIG_KLEIN_BYTES = 100 * 1024;
|
|
/**
|
* Prüft eine frisch geholte Datei auf typische Anzeichen eines toten oder
|
* übernommenen Links (Fehler-/Anmeldeseite statt Datei, leere oder plötzlich
|
* winzige Datei). Liefert einen Beanstandungstext oder `undefined`, wenn alles passt.
|
*/
|
export function inhaltBeanstanden(inhalt: Buffer, dateiname: string, inhaltstyp: string | null, vorherigeGroesse: number): string | undefined {
|
if (inhalt.length === 0) {
|
return 'Die Quelle hat eine leere Datei geliefert.';
|
}
|
|
const istWebseiteErwartet = /\.(html?|htm)$/i.test(dateiname);
|
if (!istWebseiteErwartet) {
|
const anfang = inhalt.subarray(0, 200).toString('latin1').trimStart().toLowerCase();
|
if (anfang.startsWith('<!doctype html') || anfang.startsWith('<html')) {
|
return 'Die Quelle hat eine Webseite statt der Datei geliefert — der Link ist vermutlich veraltet oder verlangt eine Anmeldung.';
|
}
|
if (inhaltstyp && inhaltstyp.toLowerCase().includes('text/html')) {
|
return `Die Quelle hat eine Webseite geliefert (${inhaltstyp}) — der Link ist vermutlich veraltet.`;
|
}
|
}
|
|
// Ein plötzlicher Einbruch auf eine winzige Datei deutet auf eine Fehlerseite hin.
|
if (vorherigeGroesse > 0 && inhalt.length < vorherigeGroesse / 2 && inhalt.length < VERDAECHTIG_KLEIN_BYTES) {
|
return `Die geholte Datei ist auffällig klein (${inhalt.length} statt bisher ${vorherigeGroesse} Bytes) — bitte die Quelle prüfen.`;
|
}
|
|
return undefined;
|
}
|
|
// --- Dateiablage: erst vollständig vorbereiten, dann ersetzen ---
|
|
function temporaerenOrdnerAnlegen(zielordner: string): string {
|
const temporaer = `${zielordner}.neu`;
|
fs.rmSync(temporaer, { recursive: true, force: true });
|
fs.mkdirSync(temporaer, { recursive: true });
|
return temporaer;
|
}
|
|
/**
|
* Ersetzt den Inhalt des Zielordners durch den des temporären Ordners.
|
* Der alte Stand wird erst angefasst, wenn der neue vollständig und geprüft ist —
|
* ein fehlgeschlagener Abgleich kann die funktionierende Datei damit nicht zerstören.
|
*/
|
function uebernehmeOrdner(temporaer: string, zielordner: string): void {
|
fs.rmSync(zielordner, { recursive: true, force: true });
|
fs.renameSync(temporaer, zielordner);
|
}
|
|
export function verwerfeOrdner(temporaer: string): void {
|
fs.rmSync(temporaer, { recursive: true, force: true });
|
}
|
|
/** Legt eine Datei als neuen, alleinigen Inhalt des Zielordners ab (alter Stand bleibt bis zuletzt erhalten). */
|
export function ersetzeDateiSicher(zielordner: string, dateiname: string, inhalt: Buffer): void {
|
const temporaer = temporaerenOrdnerAnlegen(zielordner);
|
fs.writeFileSync(path.join(temporaer, dateiname), inhalt);
|
uebernehmeOrdner(temporaer, zielordner);
|
}
|
|
/**
|
* Für Quellen, die über ein Kommandozeilen-Werkzeug in einen Ordner schreiben:
|
* liefert einen leeren temporären Ordner und übernimmt ihn erst nach erfolgreicher Prüfung.
|
*/
|
export function mitTemporaeremOrdner(zielordner: string): { ordner: string; uebernehmen: () => void; verwerfen: () => void } {
|
const temporaer = temporaerenOrdnerAnlegen(zielordner);
|
return {
|
ordner: temporaer,
|
uebernehmen: () => uebernehmeOrdner(temporaer, zielordner),
|
verwerfen: () => verwerfeOrdner(temporaer),
|
};
|
}
|
|
/** Prüft, ob im Zielordner bereits eine bestimmte Datei liegt. */
|
export function dateiVorhanden(zielordner: string, dateiname: string): boolean {
|
return !!dateiname && fs.existsSync(path.join(zielordner, dateiname));
|
}
|
|
/** Leitet einen Dateinamen aus einer URL ab (letztes Pfadsegment ohne Query). */
|
export function dateinameAusUrl(url: string): string {
|
try {
|
const segment = decodeURIComponent(new URL(url).pathname.split('/').pop() ?? '');
|
return segment || 'download.bin';
|
} catch {
|
return 'download.bin';
|
}
|
}
|