use crate::AppConfig; impl AppConfig { pub fn storage_backend(&self) -> anyhow::Result { let backend = self .env .get("APP_STORAGE_BACKEND") .or_else(|| self.env.get("STORAGE_BACKEND")) .map(|value| value.trim().to_ascii_lowercase()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "s3".to_string()); Ok(backend) } pub fn storage_path(&self) -> String { self.env .get("STORAGE_PATH") .cloned() .unwrap_or_else(|| "/data".to_string()) } pub fn storage_public_url(&self) -> String { self.env .get("APP_STORAGE_PUBLIC_URL") .or_else(|| self.env.get("STORAGE_PUBLIC_URL")) .cloned() .unwrap_or_else(|| "/files".to_string()) } pub fn storage_public_url_base(&self) -> Option { self.env .get("APP_STORAGE_PUBLIC_URL") .or_else(|| self.env.get("STORAGE_PUBLIC_URL")) .map(|value| value.trim().trim_end_matches('/').to_string()) .filter(|value| !value.is_empty()) } pub fn storage_s3_bucket(&self) -> anyhow::Result { self.env .get("APP_STORAGE_S3_BUCKET") .or_else(|| self.env.get("AWS_S3_BUCKET")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .ok_or_else(|| anyhow::anyhow!("APP_STORAGE_S3_BUCKET is required")) } pub fn storage_s3_region(&self) -> String { self.env .get("APP_STORAGE_S3_REGION") .or_else(|| self.env.get("AWS_REGION")) .or_else(|| self.env.get("AWS_DEFAULT_REGION")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) .unwrap_or_else(|| "us-east-1".to_string()) } pub fn storage_s3_endpoint_url(&self) -> Option { self.env .get("APP_STORAGE_S3_ENDPOINT_URL") .or_else(|| self.env.get("AWS_ENDPOINT_URL_S3")) .or_else(|| self.env.get("AWS_ENDPOINT_URL")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) } pub fn storage_s3_access_key_id(&self) -> Option { self.env .get("APP_STORAGE_S3_ACCESS_KEY_ID") .or_else(|| self.env.get("AWS_ACCESS_KEY_ID")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) } pub fn storage_s3_secret_access_key(&self) -> Option { self.env .get("APP_STORAGE_S3_SECRET_ACCESS_KEY") .or_else(|| self.env.get("AWS_SECRET_ACCESS_KEY")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) } pub fn storage_s3_session_token(&self) -> Option { self.env .get("APP_STORAGE_S3_SESSION_TOKEN") .or_else(|| self.env.get("AWS_SESSION_TOKEN")) .map(|value| value.trim().to_string()) .filter(|value| !value.is_empty()) } pub fn storage_s3_force_path_style(&self) -> anyhow::Result { self.parse_env("APP_STORAGE_S3_FORCE_PATH_STYLE", false) } pub fn storage_presigned_url_ttl( &self, ) -> anyhow::Result { self.parse_duration_secs("APP_STORAGE_PRESIGNED_URL_TTL_SECONDS", 900) } pub fn storage_max_file_size(&self) -> usize { self.env .get("APP_STORAGE_MAX_FILE_SIZE") .or_else(|| self.env.get("STORAGE_MAX_FILE_SIZE")) .and_then(|s| s.parse::().ok()) .unwrap_or(10 * 1024 * 1024) // 10MB default } pub fn vapid_public_key(&self) -> Option { self.env.get("VAPID_PUBLIC_KEY").cloned() } pub fn vapid_private_key(&self) -> Option { self.env.get("VAPID_PRIVATE_KEY").cloned() } pub fn vapid_sender_email(&self) -> String { self.env .get("VAPID_SENDER_EMAIL") .cloned() .unwrap_or_else(|| "mailto:admin@example.com".to_string()) } }