web framework: switch to axum

This commit is contained in:
Armaël Guéneau 2025-01-02 11:54:29 +01:00
parent edc49a6d1d
commit fcd4b4538a
5 changed files with 182 additions and 194 deletions

230
Cargo.lock generated
View file

@ -1,6 +1,6 @@
# This file is automatically @generated by Cargo. # This file is automatically @generated by Cargo.
# It is not intended for manual editing. # It is not intended for manual editing.
version = 3 version = 4
[[package]] [[package]]
name = "actix-codec" name = "actix-codec"
@ -55,14 +55,11 @@ dependencies = [
"ahash", "ahash",
"base64 0.22.1", "base64 0.22.1",
"bitflags 2.6.0", "bitflags 2.6.0",
"brotli",
"bytes", "bytes",
"bytestring", "bytestring",
"derive_more", "derive_more",
"encoding_rs", "encoding_rs",
"flate2",
"futures-core", "futures-core",
"h2 0.3.26",
"http 0.2.12", "http 0.2.12",
"httparse", "httparse",
"httpdate", "httpdate",
@ -78,17 +75,6 @@ dependencies = [
"tokio", "tokio",
"tokio-util", "tokio-util",
"tracing", "tracing",
"zstd",
]
[[package]]
name = "actix-macros"
version = "0.2.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e01ed3140b2f8d422c68afa1ed2e85d996ea619c988ac834d255db32138655cb"
dependencies = [
"quote",
"syn",
] ]
[[package]] [[package]]
@ -100,7 +86,6 @@ dependencies = [
"bytestring", "bytestring",
"cfg-if", "cfg-if",
"http 0.2.12", "http 0.2.12",
"regex",
"regex-lite", "regex-lite",
"serde", "serde",
"tracing", "tracing",
@ -162,18 +147,15 @@ checksum = "9180d76e5cc7ccbc4d60a506f2c727730b154010262df5b910eb17dbe4b8cb38"
dependencies = [ dependencies = [
"actix-codec", "actix-codec",
"actix-http", "actix-http",
"actix-macros",
"actix-router", "actix-router",
"actix-rt", "actix-rt",
"actix-server", "actix-server",
"actix-service", "actix-service",
"actix-utils", "actix-utils",
"actix-web-codegen",
"ahash", "ahash",
"bytes", "bytes",
"bytestring", "bytestring",
"cfg-if", "cfg-if",
"cookie",
"derive_more", "derive_more",
"encoding_rs", "encoding_rs",
"futures-core", "futures-core",
@ -185,7 +167,6 @@ dependencies = [
"mime", "mime",
"once_cell", "once_cell",
"pin-project-lite", "pin-project-lite",
"regex",
"regex-lite", "regex-lite",
"serde", "serde",
"serde_json", "serde_json",
@ -196,18 +177,6 @@ dependencies = [
"url", "url",
] ]
[[package]]
name = "actix-web-codegen"
version = "4.3.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f591380e2e68490b5dfaf1dd1aa0ebe78d84ba7067078512b4ea6e4492d622b8"
dependencies = [
"actix-router",
"proc-macro2",
"quote",
"syn",
]
[[package]] [[package]]
name = "addr2line" name = "addr2line"
version = "0.24.2" version = "0.24.2"
@ -245,21 +214,6 @@ dependencies = [
"memchr", "memchr",
] ]
[[package]]
name = "alloc-no-stdlib"
version = "2.0.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "cc7bb162ec39d46ab1ca8c77bf72e890535becd1751bb45f64c597edb4c8c6b3"
[[package]]
name = "alloc-stdlib"
version = "0.2.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "94fb8275041c72129eb51b7d0322c29b8387a0386127718b096429201a5d6ece"
dependencies = [
"alloc-no-stdlib",
]
[[package]] [[package]]
name = "allocator-api2" name = "allocator-api2"
version = "0.2.21" version = "0.2.21"
@ -673,6 +627,60 @@ dependencies = [
"tracing", "tracing",
] ]
[[package]]
name = "axum"
version = "0.8.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6d6fd624c75e18b3b4c6b9caf42b1afe24437daaee904069137d8bab077be8b8"
dependencies = [
"axum-core",
"bytes",
"form_urlencoded",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"hyper 1.5.2",
"hyper-util",
"itoa",
"matchit",
"memchr",
"mime",
"percent-encoding",
"pin-project-lite",
"rustversion",
"serde",
"serde_json",
"serde_path_to_error",
"serde_urlencoded",
"sync_wrapper 1.0.2",
"tokio",
"tower",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "axum-core"
version = "0.5.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "df1362f362fd16024ae199c1970ce98f9661bf5ef94b9808fee734bc3698b733"
dependencies = [
"bytes",
"futures-util",
"http 1.2.0",
"http-body 1.0.1",
"http-body-util",
"mime",
"pin-project-lite",
"rustversion",
"sync_wrapper 1.0.2",
"tower-layer",
"tower-service",
"tracing",
]
[[package]] [[package]]
name = "backtrace" name = "backtrace"
version = "0.3.74" version = "0.3.74"
@ -743,27 +751,6 @@ dependencies = [
"generic-array", "generic-array",
] ]
[[package]]
name = "brotli"
version = "6.0.0"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "74f7971dbd9326d58187408ab83117d8ac1bb9c17b085fdacd1cf2f598719b6b"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
"brotli-decompressor",
]
[[package]]
name = "brotli-decompressor"
version = "4.0.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "9a45bd2e4095a8b518033b128020dd4a55aab1c0a381ba4404a472630f4bc362"
dependencies = [
"alloc-no-stdlib",
"alloc-stdlib",
]
[[package]] [[package]]
name = "bstr" name = "bstr"
version = "1.11.1" version = "1.11.1"
@ -817,8 +804,6 @@ version = "1.2.5"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e" checksum = "c31a0499c1dc64f458ad13872de75c0eb7e3fdb0e67964610c914b034fc5956e"
dependencies = [ dependencies = [
"jobserver",
"libc",
"shlex", "shlex",
] ]
@ -884,17 +869,6 @@ version = "0.4.0"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e" checksum = "6245d59a3e82a7fc217c5828a6692dbc6dfb63a0c8c90495621f7b9d79704a0e"
[[package]]
name = "cookie"
version = "0.16.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "e859cd57d0710d9e06c381b550c06e76992472a8c6d527aecd2fc673dcc231fb"
dependencies = [
"percent-encoding",
"time",
"version_check",
]
[[package]] [[package]]
name = "core-foundation" name = "core-foundation"
version = "0.9.4" version = "0.9.4"
@ -1151,16 +1125,6 @@ dependencies = [
"subtle", "subtle",
] ]
[[package]]
name = "flate2"
version = "1.0.35"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "c936bfdafb507ebbf50b8074c54fa31c5be9a1e7e5f467dd659697041407d07c"
dependencies = [
"crc32fast",
"miniz_oxide",
]
[[package]] [[package]]
name = "fnv" name = "fnv"
version = "1.0.7" version = "1.0.7"
@ -1212,10 +1176,10 @@ name = "forgery"
version = "0.1.0" version = "0.1.0"
dependencies = [ dependencies = [
"actix-files", "actix-files",
"actix-web",
"anyhow", "anyhow",
"aws-config", "aws-config",
"aws-sdk-s3", "aws-sdk-s3",
"axum",
"forgejo-api", "forgejo-api",
"include_dir", "include_dir",
"lazy_static", "lazy_static",
@ -1534,6 +1498,7 @@ dependencies = [
"http 1.2.0", "http 1.2.0",
"http-body 1.0.1", "http-body 1.0.1",
"httparse", "httparse",
"httpdate",
"itoa", "itoa",
"pin-project-lite", "pin-project-lite",
"smallvec", "smallvec",
@ -1847,15 +1812,6 @@ version = "1.0.14"
source = "registry+https://github.com/rust-lang/crates.io-index" source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674" checksum = "d75a2a4b1b190afb6f5425f10f6a8f959d2ea0b9c2b1d79553551850539e4674"
[[package]]
name = "jobserver"
version = "0.1.32"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "48d1dbcbbeb6a7fec7e059840aa538bd62aaccf972c7346c4d9d2059312853d0"
dependencies = [
"libc",
]
[[package]] [[package]]
name = "js-sys" name = "js-sys"
version = "0.3.76" version = "0.3.76"
@ -1970,6 +1926,12 @@ dependencies = [
"hashbrown 0.15.2", "hashbrown 0.15.2",
] ]
[[package]]
name = "matchit"
version = "0.8.4"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "47e1ffaa40ddd1f3ed91f717a33c8c0ee23fff369e3aa8772b9605cc1d22f4c3"
[[package]] [[package]]
name = "md-5" name = "md-5"
version = "0.10.6" version = "0.10.6"
@ -2655,6 +2617,12 @@ dependencies = [
"untrusted", "untrusted",
] ]
[[package]]
name = "rustversion"
version = "1.0.19"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "f7c45b9784283f1b2e7fb61b42047c2fd678ef0960d4f6f1eba131594cc369d4"
[[package]] [[package]]
name = "ryu" name = "ryu"
version = "1.0.18" version = "1.0.18"
@ -2770,6 +2738,16 @@ dependencies = [
"serde", "serde",
] ]
[[package]]
name = "serde_path_to_error"
version = "0.1.16"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "af99884400da37c88f5e9146b7f1fd0fbcae8f6eec4e9da38b67d05486f814a6"
dependencies = [
"itoa",
"serde",
]
[[package]] [[package]]
name = "serde_urlencoded" name = "serde_urlencoded"
version = "0.7.1" version = "0.7.1"
@ -3184,6 +3162,28 @@ dependencies = [
"tokio", "tokio",
] ]
[[package]]
name = "tower"
version = "0.5.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "d039ad9159c98b70ecfd540b2573b97f7f52c3e8d9f8ad57a24b916a536975f9"
dependencies = [
"futures-core",
"futures-util",
"pin-project-lite",
"sync_wrapper 1.0.2",
"tokio",
"tower-layer",
"tower-service",
"tracing",
]
[[package]]
name = "tower-layer"
version = "0.3.3"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "121c2a6cda46980bb0fcd1647ffaf6cd3fc79a013de288782836f6df9c48780e"
[[package]] [[package]]
name = "tower-service" name = "tower-service"
version = "0.3.3" version = "0.3.3"
@ -3802,31 +3802,3 @@ dependencies = [
"quote", "quote",
"syn", "syn",
] ]
[[package]]
name = "zstd"
version = "0.13.2"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "fcf2b778a664581e31e389454a7072dab1647606d44f7feea22cd5abb9c9f3f9"
dependencies = [
"zstd-safe",
]
[[package]]
name = "zstd-safe"
version = "7.2.1"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "54a3ab4db68cea366acc5c897c7b4d4d1b8994a9cd6e6f841f8964566a419059"
dependencies = [
"zstd-sys",
]
[[package]]
name = "zstd-sys"
version = "2.0.13+zstd.1.5.6"
source = "registry+https://github.com/rust-lang/crates.io-index"
checksum = "38ff0f21cfee8f97d94cef41359e0c89aa6113028ab0291aa8ca0038995a95aa"
dependencies = [
"cc",
"pkg-config",
]

