From b180b471bb815bbf5386c4daf77fe0d1991fdfa5 Mon Sep 17 00:00:00 2001 From: Manuel Date: Wed, 19 Aug 2026 09:51:50 +0200 Subject: [PATCH] =?UTF-8?q?Migrations-Pr=C3=BCfsummen:=20Zeilenenden=20fix?= =?UTF-8?q?ieren=20(v0.3.1)?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Bestehende Installationen brachen beim Start mit dem Windows-Release-Binary ab: "migration 1 was previously applied but has been modified" – obwohl die Migration inhaltlich seit v0.2.0 unverändert ist. Ursache: .gitattributes enthielt nur "* text=auto". Zusammen mit core.autocrlf=true (Standard auf windows-latest-Runnern) checkt actions/checkout die .sql-Dateien als CRLF aus. sqlx::migrate! bettet den Dateiinhalt zur Compile-Zeit ein und bildet darüber eine SHA-384-Summe, die beim Start gegen _sqlx_migrations geprüft wird: 0001_initial.sql mit LF -> 0651a39f... 0001_initial.sql mit CRLF -> 90093a9a... Die GitHub-Windows-Builds v0.2.0 und v0.3.0 tragen die CRLF-Variante, lokal gebaute Exes und der Linux-Build die LF-Variante. Datenbanken, die mit einem LF-Binary angelegt wurden, kollidieren daher mit dem Windows-Release-ZIP. Zwei Änderungen: 1. .gitattributes erzwingt "*.sql text eol=lf". Ab jetzt erzeugen Windows- und Linux-Runner identische Prüfsummen. 2. db::repair_line_ending_checksums() hebt eine gespeicherte Prüfsumme vor dem Migrationslauf an – aber nur, wenn sie exakt der Zeilenenden-Variante desselben SQL entspricht, in beide Richtungen. Ohne diesen Schritt würde Punkt 1 jede Installation brechen, die mit dem CRLF-Binary aus dem GitHub-ZIP von v0.2.0/v0.3.0 angelegt wurde. Echte inhaltliche Abweichungen führen weiterhin zum Abbruch durch sqlx. sha2 wird dafür direkte Dependency (war bereits transitiv gelockt, die Auflösung ändert sich nicht). Co-Authored-By: Claude Opus 5 (1M context) --- .gitattributes | 7 +++++ ANLEITUNG.md | 4 +-- Cargo.lock | 3 +- Cargo.toml | 3 +- INSTALL.es.md | 4 +-- INSTALL.fr.md | 4 +-- INSTALL.md | 4 +-- src/db.rs | 77 ++++++++++++++++++++++++++++++++++++++++++++++++++ src/main.rs | 4 ++- 9 files changed, 99 insertions(+), 11 deletions(-) diff --git a/.gitattributes b/.gitattributes index 176a458..11c92e0 100644 --- a/.gitattributes +++ b/.gitattributes @@ -1 +1,8 @@ * text=auto + +# Migrationen MÜSSEN LF behalten: sqlx::migrate! bildet eine SHA-384-Prüfsumme +# über den Dateiinhalt und vergleicht sie mit der in _sqlx_migrations +# gespeicherten. Ein CRLF-Checkout (Windows-Buildrunner) erzeugt sonst eine +# andere Prüfsumme als ein LF-Checkout (Linux) -> "migration 1 was previously +# applied but has been modified" beim Start gegen eine bestehende Datenbank. +*.sql text eol=lf diff --git a/ANLEITUNG.md b/ANLEITUNG.md index 2329926..37da373 100644 --- a/ANLEITUNG.md +++ b/ANLEITUNG.md @@ -1,4 +1,4 @@ -# easy-occp v0.3.0 – Installation & Erststart (Windows) +# easy-occp v0.3.1 – Installation & Erststart (Windows) 🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md) @@ -171,4 +171,4 @@ Dienst/Aufgabe entfernen, Ordner löschen — es gibt keine Registry-Einträge u --- -Version 0.3.0 · Lizenz: MIT +Version 0.3.1 · Lizenz: MIT diff --git a/Cargo.lock b/Cargo.lock index 763b343..f14e541 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -578,7 +578,7 @@ checksum = "1aaf95b3e5c8f23aa320147307562d361db0ae0d51242340f558153b4eb2439b" [[package]] name = "easy-occp" -version = "0.3.0" +version = "0.3.1" dependencies = [ "anyhow", "argon2", @@ -596,6 +596,7 @@ dependencies = [ "rust-embed", "serde", "serde_json", + "sha2", "sqlx", "thiserror 2.0.18", "tokio", diff --git a/Cargo.toml b/Cargo.toml index fcbe61d..9224f3b 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -1,6 +1,6 @@ [package] name = "easy-occp" -version = "0.3.0" +version = "0.3.1" edition = "2021" rust-version = "1.80" description = "Einfaches Management-Tool für Wallboxen (OCPP 1.5/1.6/2.0.1) für KMUs" @@ -30,6 +30,7 @@ clap = { version = "4", features = ["derive"] } futures-util = "0.3" parking_lot = "0.12" base64 = "0.22" +sha2 = "0.10" dashmap = "6" printpdf = "0.7" diff --git a/INSTALL.es.md b/INSTALL.es.md index 292c6ba..761fde2 100644 --- a/INSTALL.es.md +++ b/INSTALL.es.md @@ -1,4 +1,4 @@ -# easy-occp v0.3.0 – Instalación y primer arranque (Windows) +# easy-occp v0.3.1 – Instalación y primer arranque (Windows) 🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md) @@ -175,4 +175,4 @@ Elimine el servicio/la tarea y borre la carpeta — no hay entradas en el regist --- -Versión 0.3.0 · Licencia: MIT +Versión 0.3.1 · Licencia: MIT diff --git a/INSTALL.fr.md b/INSTALL.fr.md index 71d2b8d..c4e846b 100644 --- a/INSTALL.fr.md +++ b/INSTALL.fr.md @@ -1,4 +1,4 @@ -# easy-occp v0.3.0 – Installation & premier démarrage (Windows) +# easy-occp v0.3.1 – Installation & premier démarrage (Windows) 🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md) @@ -174,4 +174,4 @@ Supprimez le service/la tâche, supprimez le dossier — il n'y a ni entrées de --- -Version 0.3.0 · Licence : MIT +Version 0.3.1 · Licence : MIT diff --git a/INSTALL.md b/INSTALL.md index 6d20419..928700a 100644 --- a/INSTALL.md +++ b/INSTALL.md @@ -1,4 +1,4 @@ -# easy-occp v0.3.0 – Installation & First Start (Windows) +# easy-occp v0.3.1 – Installation & First Start (Windows) 🌐 [English](INSTALL.md) · [Deutsch](ANLEITUNG.md) · [Français](INSTALL.fr.md) · [Español](INSTALL.es.md) @@ -172,4 +172,4 @@ Remove the service/task, delete the folder — there are no registry entries and --- -Version 0.3.0 · License: MIT +Version 0.3.1 · License: MIT diff --git a/src/db.rs b/src/db.rs index 024904d..d46c759 100644 --- a/src/db.rs +++ b/src/db.rs @@ -85,3 +85,80 @@ pub async fn setting_set(db: &SqlitePool, key: &str, value: &str) -> Result<()> .await?; Ok(()) } + +/// Repariert Prüfsummen in `_sqlx_migrations`, die sich nur durch die +/// Zeilenenden der Migrationsdatei unterscheiden. +/// +/// Hintergrund: `sqlx::migrate!` bettet den Dateiinhalt zur Compile-Zeit ein und +/// prüft beim Start eine SHA-384-Summe darüber. Wird dieselbe Migration einmal +/// aus einem LF- und einmal aus einem CRLF-Checkout gebaut (Windows-Buildrunner +/// ohne `eol=lf`), unterscheiden sich die Summen und sqlx bricht mit +/// "migration N was previously applied but has been modified" ab, obwohl das +/// SQL identisch ist. In diesem – und nur in diesem – Fall wird die gespeicherte +/// Prüfsumme auf den aktuellen Wert gehoben. +pub async fn repair_line_ending_checksums( + db: &SqlitePool, + migrator: &sqlx::migrate::Migrator, +) -> Result<()> { + let table: Option<(String,)> = sqlx::query_as( + "SELECT name FROM sqlite_master WHERE type = 'table' AND name = '_sqlx_migrations'", + ) + .fetch_optional(db) + .await + .context("Migrationstabelle konnte nicht gelesen werden")?; + if table.is_none() { + return Ok(()); + } + + let applied: Vec<(i64, Vec)> = + sqlx::query_as("SELECT version, checksum FROM _sqlx_migrations") + .fetch_all(db) + .await + .context("Angewandte Migrationen konnten nicht gelesen werden")?; + + for migration in migrator.iter() { + let Some((_, stored)) = applied.iter().find(|(v, _)| *v == migration.version) else { + continue; + }; + if stored.as_slice() == migration.checksum.as_ref() { + continue; + } + + // Gegenstück mit umgedrehten Zeilenenden bilden und prüfen. + let normalized = migration.sql.replace("\r\n", "\n"); + let variant = if migration.sql.contains("\r\n") { + normalized + } else { + normalized.replace('\n', "\r\n") + }; + if stored.as_slice() != sha384(variant.as_bytes()).as_slice() { + continue; // Echte inhaltliche Abweichung – sqlx soll abbrechen. + } + + sqlx::query("UPDATE _sqlx_migrations SET checksum = ?1 WHERE version = ?2") + .bind(migration.checksum.as_ref()) + .bind(migration.version) + .execute(db) + .await + .with_context(|| { + format!( + "Prüfsumme der Migration {} konnte nicht aktualisiert werden", + migration.version + ) + })?; + tracing::warn!( + "Migration {} ({}): Prüfsumme wich nur in den Zeilenenden ab und wurde korrigiert.", + migration.version, + migration.description + ); + } + + Ok(()) +} + +fn sha384(data: &[u8]) -> Vec { + use sha2::{Digest, Sha384}; + let mut hasher = Sha384::new(); + hasher.update(data); + hasher.finalize().to_vec() +} diff --git a/src/main.rs b/src/main.rs index f6732f9..a4f7f41 100644 --- a/src/main.rs +++ b/src/main.rs @@ -65,7 +65,9 @@ async fn main() -> Result<()> { .await .context("SQLite-Pool konnte nicht geöffnet werden")?; - sqlx::migrate!("./migrations") + let migrator = sqlx::migrate!("./migrations"); + db::repair_line_ending_checksums(&db, &migrator).await?; + migrator .run(&db) .await .context("Migrationen fehlgeschlagen")?;