-- Sicherungsaufträge, Läufe und Zeitsteuerung (Phase 8). -- -- Zwei Grundsätze prägen dieses Schema: -- -- 1. Die Datenbank hält Konfiguration und Verweise, niemals Sicherungsdaten -- (PROMPT.md §138). Ein Manifest wird über manifest_ref adressiert; sein -- Inhalt liegt im Repository. -- 2. Ein Lauf ist kein Zustandsfeld am Auftrag, sondern eine eigene Zeile. -- Ein Auftrag, dessen letzter Ausgang nur als Spalte am Auftrag stünde, -- liesse keine Historie zu — und ohne Historie ist nicht feststellbar, -- ob eine Sicherung regelmäßig gelingt. -- --------------------------------------------------------------------------- -- Repositories -- --------------------------------------------------------------------------- CREATE TABLE repositories ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- name ist die sprechende Bezeichnung. name TEXT UNIQUE NOT NULL, -- repository_type benennt die Ablageart, etwa local oder s3. repository_type TEXT NOT NULL DEFAULT 'local', -- location ist der Pfad oder die Adresse der Ablage. location TEXT NOT NULL, -- repository_uuid ist die im Repository selbst hinterlegte Kennung. -- -- Sie wird gebraucht, um ein versehentlich vertauschtes Repository zu -- erkennen: Der Katalog wird bei fremder Kennung neu aufgebaut, statt -- falsche Einträge weiterzuverwenden. repository_uuid TEXT, -- status ist der Betriebszustand. status TEXT NOT NULL DEFAULT 'active', -- hardened meldet den gehärteten Modus. hardened BOOLEAN NOT NULL DEFAULT false, -- capacity_bytes ist der Gesamtplatz, sofern ermittelbar. capacity_bytes BIGINT, -- used_bytes ist der belegte Platz beim letzten Blick. used_bytes BIGINT, -- last_checked_at ist der Zeitpunkt der letzten Zustandsprüfung. last_checked_at TIMESTAMPTZ, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- updated_at ist der Zeitpunkt der letzten Änderung in UTC. updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT repositories_status_valid CHECK (status IN ('active', 'read_only', 'unavailable', 'maintenance')) ); -- --------------------------------------------------------------------------- -- Aufbewahrungsregeln -- --------------------------------------------------------------------------- CREATE TABLE retention_policies ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- name ist die sprechende Bezeichnung. name TEXT UNIQUE NOT NULL, -- rules beschreibt die Regeln als JSON. -- -- Bewusst als JSON und nicht als Spaltensatz: Die Regeln (täglich, -- wöchentlich, monatlich, jährlich, GFS) entwickeln sich weiter, und jede -- Erweiterung wäre sonst eine Migration am Kern des Schemas. rules JSONB NOT NULL, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- updated_at ist der Zeitpunkt der letzten Änderung in UTC. updated_at TIMESTAMPTZ NOT NULL DEFAULT now() ); -- --------------------------------------------------------------------------- -- Sicherungsaufträge -- --------------------------------------------------------------------------- CREATE TABLE backup_jobs ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- name ist die eindeutige, sprechende Bezeichnung. name TEXT UNIQUE NOT NULL, -- description erläutert den Zweck. description TEXT, -- status ist der Zustand des Auftrags. status TEXT NOT NULL DEFAULT 'active', -- priority ist die Dringlichkeit. priority TEXT NOT NULL DEFAULT 'normal', -- schedule_type benennt die Art des Zeitplans. schedule_type TEXT NOT NULL, -- schedule_config beschreibt den Zeitplan als JSON. schedule_config JSONB NOT NULL DEFAULT '{}'::jsonb, -- repository_id ist das Ziel-Repository. repository_id UUID REFERENCES repositories(id) ON DELETE RESTRICT, -- retention_policy_id ist die Aufbewahrungsregel. retention_policy_id UUID REFERENCES retention_policies(id) ON DELETE SET NULL, -- encryption_policy_id ist die Verschlüsselungsregel. encryption_policy_id UUID, -- verification_policy_id ist die Prüfregel. verification_policy_id UUID, -- notification_policy_id ist die Benachrichtigungsregel. notification_policy_id UUID, -- rpo_seconds ist der zulässige Datenverlust in Sekunden. rpo_seconds BIGINT, -- rto_seconds ist die zulässige Wiederherstellungsdauer in Sekunden. rto_seconds BIGINT, -- bandwidth_limit_bps begrenzt den Durchsatz; NULL bedeutet unbegrenzt. bandwidth_limit_bps BIGINT, -- max_concurrency begrenzt gleichzeitige Läufe dieses Auftrags. max_concurrency INTEGER NOT NULL DEFAULT 1, -- retry_policy beschreibt das Wiederholungsverhalten als JSON. retry_policy JSONB NOT NULL DEFAULT '{}'::jsonb, -- next_run_at ist der berechnete nächste Zeitpunkt in UTC. -- -- Er wird gespeichert und nicht bei jedem Blick neu berechnet: Der -- Scheduler fragt im Sekundentakt, welche Aufträge fällig sind. Eine -- Berechnung über alle Aufträge bei jeder Abfrage wäre bei tausend -- Aufträgen die teuerste Schleife der Anlage. next_run_at TIMESTAMPTZ, -- last_run_at ist der Beginn des letzten Laufs in UTC. last_run_at TIMESTAMPTZ, -- last_outcome ist der Ausgang des letzten Laufs. -- -- Redundant zu backup_job_runs, aber die Übersicht braucht ihn ohne -- Verbund über eine wachsende Historientabelle. last_outcome TEXT, -- paused_at ist der Zeitpunkt einer Aussetzung in UTC. paused_at TIMESTAMPTZ, -- paused_by benennt, wer ausgesetzt hat. paused_by UUID REFERENCES users(id) ON DELETE SET NULL, -- created_by benennt den Anleger. created_by UUID REFERENCES users(id) ON DELETE SET NULL, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- updated_at ist der Zeitpunkt der letzten Änderung in UTC. updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- deleted_at ist der Zeitpunkt einer Löschung in UTC. -- -- Soft-Delete, weil die Läufe eines gelöschten Auftrags im Audit erhalten -- bleiben müssen. Ein hart gelöschter Auftrag risse die Historie mit. deleted_at TIMESTAMPTZ, CONSTRAINT backup_jobs_status_valid CHECK (status IN ('active', 'paused', 'disabled', 'error')), CONSTRAINT backup_jobs_priority_valid CHECK (priority IN ('critical', 'high', 'normal', 'low')), CONSTRAINT backup_jobs_schedule_type_valid CHECK (schedule_type IN ('manual', 'interval', 'hourly', 'daily', 'weekly', 'monthly', 'cron')), CONSTRAINT backup_jobs_concurrency_positive CHECK (max_concurrency >= 1), -- Eine Bandbreitengrenze von 0 wäre keine Grenze, sondern ein Stillstand. CONSTRAINT backup_jobs_bandwidth_positive CHECK (bandwidth_limit_bps IS NULL OR bandwidth_limit_bps > 0) ); -- Der Scheduler fragt ausschließlich nach fälligen, aktiven Aufträgen. Der -- Teilindex hält genau diese Menge klein — ausgesetzte und gelöschte Aufträge -- stehen gar nicht erst darin. CREATE INDEX backup_jobs_due_idx ON backup_jobs (next_run_at) WHERE status = 'active' AND deleted_at IS NULL; CREATE INDEX backup_jobs_status_idx ON backup_jobs (status) WHERE deleted_at IS NULL; CREATE INDEX backup_jobs_repository_idx ON backup_jobs (repository_id); -- --------------------------------------------------------------------------- -- Quellen eines Auftrags -- --------------------------------------------------------------------------- CREATE TABLE backup_job_sources ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- job_id ist der zugehörige Auftrag. job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE, -- source_type benennt die Art der Quelle. source_type TEXT NOT NULL, -- source_id ist die Kennung der Quelle innerhalb ihrer Art. -- -- Bewusst Text und nicht UUID: Ein Proxmox-Gast heißt "qemu/100", ein -- Dateipfad "/daten". Eine UUID erzwänge für jede Quelle einen eigenen -- Datensatz, auch für einen bloßen Pfad. source_id TEXT NOT NULL, -- source_name ist die sprechende Bezeichnung. source_name TEXT, -- agent_id ist der ausführende Agent, sofern die Quelle einen braucht. agent_id UUID REFERENCES agents(id) ON DELETE SET NULL, -- include_patterns beschränkt die Erfassung. include_patterns JSONB NOT NULL DEFAULT '[]'::jsonb, -- exclude_patterns nimmt Pfade aus. exclude_patterns JSONB NOT NULL DEFAULT '[]'::jsonb, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT backup_job_sources_type_valid CHECK (source_type IN ('filesystem', 'proxmox_vm', 'proxmox_container', 'windows_system', 'linux_system')), -- Dieselbe Quelle zweimal im selben Auftrag wäre ein Konfigurationsfehler, -- der die Datenmenge verdoppelte. CONSTRAINT backup_job_sources_unique UNIQUE (job_id, source_type, source_id) ); CREATE INDEX backup_job_sources_job_idx ON backup_job_sources (job_id); CREATE INDEX backup_job_sources_source_idx ON backup_job_sources (source_type, source_id); -- --------------------------------------------------------------------------- -- Abhängigkeiten zwischen Aufträgen -- --------------------------------------------------------------------------- CREATE TABLE backup_job_dependencies ( -- job_id ist der abhängige Auftrag. job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE, -- depends_on_job_id ist der vorausgesetzte Auftrag. depends_on_job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), PRIMARY KEY (job_id, depends_on_job_id), -- Ein Auftrag, der von sich selbst abhängt, liefe nie an. Längere Ringe -- muss die Anwendung erkennen — das kann eine Datenbank nicht. CONSTRAINT backup_job_dependencies_no_self CHECK (job_id <> depends_on_job_id) ); CREATE INDEX backup_job_dependencies_reverse_idx ON backup_job_dependencies (depends_on_job_id); -- --------------------------------------------------------------------------- -- Wartungsfenster -- --------------------------------------------------------------------------- CREATE TABLE maintenance_windows ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- name ist die sprechende Bezeichnung. name TEXT NOT NULL, -- window_kind benennt die Wirkung: sperrend oder erlaubend. window_kind TEXT NOT NULL DEFAULT 'blackout', -- starts_at ist der Beginn eines einmaligen Fensters in UTC. starts_at TIMESTAMPTZ, -- ends_at ist das Ende eines einmaligen Fensters in UTC. ends_at TIMESTAMPTZ, -- recurrence beschreibt ein wiederkehrendes Fenster als JSON. recurrence JSONB, -- enabled meldet ein wirksames Fenster. enabled BOOLEAN NOT NULL DEFAULT true, -- created_by benennt den Anleger. created_by UUID REFERENCES users(id) ON DELETE SET NULL, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), -- updated_at ist der Zeitpunkt der letzten Änderung in UTC. updated_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT maintenance_windows_kind_valid CHECK (window_kind IN ('blackout', 'allowed')), -- Ein Fenster ist entweder einmalig oder wiederkehrend. Beides zugleich -- wäre mehrdeutig, keines von beidem wirkungslos. CONSTRAINT maintenance_windows_definition_complete CHECK ( (recurrence IS NOT NULL AND starts_at IS NULL AND ends_at IS NULL) OR (recurrence IS NULL AND starts_at IS NOT NULL AND ends_at IS NOT NULL AND ends_at > starts_at) ) ); CREATE INDEX maintenance_windows_enabled_idx ON maintenance_windows (enabled) WHERE enabled; -- Zuordnung von Fenstern zu einzelnen Aufträgen. Fehlt ein Eintrag, gilt das -- Fenster für alle Aufträge — das ist der sichere Standard. CREATE TABLE maintenance_window_jobs ( -- window_id ist das Fenster. window_id UUID NOT NULL REFERENCES maintenance_windows(id) ON DELETE CASCADE, -- job_id ist der betroffene Auftrag. job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE, PRIMARY KEY (window_id, job_id) ); -- --------------------------------------------------------------------------- -- Sicherungsketten -- --------------------------------------------------------------------------- CREATE TABLE backup_chains ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- source_reference benennt die gesicherte Quelle. source_reference TEXT NOT NULL, -- repository_id ist das Repository der Kette. repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT, -- status ist der Zustand der Kette. status TEXT NOT NULL DEFAULT 'active', -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT backup_chains_status_valid CHECK (status IN ('active', 'broken', 'closed')) ); CREATE INDEX backup_chains_source_idx ON backup_chains (source_reference, repository_id); -- --------------------------------------------------------------------------- -- Läufe -- --------------------------------------------------------------------------- CREATE TABLE backup_job_runs ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- job_id ist der ausgeführte Auftrag. job_id UUID NOT NULL REFERENCES backup_jobs(id) ON DELETE CASCADE, -- status ist der Zustand des Laufs. status TEXT NOT NULL DEFAULT 'queued', -- trigger benennt den Auslöser: Zeitplan, Anwender oder Wiederholung. trigger TEXT NOT NULL DEFAULT 'schedule', -- attempt_number ist die Nummer des Versuchs, beginnend bei 1. attempt_number INTEGER NOT NULL DEFAULT 1, -- scheduled_for ist der geplante Zeitpunkt in UTC. -- -- Er steht neben started_at, weil die Abweichung die eigentliche Auskunft -- ist: Ein Lauf, der regelmäßig eine Stunde zu spät beginnt, hat ein -- Problem, das man ohne diesen Vergleich nicht sieht. scheduled_for TIMESTAMPTZ, -- started_at ist der tatsächliche Beginn in UTC. started_at TIMESTAMPTZ, -- completed_at ist das Ende in UTC. completed_at TIMESTAMPTZ, -- bytes_processed ist die gelesene Datenmenge. bytes_processed BIGINT NOT NULL DEFAULT 0, -- bytes_written ist die abgelegte Datenmenge. bytes_written BIGINT NOT NULL DEFAULT 0, -- bytes_transferred ist die über das Netz übertragene Menge. bytes_transferred BIGINT NOT NULL DEFAULT 0, -- throughput_bps ist der erreichte Durchsatz. throughput_bps BIGINT, -- files_processed ist die Zahl bearbeiteter Objekte. files_processed BIGINT NOT NULL DEFAULT 0, -- files_skipped ist die Zahl übergangener Objekte. -- -- Ist sie größer als 0, ist der Lauf ein Teilfehler und niemals ein -- Erfolg (PROMPT.md §138). files_skipped BIGINT NOT NULL DEFAULT 0, -- error_code ist die Fehlerkennung in SCREAMING_SNAKE_CASE. error_code TEXT, -- error_message ist die verständliche Fehlermeldung. -- -- Sie enthält niemals Geheimnisse (PROMPT.md §141); die Redaktion -- geschieht vor dem Schreiben. error_message TEXT, -- failure_class ordnet den Fehler ein und entscheidet über Wiederholung. failure_class TEXT, -- correlation_id verbindet den Lauf mit seinen Protokollzeilen. correlation_id UUID NOT NULL, -- triggered_by benennt den auslösenden Anwender bei manuellem Start. triggered_by UUID REFERENCES users(id) ON DELETE SET NULL, -- scheduler_instance benennt den Control-Server, der den Lauf hält. -- -- Nötig, um nach dem Absturz eines Servers dessen verwaiste Läufe zu -- erkennen: Ein Lauf, der als laufend gilt, dessen Server aber verschwunden -- ist, blockiert sonst den Auftrag für immer. scheduler_instance TEXT, -- heartbeat_at ist die letzte Lebendmeldung des Laufs in UTC. heartbeat_at TIMESTAMPTZ, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT backup_job_runs_status_valid CHECK (status IN ('queued', 'running', 'succeeded', 'partial_failure', 'failed', 'cancelled')), CONSTRAINT backup_job_runs_trigger_valid CHECK (trigger IN ('schedule', 'manual', 'retry', 'dependency')), CONSTRAINT backup_job_runs_attempt_positive CHECK (attempt_number >= 1), -- Ein abgeschlossener Lauf ohne Endzeitpunkt wäre in jeder Auswertung eine -- Lücke. CONSTRAINT backup_job_runs_completion_consistent CHECK ( status IN ('queued', 'running') OR completed_at IS NOT NULL ), -- Ein Lauf mit übergangenen Objekten darf nicht als Erfolg dastehen. -- Die Regel gehört in die Datenbank, weil sie sonst von jeder Codestelle -- einzeln eingehalten werden müsste — und eine davon vergisst es. CONSTRAINT backup_job_runs_skipped_is_not_success CHECK ( files_skipped = 0 OR status <> 'succeeded' ) ); CREATE INDEX backup_job_runs_job_started_idx ON backup_job_runs (job_id, started_at DESC); CREATE INDEX backup_job_runs_status_idx ON backup_job_runs (status) WHERE status IN ('queued', 'running'); CREATE INDEX backup_job_runs_correlation_idx ON backup_job_runs (correlation_id); -- Ein Auftrag darf nur einen aktiven Lauf haben. Der Teilindex erzwingt das in -- der Datenbank statt in der Anwendung: Zwei Control-Server, die gleichzeitig -- denselben fälligen Auftrag sehen, würden ihn sonst beide starten und zwei -- Sicherungen derselben Quelle erzeugen. CREATE UNIQUE INDEX backup_job_runs_single_active_idx ON backup_job_runs (job_id) WHERE status IN ('queued', 'running'); -- --------------------------------------------------------------------------- -- Backups -- --------------------------------------------------------------------------- CREATE TABLE backups ( -- id ist der öffentliche Bezeichner. id UUID PRIMARY KEY DEFAULT gen_random_uuid(), -- job_run_id ist der erzeugende Lauf. job_run_id UUID REFERENCES backup_job_runs(id) ON DELETE SET NULL, -- chain_id ist die Kette. chain_id UUID REFERENCES backup_chains(id) ON DELETE SET NULL, -- repository_id ist das Repository. repository_id UUID NOT NULL REFERENCES repositories(id) ON DELETE RESTRICT, -- parent_backup_id ist das Elternbackup einer Zusatzsicherung. parent_backup_id UUID REFERENCES backups(id) ON DELETE SET NULL, -- backup_id_in_repository ist die Kennung innerhalb des Repositorys. -- -- Sie ist die Brücke zur Ablage: Das Repository kennt keine UUIDs der -- Datenbank, und es muss ohne sie auskommen (SYNCOVA_ARCHITECTURE.md §10). backup_id_in_repository TEXT NOT NULL, -- backup_type ist die Art des Backups. backup_type TEXT NOT NULL, -- consistency_level beschreibt die erreichte Konsistenz. -- -- Sie wird niemals beschönigt: Eine anwendungskonsistente Sicherung ohne -- Gastdienst gibt es nicht. consistency_level TEXT, -- status ist der Zustand des Backups. status TEXT NOT NULL DEFAULT 'in_progress', -- manifest_ref verweist auf das Manifest im Repository. manifest_ref TEXT NOT NULL, -- manifest_hash ist die Prüfsumme des Manifests. manifest_hash TEXT, -- logical_bytes ist die Menge der Ursprungsdaten. logical_bytes BIGINT, -- unique_bytes ist die nach Deduplizierung verbleibende Menge. unique_bytes BIGINT, -- compressed_bytes ist die Menge nach Kompression. compressed_bytes BIGINT, -- encrypted_bytes ist die abgelegte Menge. encrypted_bytes BIGINT, -- started_at ist der Beginn in UTC. started_at TIMESTAMPTZ, -- completed_at ist das Ende in UTC. completed_at TIMESTAMPTZ, -- integrity_status ist das Ergebnis der letzten Integritätsprüfung. integrity_status TEXT, -- verified_at ist der Zeitpunkt der letzten Prüfung in UTC. verified_at TIMESTAMPTZ, -- immutable_until ist das Ende der Aufbewahrungspflicht in UTC. -- -- Bis dahin darf kein Löschvorgang das Backup entfernen — auch kein -- Administrator (PROMPT.md: Unveränderlichkeit). immutable_until TIMESTAMPTZ, -- created_at ist der Anlagezeitpunkt in UTC. created_at TIMESTAMPTZ NOT NULL DEFAULT now(), CONSTRAINT backups_type_valid CHECK (backup_type IN ('full', 'incremental', 'differential', 'synthetic_full')), CONSTRAINT backups_status_valid CHECK (status IN ('in_progress', 'complete', 'incomplete', 'corrupt', 'deleted')), CONSTRAINT backups_consistency_valid CHECK (consistency_level IS NULL OR consistency_level IN ('crash_consistent', 'filesystem_consistent', 'application_consistent')), -- Eine Zusatzsicherung ohne Elternbackup wäre eine Kette ohne Anfang. CONSTRAINT backups_incremental_has_parent CHECK ( backup_type <> 'incremental' OR parent_backup_id IS NOT NULL ), -- Dieselbe Kennung darf im selben Repository nur einmal vorkommen. CONSTRAINT backups_repository_unique UNIQUE (repository_id, backup_id_in_repository) ); CREATE INDEX backups_chain_idx ON backups (chain_id, started_at DESC); CREATE INDEX backups_repository_status_idx ON backups (repository_id, status); CREATE INDEX backups_job_run_idx ON backups (job_run_id); -- Für die Aufbewahrung: Welche Backups dürfen noch nicht gelöscht werden? CREATE INDEX backups_immutable_idx ON backups (immutable_until) WHERE immutable_until IS NOT NULL; -- --------------------------------------------------------------------------- -- Berechtigungen -- --------------------------------------------------------------------------- -- jobs.read, jobs.write und jobs.run stammen bereits aus 000002_identity und -- werden hier NICHT erneut angelegt. Ein zweites INSERT verletzte die -- Eindeutigkeit des Namens — die Datenbank hat das beim ersten Versuch -- richtigerweise abgelehnt. Neu ist allein das Wartungsfenster: Es greift in -- den Betrieb der gesamten Anlage ein und ist deshalb ein eigenes Recht. INSERT INTO permissions (name, description) VALUES ('maintenance_windows.write', 'Wartungsfenster einrichten und ändern'); -- Ein Wartungsfenster unterdrückt Sicherungen. Wer es setzen darf, kann damit -- den Schutz der ganzen Anlage aussetzen — das bleibt den Administratoren -- vorbehalten. INSERT INTO role_permissions (role_id, permission_id) SELECT r.id, p.id FROM roles r, permissions p WHERE r.name IN ('super_administrator', 'infrastructure_administrator') AND p.name = 'maintenance_windows.write';