1use garde::Validate;
2use serde::{Deserialize, Serialize};
3use utoipa::ToSchema;
4
5pub type Config = super::system_config::get::Response200;
6
7#[derive(Debug, ToSchema, Deserialize, Serialize, Default, Clone, Copy)]
8#[serde(rename_all = "snake_case")]
9pub enum StreamableArchiveFormat {
10 Tar,
11 TarGz,
12 TarXz,
13 TarLzip,
14 TarBz2,
15 TarLz4,
16 TarZstd,
17 #[default]
18 Zip,
19}
20
21impl std::fmt::Display for StreamableArchiveFormat {
22 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
23 write!(
24 f,
25 "{}",
26 match self {
27 StreamableArchiveFormat::Tar => "tar",
28 StreamableArchiveFormat::TarGz => "tar_gz",
29 StreamableArchiveFormat::TarXz => "tar_xz",
30 StreamableArchiveFormat::TarLzip => "tar_lzip",
31 StreamableArchiveFormat::TarBz2 => "tar_bz2",
32 StreamableArchiveFormat::TarLz4 => "tar_lz4",
33 StreamableArchiveFormat::TarZstd => "tar_zstd",
34 StreamableArchiveFormat::Zip => "zip",
35 }
36 )
37 }
38}
39
40#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
41#[serde(rename_all = "lowercase")]
42pub enum Algorithm {
43 Md5,
44 Crc32,
45 Sha1,
46 Sha224,
47 Sha256,
48 Sha384,
49 Sha512,
50 Curseforge,
51}
52
53impl std::fmt::Display for Algorithm {
54 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
55 write!(
56 f,
57 "{}",
58 match self {
59 Algorithm::Md5 => "md5",
60 Algorithm::Crc32 => "crc32",
61 Algorithm::Sha1 => "sha1",
62 Algorithm::Sha224 => "sha224",
63 Algorithm::Sha256 => "sha256",
64 Algorithm::Sha384 => "sha384",
65 Algorithm::Sha512 => "sha512",
66 Algorithm::Curseforge => "curseforge",
67 }
68 )
69 }
70}
71
72#[derive(Debug, Default, ToSchema, Deserialize, Serialize, Clone, Copy)]
73#[serde(rename_all = "snake_case")]
74pub enum DirectorySortingMode {
75 #[default]
76 NameAsc,
77 NameDesc,
78 SizeAsc,
79 SizeDesc,
80 PhysicalSizeAsc,
81 PhysicalSizeDesc,
82 ModifiedAsc,
83 ModifiedDesc,
84 CreatedAsc,
85 CreatedDesc,
86}
87
88impl std::fmt::Display for DirectorySortingMode {
89 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
90 write!(
91 f,
92 "{}",
93 match self {
94 DirectorySortingMode::NameAsc => "name_asc",
95 DirectorySortingMode::NameDesc => "name_desc",
96 DirectorySortingMode::SizeAsc => "size_asc",
97 DirectorySortingMode::SizeDesc => "size_desc",
98 DirectorySortingMode::PhysicalSizeAsc => "physical_size_asc",
99 DirectorySortingMode::PhysicalSizeDesc => "physical_size_desc",
100 DirectorySortingMode::ModifiedAsc => "modified_asc",
101 DirectorySortingMode::ModifiedDesc => "modified_desc",
102 DirectorySortingMode::CreatedAsc => "created_asc",
103 DirectorySortingMode::CreatedDesc => "created_desc",
104 }
105 )
106 }
107}
108
109#[derive(ToSchema, Debug, Clone, Copy, Deserialize, Serialize)]
110#[serde(rename_all = "lowercase")]
111pub enum ServerBackupStatus {
112 Starting,
113 Finished,
114 Failed,
115}
116
117#[derive(Debug, ToSchema, Deserialize, Serialize, Clone, Copy)]
118#[serde(rename_all = "snake_case")]
119pub enum Game {
120 MinecraftJava,
121}
122
123impl std::fmt::Display for Game {
124 fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
125 write!(
126 f,
127 "{}",
128 match self {
129 Game::MinecraftJava => "minecraft_java",
130 }
131 )
132 }
133}
134
135#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
136pub struct ScheduleVariable {
137 #[garde(length(chars, min = 1, max = 255))]
138 #[schema(min_length = 1, max_length = 255)]
139 pub variable: compact_str::CompactString,
140}
141
142#[derive(ToSchema, Clone, Deserialize, Serialize, Validate)]
143#[serde(untagged)]
144pub enum ScheduleDynamicParameter {
145 Raw(#[garde(length(chars, min = 1, max = 1024))] compact_str::CompactString),
146 Variable(#[garde(dive)] ScheduleVariable),
147}
148
149#[derive(ToSchema, Deserialize, Serialize)]
150pub struct ScheduleAction {
151 pub uuid: uuid::Uuid,
152
153 #[serde(flatten)]
154 pub inner: ScheduleActionInner,
155}
156
157#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
158#[serde(rename_all = "snake_case", tag = "type")]
159pub enum ScheduleActionInner {
160 Sleep {
161 #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
162 #[schema(minimum = 1, maximum = 86400000)]
163 duration: u64,
164 },
165 Ensure {
166 #[garde(dive, custom(ScheduleCondition::validate_nesting))]
167 condition: ScheduleCondition,
168 },
169 If {
170 #[garde(dive, custom(ScheduleCondition::validate_nesting))]
171 condition: ScheduleCondition,
172 },
173 ElseIf {
174 #[garde(dive, custom(ScheduleCondition::validate_nesting))]
175 condition: ScheduleCondition,
176 },
177 Else,
178 EndIf,
179 Exit {
180 #[garde(skip)]
181 successful: bool,
182 },
183 WaitForState {
184 #[garde(skip)]
185 ignore_failure: bool,
186
187 #[garde(skip)]
188 state: super::ServerState,
189 #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
190 #[schema(minimum = 1, maximum = 86400000)]
191 timeout: u64,
192 },
193 Format {
194 #[garde(length(chars, min = 1, max = 2048))]
195 #[schema(min_length = 1, max_length = 2048)]
196 format: String,
197 #[garde(dive)]
198 output_into: ScheduleVariable,
199 },
200 MatchRegex {
201 #[garde(dive)]
202 input: ScheduleDynamicParameter,
203
204 #[garde(skip)]
205 #[serde(with = "serde_regex")]
206 #[schema(value_type = String, format = "regex")]
207 regex: regex::Regex,
208
209 #[garde(skip)]
210 output_into: Vec<Option<ScheduleVariable>>,
211 },
212 WaitForConsoleLine {
213 #[garde(skip)]
214 ignore_failure: bool,
215
216 #[garde(dive)]
217 contains: ScheduleDynamicParameter,
218 #[garde(skip)]
219 #[serde(default)]
220 case_insensitive: bool,
221 #[garde(range(min = 1, max = 24 * 60 * 60 * 1000))]
222 #[schema(minimum = 1, maximum = 86400000)]
223 timeout: u64,
224
225 #[garde(dive)]
226 output_into: Option<ScheduleVariable>,
227 },
228 SendPower {
229 #[garde(skip)]
230 ignore_failure: bool,
231
232 #[garde(skip)]
233 action: super::ServerPowerAction,
234 },
235 SendCommand {
236 #[garde(skip)]
237 ignore_failure: bool,
238
239 #[garde(dive)]
240 command: ScheduleDynamicParameter,
241 },
242 CreateBackup {
243 #[garde(skip)]
244 ignore_failure: bool,
245 #[garde(skip)]
246 foreground: bool,
247
248 #[garde(dive)]
249 name: Option<ScheduleDynamicParameter>,
250 #[garde(skip)]
251 ignored_files: Vec<compact_str::CompactString>,
252 },
253 CreateDirectory {
254 #[garde(skip)]
255 ignore_failure: bool,
256
257 #[garde(dive)]
258 root: ScheduleDynamicParameter,
259 #[garde(dive)]
260 name: ScheduleDynamicParameter,
261 },
262 WriteFile {
263 #[garde(skip)]
264 ignore_failure: bool,
265 #[garde(skip)]
266 append: bool,
267
268 #[garde(dive)]
269 file: ScheduleDynamicParameter,
270 #[garde(dive)]
271 content: ScheduleDynamicParameter,
272 },
273 CopyFile {
274 #[garde(skip)]
275 ignore_failure: bool,
276 #[garde(skip)]
277 foreground: bool,
278
279 #[garde(dive)]
280 file: ScheduleDynamicParameter,
281 #[garde(dive)]
282 destination: ScheduleDynamicParameter,
283 },
284 DeleteFiles {
285 #[garde(skip)]
286 #[serde(default)]
287 ignore_failure: bool,
288
289 #[garde(dive)]
290 root: ScheduleDynamicParameter,
291 #[garde(skip)]
292 files: Vec<compact_str::CompactString>,
293 },
294 RenameFiles {
295 #[garde(skip)]
296 #[serde(default)]
297 ignore_failure: bool,
298
299 #[garde(dive)]
300 root: ScheduleDynamicParameter,
301 #[garde(skip)]
302 #[schema(inline)]
303 files: Vec<super::servers_server_files_rename::put::RequestBodyFiles>,
304 },
305 CompressFiles {
306 #[garde(skip)]
307 ignore_failure: bool,
308 #[garde(skip)]
309 foreground: bool,
310
311 #[garde(dive)]
312 root: ScheduleDynamicParameter,
313 #[garde(skip)]
314 files: Vec<compact_str::CompactString>,
315 #[garde(skip)]
316 format: super::ArchiveFormat,
317 #[garde(dive)]
318 name: ScheduleDynamicParameter,
319 },
320 DecompressFile {
321 #[garde(skip)]
322 ignore_failure: bool,
323 #[garde(skip)]
324 foreground: bool,
325
326 #[garde(dive)]
327 root: ScheduleDynamicParameter,
328 #[garde(dive)]
329 file: ScheduleDynamicParameter,
330 },
331 UpdateStartupVariable {
332 #[garde(skip)]
333 ignore_failure: bool,
334
335 #[garde(dive)]
336 env_variable: ScheduleDynamicParameter,
337 #[garde(dive)]
338 value: ScheduleDynamicParameter,
339 },
340 UpdateStartupCommand {
341 #[garde(skip)]
342 ignore_failure: bool,
343
344 #[garde(dive)]
345 command: ScheduleDynamicParameter,
346 },
347 UpdateStartupDockerImage {
348 #[garde(skip)]
349 ignore_failure: bool,
350
351 #[garde(dive)]
352 image: ScheduleDynamicParameter,
353 },
354}
355
356impl ScheduleActionInner {
357 pub fn permission(&self) -> Option<&'static str> {
358 match self {
359 ScheduleActionInner::Sleep { .. } => None,
360 ScheduleActionInner::Ensure { .. } => None,
361 ScheduleActionInner::If { .. } => None,
362 ScheduleActionInner::ElseIf { .. } => None,
363 ScheduleActionInner::Else => None,
364 ScheduleActionInner::EndIf => None,
365 ScheduleActionInner::Exit { .. } => None,
366 ScheduleActionInner::WaitForState { .. } => None,
367 ScheduleActionInner::Format { .. } => None,
368 ScheduleActionInner::MatchRegex { .. } => None,
369 ScheduleActionInner::WaitForConsoleLine { .. } => Some("control.read-console"),
370 ScheduleActionInner::SendPower { action, .. } => match action {
371 super::ServerPowerAction::Start => Some("control.start"),
372 super::ServerPowerAction::Stop => Some("control.stop"),
373 super::ServerPowerAction::Restart => Some("control.restart"),
374 super::ServerPowerAction::Kill => Some("control.stop"),
375 },
376 ScheduleActionInner::SendCommand { .. } => Some("control.console"),
377 ScheduleActionInner::CreateBackup { .. } => Some("backups.create"),
378 ScheduleActionInner::CreateDirectory { .. } => Some("files.create"),
379 ScheduleActionInner::WriteFile { .. } => Some("files.update"),
380 ScheduleActionInner::CopyFile { .. } => Some("files.update"),
381 ScheduleActionInner::DeleteFiles { .. } => Some("files.delete"),
382 ScheduleActionInner::RenameFiles { .. } => Some("files.update"),
383 ScheduleActionInner::CompressFiles { .. } => Some("files.archive"),
384 ScheduleActionInner::DecompressFile { .. } => Some("files.archive"),
385 ScheduleActionInner::UpdateStartupVariable { .. } => Some("startup.update"),
386 ScheduleActionInner::UpdateStartupCommand { .. } => Some("startup.command"),
387 ScheduleActionInner::UpdateStartupDockerImage { .. } => Some("startup.docker-image"),
388 }
389 }
390}
391
392#[derive(ToSchema, Validate, Deserialize, Serialize, Clone)]
393#[serde(rename_all = "snake_case", tag = "type")]
394pub enum ScheduleTrigger {
395 Cron {
396 #[garde(skip)]
397 #[schema(value_type = String, example = "* * * * * *")]
398 schedule: Box<cron::Schedule>,
399 },
400 PowerAction {
401 #[garde(skip)]
402 action: super::ServerPowerAction,
403 },
404 ServerState {
405 #[garde(skip)]
406 state: super::ServerState,
407 },
408 BackupStatus {
409 #[garde(skip)]
410 status: ServerBackupStatus,
411 },
412 ScheduleCompletion {
413 #[garde(skip)]
414 schedule: uuid::Uuid,
415 #[garde(skip)]
416 successful: bool,
417 },
418 ResourceUsage {
419 #[garde(skip)]
420 metric: ScheduleResourceMetric,
421 #[garde(skip)]
422 comparator: ScheduleConditionComparator,
423 #[garde(range(min = 0.0))]
424 value: f64,
425 #[garde(range(min = 0, max = 24 * 60 * 60))]
426 #[schema(minimum = 0, maximum = 86400)]
427 #[serde(default)]
428 for_seconds: u64,
429 },
430 ConsoleLine {
431 #[garde(length(chars, min = 1, max = 1024))]
432 #[schema(min_length = 1, max_length = 1024)]
433 contains: compact_str::CompactString,
434 #[garde(skip)]
435 #[serde(default)]
436 case_insensitive: bool,
437 #[garde(dive)]
438 output_into: Option<ScheduleVariable>,
439 },
440 Crash,
441}
442
443#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
444#[serde(rename_all = "snake_case")]
445pub enum ScheduleConditionComparator {
446 SmallerThan,
447 SmallerThanOrEquals,
448 Equal,
449 GreaterThan,
450 GreaterThanOrEquals,
451}
452
453#[derive(ToSchema, Deserialize, Serialize, Clone, Copy)]
454#[serde(rename_all = "snake_case")]
455pub enum ScheduleResourceMetric {
456 Cpu,
457 Memory,
458 Disk,
459}
460
461#[derive(ToSchema, Deserialize, Serialize, Clone, Validate)]
462#[serde(rename_all = "snake_case", tag = "type")]
463#[schema(no_recursion)]
464pub enum ScheduleCondition {
465 None,
466 And {
467 #[garde(dive)]
468 conditions: Vec<ScheduleCondition>,
469 },
470 Or {
471 #[garde(dive)]
472 conditions: Vec<ScheduleCondition>,
473 },
474 Not {
475 #[garde(dive)]
476 condition: Box<ScheduleCondition>,
477 },
478 ServerState {
479 #[garde(skip)]
480 state: super::ServerState,
481 },
482 Uptime {
483 #[garde(skip)]
484 comparator: ScheduleConditionComparator,
485 #[garde(skip)]
486 value: u64,
487 },
488 ResourceUsage {
489 #[garde(skip)]
490 metric: ScheduleResourceMetric,
491 #[garde(skip)]
492 comparator: ScheduleConditionComparator,
493 #[garde(range(min = 0.0))]
494 value: f64,
495 },
496 FileExists {
497 #[garde(length(chars, min = 1, max = 255))]
498 #[schema(min_length = 1, max_length = 255)]
499 file: String,
500 },
501 VariableExists {
502 #[garde(dive)]
503 variable: ScheduleVariable,
504 },
505 VariableEquals {
506 #[garde(dive)]
507 variable: ScheduleVariable,
508 #[garde(dive)]
509 equals: ScheduleDynamicParameter,
510 },
511 VariableContains {
512 #[garde(dive)]
513 variable: ScheduleVariable,
514 #[garde(dive)]
515 contains: ScheduleDynamicParameter,
516 },
517 VariableStartsWith {
518 #[garde(dive)]
519 variable: ScheduleVariable,
520 #[garde(dive)]
521 starts_with: ScheduleDynamicParameter,
522 },
523 VariableEndsWith {
524 #[garde(dive)]
525 variable: ScheduleVariable,
526 #[garde(dive)]
527 ends_with: ScheduleDynamicParameter,
528 },
529}
530
531impl ScheduleCondition {
532 pub const MAX_NESTING_DEPTH: usize = 3;
533
534 fn nested_within_limit(&self, depth: usize) -> bool {
535 match self {
536 ScheduleCondition::And { conditions } | ScheduleCondition::Or { conditions } => {
537 depth < Self::MAX_NESTING_DEPTH
538 && conditions.iter().all(|c| c.nested_within_limit(depth + 1))
539 }
540 ScheduleCondition::Not { condition } => {
541 depth < Self::MAX_NESTING_DEPTH && condition.nested_within_limit(depth + 1)
542 }
543 _ => true,
544 }
545 }
546
547 pub fn validate_nesting(value: &Self, _context: &()) -> garde::Result {
548 if value.nested_within_limit(0) {
549 Ok(())
550 } else {
551 Err(garde::Error::new(format!(
552 "condition may not nest groups more than {} levels deep",
553 Self::MAX_NESTING_DEPTH
554 )))
555 }
556 }
557
558 pub fn validate_optional_nesting(value: &Option<Self>, context: &()) -> garde::Result {
559 match value {
560 Some(condition) => Self::validate_nesting(condition, context),
561 None => Ok(()),
562 }
563 }
564}
565
566const FORBIDDEN_CONFIG_PATHS: &[&str] = &[
567 "uuid",
568 "token",
569 "token_id",
570 "remote",
571 "remote_headers",
572 "system.root_directory",
573 "system.log_directory",
574 "system.vmount_directory",
575 "system.data",
576 "system.archive_directory",
577 "system.backup_directory",
578 "system.tmp_directory",
579 "system.passwd.directory",
580 "system.backups.restic.repository",
581 "system.backups.restic.password_file",
582 "system.backups.mounting.path",
583 "system.username",
584 "system.user",
585 "system.passwd",
586 "docker.socket",
587 "allowed_mounts",
588];
589
590pub fn strip_config_paths(value: &mut serde_json::Value) {
591 for path in FORBIDDEN_CONFIG_PATHS {
592 let mut cursor = &mut *value;
593 let mut parts = path.split('.').peekable();
594
595 while let Some(part) = parts.next() {
596 let serde_json::Value::Object(map) = cursor else {
597 break;
598 };
599
600 if parts.peek().is_none() {
601 map.remove(part);
602 break;
603 }
604
605 match map.get_mut(part) {
606 Some(next) => cursor = next,
607 None => break,
608 }
609 }
610 }
611}