Files
easy-ocpp/src/main.rs
T
ManuelandClaude Opus 5 b180b471bb Migrations-Prüfsummen: Zeilenenden fixieren (v0.3.1)
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) <noreply@anthropic.com>
2026-08-19 09:51:50 +02:00

96 lines
2.8 KiB
Rust

use std::net::SocketAddr;
use std::path::PathBuf;
use std::sync::Arc;
use anyhow::{Context, Result};
use clap::Parser;
use sqlx::sqlite::{SqliteConnectOptions, SqlitePoolOptions};
use sqlx::SqlitePool;
use tracing_subscriber::{fmt, prelude::*, EnvFilter};
mod auth;
mod config;
mod db;
mod domain;
mod error;
mod i18n;
mod ocpp;
mod web;
pub use error::{AppError, AppResult};
#[derive(Clone)]
pub struct AppState {
pub db: SqlitePool,
pub ocpp_hub: Arc<ocpp::hub::Hub>,
pub config: Arc<config::Config>,
}
#[derive(Parser, Debug)]
#[command(name = "easy-occp", about = "Wallbox-Management-Tool (OCPP)")]
struct Cli {
/// Pfad zur Konfigurationsdatei (TOML).
#[arg(long, default_value = "config.toml")]
config: PathBuf,
/// Admin-Passwort zurücksetzen (setzt auf das angegebene Passwort).
#[arg(long, value_name = "PASSWORT")]
reset_admin: Option<String>,
}
#[tokio::main]
async fn main() -> Result<()> {
tracing_subscriber::registry()
.with(EnvFilter::try_from_default_env().unwrap_or_else(|_| EnvFilter::new("info,sqlx=warn")))
.with(fmt::layer().with_target(false))
.init();
let cli = Cli::parse();
let cfg = config::Config::load(&cli.config)?;
std::fs::create_dir_all(cfg.data_dir())
.with_context(|| format!("Konnte Datenverzeichnis {:?} nicht anlegen", cfg.data_dir()))?;
let db_url = format!("sqlite://{}", cfg.db_path().to_string_lossy().replace('\\', "/"));
tracing::info!("Datenbank: {}", db_url);
let opts = SqliteConnectOptions::new()
.filename(cfg.db_path())
.create_if_missing(true)
.journal_mode(sqlx::sqlite::SqliteJournalMode::Wal)
.busy_timeout(std::time::Duration::from_secs(5))
.foreign_keys(true);
let db = SqlitePoolOptions::new()
.max_connections(8)
.connect_with(opts)
.await
.context("SQLite-Pool konnte nicht geöffnet werden")?;
let migrator = sqlx::migrate!("./migrations");
db::repair_line_ending_checksums(&db, &migrator).await?;
migrator
.run(&db)
.await
.context("Migrationen fehlgeschlagen")?;
db::bootstrap_admin(&db, cli.reset_admin.as_deref()).await?;
let state = AppState {
db: db.clone(),
ocpp_hub: Arc::new(ocpp::hub::Hub::default()),
config: Arc::new(cfg.clone()),
};
let http_addr: SocketAddr = cfg.http.bind.parse().context("http.bind ungültig")?;
let app = web::router(state.clone());
tracing::info!("HTTP/UI hört auf http://{}", http_addr);
tracing::info!("OCPP-WebSocket unter ws://<host>:<port>/ocpp/<chargePointId>");
let listener = tokio::net::TcpListener::bind(http_addr).await?;
axum::serve(listener, app.into_make_service_with_connect_info::<SocketAddr>())
.await
.context("HTTP-Server beendet")?;
Ok(())
}