Skip to main content

wings_api/
extra.rs

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}