Skip to main content

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