simple http server
This commit is contained in:
parent
921a22e645
commit
8f0bbe4e0b
1
.gitignore
vendored
1
.gitignore
vendored
@ -1 +1,2 @@
|
|||||||
target/
|
target/
|
||||||
|
.env
|
||||||
|
|||||||
1722
Cargo.lock
generated
1722
Cargo.lock
generated
File diff suppressed because it is too large
Load Diff
@ -5,8 +5,12 @@ edition = "2024"
|
|||||||
|
|
||||||
[dependencies]
|
[dependencies]
|
||||||
axum = "0.8.9"
|
axum = "0.8.9"
|
||||||
|
dotenvy = "0.15.7"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
serde = { version = "1.0.228", features = ["derive"] }
|
||||||
serde_json = "1.0.149"
|
serde_json = "1.0.149"
|
||||||
|
sqlx = { version = "0.8.6", features = ["runtime-tokio-rustls", "postgres", "uuid", "chrono", "migrate"] }
|
||||||
|
thiserror = "2.0.18"
|
||||||
tokio = { version = "1.52.0", features = ["macros", "rt-multi-thread"] }
|
tokio = { version = "1.52.0", features = ["macros", "rt-multi-thread"] }
|
||||||
tracing = "0.1.44"
|
tracing = "0.1.44"
|
||||||
tracing-subscriber = "0.3.23"
|
tower-http = { version = "0.6.6", features = ["trace"] }
|
||||||
|
tracing-subscriber = { version = "0.3.23", features = ["env-filter"] }
|
||||||
|
|||||||
45
Dockerfile
Normal file
45
Dockerfile
Normal file
@ -0,0 +1,45 @@
|
|||||||
|
# ---- STAGE 1: Build ----
|
||||||
|
# Use an OpenJDK image that matches the version you develop with.
|
||||||
|
# It contains the JDK, but not Maven.
|
||||||
|
FROM eclipse-temurin:25-jdk-jammy AS builder
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# ---- Caching Dependencies ----
|
||||||
|
# First, copy the files that define the build, including the Maven Wrapper
|
||||||
|
COPY mvnw .
|
||||||
|
COPY .mvn .mvn
|
||||||
|
COPY pom.xml .
|
||||||
|
|
||||||
|
# Make the wrapper executable
|
||||||
|
RUN chmod +x ./mvnw
|
||||||
|
|
||||||
|
# Run a Maven command to download dependencies.
|
||||||
|
# Since pom.xml and wrapper files rarely change, this layer will be cached by Docker,
|
||||||
|
# speeding up subsequent builds significantly.
|
||||||
|
RUN ./mvnw dependency:go-offline
|
||||||
|
|
||||||
|
# ---- Building the Application ----
|
||||||
|
# Now, copy the source code. If only source code changes, the layers above are cached.
|
||||||
|
COPY src ./src
|
||||||
|
|
||||||
|
# Build the application JAR using the Maven Wrapper
|
||||||
|
RUN ./mvnw clean package -DskipTests
|
||||||
|
|
||||||
|
|
||||||
|
# ---- STAGE 2: Runtime ----
|
||||||
|
# Use a lean Eclipse Temurin JRE image for a small and secure final container.
|
||||||
|
FROM eclipse-temurin:25-jre-jammy
|
||||||
|
|
||||||
|
# Set the working directory
|
||||||
|
WORKDIR /app
|
||||||
|
|
||||||
|
# Copy only the built JAR file from the 'builder' stage
|
||||||
|
COPY --from=builder /app/target/*.jar app.jar
|
||||||
|
|
||||||
|
# Expose the application port
|
||||||
|
EXPOSE 8080
|
||||||
|
|
||||||
|
# Command to run the application
|
||||||
|
ENTRYPOINT ["java", "-jar", "app.jar"]
|
||||||
58
compose.yaml
Normal file
58
compose.yaml
Normal file
@ -0,0 +1,58 @@
|
|||||||
|
services:
|
||||||
|
# api-prod:
|
||||||
|
# build: .
|
||||||
|
# restart: unless-stopped
|
||||||
|
# container_name: qrcode-api
|
||||||
|
# ports:
|
||||||
|
# - "8080:8080"
|
||||||
|
# environment:
|
||||||
|
# DB_HOST: db-prod
|
||||||
|
# env_file:
|
||||||
|
# - ".env"
|
||||||
|
# depends_on:
|
||||||
|
# db-prod:
|
||||||
|
# condition: service_healthy
|
||||||
|
# profiles:
|
||||||
|
# - prod
|
||||||
|
#
|
||||||
|
# db-prod:
|
||||||
|
# image: postgres:18.0-alpine
|
||||||
|
# restart: unless-stopped
|
||||||
|
# container_name: qrcode-database-prod
|
||||||
|
# ports:
|
||||||
|
# - "${DB_PORT:-5432}:5432"
|
||||||
|
# environment:
|
||||||
|
# POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
# POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
# POSTGRES_DB: ${DB_NAME}
|
||||||
|
# volumes:
|
||||||
|
# - postgres_data:/var/lib/postgresql/data
|
||||||
|
# profiles:
|
||||||
|
# - prod
|
||||||
|
# healthcheck:
|
||||||
|
# test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
|
||||||
|
# interval: 5s
|
||||||
|
# timeout: 3s
|
||||||
|
# retries: 10
|
||||||
|
|
||||||
|
db-dev:
|
||||||
|
image: postgres:18.0-alpine
|
||||||
|
restart: unless-stopped
|
||||||
|
container_name: rhythm-db-dev
|
||||||
|
ports:
|
||||||
|
- "${DB_PORT}:5432"
|
||||||
|
environment:
|
||||||
|
POSTGRES_USER: ${DB_USERNAME}
|
||||||
|
POSTGRES_PASSWORD: ${DB_PASSWORD}
|
||||||
|
POSTGRES_DB: ${DB_NAME}
|
||||||
|
profiles:
|
||||||
|
- dev
|
||||||
|
healthcheck:
|
||||||
|
test: ["CMD-SHELL", "pg_isready -U ${DB_USERNAME} -d ${DB_NAME}"]
|
||||||
|
interval: 5s
|
||||||
|
timeout: 3s
|
||||||
|
retries: 10
|
||||||
|
|
||||||
|
volumes:
|
||||||
|
postgres_data:
|
||||||
|
|
||||||
52
src/config.rs
Normal file
52
src/config.rs
Normal file
@ -0,0 +1,52 @@
|
|||||||
|
use std::env;
|
||||||
|
|
||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Clone)]
|
||||||
|
pub struct Config {
|
||||||
|
pub db_user: String,
|
||||||
|
pub db_password: String,
|
||||||
|
pub db_host: String,
|
||||||
|
pub db_port: u16,
|
||||||
|
pub db_name: String,
|
||||||
|
}
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum ConfigError {
|
||||||
|
#[error("missing env var: {0}")]
|
||||||
|
MissingVar(&'static str),
|
||||||
|
#[error("invalid DB_PORT: {0}")]
|
||||||
|
InvalidPort(String),
|
||||||
|
}
|
||||||
|
|
||||||
|
impl Config {
|
||||||
|
pub fn from_env() -> Result<Self, ConfigError> {
|
||||||
|
dotenvy::dotenv().ok();
|
||||||
|
|
||||||
|
let db_user =
|
||||||
|
env::var("DB_USERNAME").map_err(|_| ConfigError::MissingVar("DB_USERNAME"))?;
|
||||||
|
let db_password =
|
||||||
|
env::var("DB_PASSWORD").map_err(|_| ConfigError::MissingVar("DB_PASSWORD"))?;
|
||||||
|
let db_host = env::var("DB_HOST").map_err(|_| ConfigError::MissingVar("DB_HOST"))?;
|
||||||
|
let db_name = env::var("DB_NAME").map_err(|_| ConfigError::MissingVar("DB_NAME"))?;
|
||||||
|
let db_port_raw = env::var("DB_PORT").map_err(|_| ConfigError::MissingVar("DB_PORT"))?;
|
||||||
|
let db_port = db_port_raw
|
||||||
|
.parse::<u16>()
|
||||||
|
.map_err(|_| ConfigError::InvalidPort(db_port_raw))?;
|
||||||
|
|
||||||
|
Ok(Self {
|
||||||
|
db_user,
|
||||||
|
db_password,
|
||||||
|
db_host,
|
||||||
|
db_port,
|
||||||
|
db_name,
|
||||||
|
})
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn database_url(&self) -> String {
|
||||||
|
format!(
|
||||||
|
"postgres://{}:{}@{}:{}/{}",
|
||||||
|
self.db_user, self.db_password, self.db_host, self.db_port, self.db_name
|
||||||
|
)
|
||||||
|
}
|
||||||
|
}
|
||||||
9
src/db/database.rs
Normal file
9
src/db/database.rs
Normal file
@ -0,0 +1,9 @@
|
|||||||
|
use sqlx::{PgPool, postgres::PgPoolOptions};
|
||||||
|
|
||||||
|
pub async fn create_pool(database_url: &str) -> Result<PgPool, crate::error::AppError> {
|
||||||
|
PgPoolOptions::new()
|
||||||
|
.max_connections(10)
|
||||||
|
.connect(database_url)
|
||||||
|
.await
|
||||||
|
.map_err(crate::error::AppError::from)
|
||||||
|
}
|
||||||
1
src/db/mod.rs
Normal file
1
src/db/mod.rs
Normal file
@ -0,0 +1 @@
|
|||||||
|
pub mod database;
|
||||||
11
src/error.rs
Normal file
11
src/error.rs
Normal file
@ -0,0 +1,11 @@
|
|||||||
|
use thiserror::Error;
|
||||||
|
|
||||||
|
#[derive(Debug, Error)]
|
||||||
|
pub enum AppError {
|
||||||
|
#[error(transparent)]
|
||||||
|
Config(#[from] crate::config::ConfigError),
|
||||||
|
#[error(transparent)]
|
||||||
|
Db(#[from] sqlx::Error),
|
||||||
|
#[error(transparent)]
|
||||||
|
Io(#[from] std::io::Error),
|
||||||
|
}
|
||||||
@ -1 +1 @@
|
|||||||
pub mod public;
|
pub mod public_router;
|
||||||
|
|||||||
@ -1,9 +0,0 @@
|
|||||||
use axum::{Router, routing::get};
|
|
||||||
|
|
||||||
pub fn router() -> Router {
|
|
||||||
Router::new().route("/health", get(get_health()))
|
|
||||||
}
|
|
||||||
|
|
||||||
fn get_health() -> &'static str {
|
|
||||||
"{'status': 'ok'}"
|
|
||||||
}
|
|
||||||
18
src/http/public_router.rs
Normal file
18
src/http/public_router.rs
Normal file
@ -0,0 +1,18 @@
|
|||||||
|
use axum::{Json, Router, routing::get};
|
||||||
|
use serde::Serialize;
|
||||||
|
|
||||||
|
#[derive(Serialize)]
|
||||||
|
struct HealthResponse {
|
||||||
|
status: &'static str,
|
||||||
|
}
|
||||||
|
|
||||||
|
pub fn router<S>() -> Router<S>
|
||||||
|
where
|
||||||
|
S: Clone + Send + Sync + 'static,
|
||||||
|
{
|
||||||
|
Router::new().route("/health", get(get_health))
|
||||||
|
}
|
||||||
|
|
||||||
|
async fn get_health() -> Json<HealthResponse> {
|
||||||
|
Json(HealthResponse { status: "ok" })
|
||||||
|
}
|
||||||
46
src/main.rs
46
src/main.rs
@ -1,11 +1,49 @@
|
|||||||
|
mod config;
|
||||||
|
mod db;
|
||||||
|
mod error;
|
||||||
mod http;
|
mod http;
|
||||||
|
|
||||||
|
use crate::db::database;
|
||||||
|
use crate::error::AppError;
|
||||||
|
use crate::http::public_router;
|
||||||
use axum::Router;
|
use axum::Router;
|
||||||
|
use tower_http::trace::{DefaultOnRequest, DefaultOnResponse, TraceLayer};
|
||||||
|
use tracing::{Level, info};
|
||||||
|
use tracing_subscriber::{EnvFilter, fmt};
|
||||||
|
|
||||||
|
#[derive(Clone)]
|
||||||
|
struct AppState {
|
||||||
|
#[allow(dead_code)]
|
||||||
|
db: sqlx::PgPool,
|
||||||
|
}
|
||||||
|
|
||||||
|
fn init_tracing() {
|
||||||
|
let filter = EnvFilter::try_from_default_env()
|
||||||
|
.unwrap_or_else(|_| EnvFilter::new("info,tower_http=info,sqlx=warn"));
|
||||||
|
|
||||||
|
fmt().with_env_filter(filter).init();
|
||||||
|
}
|
||||||
|
|
||||||
#[tokio::main]
|
#[tokio::main]
|
||||||
async fn main() {
|
async fn main() -> Result<(), AppError> {
|
||||||
let app = Router::new().merge(http::public::router());
|
init_tracing();
|
||||||
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
|
let cfg = config::Config::from_env()?;
|
||||||
axum::serve(listener, app).await.unwrap();
|
let pool = database::create_pool(&cfg.database_url()).await?;
|
||||||
|
info!("database connection established");
|
||||||
|
|
||||||
|
let state = AppState { db: pool };
|
||||||
|
let app = Router::new()
|
||||||
|
.merge(public_router::router())
|
||||||
|
.with_state(state)
|
||||||
|
.layer(
|
||||||
|
TraceLayer::new_for_http()
|
||||||
|
.on_request(DefaultOnRequest::new().level(Level::INFO))
|
||||||
|
.on_response(DefaultOnResponse::new().level(Level::INFO)),
|
||||||
|
);
|
||||||
|
|
||||||
|
let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await?;
|
||||||
|
info!("server listening on 0.0.0.0:8080");
|
||||||
|
axum::serve(listener, app).await?;
|
||||||
|
Ok(())
|
||||||
}
|
}
|
||||||
|
|||||||
Loading…
x
Reference in New Issue
Block a user