This commit is contained in:
2026-05-26 11:40:55 +02:00
parent a38970fd79
commit 68d8f2e9b8
7 changed files with 424 additions and 1 deletions
+1 -1
View File
@@ -9,7 +9,7 @@ target/
# MSVC Windows builds of rustc generate these, which store debugging information # MSVC Windows builds of rustc generate these, which store debugging information
*.pdb *.pdb
idea/
# RustRover # RustRover
# JetBrains specific template is maintained in a separate JetBrains.gitignore that can # JetBrains specific template is maintained in a separate JetBrains.gitignore that can
# be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore # be found at https://github.com/github/gitignore/blob/main/Global/JetBrains.gitignore
+12
View File
@@ -0,0 +1,12 @@
[package]
name = "minecraft_computer_craft_api_backend"
version = "0.1.0"
edition = "2021"
[dependencies]
axum = "0.8"
tokio = { version = "1", features = ["macros", "rt-multi-thread"] }
serde = { version = "1", features = ["derive"] }
serde_json = "1"
sqlx = { version = "0.8", features = ["runtime-tokio-rustls", "sqlite", "macros"] }
dotenvy = "0.15"
+43
View File
@@ -1,2 +1,45 @@
# Minecraft-Computer-Craft-API-Backend # Minecraft-Computer-Craft-API-Backend
Ein minimales Rust-API-Backend mit `axum` und SQLite (`sqlx`).
## Voraussetzungen
- Rust (stable)
- Cargo
## Starten
```powershell
cargo run
```
Standardwerte:
- `PORT=3000`
- `DATABASE_URL=sqlite://data/app.db`
Optional per Umgebungsvariablen setzen:
```powershell
$env:PORT="3000"
$env:DATABASE_URL="sqlite://data/app.db"
cargo run
```
## Endpunkte
- `GET /health` -> API-Status
- `GET /db-test` -> Testet DB-Verbindung (`SELECT 1`)
Beispielaufrufe:
```powershell
curl http://127.0.0.1:3000/health
curl http://127.0.0.1:3000/db-test
```
## Projektstruktur
- `src/main.rs` - Serverstart, Router, App-State
- `src/db.rs` - SQLite-Pool + Basisschema
- `src/routes.rs` - API-Handler
BIN
View File
Binary file not shown.
+219
View File
@@ -0,0 +1,219 @@
use sqlx::{sqlite::SqlitePoolOptions, SqlitePool, QueryBuilder, Sqlite};
use serde::{Deserialize, Serialize};
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct MeSystemLive {
pub name: String,
pub amount: i64,
#[serde(rename = "displayName")]
pub display_name: Option<String>,
}
#[derive(Debug, Serialize, Deserialize, sqlx::FromRow)]
pub struct MeSystemHistory {
pub id: i64,
pub name: String,
pub amount: i64,
pub recorded_at: i64,
}
pub async fn init_db(database_url: &str) -> Result<SqlitePool, sqlx::Error> {
if let Some(parent) = database_url
.strip_prefix("sqlite://")
.and_then(|path| std::path::Path::new(path).parent())
{
if !parent.as_os_str().is_empty() {
let _ = std::fs::create_dir_all(parent);
}
}
let pool = SqlitePoolOptions::new()
.max_connections(5)
.connect(database_url)
.await?;
// Minimales Basisschema fuer Verbindungs- und Schreibtest.
sqlx::query(
"-- Aktuelle Bestände
-- Wir führen eine ID ein, um in der History Platz zu sparen.
CREATE TABLE IF NOT EXISTS me_system_live (
id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT UNIQUE NOT NULL, -- 'minecraft:iron_ingot'
amount INTEGER NOT NULL,
display_name TEXT
);
-- Historische Aufzeichnung
-- Hier speichern wir nur noch die item_id statt den vollen Namen.
-- recorded_at als Unix Timestamp (INTEGER)
CREATE TABLE IF NOT EXISTS me_system_history (
id INTEGER PRIMARY KEY AUTOINCREMENT,
item_id INTEGER NOT NULL,
amount INTEGER NOT NULL,
recorded_at INTEGER NOT NULL DEFAULT (unixepoch()),
FOREIGN KEY (item_id) REFERENCES me_system_live(id)
);
-- Index für schnelle Zeitabfragen und Joins
CREATE INDEX IF NOT EXISTS idx_history_item_time ON me_system_history (item_id, recorded_at);
-- Index auf Name in Live Tabelle ist durch UNIQUE constraint implizit vorhanden, aber für FK lookups gut zu wissen.
",
)
.execute(&pool)
.await?;
Ok(pool)
}
pub async fn insert_me_system_live(
pool: &SqlitePool,
items: &[MeSystemLive],
) -> Result<(), sqlx::Error> {
let mut transaction = pool.begin().await?;
// 1. Alles auf 0 setzen (Reset)
sqlx::query("UPDATE me_system_live SET amount = 0 WHERE amount > 0")
.execute(&mut *transaction)
.await?;
// 2. Neue Daten einfügen/aktualisieren (Upsert)
// Wir nutzen hier name als unique key
if !items.is_empty() {
for chunk in items.chunks(4000) {
let mut query_builder: QueryBuilder<Sqlite> = QueryBuilder::new(
"INSERT INTO me_system_live (name, amount, display_name) "
);
query_builder.push_values(chunk, |mut b, item| {
b.push_bind(&item.name)
.push_bind(item.amount)
.push_bind(&item.display_name);
});
query_builder.push(
" ON CONFLICT(name) DO UPDATE SET
amount = excluded.amount,
display_name = excluded.display_name"
);
query_builder.build().execute(&mut *transaction).await?;
}
}
transaction.commit().await?;
Ok(())
}
pub async fn insert_me_system_history(
pool: &SqlitePool,
items: &[MeSystemLive],
) -> Result<(), sqlx::Error> {
if items.is_empty() {
return Ok(());
}
let mut transaction = pool.begin().await?;
// 1. Temporäre Tabelle für den Batch-Input erstellen
sqlx::query("CREATE TEMPORARY TABLE IF NOT EXISTS temp_history_input (name TEXT, amount INTEGER)")
.execute(&mut *transaction)
.await?;
// Tabelle leeren
sqlx::query("DELETE FROM temp_history_input")
.execute(&mut *transaction)
.await?;
// 2. Input Daten in Temp Tabelle schreiben
for chunk in items.chunks(4000) {
let mut query_builder: QueryBuilder<Sqlite> = QueryBuilder::new(
"INSERT INTO temp_history_input (name, amount) "
);
query_builder.push_values(chunk, |mut b, item| {
b.push_bind(&item.name)
.push_bind(item.amount);
});
query_builder.build().execute(&mut *transaction).await?;
}
// 3. Insert in History Tabelle durch Join mit Live Tabelle, um die IDs zu bekommen
sqlx::query(
"INSERT INTO me_system_history (item_id, amount)
SELECT l.id, t.amount
FROM temp_history_input t
JOIN me_system_live l ON t.name = l.name"
)
.execute(&mut *transaction)
.await?;
// 4. Cleanup
sqlx::query("DROP TABLE temp_history_input")
.execute(&mut *transaction)
.await?;
transaction.commit().await?;
Ok(())
}
pub async fn get_all_me_system_live(pool: &SqlitePool) -> Result<Vec<MeSystemLive>, sqlx::Error> {
sqlx::query_as::<_, MeSystemLive>(
r#"
SELECT name, amount, display_name
FROM me_system_live
"#,
)
.fetch_all(pool)
.await
}
pub async fn get_me_system_live(
pool: &SqlitePool,
name: &str,
) -> Result<Option<MeSystemLive>, sqlx::Error> {
sqlx::query_as::<_, MeSystemLive>(
r#"
SELECT name, amount, display_name
FROM me_system_live
WHERE name = ?
"#,
)
.bind(name)
.fetch_optional(pool)
.await
}
pub async fn get_me_system_history(
pool: &SqlitePool,
name: &str,
) -> Result<Vec<MeSystemHistory>, sqlx::Error> {
sqlx::query_as::<_, MeSystemHistory>(
r#"
SELECT h.id, l.name, h.amount, h.recorded_at
FROM me_system_history h
JOIN me_system_live l ON h.item_id = l.id
WHERE l.name = ?
ORDER BY h.recorded_at DESC
"#,
)
.bind(name)
.fetch_all(pool)
.await
}
pub async fn get_me_system_hourely_change(
pool: &SqlitePool,
names: &str,
) -> Result<Vec<MeSystemHistory>, sqlx::Error> {
sqlx::query_as::<_, MeSystemHistory>(
r#"
SELECT h.id, l.name, h.amount, h.recorded_at
FROM me_system_history h
JOIN me_system_live l ON h.item_id = l.id
WHERE l.name IN (??)
ORDER BY h.recorded_at DESC
"#,
)
.bind(names)
.fetch_all(pool)
.await
}
+45
View File
@@ -0,0 +1,45 @@
mod db;
mod routes;
use axum::{routing::{get, post}, Router};
use sqlx::SqlitePool;
use std::net::SocketAddr;
use std::sync::Arc;
use std::sync::atomic::AtomicI64;
#[derive(Clone)]
pub struct AppState {
pub db_pool: SqlitePool,
pub last_history_update: Arc<AtomicI64>,
}
#[tokio::main]
async fn main() -> Result<(), Box<dyn std::error::Error>> {
dotenvy::dotenv().ok();
let database_url = std::env::var("DATABASE_URL")
.unwrap_or_else(|_| "sqlite://data/app.db".to_string());
let port = std::env::var("PORT")
.unwrap_or_else(|_| "3000".to_string())
.parse::<u16>()?;
let db_pool = db::init_db(&database_url).await?;
let state = AppState {
db_pool,
last_history_update: Arc::new(AtomicI64::new(0)),
};
let app = Router::new()
.route("/health", get(routes::health))
.route("/db-test", get(routes::db_test))
.route("/updateMeSystem", post(routes::update_me_system))
.with_state(state);
let addr = SocketAddr::from(([0, 0, 0, 0], port));
println!("Server laeuft auf http://{}", addr);
let listener = tokio::net::TcpListener::bind(addr).await?;
axum::serve(listener, app).await?;
Ok(())
}
+104
View File
@@ -0,0 +1,104 @@
use axum::{extract::State, Json};
use serde::Serialize;
use serde_json::Value;
use crate::AppState;
use crate::db::{self, MeSystemLive};
use std::time::{SystemTime, UNIX_EPOCH};
use std::sync::atomic::Ordering;
#[derive(Serialize)]
pub struct HealthResponse {
status: &'static str,
}
#[derive(Serialize)]
pub struct DbTestResponse {
status: &'static str,
db_ok: bool,
result: i64,
}
pub async fn health() -> Json<HealthResponse> {
Json(HealthResponse { status: "ok" })
}
pub async fn db_test(
State(state): State<AppState>,
) -> Result<Json<DbTestResponse>, (axum::http::StatusCode, String)> {
let value: i64 = sqlx::query_scalar("SELECT 1")
.fetch_one(&state.db_pool)
.await
.map_err(internal_error)?;
Ok(Json(DbTestResponse {
status: "ok",
db_ok: value == 1,
result: value,
}))
}
pub async fn update_me_system(
State(state): State<AppState>,
Json(payload): Json<Vec<MeSystemLive>>
) -> Json<Value> {
println!("Received update request with {} items", payload.len());
let count = payload.len();
// 1. Immer Live-Daten aktualisieren
if let Err(e) = db::insert_me_system_live(&state.db_pool, &payload).await {
eprintln!("Error updating live data: {}", e);
return Json(serde_json::json!({
"status": "error",
"message": format!("DB Error (Live): {}", e),
"success": 0,
"errors": count
}));
}
// 2. Historie nur alle X Sekunden aktualisieren (z.B. 300 Sekunden)
let history_interval_seconds = 300;
let now = SystemTime::now()
.duration_since(UNIX_EPOCH)
.unwrap_or_default()
.as_secs() as i64;
let last_update = state.last_history_update.load(Ordering::Relaxed);
// Wenn genug Zeit vergangen ist seit dem letzten Update
if now - last_update >= history_interval_seconds {
// Versuchen, den Zeitstempel zu aktualisieren (verhindert doppelte Ausführung bei gleichzeitigen Requests)
if state.last_history_update.compare_exchange(
last_update,
now,
Ordering::Relaxed,
Ordering::Relaxed
).is_ok() {
println!("Writing history snapshot...");
// Asynchron in den Hintergrund verschieben? Nein, wir warten hier, damit wir Fehler mitbekommen.
// Performance: Das kann dauern, aber der Client wartet eh.
if let Err(e) = db::insert_me_system_history(&state.db_pool, &payload).await {
eprintln!("Error writing history: {}", e);
// Wir returnen trotzdem OK, weil Live-Daten wichtig sind.
} else {
println!("History snapshot saved.");
}
}
}
Json(serde_json::json!({
"status": "ok",
"message": format!("Processed {} items", count),
"success": count,
"errors": 0
}))
}
fn internal_error(err: sqlx::Error) -> (axum::http::StatusCode, String) {
(
axum::http::StatusCode::INTERNAL_SERVER_ERROR,
format!("DB error: {err}"),
)
}