1pub const DEFAULT_RETENTION_DAYS: i32 = 30;
5pub const DEFAULT_AUTO_PURGE_ENABLED: bool = false;
7#[cfg(feature = "server")]
9pub const MIN_RETENTION_DAYS: i32 = 1;
10#[cfg(feature = "server")]
12pub const MAX_RETENTION_DAYS: i32 = 365;
13
14#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
16pub struct TrashSettings {
17 pub auto_purge_enabled: bool,
19 pub retention_days: i32,
21}
22
23impl Default for TrashSettings {
24 fn default() -> Self {
25 Self {
26 auto_purge_enabled: DEFAULT_AUTO_PURGE_ENABLED,
27 retention_days: DEFAULT_RETENTION_DAYS,
28 }
29 }
30}
31
32impl TrashSettings {
33 #[cfg(feature = "server")]
35 pub fn clamp_retention(days: i32) -> i32 {
36 days.clamp(MIN_RETENTION_DAYS, MAX_RETENTION_DAYS)
37 }
38}
39
40pub const DEFAULT_BACKUP_AUTO_ENABLED: bool = false;
46pub const DEFAULT_BACKUP_TIME_UTC: &str = "04:00";
48pub const DEFAULT_BACKUP_RETENTION_COUNT: i32 = 30;
50pub const DEFAULT_BACKUP_INCLUDE_UPLOADS: bool = true;
52#[cfg(feature = "server")]
54pub const MIN_BACKUP_RETENTION_COUNT: i32 = 1;
55#[cfg(feature = "server")]
57pub const MAX_BACKUP_RETENTION_COUNT: i32 = 365;
58
59#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
61pub struct BackupSettings {
62 pub auto_enabled: bool,
64 pub time_utc: String,
66 pub retention_count: i32,
68 pub include_uploads: bool,
70}
71
72impl Default for BackupSettings {
73 fn default() -> Self {
74 Self {
75 auto_enabled: DEFAULT_BACKUP_AUTO_ENABLED,
76 time_utc: DEFAULT_BACKUP_TIME_UTC.to_string(),
77 retention_count: DEFAULT_BACKUP_RETENTION_COUNT,
78 include_uploads: DEFAULT_BACKUP_INCLUDE_UPLOADS,
79 }
80 }
81}
82
83impl BackupSettings {
84 #[cfg(feature = "server")]
86 pub fn clamp_retention(count: i32) -> i32 {
87 count.clamp(MIN_BACKUP_RETENTION_COUNT, MAX_BACKUP_RETENTION_COUNT)
88 }
89
90 #[cfg(feature = "server")]
92 pub fn is_valid_time_utc(s: &str) -> bool {
93 parse_hhmm(s).is_some()
94 }
95
96 #[cfg(feature = "server")]
99 pub fn normalize_time_utc(s: &str) -> String {
100 match parse_hhmm(s) {
101 Some((h, m)) => format!("{h:02}:{m:02}"),
102 None => DEFAULT_BACKUP_TIME_UTC.to_string(),
103 }
104 }
105
106 #[cfg(feature = "server")]
108 pub fn next_run_after(
109 &self,
110 now: chrono::DateTime<chrono::Utc>,
111 ) -> Option<chrono::DateTime<chrono::Utc>> {
112 let (h, m) = parse_hhmm(&self.time_utc)?;
113 let today = now.date_naive();
114 let today_at = today.and_hms_opt(h as u32, m as u32, 0)?.and_utc();
115 if today_at > now {
116 Some(today_at)
117 } else {
118 Some(
119 today
120 .succ_opt()?
121 .and_hms_opt(h as u32, m as u32, 0)?
122 .and_utc(),
123 )
124 }
125 }
126}
127
128#[cfg(feature = "server")]
130fn parse_hhmm(s: &str) -> Option<(u8, u8)> {
131 let mut parts = s.trim().split(':');
132 let h: u8 = parts.next()?.trim().parse().ok()?;
133 let m: u8 = parts.next()?.trim().parse().ok()?;
134 if parts.next().is_some() || h > 23 || m > 59 {
135 return None;
136 }
137 Some((h, m))
138}
139
140#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
142pub struct LastBackupRun {
143 pub at: String,
145 pub ok: bool,
147 pub file: Option<String>,
149 pub error: Option<String>,
151}
152
153#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
155pub struct BackupSettingsView {
156 pub settings: BackupSettings,
157 pub last_run: Option<LastBackupRun>,
159 pub next_run_at: Option<String>,
161}
162
163pub const DEFAULT_UPLOAD_CONCURRENCY: i32 = 3;
170#[cfg(feature = "server")]
172pub const MIN_UPLOAD_CONCURRENCY: i32 = 1;
173#[cfg(feature = "server")]
175pub const MAX_UPLOAD_CONCURRENCY: i32 = 8;
176
177#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
179pub struct UploadSettings {
180 pub concurrency: i32,
182}
183
184impl Default for UploadSettings {
185 fn default() -> Self {
186 Self {
187 concurrency: DEFAULT_UPLOAD_CONCURRENCY,
188 }
189 }
190}
191
192impl UploadSettings {
193 #[cfg(feature = "server")]
195 pub fn clamp_concurrency(n: i32) -> i32 {
196 n.clamp(MIN_UPLOAD_CONCURRENCY, MAX_UPLOAD_CONCURRENCY)
197 }
198}
199
200pub const DEFAULT_SITE_GITHUB_URL: &str = "";
202#[cfg(any(feature = "server", test))]
203pub const MAX_SITE_GITHUB_URL_LEN: usize = 500;
205
206#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
210pub struct SiteSettings {
211 pub github_url: String,
213}
214
215impl Default for SiteSettings {
216 fn default() -> Self {
217 Self {
218 github_url: DEFAULT_SITE_GITHUB_URL.to_string(),
219 }
220 }
221}
222
223impl SiteSettings {
224 #[cfg(any(feature = "server", test))]
225 pub fn normalize_github_url(url: &str) -> String {
232 let trimmed = url.trim();
233 if trimmed.is_empty() {
234 return String::new();
235 }
236 let limited: String = trimmed.chars().take(MAX_SITE_GITHUB_URL_LEN).collect();
237 if limited.starts_with("http://") || limited.starts_with("https://") {
238 limited
239 } else {
240 format!("https://{limited}")
241 }
242 }
243}
244
245pub const DEFAULT_APP_BASE_URL: &str = "";
258pub const DEFAULT_COOKIE_SECURE: bool = false;
260pub const DEFAULT_TRUSTED_PROXY_COUNT: u32 = 0;
262pub const DEFAULT_MAX_SESSIONS_PER_USER: u32 = 5;
264
265#[cfg(feature = "server")]
268pub const MAX_TRUSTED_PROXY_COUNT: u32 = 10;
269#[cfg(feature = "server")]
271pub const MIN_MAX_SESSIONS_PER_USER: u32 = 1;
272#[cfg(feature = "server")]
274pub const MAX_MAX_SESSIONS_PER_USER: u32 = 100;
275
276#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
280pub struct SecuritySettings {
281 pub app_base_url: String,
284 pub cookie_secure: bool,
286 pub trusted_proxy_count: u32,
288 pub max_sessions_per_user: u32,
290}
291
292impl Default for SecuritySettings {
293 fn default() -> Self {
294 Self {
295 app_base_url: DEFAULT_APP_BASE_URL.to_string(),
296 cookie_secure: DEFAULT_COOKIE_SECURE,
297 trusted_proxy_count: DEFAULT_TRUSTED_PROXY_COUNT,
298 max_sessions_per_user: DEFAULT_MAX_SESSIONS_PER_USER,
299 }
300 }
301}
302
303impl SecuritySettings {
304 #[cfg(feature = "server")]
306 pub fn clamp_trusted_proxy_count(n: u32) -> u32 {
307 n.min(MAX_TRUSTED_PROXY_COUNT)
308 }
309
310 #[cfg(feature = "server")]
312 pub fn clamp_max_sessions(n: u32) -> u32 {
313 n.clamp(MIN_MAX_SESSIONS_PER_USER, MAX_MAX_SESSIONS_PER_USER)
314 }
315
316 #[cfg(any(feature = "server", test))]
317 pub fn normalize_app_base_url(url: &str) -> String {
320 url.trim().to_string()
321 }
322}
323
324pub const DEFAULT_IMAGE_DISK_CACHE_MAX_MB: u32 = 1024;
333pub const DEFAULT_IMAGE_DISK_CACHE_MAX_AGE_HOURS: u32 = 168;
335#[cfg(feature = "server")]
337pub const MIN_IMAGE_DISK_CACHE_MAX_MB: u32 = 1;
338#[cfg(feature = "server")]
340pub const MIN_IMAGE_DISK_CACHE_MAX_AGE_HOURS: u32 = 1;
341
342#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
344pub struct ImageCacheSettings {
345 pub disk_cache_max_mb: u32,
347 pub disk_cache_max_age_hours: u32,
349}
350
351impl Default for ImageCacheSettings {
352 fn default() -> Self {
353 Self {
354 disk_cache_max_mb: DEFAULT_IMAGE_DISK_CACHE_MAX_MB,
355 disk_cache_max_age_hours: DEFAULT_IMAGE_DISK_CACHE_MAX_AGE_HOURS,
356 }
357 }
358}
359
360impl ImageCacheSettings {
361 #[cfg(feature = "server")]
363 pub fn clamp_max_mb(n: u32) -> u32 {
364 n.max(MIN_IMAGE_DISK_CACHE_MAX_MB)
365 }
366
367 #[cfg(feature = "server")]
369 pub fn clamp_max_age_hours(n: u32) -> u32 {
370 n.max(MIN_IMAGE_DISK_CACHE_MAX_AGE_HOURS)
371 }
372}
373
374pub const DEFAULT_ASSET_ORPHAN_PURGE_ENABLED: bool = true;
386pub const DEFAULT_ASSET_ORPHAN_RETENTION_DAYS: i32 = 7;
389#[cfg(feature = "server")]
391pub const MIN_ASSET_ORPHAN_RETENTION_DAYS: i32 = 1;
392#[cfg(feature = "server")]
394pub const MAX_ASSET_ORPHAN_RETENTION_DAYS: i32 = 365;
395
396#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
398pub struct AssetPurgeSettings {
399 pub auto_purge_enabled: bool,
401 pub retention_days: i32,
403}
404
405impl Default for AssetPurgeSettings {
406 fn default() -> Self {
407 Self {
408 auto_purge_enabled: DEFAULT_ASSET_ORPHAN_PURGE_ENABLED,
409 retention_days: DEFAULT_ASSET_ORPHAN_RETENTION_DAYS,
410 }
411 }
412}
413
414impl AssetPurgeSettings {
415 #[cfg(feature = "server")]
417 pub fn clamp_retention(days: i32) -> i32 {
418 days.clamp(
419 MIN_ASSET_ORPHAN_RETENTION_DAYS,
420 MAX_ASSET_ORPHAN_RETENTION_DAYS,
421 )
422 }
423}
424
425pub const DEFAULT_RATE_LIMIT_STRICT_PER_SEC: u32 = 1;
438pub const DEFAULT_RATE_LIMIT_STRICT_BURST: u32 = 5;
439pub const DEFAULT_RATE_LIMIT_UPLOAD_PER_SEC: u32 = 2;
441pub const DEFAULT_RATE_LIMIT_UPLOAD_BURST: u32 = 15;
442pub const DEFAULT_RATE_LIMIT_IMAGE_PER_SEC: u32 = 10;
444pub const DEFAULT_RATE_LIMIT_IMAGE_BURST: u32 = 50;
445pub const DEFAULT_RATE_LIMIT_COMMENT_PER_SEC: u32 = 1;
447pub const DEFAULT_RATE_LIMIT_COMMENT_BURST: u32 = 5;
448pub const DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_PER_SEC: u32 = 1;
450pub const DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_BURST: u32 = 5;
451pub const DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_DAILY: u32 = 30;
454pub const DEFAULT_RATE_LIMIT_CODE_EXEC_PER_SEC: u32 = 1;
456pub const DEFAULT_RATE_LIMIT_CODE_EXEC_BURST: u32 = 3;
457pub const DEFAULT_RATE_LIMIT_CODE_EXEC_DAILY: u32 = 50;
459pub const DEFAULT_RATE_LIMIT_UNKNOWN_PER_SEC: u32 = 30;
461pub const DEFAULT_RATE_LIMIT_UNKNOWN_BURST: u32 = 100;
462pub const DEFAULT_RATE_LIMIT_GC_INTERVAL_SECS: u32 = 300;
464
465#[cfg(feature = "server")]
467pub const MIN_RATE_LIMIT_PER_SEC: u32 = 1;
468#[cfg(feature = "server")]
470pub const MIN_RATE_LIMIT_BURST: u32 = 1;
471#[cfg(feature = "server")]
473pub const MIN_RATE_LIMIT_DAILY: u32 = 1;
474#[cfg(feature = "server")]
476pub const MIN_RATE_LIMIT_GC_INTERVAL_SECS: u32 = 1;
477
478#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
483pub struct RateLimitSettings {
484 pub strict_per_sec: u32,
486 pub strict_burst: u32,
488 pub upload_per_sec: u32,
490 pub upload_burst: u32,
492 pub image_per_sec: u32,
494 pub image_burst: u32,
496 pub comment_per_sec: u32,
498 pub comment_burst: u32,
500 pub comment_upload_per_sec: u32,
502 pub comment_upload_burst: u32,
504 pub comment_upload_daily: u32,
506 pub code_exec_per_sec: u32,
508 pub code_exec_burst: u32,
510 pub code_exec_daily: u32,
512 pub unknown_per_sec: u32,
514 pub unknown_burst: u32,
516 pub gc_interval_secs: u32,
518}
519
520impl Default for RateLimitSettings {
521 fn default() -> Self {
522 Self {
523 strict_per_sec: DEFAULT_RATE_LIMIT_STRICT_PER_SEC,
524 strict_burst: DEFAULT_RATE_LIMIT_STRICT_BURST,
525 upload_per_sec: DEFAULT_RATE_LIMIT_UPLOAD_PER_SEC,
526 upload_burst: DEFAULT_RATE_LIMIT_UPLOAD_BURST,
527 image_per_sec: DEFAULT_RATE_LIMIT_IMAGE_PER_SEC,
528 image_burst: DEFAULT_RATE_LIMIT_IMAGE_BURST,
529 comment_per_sec: DEFAULT_RATE_LIMIT_COMMENT_PER_SEC,
530 comment_burst: DEFAULT_RATE_LIMIT_COMMENT_BURST,
531 comment_upload_per_sec: DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_PER_SEC,
532 comment_upload_burst: DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_BURST,
533 comment_upload_daily: DEFAULT_RATE_LIMIT_COMMENT_UPLOAD_DAILY,
534 code_exec_per_sec: DEFAULT_RATE_LIMIT_CODE_EXEC_PER_SEC,
535 code_exec_burst: DEFAULT_RATE_LIMIT_CODE_EXEC_BURST,
536 code_exec_daily: DEFAULT_RATE_LIMIT_CODE_EXEC_DAILY,
537 unknown_per_sec: DEFAULT_RATE_LIMIT_UNKNOWN_PER_SEC,
538 unknown_burst: DEFAULT_RATE_LIMIT_UNKNOWN_BURST,
539 gc_interval_secs: DEFAULT_RATE_LIMIT_GC_INTERVAL_SECS,
540 }
541 }
542}
543
544impl RateLimitSettings {
545 #[cfg(feature = "server")]
547 pub fn clamp_per_sec(n: u32) -> u32 {
548 n.max(MIN_RATE_LIMIT_PER_SEC)
549 }
550
551 #[cfg(feature = "server")]
553 pub fn clamp_burst(n: u32) -> u32 {
554 n.max(MIN_RATE_LIMIT_BURST)
555 }
556
557 #[cfg(feature = "server")]
559 pub fn clamp_daily(n: u32) -> u32 {
560 n.max(MIN_RATE_LIMIT_DAILY)
561 }
562
563 #[cfg(feature = "server")]
565 pub fn clamp_gc_interval(n: u32) -> u32 {
566 n.max(MIN_RATE_LIMIT_GC_INTERVAL_SECS)
567 }
568}
569
570pub const DEFAULT_WEBP_QUALITY: f32 = 85.0;
580pub const DEFAULT_WEBP_METHOD: u32 = 2;
582
583#[cfg(feature = "server")]
585pub const MIN_WEBP_QUALITY: f32 = 0.0;
586#[cfg(feature = "server")]
588pub const MAX_WEBP_QUALITY: f32 = 100.0;
589#[cfg(feature = "server")]
591pub const MAX_WEBP_METHOD: u32 = 6;
592
593#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
597pub struct WebpSettings {
598 pub quality: f32,
600 pub method: u32,
602}
603
604impl Default for WebpSettings {
605 fn default() -> Self {
606 Self {
607 quality: DEFAULT_WEBP_QUALITY,
608 method: DEFAULT_WEBP_METHOD,
609 }
610 }
611}
612
613impl WebpSettings {
614 #[cfg(feature = "server")]
616 pub fn clamp_quality(q: f32) -> f32 {
617 q.clamp(MIN_WEBP_QUALITY, MAX_WEBP_QUALITY)
618 }
619
620 #[cfg(feature = "server")]
622 pub fn clamp_method(m: u32) -> u32 {
623 m.min(MAX_WEBP_METHOD)
624 }
625}
626
627pub const DEFAULT_IMAGE_MAX_DIMENSION: u32 = 8192;
637pub const DEFAULT_IMAGE_MAX_PIXELS: u64 = 50_000_000;
639pub const DEFAULT_IMAGE_DIMENSIONS_CACHE_TTL_SECS: u64 = 86400;
641
642#[cfg(feature = "server")]
644pub const MIN_IMAGE_MAX_DIMENSION: u32 = 512;
645#[cfg(feature = "server")]
647pub const MIN_IMAGE_MAX_PIXELS: u64 = 1_000_000;
648#[cfg(feature = "server")]
650pub const MIN_IMAGE_DIMENSIONS_CACHE_TTL_SECS: u64 = 1;
651
652#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
656pub struct ImageLimitSettings {
657 pub max_dimension: u32,
659 pub max_pixels: u64,
662 pub dimensions_cache_ttl_secs: u64,
664}
665
666impl Default for ImageLimitSettings {
667 fn default() -> Self {
668 Self {
669 max_dimension: DEFAULT_IMAGE_MAX_DIMENSION,
670 max_pixels: DEFAULT_IMAGE_MAX_PIXELS,
671 dimensions_cache_ttl_secs: DEFAULT_IMAGE_DIMENSIONS_CACHE_TTL_SECS,
672 }
673 }
674}
675
676impl ImageLimitSettings {
677 #[cfg(feature = "server")]
679 pub fn clamp_max_dimension(n: u32) -> u32 {
680 n.max(MIN_IMAGE_MAX_DIMENSION)
681 }
682
683 #[cfg(feature = "server")]
685 pub fn clamp_max_pixels(n: u64) -> u64 {
686 n.max(MIN_IMAGE_MAX_PIXELS)
687 }
688
689 #[cfg(feature = "server")]
691 pub fn clamp_dimensions_cache_ttl_secs(n: u64) -> u64 {
692 n.max(MIN_IMAGE_DIMENSIONS_CACHE_TTL_SECS)
693 }
694}
695
696pub const DEFAULT_RUNNER_ALLOW_NETWORK: bool = false;
707pub const DEFAULT_RUNNER_MAX_CONCURRENT: u32 = 4;
709pub const DEFAULT_RUNNER_MAX_CPU_CORES: f64 = 2.0;
711pub const DEFAULT_RUNNER_MAX_MEMORY_MB: u32 = 1024;
713pub const DEFAULT_RUNNER_MAX_TIMEOUT_SECS: u32 = 30;
715pub const DEFAULT_RUNNER_MAX_OUTPUT_BYTES: u64 = 1_048_576;
717pub const DEFAULT_RUNNER_MAX_SOURCE_BYTES: u64 = 65_536;
719pub const DEFAULT_RUNNER_QUEUE_TIMEOUT_SECS: u32 = 30;
721pub const DEFAULT_RUNNER_TASK_TTL_SECS: u32 = 300;
723
724#[cfg(feature = "server")]
726pub const MIN_RUNNER_MAX_CONCURRENT: u32 = 1;
727#[cfg(feature = "server")]
729pub const MIN_RUNNER_MAX_CPU_CORES: f64 = 0.1;
730#[cfg(feature = "server")]
732pub const MIN_RUNNER_MAX_MEMORY_MB: u32 = 16;
733#[cfg(feature = "server")]
735pub const MIN_RUNNER_MAX_TIMEOUT_SECS: u32 = 1;
736#[cfg(feature = "server")]
738pub const MAX_RUNNER_MAX_CONCURRENT: u32 = 64;
739#[cfg(feature = "server")]
741pub const MAX_RUNNER_MAX_CPU_CORES: f64 = 64.0;
742#[cfg(feature = "server")]
744pub const MAX_RUNNER_MAX_MEMORY_MB: u32 = 65_536;
745
746#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
750pub struct RunnerSettings {
751 pub allow_network: bool,
753 pub max_concurrent: u32,
755 pub max_cpu_cores: f64,
757 pub max_memory_mb: u32,
759 pub max_timeout_secs: u32,
761 pub max_output_bytes: u64,
763 pub max_source_bytes: u64,
765 pub queue_timeout_secs: u32,
767 pub task_ttl_secs: u32,
769 pub languages: Option<String>,
771}
772
773impl Default for RunnerSettings {
774 fn default() -> Self {
775 Self {
776 allow_network: DEFAULT_RUNNER_ALLOW_NETWORK,
777 max_concurrent: DEFAULT_RUNNER_MAX_CONCURRENT,
778 max_cpu_cores: DEFAULT_RUNNER_MAX_CPU_CORES,
779 max_memory_mb: DEFAULT_RUNNER_MAX_MEMORY_MB,
780 max_timeout_secs: DEFAULT_RUNNER_MAX_TIMEOUT_SECS,
781 max_output_bytes: DEFAULT_RUNNER_MAX_OUTPUT_BYTES,
782 max_source_bytes: DEFAULT_RUNNER_MAX_SOURCE_BYTES,
783 queue_timeout_secs: DEFAULT_RUNNER_QUEUE_TIMEOUT_SECS,
784 task_ttl_secs: DEFAULT_RUNNER_TASK_TTL_SECS,
785 languages: None,
786 }
787 }
788}
789
790impl RunnerSettings {
791 #[cfg(feature = "server")]
793 pub fn clamp_max_concurrent(n: u32) -> u32 {
794 n.clamp(MIN_RUNNER_MAX_CONCURRENT, MAX_RUNNER_MAX_CONCURRENT)
795 }
796
797 #[cfg(feature = "server")]
799 pub fn clamp_max_cpu_cores(n: f64) -> f64 {
800 if n.is_nan() {
801 return DEFAULT_RUNNER_MAX_CPU_CORES;
802 }
803 n.clamp(MIN_RUNNER_MAX_CPU_CORES, MAX_RUNNER_MAX_CPU_CORES)
804 }
805
806 #[cfg(feature = "server")]
808 pub fn clamp_max_memory_mb(n: u32) -> u32 {
809 n.clamp(MIN_RUNNER_MAX_MEMORY_MB, MAX_RUNNER_MAX_MEMORY_MB)
810 }
811
812 #[cfg(feature = "server")]
814 pub fn clamp_max_timeout_secs(n: u32) -> u32 {
815 n.max(MIN_RUNNER_MAX_TIMEOUT_SECS)
816 }
817
818 #[cfg(feature = "server")]
820 pub fn clamp_max_output_bytes(n: u64) -> u64 {
821 n.max(1)
822 }
823
824 #[cfg(feature = "server")]
826 pub fn clamp_max_source_bytes(n: u64) -> u64 {
827 n.max(1)
828 }
829
830 #[cfg(feature = "server")]
832 pub fn clamp_queue_timeout_secs(n: u32) -> u32 {
833 n.max(MIN_RUNNER_MAX_TIMEOUT_SECS)
834 }
835
836 #[cfg(feature = "server")]
838 pub fn clamp_task_ttl_secs(n: u32) -> u32 {
839 n.max(1)
840 }
841
842 #[cfg(feature = "server")]
844 pub fn normalize_languages(s: &str) -> Option<String> {
845 let parts: Vec<String> = s
846 .split(',')
847 .map(|t| t.trim().to_lowercase())
848 .filter(|t| !t.is_empty())
849 .collect();
850 if parts.is_empty() {
851 None
852 } else {
853 Some(parts.join(","))
854 }
855 }
856}
857
858#[derive(Debug, Clone, PartialEq, serde::Serialize, serde::Deserialize)]
863pub struct SystemInfo {
864 pub database_url_masked: String,
866 pub rust_log: String,
868 pub db_pool_size: u32,
870 pub statement_timeout_secs: u64,
872 pub ssr_cache_secs: u64,
874 pub compression_algorithms: String,
876 pub expose_version_headers: bool,
878 pub docker_socket_path: String,
880 pub mcp_token_enc_key_set: bool,
882 pub migrate_startup_timeout_secs: u64,
884 pub sysinfo_sample_secs: f64,
886}
887
888#[cfg(test)]
889mod tests {
890 use super::*;
891
892 #[test]
893 fn default_is_disabled_30_days() {
894 let s = TrashSettings::default();
895 assert!(!s.auto_purge_enabled);
896 assert_eq!(s.retention_days, 30);
897 }
898
899 #[test]
900 #[cfg(feature = "server")]
901 fn clamp_retention_keeps_valid() {
902 assert_eq!(TrashSettings::clamp_retention(7), 7);
903 assert_eq!(TrashSettings::clamp_retention(30), 30);
904 }
905
906 #[test]
907 #[cfg(feature = "server")]
908 fn clamp_retention_clamps_below_min() {
909 assert_eq!(TrashSettings::clamp_retention(0), MIN_RETENTION_DAYS);
910 assert_eq!(TrashSettings::clamp_retention(-5), MIN_RETENTION_DAYS);
911 }
912
913 #[test]
914 #[cfg(feature = "server")]
915 fn clamp_retention_clamps_above_max() {
916 assert_eq!(TrashSettings::clamp_retention(366), MAX_RETENTION_DAYS);
917 assert_eq!(TrashSettings::clamp_retention(i32::MAX), MAX_RETENTION_DAYS);
918 }
919
920 #[test]
921 #[cfg(feature = "server")]
922 fn clamp_retention_boundary() {
923 assert_eq!(
924 TrashSettings::clamp_retention(MIN_RETENTION_DAYS),
925 MIN_RETENTION_DAYS
926 );
927 assert_eq!(
928 TrashSettings::clamp_retention(MAX_RETENTION_DAYS),
929 MAX_RETENTION_DAYS
930 );
931 }
932
933 #[test]
934 fn asset_purge_default_is_enabled_7_days() {
935 let s = AssetPurgeSettings::default();
936 assert!(s.auto_purge_enabled);
937 assert_eq!(s.retention_days, 7);
938 }
939
940 #[test]
941 #[cfg(feature = "server")]
942 fn asset_purge_clamp_retention() {
943 assert_eq!(
944 AssetPurgeSettings::clamp_retention(0),
945 MIN_ASSET_ORPHAN_RETENTION_DAYS
946 );
947 assert_eq!(AssetPurgeSettings::clamp_retention(7), 7);
948 assert_eq!(
949 AssetPurgeSettings::clamp_retention(366),
950 MAX_ASSET_ORPHAN_RETENTION_DAYS
951 );
952 }
953
954 #[test]
955 fn site_settings_default_empty() {
956 let s = SiteSettings::default();
957 assert_eq!(s.github_url, "");
958 }
959
960 #[test]
961 fn normalize_github_url_empty() {
962 assert_eq!(SiteSettings::normalize_github_url(""), "");
963 assert_eq!(SiteSettings::normalize_github_url(" "), "");
964 }
965
966 #[test]
967 fn normalize_github_url_prepends_https() {
968 assert_eq!(
969 SiteSettings::normalize_github_url("github.com/DefectingCat/yggdrasil"),
970 "https://github.com/DefectingCat/yggdrasil"
971 );
972 }
973
974 #[test]
975 fn normalize_github_url_keeps_scheme() {
976 assert_eq!(
977 SiteSettings::normalize_github_url("https://github.com/foo"),
978 "https://github.com/foo"
979 );
980 assert_eq!(
981 SiteSettings::normalize_github_url("http://example.com"),
982 "http://example.com"
983 );
984 }
985
986 #[test]
987 fn normalize_github_url_neutralizes_dangerous_scheme() {
988 assert_eq!(
990 SiteSettings::normalize_github_url("javascript:alert(1)"),
991 "https://javascript:alert(1)"
992 );
993 }
994
995 #[test]
996 fn normalize_github_url_trims_and_truncates() {
997 assert_eq!(
998 SiteSettings::normalize_github_url(" github.com/x "),
999 "https://github.com/x"
1000 );
1001 let long = "a".repeat(MAX_SITE_GITHUB_URL_LEN + 50);
1002 let normalized = SiteSettings::normalize_github_url(&long);
1003 assert_eq!(normalized.len(), "https://".len() + MAX_SITE_GITHUB_URL_LEN);
1005 }
1006
1007 #[test]
1010 fn backup_settings_default() {
1011 let s = BackupSettings::default();
1012 assert!(!s.auto_enabled);
1013 assert_eq!(s.time_utc, "04:00");
1014 assert_eq!(s.retention_count, 30);
1015 assert!(s.include_uploads);
1016 }
1017
1018 #[test]
1019 fn normalize_time_pads_single_digits() {
1020 assert_eq!(BackupSettings::normalize_time_utc("4:5"), "04:05");
1021 assert_eq!(BackupSettings::normalize_time_utc(" 23:59 "), "23:59");
1022 assert_eq!(BackupSettings::normalize_time_utc("0:00"), "00:00");
1023 }
1024
1025 #[test]
1026 fn normalize_time_rejects_garbage() {
1027 for bad in ["", "abc", "24:00", "12:60", "1:2:3", "-1:30", "12:", ":30"] {
1028 assert_eq!(
1029 BackupSettings::normalize_time_utc(bad),
1030 DEFAULT_BACKUP_TIME_UTC,
1031 "非法时间应回退默认: {bad:?}"
1032 );
1033 }
1034 }
1035
1036 #[test]
1037 #[cfg(feature = "server")]
1038 fn backup_clamp_retention() {
1039 assert_eq!(
1040 BackupSettings::clamp_retention(0),
1041 MIN_BACKUP_RETENTION_COUNT
1042 );
1043 assert_eq!(
1044 BackupSettings::clamp_retention(366),
1045 MAX_BACKUP_RETENTION_COUNT
1046 );
1047 assert_eq!(BackupSettings::clamp_retention(30), 30);
1048 }
1049
1050 #[test]
1051 #[cfg(feature = "server")]
1052 fn next_run_same_day_when_future() {
1053 use chrono::{TimeZone, Utc};
1054 let s = BackupSettings {
1055 time_utc: "04:00".into(),
1056 ..Default::default()
1057 };
1058 let now = Utc.with_ymd_and_hms(2026, 8, 10, 1, 30, 0).unwrap();
1059 let next = s.next_run_after(now).expect("合法时间必有下次执行");
1060 assert_eq!(next, Utc.with_ymd_and_hms(2026, 8, 10, 4, 0, 0).unwrap());
1061 }
1062
1063 #[test]
1064 #[cfg(feature = "server")]
1065 fn next_run_rolls_to_tomorrow_when_past() {
1066 use chrono::{TimeZone, Utc};
1067 let s = BackupSettings {
1068 time_utc: "04:00".into(),
1069 ..Default::default()
1070 };
1071 for now in [
1073 Utc.with_ymd_and_hms(2026, 8, 10, 4, 0, 0).unwrap(),
1074 Utc.with_ymd_and_hms(2026, 8, 10, 23, 59, 59).unwrap(),
1075 ] {
1076 let next = s.next_run_after(now).expect("合法时间必有下次执行");
1077 assert_eq!(next, Utc.with_ymd_and_hms(2026, 8, 11, 4, 0, 0).unwrap());
1078 }
1079 }
1080
1081 #[test]
1082 #[cfg(feature = "server")]
1083 fn next_run_none_on_invalid_time() {
1084 use chrono::{TimeZone, Utc};
1085 let s = BackupSettings {
1086 time_utc: "garbage".into(),
1087 ..Default::default()
1088 };
1089 let now = Utc.with_ymd_and_hms(2026, 8, 10, 0, 0, 0).unwrap();
1090 assert!(s.next_run_after(now).is_none());
1091 }
1092
1093 #[test]
1096 fn upload_settings_default() {
1097 assert_eq!(UploadSettings::default().concurrency, 3);
1098 }
1099
1100 #[test]
1101 #[cfg(feature = "server")]
1102 fn upload_clamp_concurrency() {
1103 assert_eq!(UploadSettings::clamp_concurrency(0), MIN_UPLOAD_CONCURRENCY);
1104 assert_eq!(
1105 UploadSettings::clamp_concurrency(-1),
1106 MIN_UPLOAD_CONCURRENCY
1107 );
1108 assert_eq!(UploadSettings::clamp_concurrency(9), MAX_UPLOAD_CONCURRENCY);
1109 assert_eq!(
1110 UploadSettings::clamp_concurrency(i32::MAX),
1111 MAX_UPLOAD_CONCURRENCY
1112 );
1113 assert_eq!(UploadSettings::clamp_concurrency(5), 5);
1114 }
1115
1116 #[test]
1119 fn security_settings_default() {
1120 let s = SecuritySettings::default();
1121 assert!(s.app_base_url.is_empty());
1122 assert!(!s.cookie_secure);
1123 assert_eq!(s.trusted_proxy_count, 0);
1124 assert_eq!(s.max_sessions_per_user, 5);
1125 }
1126
1127 #[test]
1128 #[cfg(feature = "server")]
1129 fn security_clamp_trusted_proxy_count() {
1130 assert_eq!(SecuritySettings::clamp_trusted_proxy_count(0), 0);
1131 assert_eq!(SecuritySettings::clamp_trusted_proxy_count(3), 3);
1132 assert_eq!(
1133 SecuritySettings::clamp_trusted_proxy_count(99),
1134 MAX_TRUSTED_PROXY_COUNT
1135 );
1136 }
1137
1138 #[test]
1139 #[cfg(feature = "server")]
1140 fn security_clamp_max_sessions() {
1141 assert_eq!(
1142 SecuritySettings::clamp_max_sessions(0),
1143 MIN_MAX_SESSIONS_PER_USER
1144 );
1145 assert_eq!(SecuritySettings::clamp_max_sessions(5), 5);
1146 assert_eq!(
1147 SecuritySettings::clamp_max_sessions(u32::MAX),
1148 MAX_MAX_SESSIONS_PER_USER
1149 );
1150 }
1151
1152 #[test]
1153 fn security_normalize_app_base_url_trims() {
1154 assert_eq!(
1155 SecuritySettings::normalize_app_base_url(" https://x.com "),
1156 "https://x.com"
1157 );
1158 assert_eq!(SecuritySettings::normalize_app_base_url(""), "");
1159 assert_eq!(SecuritySettings::normalize_app_base_url(" "), "");
1160 }
1161
1162 #[test]
1165 fn image_cache_settings_default() {
1166 let s = ImageCacheSettings::default();
1167 assert_eq!(s.disk_cache_max_mb, 1024);
1168 assert_eq!(s.disk_cache_max_age_hours, 168);
1169 }
1170
1171 #[test]
1172 #[cfg(feature = "server")]
1173 fn image_cache_clamp_max_mb() {
1174 assert_eq!(
1175 ImageCacheSettings::clamp_max_mb(0),
1176 MIN_IMAGE_DISK_CACHE_MAX_MB
1177 );
1178 assert_eq!(ImageCacheSettings::clamp_max_mb(2048), 2048);
1179 }
1180
1181 #[test]
1182 #[cfg(feature = "server")]
1183 fn image_cache_clamp_max_age_hours() {
1184 assert_eq!(
1185 ImageCacheSettings::clamp_max_age_hours(0),
1186 MIN_IMAGE_DISK_CACHE_MAX_AGE_HOURS
1187 );
1188 assert_eq!(ImageCacheSettings::clamp_max_age_hours(720), 720);
1189 }
1190
1191 #[test]
1192 fn rate_limit_default_values() {
1193 let s = RateLimitSettings::default();
1194 assert_eq!(s.strict_per_sec, 1);
1195 assert_eq!(s.strict_burst, 5);
1196 assert_eq!(s.upload_per_sec, 2);
1197 assert_eq!(s.upload_burst, 15);
1198 assert_eq!(s.image_per_sec, 10);
1199 assert_eq!(s.image_burst, 50);
1200 assert_eq!(s.comment_per_sec, 1);
1201 assert_eq!(s.comment_burst, 5);
1202 assert_eq!(s.comment_upload_per_sec, 1);
1203 assert_eq!(s.comment_upload_burst, 5);
1204 assert_eq!(s.comment_upload_daily, 30);
1205 assert_eq!(s.code_exec_per_sec, 1);
1206 assert_eq!(s.code_exec_burst, 3);
1207 assert_eq!(s.code_exec_daily, 50);
1208 assert_eq!(s.unknown_per_sec, 30);
1209 assert_eq!(s.unknown_burst, 100);
1210 assert_eq!(s.gc_interval_secs, 300);
1211 }
1212
1213 #[test]
1214 #[cfg(feature = "server")]
1215 fn rate_limit_clamp_per_sec() {
1216 assert_eq!(RateLimitSettings::clamp_per_sec(0), MIN_RATE_LIMIT_PER_SEC);
1217 assert_eq!(RateLimitSettings::clamp_per_sec(5), 5);
1218 }
1219
1220 #[test]
1221 #[cfg(feature = "server")]
1222 fn rate_limit_clamp_burst() {
1223 assert_eq!(RateLimitSettings::clamp_burst(0), MIN_RATE_LIMIT_BURST);
1224 assert_eq!(RateLimitSettings::clamp_burst(15), 15);
1225 }
1226
1227 #[test]
1228 #[cfg(feature = "server")]
1229 fn rate_limit_clamp_daily() {
1230 assert_eq!(RateLimitSettings::clamp_daily(0), MIN_RATE_LIMIT_DAILY);
1231 assert_eq!(RateLimitSettings::clamp_daily(50), 50);
1232 }
1233
1234 #[test]
1235 #[cfg(feature = "server")]
1236 fn rate_limit_clamp_gc_interval() {
1237 assert_eq!(
1238 RateLimitSettings::clamp_gc_interval(0),
1239 MIN_RATE_LIMIT_GC_INTERVAL_SECS
1240 );
1241 assert_eq!(RateLimitSettings::clamp_gc_interval(300), 300);
1242 }
1243 #[test]
1246 fn webp_settings_default() {
1247 let s = WebpSettings::default();
1248 assert_eq!(s.quality, 85.0);
1249 assert_eq!(s.method, 2);
1250 }
1251
1252 #[test]
1253 #[cfg(feature = "server")]
1254 fn webp_clamp_quality() {
1255 assert_eq!(WebpSettings::clamp_quality(-1.0), 0.0);
1256 assert_eq!(WebpSettings::clamp_quality(50.0), 50.0);
1257 assert_eq!(WebpSettings::clamp_quality(150.0), 100.0);
1258 }
1259
1260 #[test]
1261 #[cfg(feature = "server")]
1262 fn webp_clamp_method() {
1263 assert_eq!(WebpSettings::clamp_method(0), 0);
1264 assert_eq!(WebpSettings::clamp_method(3), 3);
1265 assert_eq!(WebpSettings::clamp_method(9), MAX_WEBP_METHOD);
1266 }
1267
1268 #[test]
1271 fn image_limit_settings_default() {
1272 let s = ImageLimitSettings::default();
1273 assert_eq!(s.max_dimension, 8192);
1274 assert_eq!(s.max_pixels, 50_000_000);
1275 assert_eq!(s.dimensions_cache_ttl_secs, 86400);
1276 }
1277
1278 #[test]
1279 #[cfg(feature = "server")]
1280 fn image_limit_clamp_max_dimension() {
1281 assert_eq!(
1282 ImageLimitSettings::clamp_max_dimension(0),
1283 MIN_IMAGE_MAX_DIMENSION
1284 );
1285 assert_eq!(ImageLimitSettings::clamp_max_dimension(2048), 2048);
1286 }
1287
1288 #[test]
1289 #[cfg(feature = "server")]
1290 fn image_limit_clamp_max_pixels() {
1291 assert_eq!(
1292 ImageLimitSettings::clamp_max_pixels(0),
1293 MIN_IMAGE_MAX_PIXELS
1294 );
1295 assert_eq!(ImageLimitSettings::clamp_max_pixels(99_999_999), 99_999_999);
1296 }
1297
1298 #[test]
1299 #[cfg(feature = "server")]
1300 fn image_limit_clamp_ttl() {
1301 assert_eq!(
1302 ImageLimitSettings::clamp_dimensions_cache_ttl_secs(0),
1303 MIN_IMAGE_DIMENSIONS_CACHE_TTL_SECS
1304 );
1305 assert_eq!(
1306 ImageLimitSettings::clamp_dimensions_cache_ttl_secs(7200),
1307 7200
1308 );
1309 }
1310
1311 #[test]
1314 fn runner_settings_default() {
1315 let s = RunnerSettings::default();
1316 assert!(!s.allow_network);
1317 assert_eq!(s.max_concurrent, 4);
1318 assert_eq!(s.max_cpu_cores, 2.0);
1319 assert_eq!(s.max_memory_mb, 1024);
1320 assert_eq!(s.max_timeout_secs, 30);
1321 assert_eq!(s.max_output_bytes, 1_048_576);
1322 assert_eq!(s.max_source_bytes, 65_536);
1323 assert_eq!(s.queue_timeout_secs, 30);
1324 assert_eq!(s.task_ttl_secs, 300);
1325 assert!(s.languages.is_none());
1326 }
1327
1328 #[test]
1329 #[cfg(feature = "server")]
1330 fn runner_clamp_max_concurrent() {
1331 assert_eq!(
1332 RunnerSettings::clamp_max_concurrent(0),
1333 MIN_RUNNER_MAX_CONCURRENT
1334 );
1335 assert_eq!(RunnerSettings::clamp_max_concurrent(8), 8);
1336 assert_eq!(
1337 RunnerSettings::clamp_max_concurrent(999),
1338 MAX_RUNNER_MAX_CONCURRENT
1339 );
1340 }
1341
1342 #[test]
1343 #[cfg(feature = "server")]
1344 fn runner_clamp_max_cpu_cores() {
1345 assert_eq!(
1346 RunnerSettings::clamp_max_cpu_cores(0.0),
1347 MIN_RUNNER_MAX_CPU_CORES
1348 );
1349 assert_eq!(RunnerSettings::clamp_max_cpu_cores(4.0), 4.0);
1350 assert_eq!(
1351 RunnerSettings::clamp_max_cpu_cores(999.0),
1352 MAX_RUNNER_MAX_CPU_CORES
1353 );
1354 assert_eq!(RunnerSettings::clamp_max_cpu_cores(f64::NAN), 2.0);
1356 }
1357
1358 #[test]
1359 #[cfg(feature = "server")]
1360 fn runner_clamp_max_memory_mb() {
1361 assert_eq!(
1362 RunnerSettings::clamp_max_memory_mb(0),
1363 MIN_RUNNER_MAX_MEMORY_MB
1364 );
1365 assert_eq!(RunnerSettings::clamp_max_memory_mb(2048), 2048);
1366 assert_eq!(
1367 RunnerSettings::clamp_max_memory_mb(u32::MAX),
1368 MAX_RUNNER_MAX_MEMORY_MB
1369 );
1370 }
1371
1372 #[test]
1373 #[cfg(feature = "server")]
1374 fn runner_clamp_max_timeout_secs() {
1375 assert_eq!(
1376 RunnerSettings::clamp_max_timeout_secs(0),
1377 MIN_RUNNER_MAX_TIMEOUT_SECS
1378 );
1379 assert_eq!(RunnerSettings::clamp_max_timeout_secs(120), 120);
1380 }
1381
1382 #[test]
1383 #[cfg(feature = "server")]
1384 fn runner_clamp_byte_limits() {
1385 assert_eq!(RunnerSettings::clamp_max_output_bytes(0), 1);
1386 assert_eq!(RunnerSettings::clamp_max_source_bytes(0), 1);
1387 assert_eq!(
1388 RunnerSettings::clamp_queue_timeout_secs(0),
1389 MIN_RUNNER_MAX_TIMEOUT_SECS
1390 );
1391 assert_eq!(RunnerSettings::clamp_task_ttl_secs(0), 1);
1392 }
1393
1394 #[test]
1395 #[cfg(feature = "server")]
1396 fn runner_normalize_languages() {
1397 assert!(RunnerSettings::normalize_languages("").is_none());
1398 assert!(RunnerSettings::normalize_languages(" ,, ").is_none());
1399 assert_eq!(
1400 RunnerSettings::normalize_languages("Python, Node, RUST").as_deref(),
1401 Some("python,node,rust")
1402 );
1403 assert_eq!(
1404 RunnerSettings::normalize_languages(" go ").as_deref(),
1405 Some("go")
1406 );
1407 }
1408}