tibba_router_common/
lib.rs

1// Copyright 2025 Tree xie.
2//
3// Licensed under the Apache License, Version 2.0 (the "License");
4// you may not use this file except in compliance with the License.
5// You may obtain a copy of the License at
6//
7// http://www.apache.org/licenses/LICENSE-2.0
8//
9// Unless required by applicable law or agreed to in writing, software
10// distributed under the License is distributed on an "AS IS" BASIS,
11// WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
12// See the License for the specific language governing permissions and
13// limitations under the License.
14
15use axum::Json;
16use axum::Router;
17use axum::extract::State;
18use axum::http::header;
19use axum::response::IntoResponse;
20use axum::routing::get;
21use base64::{Engine, engine::general_purpose::STANDARD};
22use captcha::Captcha;
23use captcha::filters::{Noise, Wave};
24use serde::{Deserialize, Serialize};
25use std::io::Cursor;
26use std::time::Duration;
27use tibba_cache::RedisCache;
28use tibba_error::Error;
29use tibba_performance::get_process_system_info;
30use tibba_state::AppState;
31use tibba_util::{JsonResult, QueryParams, get_env, uuid};
32use validator::Validate;
33
34type Result<T> = std::result::Result<T, Error>;
35
36const ERROR_CATEGORY: &str = "common_router";
37
38/// Ping the server to check if it is running
39async fn ping(State(state): State<&'static AppState>) -> Result<&'static str> {
40    if !state.is_running() {
41        return Err(Error::new("Server is not running")
42            .with_category(ERROR_CATEGORY)
43            .with_status(503));
44    }
45    Ok("pong")
46}
47
48#[derive(Debug, Clone, Serialize)]
49struct ApplicationInfo {
50    uptime: String,
51    env: String,
52    os: String,
53    arch: String,
54    commit_id: String,
55    hostname: String,
56    memory_usage_mb: u32,
57    cpu_usage: u32,
58    open_files: u32,
59    total_written_mb: u32,
60    total_read_mb: u32,
61    running: bool,
62}
63
64fn format_uptime_approx(duration: Duration) -> String {
65    humantime::format_duration(duration)
66        .to_string()
67        .split(' ')
68        .take(2)
69        .collect::<Vec<_>>()
70        .join(" ")
71}
72
73/// Get the application information
74async fn get_application_info(
75    State(state): State<&'static AppState>,
76) -> JsonResult<ApplicationInfo> {
77    let uptime = state.get_started_at().elapsed().unwrap_or_default();
78    let info = os_info::get();
79    let os = info.os_type().to_string();
80    let arch = info.architecture().unwrap_or_default();
81    let performance = get_process_system_info(std::process::id() as usize);
82    let mb = 1024 * 1024;
83
84    let info = ApplicationInfo {
85        uptime: format_uptime_approx(uptime),
86        env: get_env().to_string(),
87        arch: arch.to_string(),
88        os,
89        commit_id: state.get_commit_id().to_string(),
90        hostname: hostname::get()
91            .unwrap_or_default()
92            .to_string_lossy()
93            .to_string(),
94        cpu_usage: performance.cpu_usage as u32,
95        memory_usage_mb: (performance.memory_usage / mb) as u32,
96        open_files: performance.open_files.unwrap_or_default() as u32,
97        total_written_mb: (performance.total_written_bytes / mb) as u32,
98        total_read_mb: (performance.total_read_bytes / mb) as u32,
99        running: state.is_running(),
100    };
101    Ok(Json(info))
102}
103
104#[derive(Debug, Deserialize, Clone, Validate)]
105pub struct CaptchaParams {
106    pub preview: Option<bool>,
107    pub theme: Option<String>,
108}
109
110#[derive(Debug, Clone, Serialize, Default)]
111struct CaptchaInfo {
112    id: String,
113    data: String,
114}
115
116/// Generate a captcha image
117async fn captcha(
118    State(cache): State<&'static RedisCache>,
119    QueryParams(params): QueryParams<CaptchaParams>,
120) -> Result<impl IntoResponse> {
121    let is_dark = params.theme.unwrap_or_default() == "dark";
122    // captcha is not supported send
123    let (text, data) = {
124        let mut c = Captcha::new();
125        // 设置允许0会导致0的时候不展示,后续确认
126        c.set_chars(&"123456789".chars().collect::<Vec<_>>())
127            .add_chars(4)
128            .apply_filter(Noise::new(0.4))
129            .apply_filter(Wave::new(2.0, 8.0).horizontal())
130            .apply_filter(Wave::new(2.0, 8.0).vertical())
131            .view(120, 38);
132        if is_dark {
133            c.set_color([60, 60, 60]);
134        }
135        let buffer = c.as_png().unwrap_or_default();
136        if is_dark {
137            let mut img = image::load_from_memory(&buffer)
138                .map_err(|e| Error::new(e.to_string()).with_exception(true))?;
139            img.invert();
140            let mut buffer: Vec<u8> = Vec::new();
141            img.write_to(&mut Cursor::new(&mut buffer), image::ImageFormat::Png)
142                .map_err(|e| Error::new(e.to_string()).with_exception(true))?;
143            (c.chars_as_string(), buffer)
144        } else {
145            (c.chars_as_string(), buffer)
146        }
147    };
148
149    if params.preview.unwrap_or_default() {
150        let headers = [(header::CONTENT_TYPE, "image/png")];
151        return Ok((headers, data).into_response());
152    }
153
154    let mut info = CaptchaInfo {
155        data: STANDARD.encode(data),
156        ..Default::default()
157    };
158    let id = uuid();
159    cache
160        .set(&id, &text, Some(Duration::from_secs(5 * 60)))
161        .await?;
162    info.id = id;
163
164    Ok(Json(info).into_response())
165}
166
167pub struct CommonRouterParams {
168    pub state: &'static AppState,
169    pub cache: Option<&'static RedisCache>,
170    pub commit_id: String,
171}
172
173pub fn new_common_router(params: CommonRouterParams) -> Router {
174    let r = Router::new()
175        .route("/ping", get(ping).with_state(params.state))
176        .route(
177            "/commons/application",
178            get(get_application_info).with_state(params.state),
179        );
180    let Some(cache) = params.cache else {
181        return r;
182    };
183
184    r.route("/commons/captcha", get(captcha).with_state(cache))
185}