View file

@ -14,7 +14,7 @@ url = "2"
anyhow = "1" anyhow = "1"
serde_json = "1" serde_json = "1"
rand = "0.8" rand = "0.8"
actix-web = "4" axum = { version = "0.8", features = ["form"] }
tera = "1" tera = "1"
lazy_static = "1" lazy_static = "1"
actix-files = "0.6" actix-files = "0.6"

View file

@ -57,4 +57,5 @@ Environment variables read when `STORAGE_BACKEND=s3`:
could not be locked, but delete the account after the grace period even if could not be locked, but delete the account after the grace period even if
the email could not be sent…) the email could not be sent…)
- auth: add support for connecting to the forge using oauth? - auth: add support for connecting to the forge using oauth?
- allow customizing the address & port on which to listen
- improve error handling - improve error handling

View file

@ -1,5 +1,12 @@
use actix_web::{get, post, web, App, HttpRequest, HttpResponse, HttpServer, Responder};
use anyhow::{anyhow, Context}; use anyhow::{anyhow, Context};
use axum::{
extract::{OriginalUri, Path, Query, State},
http::{header, StatusCode},
response::{Html, IntoResponse},
routing::get,
Form,
Router,
};
use forgejo_api::{Auth, Forgejo}; use forgejo_api::{Auth, Forgejo};
use lazy_static::lazy_static; use lazy_static::lazy_static;
use rand::prelude::*; use rand::prelude::*;
@ -277,13 +284,12 @@ fn approx_score(score: f32) -> ApproxScore {
} }
} }
#[get("/")] async fn get_index(
async fn index( State(data): State<Arc<AppState>>,
data: web::Data<AppState>, Query(q): Query<SortSetting>,
q: web::Query<SortSetting>, OriginalUri(uri): OriginalUri,
req: HttpRequest, ) -> Html<String> {
) -> impl Responder { eprintln!("GET {}", uri);
eprintln!("GET {}", req.uri());
let db = &data.db; let db = &data.db;
@ -340,16 +346,16 @@ async fn index(
eprintln!("rendering template..."); eprintln!("rendering template...");
let page = TEMPLATES.render("index.html", &context).unwrap(); let page = TEMPLATES.render("index.html", &context).unwrap();
eprintln!("done"); eprintln!("done");
HttpResponse::Ok().body(page) Html::from(page)
} }
async fn post_classified( async fn post_classified(
data: web::Data<AppState>, State(data): State<Arc<AppState>>,
form: web::Form<HashMap<i64, String>>, Form(form): Form<HashMap<i64, String>>,
req: HttpRequest, OriginalUri(uri): OriginalUri,
overwrite: bool, overwrite: bool,
) -> impl Responder { ) -> Result<impl IntoResponse, AppError> {
eprintln!("POST {}", req.uri()); eprintln!("POST {}", uri);
let updates: Vec<(UserId, bool)> = form let updates: Vec<(UserId, bool)> = form
.iter() .iter()
@ -366,45 +372,34 @@ async fn post_classified(
) )
.await; .await;
let res = storage::store_db(&data.storage, &data.db).await; storage::store_db(&data.storage, &data.db).await?;
eprintln!("done"); eprintln!("done");
match res { Ok((StatusCode::SEE_OTHER, [(header::LOCATION, uri.to_string())], ()))
Ok(()) => HttpResponse::SeeOther()
.insert_header(("Location", req.uri().to_string()))
.finish(),
Err(e) => {
HttpResponse::InternalServerError().body(format!("Internal server error:\n\n{e}"))
}
}
} }
#[post("/")]
async fn post_classified_index( async fn post_classified_index(
data: web::Data<AppState>, data: State<Arc<AppState>>,
form: web::Form<HashMap<i64, String>>, uri: OriginalUri,
req: HttpRequest, form: Form<HashMap<i64, String>>,
) -> impl Responder { ) -> impl IntoResponse {
post_classified(data, form, req, false).await post_classified(data, form, uri, false).await
} }
#[post("/classified")]
async fn post_classified_edit( async fn post_classified_edit(
data: web::Data<AppState>, data: State<Arc<AppState>>,
form: web::Form<HashMap<i64, String>>, uri: OriginalUri,
req: HttpRequest, form: Form<HashMap<i64, String>>,
) -> impl Responder { ) -> impl IntoResponse {
post_classified(data, form, req, true).await post_classified(data, form, uri, true).await
} }
#[get("/classified")] async fn get_classified(
async fn classified( State(data): State<Arc<AppState>>,
data: web::Data<AppState>, OriginalUri(uri): OriginalUri,
_q: web::Query<SortSetting>, ) -> Html<String> {
req: HttpRequest, eprintln!("GET {}", uri);
) -> impl Responder {
eprintln!("GET {}", req.uri());
let db = &data.db; let db = &data.db;
let mut users: Vec<(UserId, UserData, f32, bool)> = db.with_userdb(|udb| { let mut users: Vec<(UserId, UserData, f32, bool)> = db.with_userdb(|udb| {
@ -427,23 +422,21 @@ async fn classified(
eprintln!("rendering template..."); eprintln!("rendering template...");
let page = TEMPLATES.render("classified.html", &context).unwrap(); let page = TEMPLATES.render("classified.html", &context).unwrap();
eprintln!("done"); eprintln!("done");
HttpResponse::Ok().body(page) Html::from(page)
} }
const STATIC_DIR: include_dir::Dir = include_dir::include_dir!("static"); const STATIC_DIR: include_dir::Dir = include_dir::include_dir!("static");
#[get("/static/{filename:.*}")] async fn get_static_(Path(filename): Path<String>, OriginalUri(uri): OriginalUri) -> impl IntoResponse {
async fn static_(req: HttpRequest) -> impl Responder { eprintln!("GET {}", uri);
eprintln!("GET {}", req.uri());
let path: String = req.match_info().query("filename").parse().unwrap(); match STATIC_DIR.get_file(filename) {
match STATIC_DIR.get_file(path) { None => (StatusCode::NOT_FOUND, "404 Not found").into_response(),
None => HttpResponse::NotFound().body("404 Not found"), Some(page) => page.contents().into_response(),
Some(page) => HttpResponse::Ok().body(page.contents()),
} }
} }
#[actix_web::main] #[tokio::main]
async fn main() -> anyhow::Result<()> { async fn main() -> anyhow::Result<()> {
eprintln!("Eval templates"); eprintln!("Eval templates");
let _ = *TEMPLATES; let _ = *TEMPLATES;
@ -455,7 +448,7 @@ async fn main() -> anyhow::Result<()> {
eprintln!("Load users and repos"); eprintln!("Load users and repos");
let db = load_db(&storage, &forge).await?; let db = load_db(&storage, &forge).await?;
let st = web::Data::new(AppState { let shared_state = Arc::new(AppState {
db: db.clone(), db: db.clone(),
storage: storage.clone(), storage: storage.clone(),
forge: forge.clone(), forge: forge.clone(),
@ -489,17 +482,16 @@ async fn main() -> anyhow::Result<()> {
println!("Listening on http://127.0.0.1:8080"); println!("Listening on http://127.0.0.1:8080");
let webserver = HttpServer::new(move || { let app = Router::new()
App::new() .route("/", get(get_index).post(post_classified_index))
.app_data(st.clone()) .route("/classified", get(get_classified).post(post_classified_edit))
.service(static_) .route("/static/{*filename}", get(get_static_))
.service(index) .with_state(shared_state);
.service(classified)
.service(post_classified_index) let webserver = {
.service(post_classified_edit) let listener = tokio::net::TcpListener::bind("0.0.0.0:8080").await.unwrap();
}) axum::serve(listener, app)
.bind(("127.0.0.1", 8080))? };
.run();
tokio::select! { tokio::select! {
_ = workers.join_all() => { _ = workers.join_all() => {
@ -514,3 +506,26 @@ async fn main() -> anyhow::Result<()> {
Ok(()) Ok(())
} }
// setup to allow turning anyhow errors into axum "internal server error" responses
struct AppError(anyhow::Error);
impl IntoResponse for AppError {
fn into_response(self) -> axum::response::Response {
(
StatusCode::INTERNAL_SERVER_ERROR,
format!("Internal server error:\n{}", self.0),
)
.into_response()
}
}
impl<E> From<E> for AppError
where
E: Into<anyhow::Error>,
{
fn from(err: E) -> Self {
Self(err.into())
}
}

View file

@ -203,7 +203,7 @@ pub async fn try_lock_and_notify_user(
let user = u.userdata(user_id).unwrap(); let user = u.userdata(user_id).unwrap();
(user.login.clone(), user.email.clone(), u.is_spam(user_id)) (user.login.clone(), user.email.clone(), u.is_spam(user_id))
}); });
let is_spam = match is_spam{ let is_spam = match is_spam {
Some(IsSpam::Spam { Some(IsSpam::Spam {
classified_at, classified_at,
locked, locked,