1use crate::{
2 models::{InsertQueryBuilder, UpdateQueryBuilder},
3 prelude::*,
4};
5use garde::Validate;
6use indexmap::IndexMap;
7use serde::{Deserialize, Serialize};
8use sqlx::{Row, postgres::PgRow};
9use std::{
10 collections::BTreeMap,
11 sync::{Arc, LazyLock},
12};
13use utoipa::ToSchema;
14
15#[derive(ToSchema, Serialize, Deserialize, Validate, Clone)]
16pub struct BackupConfigsS3 {
17 #[garde(length(chars, min = 1, max = 255))]
18 #[schema(min_length = 1, max_length = 255)]
19 pub access_key: compact_str::CompactString,
20 #[garde(length(chars, min = 1, max = 255))]
21 #[schema(min_length = 1, max_length = 255)]
22 pub secret_key: compact_str::CompactString,
23 #[garde(length(chars, min = 1, max = 255))]
24 #[schema(min_length = 1, max_length = 255)]
25 pub bucket: compact_str::CompactString,
26 #[garde(length(chars, min = 1, max = 255))]
27 #[schema(min_length = 1, max_length = 255)]
28 pub region: compact_str::CompactString,
29 #[garde(length(chars, min = 1, max = 255), url)]
30 #[schema(min_length = 1, max_length = 255, format = "uri")]
31 pub endpoint: compact_str::CompactString,
32 #[garde(skip)]
33 pub path_style: bool,
34 #[garde(skip)]
35 pub part_size: u64,
36}
37
38impl BackupConfigsS3 {
39 pub async fn encrypt(
40 &mut self,
41 database: &crate::database::Database,
42 ) -> Result<(), anyhow::Error> {
43 self.secret_key = base32::encode(
44 base32::Alphabet::Z,
45 &database.encrypt(self.secret_key.clone()).await?,
46 )
47 .into();
48
49 Ok(())
50 }
51
52 pub async fn decrypt(
53 &mut self,
54 database: &crate::database::Database,
55 ) -> Result<(), anyhow::Error> {
56 self.secret_key = database
57 .decrypt(base32::decode(base32::Alphabet::Z, &self.secret_key).unwrap())
58 .await?;
59
60 Ok(())
61 }
62
63 pub fn censor(&mut self) {
64 self.secret_key = "".into();
65 }
66
67 pub fn into_client(self) -> Result<Box<s3::Bucket>, s3::error::S3Error> {
68 let mut bucket = s3::Bucket::new(
69 &self.bucket,
70 s3::Region::Custom {
71 region: self.region.into(),
72 endpoint: self.endpoint.into(),
73 },
74 s3::creds::Credentials::new(
75 Some(&self.access_key),
76 Some(&self.secret_key),
77 None,
78 None,
79 None,
80 )
81 .unwrap(),
82 )?;
83
84 if self.path_style {
85 bucket.set_path_style();
86 }
87
88 Ok(bucket)
89 }
90}
91
92#[derive(ToSchema, Serialize, Deserialize, Validate, Clone)]
93pub struct BackupConfigsRestic {
94 #[garde(length(chars, min = 3, max = 255))]
95 #[schema(min_length = 3, max_length = 255)]
96 pub repository: compact_str::CompactString,
97 #[garde(skip)]
98 pub retry_lock_seconds: u64,
99
100 #[garde(skip)]
101 pub environment: IndexMap<compact_str::CompactString, compact_str::CompactString>,
102}
103
104impl BackupConfigsRestic {
105 pub async fn encrypt(
106 &mut self,
107 database: &crate::database::Database,
108 ) -> Result<(), anyhow::Error> {
109 for value in self.environment.values_mut() {
110 *value =
111 base32::encode(base32::Alphabet::Z, &database.encrypt(value.clone()).await?).into();
112 }
113
114 Ok(())
115 }
116
117 pub async fn decrypt(
118 &mut self,
119 database: &crate::database::Database,
120 ) -> Result<(), anyhow::Error> {
121 for value in self.environment.values_mut() {
122 *value = database
123 .decrypt(base32::decode(base32::Alphabet::Z, value).unwrap())
124 .await?;
125 }
126
127 Ok(())
128 }
129
130 pub fn censor(&mut self) {
131 for (key, value) in self.environment.iter_mut() {
132 if key == "RESTIC_PASSWORD" || key == "AWS_SECRET_ACCESS_KEY" {
133 *value = "".into();
134 }
135 }
136 }
137}
138
139#[derive(ToSchema, Serialize, Deserialize, Default, Validate, Clone)]
140pub struct BackupConfigs {
141 #[garde(dive)]
142 pub s3: Option<BackupConfigsS3>,
143 #[garde(dive)]
144 pub restic: Option<BackupConfigsRestic>,
145}
146
147impl BackupConfigs {
148 pub async fn encrypt(
149 &mut self,
150 database: &crate::database::Database,
151 ) -> Result<(), anyhow::Error> {
152 if let Some(s3) = &mut self.s3 {
153 s3.encrypt(database).await?;
154 }
155 if let Some(restic) = &mut self.restic {
156 restic.encrypt(database).await?;
157 }
158
159 Ok(())
160 }
161
162 pub async fn decrypt(
163 &mut self,
164 database: &crate::database::Database,
165 ) -> Result<(), anyhow::Error> {
166 if let Some(s3) = &mut self.s3 {
167 s3.decrypt(database).await?;
168 }
169 if let Some(restic) = &mut self.restic {
170 restic.decrypt(database).await?;
171 }
172
173 Ok(())
174 }
175
176 pub fn censor(&mut self) {
177 if let Some(s3) = &mut self.s3 {
178 s3.censor();
179 }
180 if let Some(restic) = &mut self.restic {
181 restic.censor();
182 }
183 }
184}
185
186#[derive(Serialize, Deserialize, Clone)]
187pub struct BackupConfiguration {
188 pub uuid: uuid::Uuid,
189
190 pub name: compact_str::CompactString,
191 pub description: Option<compact_str::CompactString>,
192
193 pub maintenance_enabled: bool,
194
195 pub backup_disk: super::server_backup::BackupDisk,
196 pub backup_configs: BackupConfigs,
197
198 pub created: chrono::NaiveDateTime,
199}
200
201impl BaseModel for BackupConfiguration {
202 const NAME: &'static str = "backup_configuration";
203
204 #[inline]
205 fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
206 let prefix = prefix.unwrap_or_default();
207
208 BTreeMap::from([
209 (
210 "backup_configurations.uuid",
211 compact_str::format_compact!("{prefix}uuid"),
212 ),
213 (
214 "backup_configurations.name",
215 compact_str::format_compact!("{prefix}name"),
216 ),
217 (
218 "backup_configurations.description",
219 compact_str::format_compact!("{prefix}description"),
220 ),
221 (
222 "backup_configurations.maintenance_enabled",
223 compact_str::format_compact!("{prefix}maintenance_enabled"),
224 ),
225 (
226 "backup_configurations.backup_disk",
227 compact_str::format_compact!("{prefix}backup_disk"),
228 ),
229 (
230 "backup_configurations.backup_configs",
231 compact_str::format_compact!("{prefix}backup_configs"),
232 ),
233 (
234 "backup_configurations.created",
235 compact_str::format_compact!("{prefix}created"),
236 ),
237 ])
238 }
239
240 #[inline]
241 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
242 let prefix = prefix.unwrap_or_default();
243
244 Ok(Self {
245 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
246 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
247 description: row
248 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
249 maintenance_enabled: row
250 .try_get(compact_str::format_compact!("{prefix}maintenance_enabled").as_str())?,
251 backup_disk: row
252 .try_get(compact_str::format_compact!("{prefix}backup_disk").as_str())?,
253 backup_configs: serde_json::from_value(
254 row.get(compact_str::format_compact!("{prefix}backup_configs").as_str()),
255 )
256 .unwrap_or_default(),
257 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
258 })
259 }
260}
261
262impl BackupConfiguration {
263 pub async fn all_with_pagination(
264 database: &crate::database::Database,
265 page: i64,
266 per_page: i64,
267 search: Option<&str>,
268 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
269 let offset = (page - 1) * per_page;
270
271 let rows = sqlx::query(&format!(
272 r#"
273 SELECT {}, COUNT(*) OVER() AS total_count
274 FROM backup_configurations
275 WHERE $1 IS NULL OR backup_configurations.name ILIKE '%' || $1 || '%'
276 ORDER BY backup_configurations.created
277 LIMIT $2 OFFSET $3
278 "#,
279 Self::columns_sql(None)
280 ))
281 .bind(search)
282 .bind(per_page)
283 .bind(offset)
284 .fetch_all(database.read())
285 .await?;
286
287 Ok(super::Pagination {
288 total: rows
289 .first()
290 .map_or(Ok(0), |row| row.try_get("total_count"))?,
291 per_page,
292 page,
293 data: rows
294 .into_iter()
295 .map(|row| Self::map(None, &row))
296 .try_collect_vec()?,
297 })
298 }
299
300 #[inline]
301 pub async fn into_admin_api_object(
302 mut self,
303 database: &crate::database::Database,
304 ) -> Result<AdminApiBackupConfiguration, crate::database::DatabaseError> {
305 self.backup_configs.decrypt(database).await?;
306
307 Ok(AdminApiBackupConfiguration {
308 uuid: self.uuid,
309 name: self.name,
310 maintenance_enabled: self.maintenance_enabled,
311 description: self.description,
312 backup_disk: self.backup_disk,
313 backup_configs: self.backup_configs,
314 created: self.created.and_utc(),
315 })
316 }
317}
318
319#[async_trait::async_trait]
320impl ByUuid for BackupConfiguration {
321 async fn by_uuid(
322 database: &crate::database::Database,
323 uuid: uuid::Uuid,
324 ) -> Result<Self, crate::database::DatabaseError> {
325 let row = sqlx::query(&format!(
326 r#"
327 SELECT {}
328 FROM backup_configurations
329 WHERE backup_configurations.uuid = $1
330 "#,
331 Self::columns_sql(None)
332 ))
333 .bind(uuid)
334 .fetch_one(database.read())
335 .await?;
336
337 Self::map(None, &row)
338 }
339}
340
341#[derive(ToSchema, Deserialize, Validate)]
342pub struct CreateBackupConfigurationOptions {
343 #[garde(length(chars, min = 3, max = 255))]
344 #[schema(min_length = 3, max_length = 255)]
345 pub name: compact_str::CompactString,
346 #[garde(length(chars, min = 1, max = 1024))]
347 #[schema(min_length = 1, max_length = 1024)]
348 pub description: Option<compact_str::CompactString>,
349 #[garde(skip)]
350 pub maintenance_enabled: bool,
351 #[garde(skip)]
352 pub backup_disk: super::server_backup::BackupDisk,
353 #[garde(dive)]
354 pub backup_configs: BackupConfigs,
355}
356
357#[async_trait::async_trait]
358impl CreatableModel for BackupConfiguration {
359 type CreateOptions<'a> = CreateBackupConfigurationOptions;
360 type CreateResult = Self;
361
362 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
363 static CREATE_LISTENERS: LazyLock<CreateListenerList<BackupConfiguration>> =
364 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
365
366 &CREATE_LISTENERS
367 }
368
369 async fn create(
370 state: &crate::State,
371 mut options: Self::CreateOptions<'_>,
372 ) -> Result<Self, crate::database::DatabaseError> {
373 options.validate()?;
374
375 let mut transaction = state.database.write().begin().await?;
376
377 let mut query_builder = InsertQueryBuilder::new("backup_configurations");
378
379 Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
380 .await?;
381
382 options.backup_configs.encrypt(&state.database).await?;
383
384 query_builder
385 .set("name", &options.name)
386 .set("description", &options.description)
387 .set("maintenance_enabled", options.maintenance_enabled)
388 .set("backup_disk", options.backup_disk)
389 .set(
390 "backup_configs",
391 serde_json::to_value(&options.backup_configs)?,
392 );
393
394 let row = query_builder
395 .returning(&Self::columns_sql(None))
396 .fetch_one(&mut *transaction)
397 .await?;
398 let backup_configuration = Self::map(None, &row)?;
399
400 transaction.commit().await?;
401
402 Ok(backup_configuration)
403 }
404}
405
406#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
407pub struct UpdateBackupConfigurationOptions {
408 #[garde(length(chars, min = 3, max = 255))]
409 #[schema(min_length = 3, max_length = 255)]
410 pub name: Option<compact_str::CompactString>,
411 #[garde(length(chars, min = 1, max = 1024))]
412 #[schema(min_length = 1, max_length = 1024)]
413 #[serde(
414 default,
415 skip_serializing_if = "Option::is_none",
416 with = "::serde_with::rust::double_option"
417 )]
418 pub description: Option<Option<compact_str::CompactString>>,
419 #[garde(skip)]
420 pub maintenance_enabled: Option<bool>,
421 #[garde(skip)]
422 pub backup_disk: Option<super::server_backup::BackupDisk>,
423 #[garde(dive)]
424 pub backup_configs: Option<BackupConfigs>,
425}
426
427#[async_trait::async_trait]
428impl UpdatableModel for BackupConfiguration {
429 type UpdateOptions = UpdateBackupConfigurationOptions;
430
431 fn get_update_handlers() -> &'static LazyLock<UpdateListenerList<Self>> {
432 static UPDATE_LISTENERS: LazyLock<UpdateListenerList<BackupConfiguration>> =
433 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
434
435 &UPDATE_LISTENERS
436 }
437
438 async fn update(
439 &mut self,
440 state: &crate::State,
441 mut options: Self::UpdateOptions,
442 ) -> Result<(), crate::database::DatabaseError> {
443 options.validate()?;
444
445 let mut transaction = state.database.write().begin().await?;
446
447 let mut query_builder = UpdateQueryBuilder::new("backup_configurations");
448
449 Self::run_update_handlers(
450 self,
451 &mut options,
452 &mut query_builder,
453 state,
454 &mut transaction,
455 )
456 .await?;
457
458 query_builder
459 .set("name", options.name.as_ref())
460 .set(
461 "description",
462 options.description.as_ref().map(|d| d.as_ref()),
463 )
464 .set("maintenance_enabled", options.maintenance_enabled)
465 .set("backup_disk", options.backup_disk)
466 .set(
467 "backup_configs",
468 if let Some(backup_configs) = &mut options.backup_configs {
469 backup_configs.encrypt(&state.database).await?;
470
471 Some(serde_json::to_value(backup_configs)?)
472 } else {
473 None
474 },
475 )
476 .where_eq("uuid", self.uuid);
477
478 query_builder.execute(&mut *transaction).await?;
479
480 if let Some(name) = options.name {
481 self.name = name;
482 }
483 if let Some(description) = options.description {
484 self.description = description;
485 }
486 if let Some(maintenance_enabled) = options.maintenance_enabled {
487 self.maintenance_enabled = maintenance_enabled;
488 }
489 if let Some(backup_disk) = options.backup_disk {
490 self.backup_disk = backup_disk;
491 }
492 if let Some(backup_configs) = options.backup_configs {
493 self.backup_configs = backup_configs;
494 }
495
496 transaction.commit().await?;
497
498 Ok(())
499 }
500}
501
502#[async_trait::async_trait]
503impl DeletableModel for BackupConfiguration {
504 type DeleteOptions = ();
505
506 fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
507 static DELETE_LISTENERS: LazyLock<DeleteListenerList<BackupConfiguration>> =
508 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
509
510 &DELETE_LISTENERS
511 }
512
513 async fn delete(
514 &self,
515 state: &crate::State,
516 options: Self::DeleteOptions,
517 ) -> Result<(), anyhow::Error> {
518 let mut transaction = state.database.write().begin().await?;
519
520 self.run_delete_handlers(&options, state, &mut transaction)
521 .await?;
522
523 sqlx::query(
524 r#"
525 DELETE FROM backup_configurations
526 WHERE backup_configurations.uuid = $1
527 "#,
528 )
529 .bind(self.uuid)
530 .execute(&mut *transaction)
531 .await?;
532
533 transaction.commit().await?;
534
535 Ok(())
536 }
537}
538
539#[derive(ToSchema, Serialize)]
540#[schema(title = "BackupConfiguration")]
541pub struct AdminApiBackupConfiguration {
542 pub uuid: uuid::Uuid,
543
544 pub name: compact_str::CompactString,
545 pub maintenance_enabled: bool,
546 pub description: Option<compact_str::CompactString>,
547
548 pub backup_disk: super::server_backup::BackupDisk,
549 pub backup_configs: BackupConfigs,
550
551 pub created: chrono::DateTime<chrono::Utc>,
552}