shared/models/
server_mount.rs

1use crate::{models::InsertQueryBuilder, prelude::*, storage::StorageUrlRetriever};
2use garde::Validate;
3use serde::{Deserialize, Serialize};
4use sqlx::{Row, postgres::PgRow};
5use std::{
6    collections::BTreeMap,
7    sync::{Arc, LazyLock},
8};
9use utoipa::ToSchema;
10
11#[derive(Serialize, Deserialize)]
12pub struct ServerMount {
13    pub mount: Fetchable<super::mount::Mount>,
14    pub server: Option<Fetchable<super::server::Server>>,
15
16    pub created: Option<chrono::NaiveDateTime>,
17}
18
19impl BaseModel for ServerMount {
20    const NAME: &'static str = "server_mount";
21
22    #[inline]
23    fn columns(prefix: Option<&str>) -> BTreeMap<&'static str, compact_str::CompactString> {
24        let prefix = prefix.unwrap_or_default();
25
26        BTreeMap::from([
27            (
28                "server_mounts.mount_uuid",
29                compact_str::format_compact!("{prefix}mount_uuid"),
30            ),
31            (
32                "server_mounts.server_uuid",
33                compact_str::format_compact!("{prefix}server_uuid"),
34            ),
35            (
36                "server_mounts.created",
37                compact_str::format_compact!("{prefix}created"),
38            ),
39        ])
40    }
41
42    #[inline]
43    fn map(prefix: Option<&str>, row: &PgRow) -> Result<Self, crate::database::DatabaseError> {
44        let prefix = prefix.unwrap_or_default();
45
46        Ok(Self {
47            mount: super::mount::Mount::get_fetchable(
48                row.try_get(compact_str::format_compact!("{prefix}mount_uuid").as_str())
49                    .or_else(|_| row.try_get("alt_mount_uuid"))?,
50            ),
51            server: super::server::Server::get_fetchable_from_row(
52                row,
53                compact_str::format_compact!("{prefix}server_uuid"),
54            ),
55            created: row.try_get(compact_str::format_compact!("{prefix}created").as_str())?,
56        })
57    }
58}
59
60impl ServerMount {
61    pub async fn by_server_uuid_mount_uuid(
62        database: &crate::database::Database,
63        server_uuid: uuid::Uuid,
64        mount_uuid: uuid::Uuid,
65    ) -> Result<Option<Self>, crate::database::DatabaseError> {
66        let row = sqlx::query(&format!(
67            r#"
68            SELECT {}
69            FROM server_mounts
70            WHERE server_mounts.server_uuid = $1 AND server_mounts.mount_uuid = $2
71            "#,
72            Self::columns_sql(None)
73        ))
74        .bind(server_uuid)
75        .bind(mount_uuid)
76        .fetch_optional(database.read())
77        .await?;
78
79        row.try_map(|row| Self::map(None, &row))
80    }
81
82    pub async fn by_server_uuid_with_pagination(
83        database: &crate::database::Database,
84        server_uuid: uuid::Uuid,
85        page: i64,
86        per_page: i64,
87        search: Option<&str>,
88    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
89        let offset = (page - 1) * per_page;
90
91        let rows = sqlx::query(&format!(
92            r#"
93            SELECT {}, COUNT(*) OVER() AS total_count
94            FROM server_mounts
95            JOIN mounts ON mounts.uuid = server_mounts.mount_uuid
96            WHERE server_mounts.server_uuid = $1 AND ($2 IS NULL OR mounts.name ILIKE '%' || $2 || '%')
97            ORDER BY server_mounts.mount_uuid ASC
98            LIMIT $3 OFFSET $4
99            "#,
100            Self::columns_sql(None)
101        ))
102        .bind(server_uuid)
103        .bind(search)
104        .bind(per_page)
105        .bind(offset)
106        .fetch_all(database.read())
107        .await?;
108
109        Ok(super::Pagination {
110            total: rows
111                .first()
112                .map_or(Ok(0), |row| row.try_get("total_count"))?,
113            per_page,
114            page,
115            data: rows
116                .into_iter()
117                .map(|row| Self::map(None, &row))
118                .try_collect_vec()?,
119        })
120    }
121
122    pub async fn available_by_server_with_pagination(
123        database: &crate::database::Database,
124        server: &super::server::Server,
125        page: i64,
126        per_page: i64,
127        search: Option<&str>,
128    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
129        let offset = (page - 1) * per_page;
130
131        let rows = sqlx::query(&format!(
132            r#"
133            SELECT {}, mounts.uuid AS alt_mount_uuid, COUNT(*) OVER() AS total_count
134            FROM mounts
135            JOIN node_mounts ON mounts.uuid = node_mounts.mount_uuid AND node_mounts.node_uuid = $1
136            JOIN nest_egg_mounts ON mounts.uuid = nest_egg_mounts.mount_uuid AND nest_egg_mounts.egg_uuid = $2
137            LEFT JOIN server_mounts ON server_mounts.mount_uuid = mounts.uuid AND server_mounts.server_uuid = $3
138            WHERE $4 IS NULL OR mounts.name ILIKE '%' || $4 || '%'
139            ORDER BY mounts.created
140            LIMIT $5 OFFSET $6
141            "#,
142            Self::columns_sql(None)
143        ))
144        .bind(server.node.uuid)
145        .bind(server.egg.uuid)
146        .bind(server.uuid)
147        .bind(search)
148        .bind(per_page)
149        .bind(offset)
150        .fetch_all(database.read())
151        .await
152        ?;
153
154        Ok(super::Pagination {
155            total: rows
156                .first()
157                .map_or(Ok(0), |row| row.try_get("total_count"))?,
158            per_page,
159            page,
160            data: rows
161                .into_iter()
162                .map(|row| Self::map(None, &row))
163                .try_collect_vec()?,
164        })
165    }
166
167    pub async fn mountable_by_server_with_pagination(
168        database: &crate::database::Database,
169        server: &super::server::Server,
170        page: i64,
171        per_page: i64,
172        search: Option<&str>,
173    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
174        let offset = (page - 1) * per_page;
175
176        let rows = sqlx::query(&format!(
177            r#"
178            SELECT {}, mounts.uuid AS alt_mount_uuid, COUNT(*) OVER() AS total_count
179            FROM mounts
180            JOIN node_mounts ON mounts.uuid = node_mounts.mount_uuid AND node_mounts.node_uuid = $1
181            JOIN nest_egg_mounts ON mounts.uuid = nest_egg_mounts.mount_uuid AND nest_egg_mounts.egg_uuid = $2
182            LEFT JOIN server_mounts ON server_mounts.mount_uuid = mounts.uuid AND server_mounts.server_uuid = $3
183            WHERE mounts.user_mountable = TRUE AND ($4 IS NULL OR mounts.name ILIKE '%' || $4 || '%')
184            ORDER BY mounts.created
185            LIMIT $5 OFFSET $6
186            "#,
187            Self::columns_sql(None)
188        ))
189        .bind(server.node.uuid)
190        .bind(server.egg.uuid)
191        .bind(server.uuid)
192        .bind(search)
193        .bind(per_page)
194        .bind(offset)
195        .fetch_all(database.read())
196        .await
197        ?;
198
199        Ok(super::Pagination {
200            total: rows
201                .first()
202                .map_or(Ok(0), |row| row.try_get("total_count"))?,
203            per_page,
204            page,
205            data: rows
206                .into_iter()
207                .map(|row| Self::map(None, &row))
208                .try_collect_vec()?,
209        })
210    }
211
212    pub async fn by_mount_uuid_with_pagination(
213        database: &crate::database::Database,
214        mount_uuid: uuid::Uuid,
215        page: i64,
216        per_page: i64,
217        search: Option<&str>,
218    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
219        let offset = (page - 1) * per_page;
220
221        let rows = sqlx::query(&format!(
222            r#"
223            SELECT {}, COUNT(*) OVER() AS total_count
224            FROM server_mounts
225            JOIN servers ON servers.uuid = server_mounts.server_uuid
226            WHERE server_mounts.mount_uuid = $1 AND ($2 IS NULL OR servers.name ILIKE '%' || $2 || '%')
227            ORDER BY server_mounts.mount_uuid ASC
228            LIMIT $3 OFFSET $4
229            "#,
230            Self::columns_sql(None)
231        ))
232        .bind(mount_uuid)
233        .bind(search)
234        .bind(per_page)
235        .bind(offset)
236        .fetch_all(database.read())
237        .await?;
238
239        Ok(super::Pagination {
240            total: rows
241                .first()
242                .map_or(Ok(0), |row| row.try_get("total_count"))?,
243            per_page,
244            page,
245            data: rows
246                .into_iter()
247                .map(|row| Self::map(None, &row))
248                .try_collect_vec()?,
249        })
250    }
251
252    #[inline]
253    pub async fn into_api_object(
254        self,
255        database: &crate::database::Database,
256    ) -> Result<ApiServerMount, anyhow::Error> {
257        let mount = self.mount.fetch_cached(database).await?;
258
259        Ok(ApiServerMount {
260            uuid: mount.uuid,
261            name: mount.name,
262            description: mount.description,
263            target: mount.target,
264            read_only: mount.read_only,
265            created: self.created.map(|dt| dt.and_utc()),
266        })
267    }
268
269    #[inline]
270    pub async fn into_admin_server_api_object(
271        self,
272        database: &crate::database::Database,
273        storage_url_retriever: &StorageUrlRetriever<'_>,
274    ) -> Result<AdminApiServerServerMount, anyhow::Error> {
275        let created = match self.created {
276            Some(created) => created,
277            None => {
278                return Err(anyhow::anyhow!(
279                    "This mount does not have a server attached"
280                ));
281            }
282        };
283        let server = match self.server {
284            Some(server) => server.fetch_cached(database).await?,
285            None => {
286                return Err(anyhow::anyhow!(
287                    "This mount does not have a server attached"
288                ));
289            }
290        };
291
292        Ok(AdminApiServerServerMount {
293            server: server
294                .into_admin_api_object(database, storage_url_retriever)
295                .await?,
296            created: created.and_utc(),
297        })
298    }
299
300    #[inline]
301    pub async fn into_admin_api_object(
302        self,
303        database: &crate::database::Database,
304    ) -> Result<AdminApiServerMount, anyhow::Error> {
305        let mount = self.mount.fetch_cached(database).await?;
306
307        Ok(AdminApiServerMount {
308            mount: mount.into_admin_api_object(),
309            created: self.created.map(|dt| dt.and_utc()),
310        })
311    }
312}
313
314#[derive(Validate)]
315pub struct CreateServerMountOptions {
316    #[garde(skip)]
317    pub server_uuid: uuid::Uuid,
318    #[garde(skip)]
319    pub mount_uuid: uuid::Uuid,
320}
321
322#[async_trait::async_trait]
323impl CreatableModel for ServerMount {
324    type CreateOptions<'a> = CreateServerMountOptions;
325    type CreateResult = Self;
326
327    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
328        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerMount>> =
329            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
330
331        &CREATE_LISTENERS
332    }
333
334    async fn create(
335        state: &crate::State,
336        mut options: Self::CreateOptions<'_>,
337    ) -> Result<Self, crate::database::DatabaseError> {
338        options.validate()?;
339
340        super::mount::Mount::by_uuid_optional_cached(&state.database, options.mount_uuid)
341            .await?
342            .ok_or(crate::database::InvalidRelationError("mount"))?;
343
344        let mut transaction = state.database.write().begin().await?;
345
346        let mut query_builder = InsertQueryBuilder::new("server_mounts");
347
348        Self::run_create_handlers(&mut options, &mut query_builder, state, &mut transaction)
349            .await?;
350
351        query_builder
352            .set("server_uuid", options.server_uuid)
353            .set("mount_uuid", options.mount_uuid);
354
355        query_builder.execute(&mut *transaction).await?;
356
357        transaction.commit().await?;
358
359        Self::by_server_uuid_mount_uuid(&state.database, options.server_uuid, options.mount_uuid)
360            .await?
361            .ok_or(sqlx::Error::RowNotFound.into())
362    }
363}
364
365#[async_trait::async_trait]
366impl DeletableModel for ServerMount {
367    type DeleteOptions = ();
368
369    fn get_delete_handlers() -> &'static LazyLock<DeleteListenerList<Self>> {
370        static DELETE_LISTENERS: LazyLock<DeleteListenerList<ServerMount>> =
371            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
372
373        &DELETE_LISTENERS
374    }
375
376    async fn delete(
377        &self,
378        state: &crate::State,
379        options: Self::DeleteOptions,
380    ) -> Result<(), anyhow::Error> {
381        let server_uuid = match &self.server {
382            Some(server) => server.uuid,
383            None => {
384                return Err(anyhow::anyhow!(
385                    "This server mount does not have a server attached, cannot delete"
386                ));
387            }
388        };
389
390        let mut transaction = state.database.write().begin().await?;
391
392        self.run_delete_handlers(&options, state, &mut transaction)
393            .await?;
394
395        sqlx::query(
396            r#"
397            DELETE FROM server_mounts
398            WHERE server_mounts.server_uuid = $1 AND server_mounts.mount_uuid = $2
399            "#,
400        )
401        .bind(server_uuid)
402        .bind(self.mount.uuid)
403        .execute(&mut *transaction)
404        .await?;
405
406        transaction.commit().await?;
407
408        Ok(())
409    }
410}
411
412#[derive(ToSchema, Serialize)]
413#[schema(title = "ServerMount")]
414pub struct ApiServerMount {
415    pub uuid: uuid::Uuid,
416
417    pub name: compact_str::CompactString,
418    pub description: Option<compact_str::CompactString>,
419
420    pub target: compact_str::CompactString,
421    pub read_only: bool,
422
423    pub created: Option<chrono::DateTime<chrono::Utc>>,
424}
425
426#[derive(ToSchema, Serialize)]
427#[schema(title = "AdminServerServerMount")]
428pub struct AdminApiServerServerMount {
429    pub server: super::server::AdminApiServer,
430
431    pub created: chrono::DateTime<chrono::Utc>,
432}
433
434#[derive(ToSchema, Serialize)]
435#[schema(title = "AdminServerMount")]
436pub struct AdminApiServerMount {
437    pub mount: super::mount::AdminApiMount,
438
439    pub created: Option<chrono::DateTime<chrono::Utc>>,
440}