feat: initial tools service with document scanner, image & PDF tools
Self-hosted document scanner and media processing tools. - Rust Axum gateway + worker pool with NATS JetStream - Next.js 16 frontend with shadcn/ui - Scanner pipeline: edge detection, warp, binarization, OCR - Image tools: compress, resize, convert - PDF tools: merge, split, compress - CI/CD with Docker multi-stage build Co-Authored-By: Kilo <kilo@kilo.ai>
This commit is contained in:
@@ -0,0 +1,66 @@
|
||||
use axum::{
|
||||
http::StatusCode,
|
||||
response::{IntoResponse, Response},
|
||||
Json,
|
||||
};
|
||||
use serde_json::json;
|
||||
|
||||
/// Unified JSON error response format.
|
||||
#[derive(Debug)]
|
||||
pub struct AppError {
|
||||
pub status_code: StatusCode,
|
||||
pub code: String,
|
||||
pub message: String,
|
||||
}
|
||||
|
||||
impl AppError {
|
||||
pub fn bad_request(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
status_code: StatusCode::BAD_REQUEST,
|
||||
code: "bad_request".to_string(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn not_found(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
status_code: StatusCode::NOT_FOUND,
|
||||
code: "not_found".to_string(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn too_large(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
status_code: StatusCode::PAYLOAD_TOO_LARGE,
|
||||
code: "file_too_large".to_string(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn rate_limited(retry_after: u64) -> Self {
|
||||
Self {
|
||||
status_code: StatusCode::TOO_MANY_REQUESTS,
|
||||
code: "rate_limit_exceeded".to_string(),
|
||||
message: format!("Rate limit exceeded. Retry after {} seconds", retry_after),
|
||||
}
|
||||
}
|
||||
|
||||
pub fn internal(message: impl Into<String>) -> Self {
|
||||
Self {
|
||||
status_code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||
code: "internal_error".to_string(),
|
||||
message: message.into(),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl IntoResponse for AppError {
|
||||
fn into_response(self) -> Response {
|
||||
let body = json!({
|
||||
"error": self.message,
|
||||
"code": self.code,
|
||||
});
|
||||
(self.status_code, Json(body)).into_response()
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,2 @@
|
||||
pub mod error_handler;
|
||||
pub mod request_id;
|
||||
@@ -0,0 +1,73 @@
|
||||
use std::future::Future;
|
||||
use std::pin::Pin;
|
||||
use std::sync::atomic::{AtomicU64, Ordering};
|
||||
use std::task::{Context, Poll};
|
||||
|
||||
use axum::{extract::Request, response::Response};
|
||||
use tower::{Layer, Service};
|
||||
use uuid::Uuid;
|
||||
|
||||
/// Middleware that adds a unique X-Request-Id header to every request.
|
||||
#[derive(Clone, Default)]
|
||||
pub struct RequestIdLayer;
|
||||
|
||||
impl<S> Layer<S> for RequestIdLayer {
|
||||
type Service = RequestIdMiddleware<S>;
|
||||
|
||||
fn layer(&self, inner: S) -> Self::Service {
|
||||
RequestIdMiddleware {
|
||||
inner,
|
||||
counter: AtomicU64::new(0),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
pub struct RequestIdMiddleware<S> {
|
||||
inner: S,
|
||||
counter: AtomicU64,
|
||||
}
|
||||
|
||||
impl<S: Clone> Clone for RequestIdMiddleware<S> {
|
||||
fn clone(&self) -> Self {
|
||||
Self {
|
||||
inner: self.inner.clone(),
|
||||
counter: AtomicU64::new(self.counter.load(Ordering::Relaxed)),
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
impl<S, ReqBody, ResBody> Service<Request<ReqBody>> for RequestIdMiddleware<S>
|
||||
where
|
||||
S: Service<Request<ReqBody>, Response = Response<ResBody>>,
|
||||
S::Future: Send + 'static,
|
||||
S::Error: 'static,
|
||||
ReqBody: Send + 'static,
|
||||
ResBody: Default + Send + 'static,
|
||||
{
|
||||
type Response = Response<ResBody>;
|
||||
type Error = S::Error;
|
||||
type Future = Pin<Box<dyn Future<Output = Result<Self::Response, Self::Error>> + Send>>;
|
||||
|
||||
fn poll_ready(&mut self, cx: &mut Context<'_>) -> Poll<Result<(), Self::Error>> {
|
||||
self.inner.poll_ready(cx)
|
||||
}
|
||||
|
||||
fn call(&mut self, req: Request<ReqBody>) -> Self::Future {
|
||||
let request_id = Uuid::new_v4().to_string();
|
||||
let (mut parts, body) = req.into_parts();
|
||||
parts
|
||||
.headers
|
||||
.insert("x-request-id", request_id.parse().unwrap());
|
||||
|
||||
let req = Request::from_parts(parts, body);
|
||||
let fut = self.inner.call(req);
|
||||
|
||||
Box::pin(async move {
|
||||
let mut response: Response<ResBody> = fut.await?;
|
||||
response
|
||||
.headers_mut()
|
||||
.insert("x-request-id", request_id.parse().unwrap());
|
||||
Ok(response)
|
||||
})
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user