init
This commit is contained in:
+1
-1
@@ -9,7 +9,7 @@ target/
|
||||
|
||||
# MSVC Windows builds of rustc generate these, which store debugging information
|
||||
*.pdb
|
||||
|
||||
idea/
|
||||
# RustRover
|
||||
# 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
|
||||
|
||||
+12
@@ -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"
|
||||
@@ -1,2 +1,45 @@
|
||||
# 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
Binary file not shown.
@@ -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
@@ -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
@@ -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}"),
|
||||
)
|
||||
}
|
||||
Reference in New Issue
Block a user