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 if let Some(decoded) = base32::decode(base32::Alphabet::Z, &self.secret_key) {
57 self.secret_key = database.decrypt(decoded).await?;
58 }
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 if let Some(decoded) = base32::decode(base32::Alphabet::Z, value) {
123 *value = database.decrypt(decoded).await?;
124 }
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 pub shared: bool,
195
196 pub backup_disk: super::server_backup::BackupDisk,
197 pub backup_configs: BackupConfigs,
198
199 pub created: chrono::NaiveDateTime,
200
201 extension_data: super::ModelExtensionData,
202}
203
204impl BaseModel for BackupConfiguration {
205 const NAME: &'static str = "backup_configuration";
206
207 fn get_extension_list() -> &'static super::ModelExtensionList {
208 static EXTENSIONS: LazyLock<super::ModelExtensionList> =
209 LazyLock::new(|| std::sync::RwLock::new(Vec::new()));
210
211 &EXTENSIONS
212 }
213
214 fn get_extension_data(&self) -> &super::ModelExtensionData {
215 &self.extension_data
216 }
217
218 #[inline]
219 fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
220 let prefix = prefix.unwrap_or_default();
221
222 BTreeMap::from([
223 (
224 "backup_configurations.uuid",
225 compact_str::format_compact!("{prefix}uuid"),
226 ),
227 (
228 "backup_configurations.name",
229 compact_str::format_compact!("{prefix}name"),
230 ),
231 (
232 "backup_configurations.description",
233 compact_str::format_compact!("{prefix}description"),
234 ),
235 (
236 "backup_configurations.maintenance_enabled",
237 compact_str::format_compact!("{prefix}maintenance_enabled"),
238 ),
239 (
240 "backup_configurations.shared",
241 compact_str::format_compact!("{prefix}shared"),
242 ),
243 (
244 "backup_configurations.backup_disk",
245 compact_str::format_compact!("{prefix}backup_disk"),
246 ),
247 (
248 "backup_configurations.backup_configs",
249 compact_str::format_compact!("{prefix}backup_configs"),
250 ),
251 (
252 "backup_configurations.created",
253 compact_str::format_compact!("{prefix}created"),
254 ),
255 ])
256 }
257
258 #[inline]
259 fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
260 let prefix = prefix.unwrap_or_default();
261
262 Ok(Self {
263 uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
264 name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
265 description: row
266 .try_get(compact_str::format_compact!("{prefix}description").as_str())?,
267 maintenance_enabled: row
268 .try_get(compact_str::format_compact!("{prefix}maintenance_enabled").as_str())?,
269 shared: row.try_get(compact_str::format_compact!("{prefix}shared").as_str())?,
270 backup_disk: row
271 .try_get(compact_str::format_compact!("{prefix}backup_disk").as_str())?,
272 backup_configs: serde_json::from_value(
273 row.get(compact_str::format_compact!("{prefix}backup_configs").as_str()),
274 )
275 .unwrap_or_default(),
276 created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
277 extension_data: Self::map_extensions(prefix, row)?,
278 })
279 }
280}
281
282impl BackupConfiguration {
283 pub async fn all_with_pagination(
284 database: &crate::database::Database,
285 page: i64,
286 per_page: i64,
287 search: Option<&str>,
288 ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
289 let offset = (page - 1) * per_page;
290
291 let rows = sqlx::query(&format!(
292 r#"
293 SELECT {}, COUNT(*) OVER() AS total_count
294 FROM backup_configurations
295 WHERE $1 IS NULL OR backup_configurations.name ILIKE '%' || $1 || '%'
296 ORDER BY backup_configurations.created
297 LIMIT $2 OFFSET $3
298 "#,
299 Self::columns_sql(None)
300 ))
301 .bind(search)
302 .bind(per_page)
303 .bind(offset)
304 .fetch_all(database.read())
305 .await?;
306
307 Ok(super::Pagination {
308 total: rows
309 .first()
310 .map_or(Ok(0), |row| row.try_get("total_count"))?,
311 per_page,
312 page,
313 data: rows
314 .into_iter()
315 .map(|row| Self::map(None, &row))
316 .try_collect_vec()?,
317 })
318 }
319}
320
321#[async_trait::async_trait]
322impl IntoAdminApiObject for BackupConfiguration {
323 type AdminApiObject = AdminApiBackupConfiguration;
324 type ExtraArgs<'a> = ();
325
326 async fn into_admin_api_object<'a>(
327 mut self,
328 state: &crate::State,
329 _args: Self::ExtraArgs<'a>,
330 ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
331 let api_object = AdminApiBackupConfiguration::init_hooks(&self, state).await?;
332
333 self.backup_configs.decrypt(&state.database).await?;
334
335 let api_object = finish_extendible!(
336 AdminApiBackupConfiguration {
337 uuid: self.uuid,
338 name: self.name,
339 description: self.description,
340 maintenance_enabled: self.maintenance_enabled,
341 shared: self.shared,
342 backup_disk: self.backup_disk,
343 backup_configs: self.backup_configs,
344 created: self.created.and_utc(),
345 },
346 api_object,
347 state
348 )?;
349
350 Ok(api_object)
351 }
352}
353
354#[async_trait::async_trait]
355impl ByUuid for BackupConfiguration {
356 async fn by_uuid(
357 database: &crate::database::Database,
358 uuid: uuid::Uuid,
359 ) -> Result<Self, crate::database::DatabaseError> {
360 let row = sqlx::query(&format!(
361 r#"
362 SELECT {}
363 FROM backup_configurations
364 WHERE backup_configurations.uuid = $1
365 "#,
366 Self::columns_sql(None)
367 ))
368 .bind(uuid)
369 .fetch_one(database.read())
370 .await?;
371
372 Self::map(None, &row)
373 }
374
375 async fn by_uuid_with_transaction(
376 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
377 uuid: uuid::Uuid,
378 ) -> Result<Self, crate::database::DatabaseError> {
379 let row = sqlx::query(&format!(
380 r#"
381 SELECT {}
382 FROM backup_configurations
383 WHERE backup_configurations.uuid = $1
384 "#,
385 Self::columns_sql(None)
386 ))
387 .bind(uuid)
388 .fetch_one(&mut **transaction)
389 .await?;
390
391 Self::map(None, &row)
392 }
393}
394
395#[derive(ToSchema, Deserialize, Validate)]
396pub struct CreateBackupConfigurationOptions {
397 #[garde(length(chars, min = 1, max = 255))]
398 #[schema(min_length = 1, max_length = 255)]
399 pub name: compact_str::CompactString,
400 #[garde(length(chars, min = 1, max = 1024))]
401 #[schema(min_length = 1, max_length = 1024)]
402 pub description: Option<compact_str::CompactString>,
403 #[garde(skip)]
404 pub maintenance_enabled: bool,
405 #[garde(skip)]
406 pub shared: bool,
407 #[garde(skip)]
408 pub backup_disk: super::server_backup::BackupDisk,
409 #[garde(dive)]
410 pub backup_configs: BackupConfigs,
411}
412
413#[async_trait::async_trait]
414impl CreatableModel for BackupConfiguration {
415 type CreateOptions<'a> = CreateBackupConfigurationOptions;
416 type CreateResult = Self;
417
418 fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
419 static CREATE_LISTENERS: LazyLock<CreateListenerList<BackupConfiguration>> =
420 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
421
422 &CREATE_LISTENERS
423 }
424
425 async fn create_with_transaction(
426 state: &crate::State,
427 mut options: Self::CreateOptions<'_>,
428 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
429 ) -> Result<Self, crate::database::DatabaseError> {
430 options.validate()?;
431
432 let mut query_builder = InsertQueryBuilder::new("backup_configurations");
433
434 Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
435
436 options.backup_configs.encrypt(&state.database).await?;
437
438 query_builder
439 .set("name", &options.name)
440 .set("description", &options.description)
441 .set("maintenance_enabled", options.maintenance_enabled)
442 .set("shared", options.shared)
443 .set("backup_disk", options.backup_disk)
444 .set(
445 "backup_configs",
446 serde_json::to_value(&options.backup_configs)?,
447 );
448
449 let row = query_builder
450 .returning(&Self::columns_sql(None))
451 .fetch_one(&mut **transaction)
452 .await?;
453 let mut backup_configuration = Self::map(None, &row)?;
454
455 Self::run_after_create_handlers(&mut backup_configuration, &options, state, transaction)
456 .await?;
457
458 Ok(backup_configuration)
459 }
460}
461
462#[derive(ToSchema, Serialize, Deserialize, Validate, Clone, Default)]
463pub struct UpdateBackupConfigurationOptions {
464 #[garde(length(chars, min = 1, max = 255))]
465 #[schema(min_length = 1, max_length = 255)]
466 pub name: Option<compact_str::CompactString>,
467 #[garde(length(chars, min = 1, max = 1024))]
468 #[schema(min_length = 1, max_length = 1024)]
469 #[serde(
470 default,
471 skip_serializing_if = "Option::is_none",
472 with = "::serde_with::rust::double_option"
473 )]
474 pub description: Option<Option<compact_str::CompactString>>,
475 #[garde(skip)]
476 pub maintenance_enabled: Option<bool>,
477 #[garde(skip)]
478 pub shared: Option<bool>,
479 #[garde(skip)]
480 pub backup_disk: Option<super::server_backup::BackupDisk>,
481 #[garde(dive)]
482 pub backup_configs: Option<BackupConfigs>,
483}
484
485#[async_trait::async_trait]
486impl UpdatableModel for BackupConfiguration {
487 type UpdateOptions = UpdateBackupConfigurationOptions;
488
489 fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
490 static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<BackupConfiguration>> =
491 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
492
493 &UPDATE_LISTENERS
494 }
495
496 async fn update_with_transaction(
497 &mut self,
498 state: &crate::State,
499 mut options: Self::UpdateOptions,
500 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
501 ) -> Result<(), crate::database::DatabaseError> {
502 options.validate()?;
503
504 let mut query_builder = UpdateQueryBuilder::new("backup_configurations");
505
506 self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
507 .await?;
508
509 query_builder
510 .set("name", options.name.as_ref())
511 .set(
512 "description",
513 options.description.as_ref().map(|d| d.as_ref()),
514 )
515 .set("maintenance_enabled", options.maintenance_enabled)
516 .set("shared", options.shared)
517 .set("backup_disk", options.backup_disk)
518 .set(
519 "backup_configs",
520 if let Some(backup_configs) = &mut options.backup_configs {
521 backup_configs.encrypt(&state.database).await?;
522
523 Some(serde_json::to_value(backup_configs)?)
524 } else {
525 None
526 },
527 )
528 .where_eq("uuid", self.uuid);
529
530 query_builder.execute(&mut **transaction).await?;
531
532 if let Some(name) = options.name {
533 self.name = name;
534 }
535 if let Some(description) = options.description {
536 self.description = description;
537 }
538 if let Some(maintenance_enabled) = options.maintenance_enabled {
539 self.maintenance_enabled = maintenance_enabled;
540 }
541 if let Some(shared) = options.shared {
542 self.shared = shared;
543 }
544 if let Some(backup_disk) = options.backup_disk {
545 self.backup_disk = backup_disk;
546 }
547 if let Some(backup_configs) = options.backup_configs {
548 self.backup_configs = backup_configs;
549 }
550
551 self.run_after_update_handlers(state, transaction).await?;
552
553 Ok(())
554 }
555}
556
557#[async_trait::async_trait]
558impl DeletableModel for BackupConfiguration {
559 type DeleteOptions = ();
560
561 fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
562 static DELETE_LISTENERS: LazyLock<DeleteHandlerList<BackupConfiguration>> =
563 LazyLock::new(|| Arc::new(ModelHandlerList::default()));
564
565 &DELETE_LISTENERS
566 }
567
568 async fn delete_with_transaction(
569 &self,
570 state: &crate::State,
571 options: Self::DeleteOptions,
572 transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
573 ) -> Result<(), anyhow::Error> {
574 self.run_delete_handlers(&options, state, transaction)
575 .await?;
576
577 sqlx::query(
578 r#"
579 DELETE FROM backup_configurations
580 WHERE backup_configurations.uuid = $1
581 "#,
582 )
583 .bind(self.uuid)
584 .execute(&mut **transaction)
585 .await?;
586
587 self.run_after_delete_handlers(&options, state, transaction)
588 .await?;
589
590 Ok(())
591 }
592}
593
594#[schema_extension_derive::extendible]
595#[init_args(BackupConfiguration, crate::State)]
596#[hook_args(crate::State)]
597#[derive(ToSchema, Serialize)]
598#[schema(title = "BackupConfiguration")]
599pub struct AdminApiBackupConfiguration {
600 pub uuid: uuid::Uuid,
601
602 pub name: compact_str::CompactString,
603 pub description: Option<compact_str::CompactString>,
604
605 pub maintenance_enabled: bool,
606 pub shared: bool,
607
608 pub backup_disk: super::server_backup::BackupDisk,
609 pub backup_configs: BackupConfigs,
610
611 pub created: chrono::DateTime<chrono::Utc>,
612}