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