Skip to main content

shared/models/
backup_configuration.rs

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}