Skip to main content

shared/models/
server_backup_group.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder, server_backup::retention::BackupRetention},
3    prelude::*,
4};
5use garde::Validate;
6use serde::{Deserialize, Serialize};
7use sqlx::{Row, postgres::PgRow};
8use std::{
9    collections::BTreeMap,
10    sync::{Arc, LazyLock},
11};
12use utoipa::ToSchema;
13
14#[derive(Serialize, Deserialize, Clone)]
15pub struct ServerBackupGroup {
16    pub uuid: uuid::Uuid,
17    pub server_uuid: uuid::Uuid,
18
19    pub name: compact_str::CompactString,
20    pub order: i16,
21    pub retention: BackupRetention,
22
23    pub created: chrono::NaiveDateTime,
24
25    extension_data: super::ModelExtensionData,
26}
27
28impl BaseModel for ServerBackupGroup {
29    const NAME: &'static str = "server_backup_group";
30
31    fn get_extension_list() -> &'static super::ModelExtensionList {
32        static EXTENSIONS: LazyLock<super::ModelExtensionList> =
33            LazyLock::new(|| parking_lot::RwLock::new(Vec::new()));
34
35        &EXTENSIONS
36    }
37
38    fn get_extension_data(&self) -> &super::ModelExtensionData {
39        &self.extension_data
40    }
41
42    #[inline]
43    fn base_columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
44        let prefix = prefix.unwrap_or_default();
45
46        BTreeMap::from([
47            (
48                "server_backup_groups.uuid",
49                compact_str::format_compact!("{prefix}uuid"),
50            ),
51            (
52                "server_backup_groups.server_uuid",
53                compact_str::format_compact!("{prefix}server_uuid"),
54            ),
55            (
56                "server_backup_groups.name",
57                compact_str::format_compact!("{prefix}name"),
58            ),
59            (
60                "server_backup_groups.order_",
61                compact_str::format_compact!("{prefix}order"),
62            ),
63            (
64                "server_backup_groups.retention",
65                compact_str::format_compact!("{prefix}retention"),
66            ),
67            (
68                "server_backup_groups.created",
69                compact_str::format_compact!("{prefix}created"),
70            ),
71        ])
72    }
73
74    #[inline]
75    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
76        let prefix = prefix.unwrap_or_default();
77
78        Ok(Self {
79            uuid: row.try_get(compact_str::format_compact!("{prefix}uuid").as_str())?,
80            server_uuid: row
81                .try_get(compact_str::format_compact!("{prefix}server_uuid").as_str())?,
82            name: row.try_get(compact_str::format_compact!("{prefix}name").as_str())?,
83            order: row.try_get(compact_str::format_compact!("{prefix}order").as_str())?,
84            retention: serde_json::from_value(
85                row.try_get(compact_str::format_compact!("{prefix}retention").as_str())?,
86            )?,
87            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
88            extension_data: Self::map_extensions(prefix, row)?,
89        })
90    }
91
92    fn cache_invalidation_keys(&self) -> Vec<compact_str::CompactString> {
93        vec![compact_str::format_compact!(
94            "{}::{}",
95            Self::NAME,
96            self.uuid
97        )]
98    }
99}
100
101impl ServerBackupGroup {
102    pub async fn by_server_uuid_uuid(
103        database: &crate::database::Database,
104        server_uuid: uuid::Uuid,
105        uuid: uuid::Uuid,
106    ) -> Result<Option<Self>, crate::database::DatabaseError> {
107        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
108            r#"
109            SELECT {}
110            FROM server_backup_groups
111            WHERE server_backup_groups.server_uuid = $1 AND server_backup_groups.uuid = $2
112            "#,
113            Self::columns_sql(None)
114        )))
115        .bind(server_uuid)
116        .bind(uuid)
117        .fetch_optional(database.read())
118        .await?;
119
120        row.try_map(|row| Self::map(None, &row))
121    }
122
123    pub async fn by_server_uuid_with_pagination(
124        database: &crate::database::Database,
125        server_uuid: uuid::Uuid,
126        page: i64,
127        per_page: i64,
128        search: Option<&str>,
129    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
130        super::PaginatedQueryBuilder::new(
131            Self::columns_sql(None),
132            format!(
133                r#"
134                FROM server_backup_groups
135                WHERE server_backup_groups.server_uuid = $1 AND {search}
136                "#,
137                search = super::search_sql(
138                    2,
139                    &["server_backup_groups.name"],
140                    &["server_backup_groups.uuid"]
141                ),
142            ),
143            "server_backup_groups.order_, server_backup_groups.created, server_backup_groups.uuid",
144        )
145        .bind(server_uuid)
146        .bind(search)
147        .fetch(database, page, per_page)
148        .await
149    }
150
151    pub async fn all_by_server_uuid(
152        database: &crate::database::Database,
153        server_uuid: uuid::Uuid,
154    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
155        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
156            r#"
157            SELECT {}
158            FROM server_backup_groups
159            WHERE server_backup_groups.server_uuid = $1
160            ORDER BY server_backup_groups.order_, server_backup_groups.created, server_backup_groups.uuid
161            "#,
162            Self::columns_sql(None)
163        )))
164        .bind(server_uuid)
165        .fetch_all(database.read())
166        .await?;
167
168        rows.into_iter()
169            .map(|row| Self::map(None, &row))
170            .try_collect_vec()
171    }
172
173    pub async fn count_by_server_uuid(
174        database: &crate::database::Database,
175        server_uuid: uuid::Uuid,
176    ) -> Result<i64, sqlx::Error> {
177        sqlx::query_scalar(
178            r#"
179            SELECT COUNT(*)
180            FROM server_backup_groups
181            WHERE server_backup_groups.server_uuid = $1
182            "#,
183        )
184        .bind(server_uuid)
185        .fetch_one(database.read())
186        .await
187    }
188
189    /// (total_backups, usable_backups, usable_unlocked_backups)
190    async fn backup_counts(
191        &self,
192        database: &crate::database::Database,
193    ) -> Result<(i64, i64, i64), crate::database::DatabaseError> {
194        let row = sqlx::query!(
195            r#"
196            SELECT
197                COUNT(*) AS "total!",
198                COUNT(*) FILTER (
199                    WHERE server_backups.successful AND server_backups.completed IS NOT NULL
200                ) AS "usable!",
201                COUNT(*) FILTER (
202                    WHERE server_backups.successful
203                        AND server_backups.completed IS NOT NULL
204                        AND NOT server_backups.locked
205                ) AS "usable_unlocked!"
206            FROM server_backups
207            WHERE server_backups.backup_group_uuid = $1 AND server_backups.deleted IS NULL
208            "#,
209            self.uuid,
210        )
211        .fetch_one(database.read())
212        .await?;
213
214        Ok((row.total, row.usable, row.usable_unlocked))
215    }
216}
217
218#[async_trait::async_trait]
219impl IntoApiObject for ServerBackupGroup {
220    type ApiObject = ApiServerBackupGroup;
221    type ExtraArgs<'a> = ();
222
223    async fn into_api_object<'a>(
224        self,
225        state: &crate::State,
226        _args: Self::ExtraArgs<'a>,
227    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
228        let (total_backups, usable_backups, usable_unlocked_backups) =
229            self.backup_counts(&state.database).await?;
230
231        let api_object = ApiServerBackupGroup::init_hooks(&self, state).await?;
232
233        let api_object = finish_extendible!(
234            ApiServerBackupGroup {
235                uuid: self.uuid,
236                name: self.name,
237                order: self.order,
238                retention: self.retention,
239                total_backups,
240                usable_backups,
241                usable_unlocked_backups,
242                created: self.created.and_utc(),
243            },
244            api_object,
245            state
246        )?;
247
248        Ok(api_object)
249    }
250}
251
252#[derive(ToSchema, Deserialize, Validate)]
253pub struct CreateServerBackupGroupOptions {
254    #[garde(skip)]
255    pub server_uuid: uuid::Uuid,
256    #[garde(length(chars, min = 1, max = 255))]
257    #[schema(min_length = 1, max_length = 255)]
258    pub name: compact_str::CompactString,
259    #[garde(dive)]
260    #[serde(default)]
261    pub retention: BackupRetention,
262}
263
264#[async_trait::async_trait]
265impl CreatableModel for ServerBackupGroup {
266    type CreateOptions<'a> = CreateServerBackupGroupOptions;
267    type CreateResult = Self;
268
269    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
270        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerBackupGroup>> =
271            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
272
273        &CREATE_LISTENERS
274    }
275
276    async fn create_with_transaction(
277        state: &crate::State,
278        mut options: Self::CreateOptions<'_>,
279        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
280    ) -> Result<Self, crate::database::DatabaseError> {
281        options.validate()?;
282
283        let mut query_builder = InsertQueryBuilder::new("server_backup_groups");
284
285        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
286
287        query_builder
288            .set("server_uuid", options.server_uuid)
289            .set("name", &options.name)
290            .set("retention", serde_json::to_value(&options.retention)?);
291
292        let row = query_builder
293            .returning(&Self::columns_sql(None))
294            .fetch_one(&mut **transaction)
295            .await?;
296        let mut group = Self::map(None, &row)?;
297
298        Self::run_after_create_handlers(&mut group, &options, state, transaction).await?;
299
300        Ok(group)
301    }
302}
303
304#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
305pub struct UpdateServerBackupGroupOptions {
306    #[garde(length(chars, min = 1, max = 255))]
307    #[schema(min_length = 1, max_length = 255)]
308    pub name: Option<compact_str::CompactString>,
309    #[garde(dive)]
310    #[schema(nullable = false)]
311    #[serde(
312        default,
313        skip_serializing_if = "Option::is_none",
314        deserialize_with = "crate::deserialize::deserialize_non_null_option"
315    )]
316    pub retention: Option<BackupRetention>,
317}
318
319#[async_trait::async_trait]
320impl UpdatableModel for ServerBackupGroup {
321    type UpdateOptions = UpdateServerBackupGroupOptions;
322
323    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
324        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerBackupGroup>> =
325            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
326
327        &UPDATE_LISTENERS
328    }
329
330    async fn update_with_transaction(
331        &mut self,
332        state: &crate::State,
333        mut options: Self::UpdateOptions,
334        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
335    ) -> Result<(), crate::database::DatabaseError> {
336        options.validate()?;
337
338        let mut query_builder = UpdateQueryBuilder::new("server_backup_groups");
339
340        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
341            .await?;
342
343        query_builder
344            .set("name", options.name.as_ref())
345            .set(
346                "retention",
347                options
348                    .retention
349                    .as_ref()
350                    .map(serde_json::to_value)
351                    .transpose()?,
352            )
353            .where_eq("uuid", self.uuid);
354
355        query_builder.execute(&mut **transaction).await?;
356
357        if let Some(name) = options.name {
358            self.name = name;
359        }
360        if let Some(retention) = options.retention {
361            self.retention = retention;
362        }
363
364        self.run_after_update_handlers(state, transaction).await?;
365
366        Ok(())
367    }
368}
369
370#[async_trait::async_trait]
371impl ByUuid for ServerBackupGroup {
372    async fn by_uuid(
373        database: &crate::database::Database,
374        uuid: uuid::Uuid,
375    ) -> Result<Self, crate::database::DatabaseError> {
376        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
377            r#"
378            SELECT {}
379            FROM server_backup_groups
380            WHERE server_backup_groups.uuid = $1
381            "#,
382            Self::columns_sql(None)
383        )))
384        .bind(uuid)
385        .fetch_one(database.read())
386        .await?;
387
388        Self::map(None, &row)
389    }
390
391    async fn by_uuid_with_transaction(
392        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
393        uuid: uuid::Uuid,
394    ) -> Result<Self, crate::database::DatabaseError> {
395        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
396            r#"
397            SELECT {}
398            FROM server_backup_groups
399            WHERE server_backup_groups.uuid = $1
400            "#,
401            Self::columns_sql(None)
402        )))
403        .bind(uuid)
404        .fetch_one(&mut **transaction)
405        .await?;
406
407        Self::map(None, &row)
408    }
409}
410
411#[derive(Clone, Default)]
412pub struct DeleteServerBackupGroupOptions {
413    pub lock_backups: bool,
414}
415
416#[async_trait::async_trait]
417impl DeletableModel for ServerBackupGroup {
418    type DeleteOptions = DeleteServerBackupGroupOptions;
419
420    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
421        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerBackupGroup>> =
422            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
423
424        &DELETE_LISTENERS
425    }
426
427    async fn delete_with_transaction(
428        &self,
429        state: &crate::State,
430        options: Self::DeleteOptions,
431        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
432    ) -> Result<(), anyhow::Error> {
433        sqlx::query(
434            r#"
435            SELECT server_backup_groups.uuid
436            FROM server_backup_groups
437            WHERE server_backup_groups.uuid = $1
438            FOR UPDATE
439            "#,
440        )
441        .bind(self.uuid)
442        .fetch_optional(&mut **transaction)
443        .await?;
444
445        self.run_delete_handlers(&options, state, transaction)
446            .await?;
447
448        if options.lock_backups {
449            sqlx::query!(
450                "UPDATE server_backups
451                SET locked = true
452                WHERE server_backups.backup_group_uuid = $1 AND server_backups.deleted IS NULL",
453                self.uuid,
454            )
455            .execute(&mut **transaction)
456            .await?;
457        }
458
459        sqlx::query!(
460            "DELETE FROM server_backup_groups
461            WHERE server_backup_groups.uuid = $1",
462            self.uuid,
463        )
464        .execute(&mut **transaction)
465        .await?;
466
467        self.run_after_delete_handlers(&options, state, transaction)
468            .await?;
469
470        Ok(())
471    }
472}
473
474#[schema_extension_derive::extendible]
475#[init_args(ServerBackupGroup, crate::State)]
476#[hook_args(crate::State)]
477#[derive(ToSchema, Serialize)]
478#[schema(title = "ServerBackupGroup")]
479pub struct ApiServerBackupGroup {
480    pub uuid: uuid::Uuid,
481
482    pub name: compact_str::CompactString,
483    pub order: i16,
484    pub retention: BackupRetention,
485
486    pub total_backups: i64,
487    pub usable_backups: i64,
488    pub usable_unlocked_backups: i64,
489
490    pub created: chrono::DateTime<chrono::Utc>,
491}