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 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}