Skip to main content

shared/models/
server_backup_group.rs

1use crate::{
2    models::{InsertQueryBuilder, UpdateQueryBuilder},
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 retention_count: Option<i32>,
21    pub retention_days: Option<i32>,
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.retention_count",
61                compact_str::format_compact!("{prefix}retention_count"),
62            ),
63            (
64                "server_backup_groups.retention_days",
65                compact_str::format_compact!("{prefix}retention_days"),
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            retention_count: row
84                .try_get(compact_str::format_compact!("{prefix}retention_count").as_str())?,
85            retention_days: row
86                .try_get(compact_str::format_compact!("{prefix}retention_days").as_str())?,
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
93impl ServerBackupGroup {
94    pub async fn by_server_uuid_uuid(
95        database: &crate::database::Database,
96        server_uuid: uuid::Uuid,
97        uuid: uuid::Uuid,
98    ) -> Result<Option<Self>, crate::database::DatabaseError> {
99        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
100            r#"
101            SELECT {}
102            FROM server_backup_groups
103            WHERE server_backup_groups.server_uuid = $1 AND server_backup_groups.uuid = $2
104            "#,
105            Self::columns_sql(None)
106        )))
107        .bind(server_uuid)
108        .bind(uuid)
109        .fetch_optional(database.read())
110        .await?;
111
112        row.try_map(|row| Self::map(None, &row))
113    }
114
115    pub async fn by_server_uuid_with_pagination(
116        database: &crate::database::Database,
117        server_uuid: uuid::Uuid,
118        page: i64,
119        per_page: i64,
120        search: Option<&str>,
121    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
122        let offset = (page - 1) * per_page;
123
124        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
125            r#"
126            SELECT {}, COUNT(*) OVER() AS total_count
127            FROM server_backup_groups
128            WHERE server_backup_groups.server_uuid = $1 AND ($2 IS NULL OR server_backup_groups.name ILIKE '%' || $2 || '%')
129            ORDER BY server_backup_groups.created
130            LIMIT $3 OFFSET $4
131            "#,
132            Self::columns_sql(None)
133        )))
134        .bind(server_uuid)
135        .bind(search)
136        .bind(per_page)
137        .bind(offset)
138        .fetch_all(database.read())
139        .await?;
140
141        Ok(super::Pagination {
142            total: rows
143                .first()
144                .map_or(Ok(0), |row| row.try_get("total_count"))?,
145            per_page,
146            page,
147            data: rows
148                .into_iter()
149                .map(|row| Self::map(None, &row))
150                .try_collect_vec()?,
151        })
152    }
153
154    pub async fn all_by_server_uuid(
155        database: &crate::database::Database,
156        server_uuid: uuid::Uuid,
157    ) -> Result<Vec<Self>, crate::database::DatabaseError> {
158        let rows = sqlx::query(sqlx::AssertSqlSafe(format!(
159            r#"
160            SELECT {}
161            FROM server_backup_groups
162            WHERE server_backup_groups.server_uuid = $1
163            ORDER BY server_backup_groups.created
164            "#,
165            Self::columns_sql(None)
166        )))
167        .bind(server_uuid)
168        .fetch_all(database.read())
169        .await?;
170
171        rows.into_iter()
172            .map(|row| Self::map(None, &row))
173            .try_collect_vec()
174    }
175
176    pub async fn count_by_server_uuid(
177        database: &crate::database::Database,
178        server_uuid: uuid::Uuid,
179    ) -> Result<i64, sqlx::Error> {
180        sqlx::query_scalar(
181            r#"
182            SELECT COUNT(*)
183            FROM server_backup_groups
184            WHERE server_backup_groups.server_uuid = $1
185            "#,
186        )
187        .bind(server_uuid)
188        .fetch_one(database.read())
189        .await
190    }
191
192    /// (total_backups, usable_backups, usable_unlocked_backups)
193    async fn backup_counts(
194        &self,
195        database: &crate::database::Database,
196    ) -> Result<(i64, i64, i64), crate::database::DatabaseError> {
197        let row = sqlx::query!(
198            r#"
199            SELECT
200                COUNT(*) AS "total!",
201                COUNT(*) FILTER (
202                    WHERE server_backups.successful AND server_backups.completed IS NOT NULL
203                ) AS "usable!",
204                COUNT(*) FILTER (
205                    WHERE server_backups.successful
206                        AND server_backups.completed IS NOT NULL
207                        AND NOT server_backups.locked
208                ) AS "usable_unlocked!"
209            FROM server_backups
210            WHERE server_backups.backup_group_uuid = $1 AND server_backups.deleted IS NULL
211            "#,
212            self.uuid,
213        )
214        .fetch_one(database.read())
215        .await?;
216
217        Ok((row.total, row.usable, row.usable_unlocked))
218    }
219}
220
221#[async_trait::async_trait]
222impl IntoApiObject for ServerBackupGroup {
223    type ApiObject = ApiServerBackupGroup;
224    type ExtraArgs<'a> = ();
225
226    async fn into_api_object<'a>(
227        self,
228        state: &crate::State,
229        _args: Self::ExtraArgs<'a>,
230    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
231        let (total_backups, usable_backups, usable_unlocked_backups) =
232            self.backup_counts(&state.database).await?;
233
234        let api_object = ApiServerBackupGroup::init_hooks(&self, state).await?;
235
236        let api_object = finish_extendible!(
237            ApiServerBackupGroup {
238                uuid: self.uuid,
239                name: self.name,
240                retention_count: self.retention_count,
241                retention_days: self.retention_days,
242                total_backups,
243                usable_backups,
244                usable_unlocked_backups,
245                created: self.created.and_utc(),
246            },
247            api_object,
248            state
249        )?;
250
251        Ok(api_object)
252    }
253}
254
255#[derive(ToSchema, Deserialize, Validate)]
256pub struct CreateServerBackupGroupOptions {
257    #[garde(skip)]
258    pub server_uuid: uuid::Uuid,
259    #[garde(length(chars, min = 1, max = 255))]
260    #[schema(min_length = 1, max_length = 255)]
261    pub name: compact_str::CompactString,
262    #[garde(range(min = 1))]
263    #[schema(minimum = 1)]
264    pub retention_count: Option<i32>,
265    #[garde(range(min = 1))]
266    #[schema(minimum = 1)]
267    pub retention_days: Option<i32>,
268}
269
270#[async_trait::async_trait]
271impl CreatableModel for ServerBackupGroup {
272    type CreateOptions<'a> = CreateServerBackupGroupOptions;
273    type CreateResult = Self;
274
275    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
276        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerBackupGroup>> =
277            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
278
279        &CREATE_LISTENERS
280    }
281
282    async fn create_with_transaction(
283        state: &crate::State,
284        mut options: Self::CreateOptions<'_>,
285        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
286    ) -> Result<Self, crate::database::DatabaseError> {
287        options.validate()?;
288
289        let mut query_builder = InsertQueryBuilder::new("server_backup_groups");
290
291        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
292
293        query_builder
294            .set("server_uuid", options.server_uuid)
295            .set("name", &options.name)
296            .set("retention_count", options.retention_count)
297            .set("retention_days", options.retention_days);
298
299        let row = query_builder
300            .returning(&Self::columns_sql(None))
301            .fetch_one(&mut **transaction)
302            .await?;
303        let mut group = Self::map(None, &row)?;
304
305        Self::run_after_create_handlers(&mut group, &options, state, transaction).await?;
306
307        Ok(group)
308    }
309}
310
311#[derive(ToSchema, Serialize, Deserialize, Validate, Default)]
312pub struct UpdateServerBackupGroupOptions {
313    #[garde(length(chars, min = 1, max = 255))]
314    #[schema(min_length = 1, max_length = 255)]
315    pub name: Option<compact_str::CompactString>,
316    #[garde(inner(range(min = 1)))]
317    #[schema(minimum = 1)]
318    #[serde(default, with = "::serde_with::rust::double_option")]
319    pub retention_count: Option<Option<i32>>,
320    #[garde(inner(range(min = 1)))]
321    #[schema(minimum = 1)]
322    #[serde(default, with = "::serde_with::rust::double_option")]
323    pub retention_days: Option<Option<i32>>,
324}
325
326#[async_trait::async_trait]
327impl UpdatableModel for ServerBackupGroup {
328    type UpdateOptions = UpdateServerBackupGroupOptions;
329
330    fn get_update_handlers() -> &'static LazyLock<UpdateHandlerList<Self>> {
331        static UPDATE_LISTENERS: LazyLock<UpdateHandlerList<ServerBackupGroup>> =
332            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
333
334        &UPDATE_LISTENERS
335    }
336
337    async fn update_with_transaction(
338        &mut self,
339        state: &crate::State,
340        mut options: Self::UpdateOptions,
341        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
342    ) -> Result<(), crate::database::DatabaseError> {
343        options.validate()?;
344
345        let mut query_builder = UpdateQueryBuilder::new("server_backup_groups");
346
347        self.run_update_handlers(&mut options, &mut query_builder, state, transaction)
348            .await?;
349
350        query_builder
351            .set("name", options.name.as_ref())
352            .set("retention_count", options.retention_count)
353            .set("retention_days", options.retention_days)
354            .where_eq("uuid", self.uuid);
355
356        query_builder.execute(&mut **transaction).await?;
357
358        if let Some(name) = options.name {
359            self.name = name;
360        }
361        if let Some(retention_count) = options.retention_count {
362            self.retention_count = retention_count;
363        }
364        if let Some(retention_days) = options.retention_days {
365            self.retention_days = retention_days;
366        }
367
368        self.run_after_update_handlers(state, transaction).await?;
369
370        Ok(())
371    }
372}
373
374#[async_trait::async_trait]
375impl ByUuid for ServerBackupGroup {
376    async fn by_uuid(
377        database: &crate::database::Database,
378        uuid: uuid::Uuid,
379    ) -> Result<Self, crate::database::DatabaseError> {
380        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
381            r#"
382            SELECT {}
383            FROM server_backup_groups
384            WHERE server_backup_groups.uuid = $1
385            "#,
386            Self::columns_sql(None)
387        )))
388        .bind(uuid)
389        .fetch_one(database.read())
390        .await?;
391
392        Self::map(None, &row)
393    }
394
395    async fn by_uuid_with_transaction(
396        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
397        uuid: uuid::Uuid,
398    ) -> Result<Self, crate::database::DatabaseError> {
399        let row = sqlx::query(sqlx::AssertSqlSafe(format!(
400            r#"
401            SELECT {}
402            FROM server_backup_groups
403            WHERE server_backup_groups.uuid = $1
404            "#,
405            Self::columns_sql(None)
406        )))
407        .bind(uuid)
408        .fetch_one(&mut **transaction)
409        .await?;
410
411        Self::map(None, &row)
412    }
413}
414
415#[derive(Clone, Default)]
416pub struct DeleteServerBackupGroupOptions {
417    pub lock_backups: bool,
418}
419
420#[async_trait::async_trait]
421impl DeletableModel for ServerBackupGroup {
422    type DeleteOptions = DeleteServerBackupGroupOptions;
423
424    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
425        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerBackupGroup>> =
426            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
427
428        &DELETE_LISTENERS
429    }
430
431    async fn delete_with_transaction(
432        &self,
433        state: &crate::State,
434        options: Self::DeleteOptions,
435        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
436    ) -> Result<(), anyhow::Error> {
437        self.run_delete_handlers(&options, state, transaction)
438            .await?;
439
440        if options.lock_backups {
441            sqlx::query!(
442                "UPDATE server_backups
443                SET locked = true
444                WHERE server_backups.backup_group_uuid = $1 AND server_backups.deleted IS NULL",
445                self.uuid,
446            )
447            .execute(&mut **transaction)
448            .await?;
449        }
450
451        sqlx::query!(
452            "DELETE FROM server_backup_groups
453            WHERE server_backup_groups.uuid = $1",
454            self.uuid,
455        )
456        .execute(&mut **transaction)
457        .await?;
458
459        self.run_after_delete_handlers(&options, state, transaction)
460            .await?;
461
462        Ok(())
463    }
464}
465
466#[schema_extension_derive::extendible]
467#[init_args(ServerBackupGroup, crate::State)]
468#[hook_args(crate::State)]
469#[derive(ToSchema, Serialize)]
470#[schema(title = "ServerBackupGroup")]
471pub struct ApiServerBackupGroup {
472    pub uuid: uuid::Uuid,
473
474    pub name: compact_str::CompactString,
475    pub retention_count: Option<i32>,
476    pub retention_days: Option<i32>,
477
478    pub total_backups: i64,
479    pub usable_backups: i64,
480    pub usable_unlocked_backups: i64,
481
482    pub created: chrono::DateTime<chrono::Utc>,
483}