Files
ystp/src/config.rs
2026-07-26 05:47:19 +08:00

218 lines
7.8 KiB
Rust

use crate::error::{AppError, ErrorCode};
static TRUST_PROXY_HEADERS: std::sync::OnceLock<bool> = std::sync::OnceLock::new();
#[derive(Debug, Clone)]
pub struct Config {
pub role: String,
pub host: String,
pub port: u16,
pub public_base_url: String,
pub database_url: String,
pub database_max_connections: u32,
pub redis_url: String,
pub worker_task_concurrency: u32,
pub worker_concurrency: u32,
pub image_processing_concurrency: u32,
pub zip_build_concurrency: u32,
pub zip_max_entries: u32,
pub zip_max_uncompressed_bytes: u64,
pub jwt_secret: String,
pub jwt_expiry_hours: i64,
pub api_key_pepper: String,
pub stripe_secret_key: Option<String>,
pub stripe_webhook_secret: Option<String>,
pub stripe_api_base_url: String,
pub storage_path: String,
pub allow_anonymous_upload: bool,
pub anon_max_file_size_mb: u64,
pub anon_max_files_per_batch: u32,
pub anon_daily_units: u32,
pub anon_retention_hours: u64,
pub max_image_pixels: u64,
pub idempotency_ttl_hours: u64,
pub mail_enabled: bool,
pub mail_log_links_when_disabled: bool,
pub mail_provider: String,
pub mail_from: String,
pub mail_password: String,
pub mail_from_name: String,
pub mail_smtp_host: Option<String>,
pub mail_smtp_port: Option<u16>,
pub mail_smtp_encryption: Option<String>,
}
impl Config {
pub fn from_env() -> Result<Self, AppError> {
let role = env_string("IMAGEFORGE_ROLE").unwrap_or_else(|| "api".to_string());
let host = env_string("HOST").unwrap_or_else(|| "0.0.0.0".to_string());
let port = env_u16("PORT").unwrap_or(8080);
let public_base_url =
env_string("PUBLIC_BASE_URL").unwrap_or_else(|| "http://localhost:8080".to_string());
let database_url = env_string("DATABASE_URL")
.ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 DATABASE_URL"))?;
let database_max_connections = env_u32("DATABASE_MAX_CONNECTIONS").unwrap_or(10);
let redis_url = env_string("REDIS_URL")
.ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 REDIS_URL"))?;
let worker_task_concurrency = env_u32("WORKER_TASK_CONCURRENCY")
.filter(|value| *value > 0)
.unwrap_or(4);
let worker_concurrency = env_u32("WORKER_CONCURRENCY")
.filter(|value| *value > 0)
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|v| v.get() as u32)
.unwrap_or(4)
});
let image_processing_concurrency = env_u32("IMAGE_PROCESSING_CONCURRENCY")
.filter(|value| *value > 0)
.unwrap_or_else(|| {
std::thread::available_parallelism()
.map(|v| v.get() as u32)
.unwrap_or(4)
});
let zip_build_concurrency = env_u32("ZIP_BUILD_CONCURRENCY")
.filter(|value| *value > 0)
.unwrap_or(2);
let zip_max_entries = env_u32("ZIP_MAX_ENTRIES")
.filter(|value| *value > 0)
.unwrap_or(200);
let zip_max_uncompressed_bytes = env_u64("ZIP_MAX_UNCOMPRESSED_BYTES")
.filter(|value| *value > 0)
.unwrap_or(2 * 1024 * 1024 * 1024);
let jwt_secret = env_string("JWT_SECRET")
.ok_or_else(|| AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 JWT_SECRET"))?;
let jwt_expiry_hours = env_i64("JWT_EXPIRY_HOURS").unwrap_or(168);
let api_key_pepper = env_string("API_KEY_PEPPER").ok_or_else(|| {
AppError::new(ErrorCode::InvalidRequest, "缺少环境变量 API_KEY_PEPPER")
})?;
let billing_provider =
env_string("BILLING_PROVIDER").unwrap_or_else(|| "stripe".to_string());
if !billing_provider.eq_ignore_ascii_case("stripe") {
return Err(AppError::new(
ErrorCode::InvalidRequest,
"BILLING_PROVIDER 目前仅支持 stripe",
));
}
let stripe_secret_key = env_string("STRIPE_SECRET_KEY");
let stripe_webhook_secret = env_string("STRIPE_WEBHOOK_SECRET");
let stripe_api_base_url = env_string("STRIPE_API_BASE_URL")
.unwrap_or_else(|| "https://api.stripe.com".to_string())
.trim_end_matches('/')
.to_string();
let storage_path = env_string("STORAGE_PATH").unwrap_or_else(|| "./uploads".to_string());
let allow_anonymous_upload = env_bool("ALLOW_ANONYMOUS_UPLOAD").unwrap_or(true);
let anon_max_file_size_mb = env_u64("ANON_MAX_FILE_SIZE_MB").unwrap_or(5);
let anon_max_files_per_batch = env_u32("ANON_MAX_FILES_PER_BATCH").unwrap_or(5);
let anon_daily_units = env_u32("ANON_DAILY_UNITS").unwrap_or(10);
let anon_retention_hours = env_u64("ANON_RETENTION_HOURS").unwrap_or(24);
let max_image_pixels = env_u64("MAX_IMAGE_PIXELS").unwrap_or(40_000_000);
let idempotency_ttl_hours = env_u64("IDEMPOTENCY_TTL_HOURS").unwrap_or(24);
let mail_enabled = env_bool("MAIL_ENABLED").unwrap_or(false);
let mail_log_links_when_disabled =
env_bool("MAIL_LOG_LINKS_WHEN_DISABLED").unwrap_or(false);
let mail_provider = env_string("MAIL_PROVIDER").unwrap_or_else(|| "qq".to_string());
let mail_from =
env_string("MAIL_FROM").unwrap_or_else(|| "noreply@example.com".to_string());
let mail_password = env_string("MAIL_PASSWORD").unwrap_or_default();
let mail_from_name =
env_string("MAIL_FROM_NAME").unwrap_or_else(|| "ImageForge".to_string());
let mail_smtp_host = env_string("MAIL_SMTP_HOST");
let mail_smtp_port = env_u16("MAIL_SMTP_PORT");
let mail_smtp_encryption = env_string("MAIL_SMTP_ENCRYPTION");
Ok(Self {
role,
host,
port,
public_base_url,
database_url,
database_max_connections,
redis_url,
worker_task_concurrency,
worker_concurrency,
image_processing_concurrency,
zip_build_concurrency,
zip_max_entries,
zip_max_uncompressed_bytes,
jwt_secret,
jwt_expiry_hours,
api_key_pepper,
stripe_secret_key,
stripe_webhook_secret,
stripe_api_base_url,
storage_path,
allow_anonymous_upload,
anon_max_file_size_mb,
anon_max_files_per_batch,
anon_daily_units,
anon_retention_hours,
max_image_pixels,
idempotency_ttl_hours,
mail_enabled,
mail_log_links_when_disabled,
mail_provider,
mail_from,
mail_password,
mail_from_name,
mail_smtp_host,
mail_smtp_port,
mail_smtp_encryption,
})
}
}
fn env_string(key: &str) -> Option<String> {
std::env::var(key)
.ok()
.filter(|value| !value.trim().is_empty())
}
fn env_u16(key: &str) -> Option<u16> {
env_string(key).and_then(|v| v.parse::<u16>().ok())
}
fn env_u32(key: &str) -> Option<u32> {
env_string(key).and_then(|v| v.parse::<u32>().ok())
}
fn env_i64(key: &str) -> Option<i64> {
env_string(key).and_then(|v| v.parse::<i64>().ok())
}
fn env_u64(key: &str) -> Option<u64> {
env_string(key).and_then(|v| v.parse::<u64>().ok())
}
fn env_bool(key: &str) -> Option<bool> {
env_string(key).and_then(|v| match v.trim().to_ascii_lowercase().as_str() {
"1" | "true" | "yes" | "y" | "on" => Some(true),
"0" | "false" | "no" | "n" | "off" => Some(false),
_ => None,
})
}
pub fn trust_proxy_headers() -> bool {
*TRUST_PROXY_HEADERS.get_or_init(|| env_bool("TRUST_PROXY_HEADERS").unwrap_or(false))
}