shared/
lib.rs

1//! Shared library for the Calagopus Panel.
2//!
3//! This library contains code that is shared between the backend and extensions.
4//! It includes models, utilities, and other common functionality to avoid repetition
5//! and ensure consistency across the project. If something for a job exists in here,
6//! it's generally preferred to be used instead of re-implementing it elsewhere.
7
8use anyhow::Context;
9use colored::Colorize;
10use include_dir::{Dir, include_dir};
11use serde::{Deserialize, Serialize};
12use std::{
13    sync::{Arc, LazyLock},
14    time::Instant,
15};
16use utoipa::ToSchema;
17
18pub mod cache;
19pub mod cap;
20pub mod captcha;
21pub mod database;
22pub mod deserialize;
23pub mod env;
24pub mod events;
25pub mod extensions;
26pub mod extract;
27pub mod jwt;
28pub mod mail;
29pub mod models;
30pub mod ntp;
31pub mod payload;
32pub mod permissions;
33pub mod prelude;
34pub mod response;
35pub mod settings;
36pub mod storage;
37pub mod telemetry;
38pub mod utils;
39
40pub use payload::Payload;
41pub use schema_extension_core::Extendible;
42
43pub const VERSION: &str = env!("CARGO_PKG_VERSION");
44pub const GIT_COMMIT: &str = env!("CARGO_GIT_COMMIT");
45pub const GIT_BRANCH: &str = env!("CARGO_GIT_BRANCH");
46pub const TARGET: &str = env!("CARGO_TARGET");
47
48pub fn full_version() -> String {
49    if GIT_BRANCH == "unknown" {
50        VERSION.to_string()
51    } else {
52        format!("{VERSION}:{GIT_COMMIT}@{GIT_BRANCH}")
53    }
54}
55
56pub const BUFFER_SIZE: usize = 32 * 1024;
57
58pub type GetIp = axum::extract::Extension<std::net::IpAddr>;
59
60#[derive(ToSchema, Serialize)]
61pub struct ApiError {
62    pub errors: Vec<String>,
63}
64
65impl ApiError {
66    #[inline]
67    pub fn new_value(errors: &[&str]) -> serde_json::Value {
68        serde_json::json!({
69            "errors": errors,
70        })
71    }
72
73    #[inline]
74    pub fn new_strings_value(errors: Vec<String>) -> serde_json::Value {
75        serde_json::json!({
76            "errors": errors,
77        })
78    }
79
80    #[inline]
81    pub fn new_wings_value(error: wings_api::ApiError) -> serde_json::Value {
82        serde_json::json!({
83            "errors": [error.error],
84        })
85    }
86}
87
88#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
89#[serde(rename_all = "snake_case")]
90pub enum AppContainerType {
91    Official,
92    OfficialHeavy,
93    Unknown,
94    None,
95}
96
97pub struct AppState {
98    pub start_time: Instant,
99    pub container_type: AppContainerType,
100    pub version: String,
101
102    pub client: reqwest::Client,
103
104    pub extensions: Arc<extensions::manager::ExtensionManager>,
105    pub background_tasks: Arc<extensions::background_tasks::BackgroundTaskManager>,
106    pub shutdown_handlers: Arc<extensions::shutdown_handlers::ShutdownHandlerManager>,
107    pub settings: Arc<settings::Settings>,
108    pub jwt: Arc<jwt::Jwt>,
109    pub ntp: Arc<ntp::Ntp>,
110    pub storage: Arc<storage::Storage>,
111    pub captcha: Arc<captcha::Captcha>,
112    pub mail: Arc<mail::Mail>,
113    pub database: Arc<database::Database>,
114    pub cache: Arc<cache::Cache>,
115    pub env: Arc<env::Env>,
116}
117
118impl AppState {
119    pub async fn new_cli(env: Option<Arc<env::Env>>) -> Result<State, anyhow::Error> {
120        let env = match env {
121            Some(env) => env,
122            None => {
123                eprintln!(
124                    "{}",
125                    "please setup the new panel environment before using this command.".red()
126                );
127                std::process::exit(1);
128            }
129        };
130
131        let jwt = Arc::new(jwt::Jwt::new(&env));
132        let ntp = ntp::Ntp::new();
133        let cache = cache::Cache::new(&env).await;
134        let database = Arc::new(database::Database::new(&env, cache.clone()).await);
135
136        let background_tasks =
137            Arc::new(extensions::background_tasks::BackgroundTaskManager::default());
138        let shutdown_handlers =
139            Arc::new(extensions::shutdown_handlers::ShutdownHandlerManager::default());
140        let settings = Arc::new(
141            settings::Settings::new(database.clone())
142                .await
143                .context("failed to load settings")?,
144        );
145        let storage = Arc::new(storage::Storage::new(settings.clone()));
146        let captcha = Arc::new(captcha::Captcha::new(settings.clone()));
147        let mail = Arc::new(mail::Mail::new(settings.clone()));
148
149        let state = Arc::new(AppState {
150            start_time: Instant::now(),
151            container_type: match std::env::var("OCI_CONTAINER").as_deref() {
152                Ok("official") => AppContainerType::Official,
153                Ok("official-heavy") => AppContainerType::OfficialHeavy,
154                Ok(_) => AppContainerType::Unknown,
155                Err(_) => AppContainerType::None,
156            },
157            version: full_version(),
158
159            client: reqwest::ClientBuilder::new()
160                .user_agent(format!("github.com/calagopus/panel {}", VERSION))
161                .build()
162                .unwrap(),
163
164            extensions: Arc::new(extensions::manager::ExtensionManager::new(vec![])),
165            background_tasks: background_tasks.clone(),
166            shutdown_handlers: shutdown_handlers.clone(),
167            settings: settings.clone(),
168            jwt,
169            ntp,
170            storage,
171            captcha,
172            mail,
173            database: database.clone(),
174            cache: cache.clone(),
175            env: env.clone(),
176        });
177
178        Ok(state)
179    }
180}
181
182pub type State = Arc<AppState>;
183pub type GetState = axum::extract::State<State>;
184
185#[inline(always)]
186#[cold]
187fn cold_path() {}
188
189#[inline(always)]
190pub fn likely(b: bool) -> bool {
191    if b {
192        true
193    } else {
194        cold_path();
195        false
196    }
197}
198
199#[inline(always)]
200pub fn unlikely(b: bool) -> bool {
201    if b {
202        cold_path();
203        true
204    } else {
205        false
206    }
207}
208
209pub const FRONTEND_ASSETS: Dir<'_> = include_dir!("$CARGO_MANIFEST_DIR/../frontend/dist");
210
211pub static FRONTEND_LANGUAGES: LazyLock<Vec<compact_str::CompactString>> = LazyLock::new(|| {
212    let mut languages = Vec::new();
213
214    let Some(translations) = FRONTEND_ASSETS.get_dir("translations") else {
215        return languages;
216    };
217
218    for translation in translations.files() {
219        let Some(file_name) = translation.path().file_name() else {
220            continue;
221        };
222
223        languages.push(file_name.to_string_lossy().trim_end_matches(".json").into());
224    }
225
226    languages
227});