Skip to main content

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