]>
Commit | Line | Data |
---|---|---|
3e276f6f FG |
1 | use std::str::FromStr; |
2 | ||
4f0dd337 | 3 | use anyhow::bail; |
3e276f6f | 4 | use regex::Regex; |
e3619d41 DM |
5 | use serde::{Deserialize, Serialize}; |
6 | ||
6ef1b649 | 7 | use proxmox_schema::*; |
e3619d41 DM |
8 | |
9 | use crate::{ | |
abd82485 FG |
10 | Authid, BackupNamespace, BackupType, RateLimitConfig, Userid, BACKUP_GROUP_SCHEMA, |
11 | BACKUP_NAMESPACE_SCHEMA, DATASTORE_SCHEMA, DRIVE_NAME_SCHEMA, MEDIA_POOL_NAME_SCHEMA, | |
12 | NS_MAX_DEPTH_REDUCED_SCHEMA, PROXMOX_SAFE_ID_FORMAT, REMOTE_ID_SCHEMA, | |
e40c7fb9 | 13 | SINGLE_LINE_COMMENT_SCHEMA, |
e3619d41 DM |
14 | }; |
15 | ||
b22d785c | 16 | const_regex! { |
e3619d41 DM |
17 | |
18 | /// Regex for verification jobs 'DATASTORE:ACTUAL_JOB_ID' | |
19 | pub VERIFICATION_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):"); | |
4ec73327 HL |
20 | /// Regex for sync jobs '(REMOTE|\-):REMOTE_DATASTORE:LOCAL_DATASTORE:(?:LOCAL_NS_ANCHOR:)ACTUAL_JOB_ID' |
21 | pub SYNC_JOB_WORKER_ID_REGEX = concat!(r"^(", PROXMOX_SAFE_ID_REGEX_STR!(), r"|\-):(", PROXMOX_SAFE_ID_REGEX_STR!(), r"):(", PROXMOX_SAFE_ID_REGEX_STR!(), r")(?::(", BACKUP_NS_RE!(), r"))?:"); | |
e3619d41 DM |
22 | } |
23 | ||
e3619d41 DM |
24 | pub const JOB_ID_SCHEMA: Schema = StringSchema::new("Job ID.") |
25 | .format(&PROXMOX_SAFE_ID_FORMAT) | |
26 | .min_length(3) | |
27 | .max_length(32) | |
28 | .schema(); | |
29 | ||
b22d785c TL |
30 | pub const SYNC_SCHEDULE_SCHEMA: Schema = StringSchema::new("Run sync job at specified schedule.") |
31 | .format(&ApiStringFormat::VerifyFn( | |
32 | proxmox_time::verify_calendar_event, | |
33 | )) | |
e3619d41 DM |
34 | .type_text("<calendar-event>") |
35 | .schema(); | |
36 | ||
b22d785c TL |
37 | pub const GC_SCHEDULE_SCHEMA: Schema = |
38 | StringSchema::new("Run garbage collection job at specified schedule.") | |
39 | .format(&ApiStringFormat::VerifyFn( | |
40 | proxmox_time::verify_calendar_event, | |
41 | )) | |
42 | .type_text("<calendar-event>") | |
43 | .schema(); | |
44 | ||
45 | pub const PRUNE_SCHEDULE_SCHEMA: Schema = StringSchema::new("Run prune job at specified schedule.") | |
46 | .format(&ApiStringFormat::VerifyFn( | |
47 | proxmox_time::verify_calendar_event, | |
48 | )) | |
e3619d41 DM |
49 | .type_text("<calendar-event>") |
50 | .schema(); | |
51 | ||
b22d785c TL |
52 | pub const VERIFICATION_SCHEDULE_SCHEMA: Schema = |
53 | StringSchema::new("Run verify job at specified schedule.") | |
54 | .format(&ApiStringFormat::VerifyFn( | |
55 | proxmox_time::verify_calendar_event, | |
56 | )) | |
57 | .type_text("<calendar-event>") | |
58 | .schema(); | |
e3619d41 DM |
59 | |
60 | pub const REMOVE_VANISHED_BACKUPS_SCHEMA: Schema = BooleanSchema::new( | |
b22d785c TL |
61 | "Delete vanished backups. This remove the local copy if the remote backup was deleted.", |
62 | ) | |
63 | .default(false) | |
64 | .schema(); | |
e3619d41 DM |
65 | |
66 | #[api( | |
67 | properties: { | |
68 | "next-run": { | |
69 | description: "Estimated time of the next run (UNIX epoch).", | |
70 | optional: true, | |
71 | type: Integer, | |
72 | }, | |
73 | "last-run-state": { | |
74 | description: "Result of the last run.", | |
75 | optional: true, | |
76 | type: String, | |
77 | }, | |
78 | "last-run-upid": { | |
79 | description: "Task UPID of the last run.", | |
80 | optional: true, | |
81 | type: String, | |
82 | }, | |
83 | "last-run-endtime": { | |
84 | description: "Endtime of the last run.", | |
85 | optional: true, | |
86 | type: Integer, | |
87 | }, | |
88 | } | |
89 | )] | |
aca9222e | 90 | #[derive(Serialize, Deserialize, Default, Clone, PartialEq)] |
b22d785c | 91 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
92 | /// Job Scheduling Status |
93 | pub struct JobScheduleStatus { | |
b22d785c | 94 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 95 | pub next_run: Option<i64>, |
b22d785c | 96 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 97 | pub last_run_state: Option<String>, |
b22d785c | 98 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 99 | pub last_run_upid: Option<String>, |
b22d785c | 100 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
101 | pub last_run_endtime: Option<i64>, |
102 | } | |
103 | ||
104 | #[api()] | |
f680e72f | 105 | #[derive(Debug, Copy, Clone, PartialEq, Eq, Serialize, Deserialize)] |
e3619d41 DM |
106 | #[serde(rename_all = "lowercase")] |
107 | /// When do we send notifications | |
108 | pub enum Notify { | |
109 | /// Never send notification | |
110 | Never, | |
111 | /// Send notifications for failed and successful jobs | |
112 | Always, | |
113 | /// Send notifications for failed jobs only | |
114 | Error, | |
115 | } | |
116 | ||
117 | #[api( | |
118 | properties: { | |
119 | gc: { | |
120 | type: Notify, | |
121 | optional: true, | |
122 | }, | |
123 | verify: { | |
124 | type: Notify, | |
125 | optional: true, | |
126 | }, | |
127 | sync: { | |
128 | type: Notify, | |
129 | optional: true, | |
130 | }, | |
cf91a072 DC |
131 | prune: { |
132 | type: Notify, | |
133 | optional: true, | |
134 | }, | |
e3619d41 DM |
135 | }, |
136 | )] | |
137 | #[derive(Debug, Serialize, Deserialize)] | |
138 | /// Datastore notify settings | |
139 | pub struct DatastoreNotify { | |
140 | /// Garbage collection settings | |
ded3a888 | 141 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
142 | pub gc: Option<Notify>, |
143 | /// Verify job setting | |
ded3a888 | 144 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
145 | pub verify: Option<Notify>, |
146 | /// Sync job setting | |
ded3a888 | 147 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 148 | pub sync: Option<Notify>, |
cf91a072 | 149 | /// Prune job setting |
ded3a888 | 150 | #[serde(skip_serializing_if = "Option::is_none")] |
cf91a072 | 151 | pub prune: Option<Notify>, |
e3619d41 DM |
152 | } |
153 | ||
dfe17914 MS |
154 | pub const DATASTORE_NOTIFY_STRING_SCHEMA: Schema = StringSchema::new( |
155 | "Datastore notification setting, enum can be one of 'always', 'never', or 'error'.", | |
156 | ) | |
157 | .format(&ApiStringFormat::PropertyString( | |
158 | &DatastoreNotify::API_SCHEMA, | |
159 | )) | |
160 | .schema(); | |
e3619d41 DM |
161 | |
162 | pub const IGNORE_VERIFIED_BACKUPS_SCHEMA: Schema = BooleanSchema::new( | |
b22d785c TL |
163 | "Do not verify backups that are already verified if their verification is not outdated.", |
164 | ) | |
165 | .default(true) | |
166 | .schema(); | |
e3619d41 | 167 | |
b22d785c | 168 | pub const VERIFICATION_OUTDATED_AFTER_SCHEMA: Schema = |
7f3b4a94 TL |
169 | IntegerSchema::new("Days after that a verification becomes outdated. (0 is deprecated)'") |
170 | .minimum(0) | |
b22d785c | 171 | .schema(); |
e3619d41 DM |
172 | |
173 | #[api( | |
174 | properties: { | |
175 | id: { | |
176 | schema: JOB_ID_SCHEMA, | |
177 | }, | |
178 | store: { | |
179 | schema: DATASTORE_SCHEMA, | |
180 | }, | |
181 | "ignore-verified": { | |
182 | optional: true, | |
183 | schema: IGNORE_VERIFIED_BACKUPS_SCHEMA, | |
184 | }, | |
185 | "outdated-after": { | |
186 | optional: true, | |
187 | schema: VERIFICATION_OUTDATED_AFTER_SCHEMA, | |
188 | }, | |
189 | comment: { | |
190 | optional: true, | |
191 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
192 | }, | |
193 | schedule: { | |
194 | optional: true, | |
195 | schema: VERIFICATION_SCHEDULE_SCHEMA, | |
196 | }, | |
59229bd7 TL |
197 | ns: { |
198 | optional: true, | |
199 | schema: BACKUP_NAMESPACE_SCHEMA, | |
200 | }, | |
0b1edf29 TL |
201 | "max-depth": { |
202 | optional: true, | |
203 | schema: crate::NS_MAX_DEPTH_SCHEMA, | |
204 | }, | |
e3619d41 DM |
205 | } |
206 | )] | |
65c9e406 | 207 | #[derive(Serialize, Deserialize, Updater, Clone, PartialEq)] |
b22d785c | 208 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
209 | /// Verification Job |
210 | pub struct VerificationJobConfig { | |
211 | /// unique ID to address this job | |
ffa403b5 | 212 | #[updater(skip)] |
e3619d41 | 213 | pub id: String, |
8772ca72 | 214 | /// the datastore ID this verification job affects |
e3619d41 | 215 | pub store: String, |
b22d785c | 216 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
217 | /// if not set to false, check the age of the last snapshot verification to filter |
218 | /// out recent ones, depending on 'outdated_after' configuration. | |
219 | pub ignore_verified: Option<bool>, | |
b22d785c | 220 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
221 | /// Reverify snapshots after X days, never if 0. Ignored if 'ignore_verified' is false. |
222 | pub outdated_after: Option<i64>, | |
b22d785c | 223 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 224 | pub comment: Option<String>, |
b22d785c | 225 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
226 | /// when to schedule this job in calendar event notation |
227 | pub schedule: Option<String>, | |
59229bd7 TL |
228 | #[serde(skip_serializing_if = "Option::is_none", default)] |
229 | /// on which backup namespace to run the verification recursively | |
230 | pub ns: Option<BackupNamespace>, | |
0b1edf29 TL |
231 | #[serde(skip_serializing_if = "Option::is_none", default)] |
232 | /// how deep the verify should go from the `ns` level downwards. Passing 0 verifies only the | |
233 | /// snapshots on the same level as the passed `ns`, or the datastore root if none. | |
234 | pub max_depth: Option<usize>, | |
e3619d41 DM |
235 | } |
236 | ||
0aa5815f | 237 | impl VerificationJobConfig { |
abd82485 FG |
238 | pub fn acl_path(&self) -> Vec<&str> { |
239 | match self.ns.as_ref() { | |
240 | Some(ns) => ns.acl_path(&self.store), | |
241 | None => vec!["datastore", &self.store], | |
0aa5815f FG |
242 | } |
243 | } | |
244 | } | |
245 | ||
e3619d41 DM |
246 | #[api( |
247 | properties: { | |
248 | config: { | |
249 | type: VerificationJobConfig, | |
250 | }, | |
251 | status: { | |
252 | type: JobScheduleStatus, | |
253 | }, | |
254 | }, | |
255 | )] | |
65c9e406 | 256 | #[derive(Serialize, Deserialize, Clone, PartialEq)] |
b22d785c | 257 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
258 | /// Status of Verification Job |
259 | pub struct VerificationJobStatus { | |
260 | #[serde(flatten)] | |
261 | pub config: VerificationJobConfig, | |
262 | #[serde(flatten)] | |
263 | pub status: JobScheduleStatus, | |
264 | } | |
265 | ||
266 | #[api( | |
267 | properties: { | |
268 | store: { | |
269 | schema: DATASTORE_SCHEMA, | |
270 | }, | |
271 | pool: { | |
272 | schema: MEDIA_POOL_NAME_SCHEMA, | |
273 | }, | |
274 | drive: { | |
275 | schema: DRIVE_NAME_SCHEMA, | |
276 | }, | |
277 | "eject-media": { | |
278 | description: "Eject media upon job completion.", | |
279 | type: bool, | |
280 | optional: true, | |
281 | }, | |
282 | "export-media-set": { | |
283 | description: "Export media set upon job completion.", | |
284 | type: bool, | |
285 | optional: true, | |
286 | }, | |
287 | "latest-only": { | |
288 | description: "Backup latest snapshots only.", | |
289 | type: bool, | |
290 | optional: true, | |
291 | }, | |
292 | "notify-user": { | |
293 | optional: true, | |
294 | type: Userid, | |
295 | }, | |
062edce2 | 296 | "group-filter": { |
91357c20 DC |
297 | schema: GROUP_FILTER_LIST_SCHEMA, |
298 | optional: true, | |
299 | }, | |
999293bb DC |
300 | ns: { |
301 | type: BackupNamespace, | |
302 | optional: true, | |
303 | }, | |
12d33461 | 304 | "max-depth": { |
999293bb DC |
305 | schema: crate::NS_MAX_DEPTH_SCHEMA, |
306 | optional: true, | |
307 | }, | |
e3619d41 DM |
308 | } |
309 | )] | |
65c9e406 | 310 | #[derive(Serialize, Deserialize, Clone, Updater, PartialEq)] |
b22d785c | 311 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
312 | /// Tape Backup Job Setup |
313 | pub struct TapeBackupJobSetup { | |
314 | pub store: String, | |
315 | pub pool: String, | |
316 | pub drive: String, | |
b22d785c | 317 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 318 | pub eject_media: Option<bool>, |
b22d785c | 319 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 320 | pub export_media_set: Option<bool>, |
b22d785c | 321 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
322 | pub latest_only: Option<bool>, |
323 | /// Send job email notification to this user | |
b22d785c | 324 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 325 | pub notify_user: Option<Userid>, |
b22d785c | 326 | #[serde(skip_serializing_if = "Option::is_none")] |
062edce2 | 327 | pub group_filter: Option<Vec<GroupFilter>>, |
999293bb DC |
328 | #[serde(skip_serializing_if = "Option::is_none", default)] |
329 | pub ns: Option<BackupNamespace>, | |
330 | #[serde(skip_serializing_if = "Option::is_none", default)] | |
12d33461 | 331 | pub max_depth: Option<usize>, |
e3619d41 DM |
332 | } |
333 | ||
334 | #[api( | |
335 | properties: { | |
336 | id: { | |
337 | schema: JOB_ID_SCHEMA, | |
338 | }, | |
339 | setup: { | |
340 | type: TapeBackupJobSetup, | |
341 | }, | |
342 | comment: { | |
343 | optional: true, | |
344 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
345 | }, | |
346 | schedule: { | |
347 | optional: true, | |
348 | schema: SYNC_SCHEDULE_SCHEMA, | |
349 | }, | |
350 | } | |
351 | )] | |
65c9e406 | 352 | #[derive(Serialize, Deserialize, Clone, Updater, PartialEq)] |
b22d785c | 353 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
354 | /// Tape Backup Job |
355 | pub struct TapeBackupJobConfig { | |
cdc83c4e | 356 | #[updater(skip)] |
e3619d41 DM |
357 | pub id: String, |
358 | #[serde(flatten)] | |
359 | pub setup: TapeBackupJobSetup, | |
b22d785c | 360 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 361 | pub comment: Option<String>, |
b22d785c | 362 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
363 | pub schedule: Option<String>, |
364 | } | |
365 | ||
366 | #[api( | |
367 | properties: { | |
368 | config: { | |
369 | type: TapeBackupJobConfig, | |
370 | }, | |
371 | status: { | |
372 | type: JobScheduleStatus, | |
373 | }, | |
374 | }, | |
375 | )] | |
65c9e406 | 376 | #[derive(Serialize, Deserialize, Clone, PartialEq)] |
b22d785c | 377 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
378 | /// Status of Tape Backup Job |
379 | pub struct TapeBackupJobStatus { | |
380 | #[serde(flatten)] | |
381 | pub config: TapeBackupJobConfig, | |
382 | #[serde(flatten)] | |
383 | pub status: JobScheduleStatus, | |
384 | /// Next tape used (best guess) | |
b22d785c | 385 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 DM |
386 | pub next_media_label: Option<String>, |
387 | } | |
388 | ||
3e276f6f FG |
389 | #[derive(Clone, Debug)] |
390 | /// Filter for matching `BackupGroup`s, for use with `BackupGroup::filter`. | |
59c92736 | 391 | pub enum FilterType { |
3e276f6f | 392 | /// BackupGroup type - either `vm`, `ct`, or `host`. |
38aa71fc | 393 | BackupType(BackupType), |
3e276f6f FG |
394 | /// Full identifier of BackupGroup, including type |
395 | Group(String), | |
396 | /// A regular expression matched against the full identifier of the BackupGroup | |
397 | Regex(Regex), | |
398 | } | |
399 | ||
59c92736 | 400 | impl PartialEq for FilterType { |
aca9222e DM |
401 | fn eq(&self, other: &Self) -> bool { |
402 | match (self, other) { | |
403 | (Self::BackupType(a), Self::BackupType(b)) => a == b, | |
404 | (Self::Group(a), Self::Group(b)) => a == b, | |
405 | (Self::Regex(a), Self::Regex(b)) => a.as_str() == b.as_str(), | |
406 | _ => false, | |
407 | } | |
408 | } | |
409 | } | |
410 | ||
4f0dd337 WB |
411 | impl std::str::FromStr for FilterType { |
412 | type Err = anyhow::Error; | |
413 | ||
414 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
415 | Ok(match s.split_once(':') { | |
416 | Some(("group", value)) => BACKUP_GROUP_SCHEMA.parse_simple_value(value).map(|_| FilterType::Group(value.to_string()))?, | |
417 | Some(("type", value)) => FilterType::BackupType(value.parse()?), | |
418 | Some(("regex", value)) => FilterType::Regex(Regex::new(value)?), | |
419 | Some((ty, _value)) => bail!("expected 'group', 'type' or 'regex' prefix, got '{}'", ty), | |
420 | None => bail!("input doesn't match expected format '<group:GROUP||type:<vm|ct|host>|regex:REGEX>'"), | |
421 | }) | |
422 | } | |
423 | } | |
424 | ||
126cf33c WB |
425 | // used for serializing below, caution! |
426 | impl std::fmt::Display for FilterType { | |
427 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
428 | match self { | |
429 | FilterType::BackupType(backup_type) => write!(f, "type:{}", backup_type), | |
430 | FilterType::Group(backup_group) => write!(f, "group:{}", backup_group), | |
431 | FilterType::Regex(regex) => write!(f, "regex:{}", regex.as_str()), | |
432 | } | |
433 | } | |
434 | } | |
435 | ||
59c92736 PH |
436 | #[derive(Clone, Debug)] |
437 | pub struct GroupFilter { | |
438 | pub is_exclude: bool, | |
439 | pub filter_type: FilterType, | |
440 | } | |
441 | ||
442 | impl PartialEq for GroupFilter { | |
443 | fn eq(&self, other: &Self) -> bool { | |
444 | self.filter_type == other.filter_type && self.is_exclude == other.is_exclude | |
445 | } | |
446 | } | |
447 | ||
448 | impl Eq for GroupFilter {} | |
449 | ||
3e276f6f FG |
450 | impl std::str::FromStr for GroupFilter { |
451 | type Err = anyhow::Error; | |
452 | ||
453 | fn from_str(s: &str) -> Result<Self, Self::Err> { | |
59c92736 PH |
454 | let (is_exclude, type_str) = match s.split_once(':') { |
455 | Some(("include", value)) => (false, value), | |
456 | Some(("exclude", value)) => (true, value), | |
457 | _ => (false, s), | |
458 | }; | |
459 | ||
59c92736 PH |
460 | Ok(GroupFilter { |
461 | is_exclude, | |
4f0dd337 | 462 | filter_type: type_str.parse()?, |
59c92736 | 463 | }) |
3e276f6f FG |
464 | } |
465 | } | |
466 | ||
467 | // used for serializing below, caution! | |
468 | impl std::fmt::Display for GroupFilter { | |
469 | fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { | |
126cf33c WB |
470 | if self.is_exclude { |
471 | f.write_str("exclude:")?; | |
3e276f6f | 472 | } |
126cf33c | 473 | std::fmt::Display::fmt(&self.filter_type, f) |
3e276f6f FG |
474 | } |
475 | } | |
476 | ||
25877d05 DM |
477 | proxmox_serde::forward_deserialize_to_from_str!(GroupFilter); |
478 | proxmox_serde::forward_serialize_to_display!(GroupFilter); | |
3e276f6f FG |
479 | |
480 | fn verify_group_filter(input: &str) -> Result<(), anyhow::Error> { | |
481 | GroupFilter::from_str(input).map(|_| ()) | |
482 | } | |
483 | ||
484 | pub const GROUP_FILTER_SCHEMA: Schema = StringSchema::new( | |
64dec8d6 | 485 | "Group filter based on group identifier ('group:GROUP'), group type ('type:<vm|ct|host>'), or regex ('regex:RE'). Can be inverted by prepending 'exclude:'.") |
3e276f6f | 486 | .format(&ApiStringFormat::VerifyFn(verify_group_filter)) |
59c92736 | 487 | .type_text("[<exclude:|include:>]<type:<vm|ct|host>|group:GROUP|regex:RE>") |
3e276f6f FG |
488 | .schema(); |
489 | ||
b22d785c TL |
490 | pub const GROUP_FILTER_LIST_SCHEMA: Schema = |
491 | ArraySchema::new("List of group filters.", &GROUP_FILTER_SCHEMA).schema(); | |
3e276f6f | 492 | |
9b67352a SH |
493 | pub const TRANSFER_LAST_SCHEMA: Schema = |
494 | IntegerSchema::new("Limit transfer to last N snapshots (per group), skipping others") | |
495 | .minimum(1) | |
496 | .schema(); | |
497 | ||
e3619d41 DM |
498 | #[api( |
499 | properties: { | |
500 | id: { | |
501 | schema: JOB_ID_SCHEMA, | |
502 | }, | |
503 | store: { | |
504 | schema: DATASTORE_SCHEMA, | |
505 | }, | |
c06c1b4b FG |
506 | ns: { |
507 | type: BackupNamespace, | |
508 | optional: true, | |
509 | }, | |
e3619d41 DM |
510 | "owner": { |
511 | type: Authid, | |
512 | optional: true, | |
513 | }, | |
514 | remote: { | |
515 | schema: REMOTE_ID_SCHEMA, | |
4ec73327 | 516 | optional: true, |
e3619d41 DM |
517 | }, |
518 | "remote-store": { | |
519 | schema: DATASTORE_SCHEMA, | |
520 | }, | |
c06c1b4b FG |
521 | "remote-ns": { |
522 | type: BackupNamespace, | |
523 | optional: true, | |
524 | }, | |
e3619d41 DM |
525 | "remove-vanished": { |
526 | schema: REMOVE_VANISHED_BACKUPS_SCHEMA, | |
527 | optional: true, | |
528 | }, | |
c06c1b4b | 529 | "max-depth": { |
e40c7fb9 | 530 | schema: NS_MAX_DEPTH_REDUCED_SCHEMA, |
c06c1b4b FG |
531 | optional: true, |
532 | }, | |
e3619d41 DM |
533 | comment: { |
534 | optional: true, | |
535 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
536 | }, | |
6eb756bc DM |
537 | limit: { |
538 | type: RateLimitConfig, | |
539 | }, | |
e3619d41 DM |
540 | schedule: { |
541 | optional: true, | |
542 | schema: SYNC_SCHEDULE_SCHEMA, | |
543 | }, | |
062edce2 | 544 | "group-filter": { |
5f83d3f6 FG |
545 | schema: GROUP_FILTER_LIST_SCHEMA, |
546 | optional: true, | |
547 | }, | |
9b67352a SH |
548 | "transfer-last": { |
549 | schema: TRANSFER_LAST_SCHEMA, | |
550 | optional: true, | |
551 | }, | |
e3619d41 DM |
552 | } |
553 | )] | |
aca9222e | 554 | #[derive(Serialize, Deserialize, Clone, Updater, PartialEq)] |
b22d785c | 555 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
556 | /// Sync Job |
557 | pub struct SyncJobConfig { | |
5bd77f00 | 558 | #[updater(skip)] |
e3619d41 DM |
559 | pub id: String, |
560 | pub store: String, | |
b22d785c | 561 | #[serde(skip_serializing_if = "Option::is_none")] |
c06c1b4b FG |
562 | pub ns: Option<BackupNamespace>, |
563 | #[serde(skip_serializing_if = "Option::is_none")] | |
e3619d41 | 564 | pub owner: Option<Authid>, |
4ec73327 HL |
565 | #[serde(skip_serializing_if = "Option::is_none")] |
566 | /// None implies local sync. | |
567 | pub remote: Option<String>, | |
e3619d41 | 568 | pub remote_store: String, |
b22d785c | 569 | #[serde(skip_serializing_if = "Option::is_none")] |
c06c1b4b FG |
570 | pub remote_ns: Option<BackupNamespace>, |
571 | #[serde(skip_serializing_if = "Option::is_none")] | |
e3619d41 | 572 | pub remove_vanished: Option<bool>, |
b9310489 FG |
573 | #[serde(skip_serializing_if = "Option::is_none")] |
574 | pub max_depth: Option<usize>, | |
b22d785c | 575 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 576 | pub comment: Option<String>, |
b22d785c | 577 | #[serde(skip_serializing_if = "Option::is_none")] |
e3619d41 | 578 | pub schedule: Option<String>, |
b22d785c | 579 | #[serde(skip_serializing_if = "Option::is_none")] |
062edce2 | 580 | pub group_filter: Option<Vec<GroupFilter>>, |
6eb756bc DM |
581 | #[serde(flatten)] |
582 | pub limit: RateLimitConfig, | |
9b67352a SH |
583 | #[serde(skip_serializing_if = "Option::is_none")] |
584 | pub transfer_last: Option<usize>, | |
e3619d41 DM |
585 | } |
586 | ||
83e30003 | 587 | impl SyncJobConfig { |
abd82485 FG |
588 | pub fn acl_path(&self) -> Vec<&str> { |
589 | match self.ns.as_ref() { | |
590 | Some(ns) => ns.acl_path(&self.store), | |
591 | None => vec!["datastore", &self.store], | |
83e30003 FG |
592 | } |
593 | } | |
594 | } | |
595 | ||
e3619d41 DM |
596 | #[api( |
597 | properties: { | |
598 | config: { | |
599 | type: SyncJobConfig, | |
600 | }, | |
601 | status: { | |
602 | type: JobScheduleStatus, | |
603 | }, | |
604 | }, | |
605 | )] | |
aca9222e | 606 | #[derive(Serialize, Deserialize, Clone, PartialEq)] |
b22d785c | 607 | #[serde(rename_all = "kebab-case")] |
e3619d41 DM |
608 | /// Status of Sync Job |
609 | pub struct SyncJobStatus { | |
610 | #[serde(flatten)] | |
611 | pub config: SyncJobConfig, | |
612 | #[serde(flatten)] | |
613 | pub status: JobScheduleStatus, | |
614 | } | |
5557af0e WB |
615 | |
616 | /// These are used separately without `ns`/`max-depth` sometimes in the API, specifically in the API | |
617 | /// call to prune a specific group, where `max-depth` makes no sense. | |
618 | #[api( | |
619 | properties: { | |
620 | "keep-last": { | |
621 | schema: crate::PRUNE_SCHEMA_KEEP_LAST, | |
622 | optional: true, | |
623 | }, | |
624 | "keep-hourly": { | |
625 | schema: crate::PRUNE_SCHEMA_KEEP_HOURLY, | |
626 | optional: true, | |
627 | }, | |
628 | "keep-daily": { | |
629 | schema: crate::PRUNE_SCHEMA_KEEP_DAILY, | |
630 | optional: true, | |
631 | }, | |
632 | "keep-weekly": { | |
633 | schema: crate::PRUNE_SCHEMA_KEEP_WEEKLY, | |
634 | optional: true, | |
635 | }, | |
636 | "keep-monthly": { | |
637 | schema: crate::PRUNE_SCHEMA_KEEP_MONTHLY, | |
638 | optional: true, | |
639 | }, | |
640 | "keep-yearly": { | |
641 | schema: crate::PRUNE_SCHEMA_KEEP_YEARLY, | |
642 | optional: true, | |
643 | }, | |
644 | } | |
645 | )] | |
aca9222e | 646 | #[derive(Serialize, Deserialize, Default, Updater, Clone, PartialEq)] |
5557af0e WB |
647 | #[serde(rename_all = "kebab-case")] |
648 | /// Common pruning options | |
649 | pub struct KeepOptions { | |
650 | #[serde(skip_serializing_if = "Option::is_none")] | |
651 | pub keep_last: Option<u64>, | |
652 | #[serde(skip_serializing_if = "Option::is_none")] | |
653 | pub keep_hourly: Option<u64>, | |
654 | #[serde(skip_serializing_if = "Option::is_none")] | |
655 | pub keep_daily: Option<u64>, | |
656 | #[serde(skip_serializing_if = "Option::is_none")] | |
657 | pub keep_weekly: Option<u64>, | |
658 | #[serde(skip_serializing_if = "Option::is_none")] | |
659 | pub keep_monthly: Option<u64>, | |
660 | #[serde(skip_serializing_if = "Option::is_none")] | |
661 | pub keep_yearly: Option<u64>, | |
662 | } | |
663 | ||
664 | impl KeepOptions { | |
665 | pub fn keeps_something(&self) -> bool { | |
666 | self.keep_last.unwrap_or(0) | |
667 | + self.keep_hourly.unwrap_or(0) | |
668 | + self.keep_daily.unwrap_or(0) | |
a8d3f194 | 669 | + self.keep_weekly.unwrap_or(0) |
5557af0e WB |
670 | + self.keep_monthly.unwrap_or(0) |
671 | + self.keep_yearly.unwrap_or(0) | |
672 | > 0 | |
673 | } | |
674 | } | |
675 | ||
676 | #[api( | |
677 | properties: { | |
678 | keep: { | |
679 | type: KeepOptions, | |
680 | }, | |
681 | ns: { | |
682 | type: BackupNamespace, | |
683 | optional: true, | |
684 | }, | |
685 | "max-depth": { | |
686 | schema: NS_MAX_DEPTH_REDUCED_SCHEMA, | |
687 | optional: true, | |
688 | }, | |
689 | } | |
690 | )] | |
65c9e406 | 691 | #[derive(Serialize, Deserialize, Default, Updater, Clone, PartialEq)] |
5557af0e WB |
692 | #[serde(rename_all = "kebab-case")] |
693 | /// Common pruning options | |
694 | pub struct PruneJobOptions { | |
695 | #[serde(flatten)] | |
696 | pub keep: KeepOptions, | |
697 | ||
698 | /// The (optional) recursion depth | |
699 | #[serde(skip_serializing_if = "Option::is_none")] | |
700 | pub max_depth: Option<usize>, | |
701 | ||
702 | #[serde(skip_serializing_if = "Option::is_none")] | |
703 | pub ns: Option<BackupNamespace>, | |
704 | } | |
705 | ||
706 | impl PruneJobOptions { | |
707 | pub fn keeps_something(&self) -> bool { | |
708 | self.keep.keeps_something() | |
709 | } | |
710 | ||
711 | pub fn acl_path<'a>(&'a self, store: &'a str) -> Vec<&'a str> { | |
712 | match &self.ns { | |
713 | Some(ns) => ns.acl_path(store), | |
714 | None => vec!["datastore", store], | |
715 | } | |
716 | } | |
717 | } | |
718 | ||
719 | #[api( | |
720 | properties: { | |
721 | disable: { | |
722 | type: Boolean, | |
723 | optional: true, | |
724 | default: false, | |
725 | }, | |
726 | id: { | |
727 | schema: JOB_ID_SCHEMA, | |
728 | }, | |
729 | store: { | |
730 | schema: DATASTORE_SCHEMA, | |
731 | }, | |
732 | schedule: { | |
733 | schema: PRUNE_SCHEDULE_SCHEMA, | |
5557af0e WB |
734 | }, |
735 | comment: { | |
736 | optional: true, | |
737 | schema: SINGLE_LINE_COMMENT_SCHEMA, | |
738 | }, | |
739 | options: { | |
740 | type: PruneJobOptions, | |
741 | }, | |
742 | }, | |
743 | )] | |
65c9e406 | 744 | #[derive(Deserialize, Serialize, Updater, Clone, PartialEq)] |
5557af0e WB |
745 | #[serde(rename_all = "kebab-case")] |
746 | /// Prune configuration. | |
747 | pub struct PruneJobConfig { | |
748 | /// unique ID to address this job | |
749 | #[updater(skip)] | |
750 | pub id: String, | |
751 | ||
752 | pub store: String, | |
753 | ||
754 | /// Disable this job. | |
755 | #[serde(default, skip_serializing_if = "is_false")] | |
756 | #[updater(serde(skip_serializing_if = "Option::is_none"))] | |
757 | pub disable: bool, | |
758 | ||
759 | pub schedule: String, | |
760 | ||
761 | #[serde(skip_serializing_if = "Option::is_none")] | |
762 | pub comment: Option<String>, | |
763 | ||
764 | #[serde(flatten)] | |
765 | pub options: PruneJobOptions, | |
766 | } | |
767 | ||
768 | impl PruneJobConfig { | |
769 | pub fn acl_path(&self) -> Vec<&str> { | |
770 | self.options.acl_path(&self.store) | |
771 | } | |
772 | } | |
773 | ||
774 | fn is_false(b: &bool) -> bool { | |
775 | !b | |
776 | } | |
777 | ||
778 | #[api( | |
779 | properties: { | |
780 | config: { | |
781 | type: PruneJobConfig, | |
782 | }, | |
783 | status: { | |
784 | type: JobScheduleStatus, | |
785 | }, | |
786 | }, | |
787 | )] | |
65c9e406 | 788 | #[derive(Serialize, Deserialize, Clone, PartialEq)] |
5557af0e WB |
789 | #[serde(rename_all = "kebab-case")] |
790 | /// Status of prune job | |
791 | pub struct PruneJobStatus { | |
792 | #[serde(flatten)] | |
793 | pub config: PruneJobConfig, | |
794 | #[serde(flatten)] | |
795 | pub status: JobScheduleStatus, | |
796 | } |