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(|| parking_lot::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(sqlx::AssertSqlSafe(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        super::PaginatedQueryBuilder::new(
104            Self::columns_sql(None),
105            format!(
106                r#"
107                FROM server_mounts
108                JOIN mounts ON mounts.uuid = server_mounts.mount_uuid
109                WHERE server_mounts.server_uuid = $1 AND {search}
110                "#,
111                search = super::search_sql(2, &["mounts.name"], &["mounts.uuid"]),
112            ),
113            "server_mounts.mount_uuid ASC",
114        )
115        .bind(server_uuid)
116        .bind(search)
117        .fetch(database, page, per_page)
118        .await
119    }
120
121    pub async fn available_by_server_with_pagination(
122        database: &crate::database::Database,
123        server: &super::server::Server,
124        page: i64,
125        per_page: i64,
126        search: Option<&str>,
127    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
128        super::PaginatedQueryBuilder::new(
129            format!("{}, mounts.uuid AS alt_mount_uuid", Self::columns_sql(None)),
130            format!(
131                r#"
132                FROM mounts
133                JOIN node_mounts ON mounts.uuid = node_mounts.mount_uuid AND node_mounts.node_uuid = $1
134                JOIN nest_egg_mounts ON mounts.uuid = nest_egg_mounts.mount_uuid AND nest_egg_mounts.egg_uuid = $2
135                LEFT JOIN server_mounts ON server_mounts.mount_uuid = mounts.uuid AND server_mounts.server_uuid = $3
136                WHERE {search}
137                "#,
138                search = super::search_sql(4, &["mounts.name"], &["mounts.uuid"]),
139            ),
140            "mounts.created, mounts.uuid",
141        )
142        .bind(server.node.uuid)
143        .bind(server.egg.uuid)
144        .bind(server.uuid)
145        .bind(search)
146        .fetch(database, page, per_page)
147        .await
148    }
149
150    pub async fn mountable_by_server_with_pagination(
151        database: &crate::database::Database,
152        server: &super::server::Server,
153        page: i64,
154        per_page: i64,
155        search: Option<&str>,
156    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
157        super::PaginatedQueryBuilder::new(
158            format!("{}, mounts.uuid AS alt_mount_uuid", Self::columns_sql(None)),
159            format!(
160                r#"
161                FROM mounts
162                JOIN node_mounts ON mounts.uuid = node_mounts.mount_uuid AND node_mounts.node_uuid = $1
163                JOIN nest_egg_mounts ON mounts.uuid = nest_egg_mounts.mount_uuid AND nest_egg_mounts.egg_uuid = $2
164                LEFT JOIN server_mounts ON server_mounts.mount_uuid = mounts.uuid AND server_mounts.server_uuid = $3
165                WHERE mounts.user_mountable = TRUE AND {search}
166                "#,
167                search = super::search_sql(4, &["mounts.name"], &["mounts.uuid"]),
168            ),
169            "mounts.created, mounts.uuid",
170        )
171        .bind(server.node.uuid)
172        .bind(server.egg.uuid)
173        .bind(server.uuid)
174        .bind(search)
175        .fetch(database, page, per_page)
176        .await
177    }
178
179    pub async fn by_mount_uuid_with_pagination(
180        database: &crate::database::Database,
181        mount_uuid: uuid::Uuid,
182        page: i64,
183        per_page: i64,
184        search: Option<&str>,
185    ) -> Result<super::Pagination<Self>, crate::database::DatabaseError> {
186        super::PaginatedQueryBuilder::new(
187            Self::columns_sql(None),
188            format!(
189                r#"
190                FROM server_mounts
191                JOIN servers ON servers.uuid = server_mounts.server_uuid
192                WHERE server_mounts.mount_uuid = $1 AND {search}
193                "#,
194                search = super::search_sql(2, &["servers.name"], &["servers.uuid"]),
195            ),
196            "server_mounts.mount_uuid ASC",
197        )
198        .bind(mount_uuid)
199        .bind(search)
200        .fetch(database, page, per_page)
201        .await
202    }
203
204    pub async fn into_admin_server_api_object(
205        self,
206        state: &crate::State,
207        storage_url_retriever: &StorageUrlRetriever<'_>,
208    ) -> Result<AdminApiServerServerMount, crate::database::DatabaseError> {
209        let created = match self.created {
210            Some(created) => created,
211            None => {
212                return Err(crate::database::DatabaseError::Any(anyhow::anyhow!(
213                    "This mount does not have a server attached"
214                )));
215            }
216        };
217        let server = match self.server {
218            Some(server) => server.fetch_cached(&state.database).await?,
219            None => {
220                return Err(crate::database::DatabaseError::Any(anyhow::anyhow!(
221                    "This mount does not have a server attached"
222                )));
223            }
224        };
225
226        Ok(AdminApiServerServerMount {
227            server: server
228                .into_admin_api_object(state, storage_url_retriever)
229                .await?,
230            created: created.and_utc(),
231        })
232    }
233}
234
235#[async_trait::async_trait]
236impl IntoApiObject for ServerMount {
237    type ApiObject = ApiServerMount;
238    type ExtraArgs<'a> = ();
239
240    async fn into_api_object<'a>(
241        self,
242        state: &crate::State,
243        _args: Self::ExtraArgs<'a>,
244    ) -> Result<Self::ApiObject, crate::database::DatabaseError> {
245        let api_object = ApiServerMount::init_hooks(&self, state).await?;
246        let mount = self.mount.fetch_cached(&state.database).await?;
247
248        let api_object = finish_extendible!(
249            ApiServerMount {
250                uuid: mount.uuid,
251                name: mount.name,
252                description: mount.description,
253                target: mount.target,
254                read_only: mount.read_only,
255                created: self.created.map(|dt| dt.and_utc()),
256            },
257            api_object,
258            state
259        )?;
260
261        Ok(api_object)
262    }
263}
264
265#[async_trait::async_trait]
266impl IntoAdminApiObject for ServerMount {
267    type AdminApiObject = AdminApiServerMount;
268    type ExtraArgs<'a> = ();
269
270    async fn into_admin_api_object<'a>(
271        self,
272        state: &crate::State,
273        _args: Self::ExtraArgs<'a>,
274    ) -> Result<Self::AdminApiObject, crate::database::DatabaseError> {
275        let api_object = AdminApiServerMount::init_hooks(&self, state).await?;
276        let mount = self.mount.fetch_cached(&state.database).await?;
277
278        let api_object = finish_extendible!(
279            AdminApiServerMount {
280                mount: mount.into_admin_api_object(state, ()).await?,
281                created: self.created.map(|dt| dt.and_utc()),
282            },
283            api_object,
284            state
285        )?;
286
287        Ok(api_object)
288    }
289}
290
291#[derive(Validate)]
292pub struct CreateServerMountOptions {
293    #[garde(skip)]
294    pub server_uuid: uuid::Uuid,
295    #[garde(skip)]
296    pub mount_uuid: uuid::Uuid,
297}
298
299#[async_trait::async_trait]
300impl CreatableModel for ServerMount {
301    type CreateOptions<'a> = CreateServerMountOptions;
302    type CreateResult = Self;
303
304    fn get_create_handlers() -> &'static LazyLock<CreateListenerList<Self>> {
305        static CREATE_LISTENERS: LazyLock<CreateListenerList<ServerMount>> =
306            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
307
308        &CREATE_LISTENERS
309    }
310
311    async fn create_with_transaction(
312        state: &crate::State,
313        mut options: Self::CreateOptions<'_>,
314        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
315    ) -> Result<Self, crate::database::DatabaseError> {
316        options.validate()?;
317
318        super::mount::Mount::by_uuid_optional_cached(&state.database, options.mount_uuid)
319            .await?
320            .ok_or(crate::database::InvalidRelationError("mount"))?;
321
322        let mut query_builder = InsertQueryBuilder::new("server_mounts");
323
324        Self::run_create_handlers(&mut options, &mut query_builder, state, transaction).await?;
325
326        query_builder
327            .set("server_uuid", options.server_uuid)
328            .set("mount_uuid", options.mount_uuid);
329
330        let row = query_builder
331            .returning(&Self::columns_sql(None))
332            .fetch_one(&mut **transaction)
333            .await?;
334        let mut server_mount = Self::map(None, &row)?;
335
336        Self::run_after_create_handlers(&mut server_mount, &options, state, transaction).await?;
337
338        Ok(server_mount)
339    }
340}
341
342#[async_trait::async_trait]
343impl DeletableModel for ServerMount {
344    type DeleteOptions = ();
345
346    fn get_delete_handlers() -> &'static LazyLock<DeleteHandlerList<Self>> {
347        static DELETE_LISTENERS: LazyLock<DeleteHandlerList<ServerMount>> =
348            LazyLock::new(|| Arc::new(ModelHandlerList::default()));
349
350        &DELETE_LISTENERS
351    }
352
353    async fn delete_with_transaction(
354        &self,
355        state: &crate::State,
356        options: Self::DeleteOptions,
357        transaction: &mut sqlx::Transaction<'_, sqlx::Postgres>,
358    ) -> Result<(), anyhow::Error> {
359        let server_uuid = match &self.server {
360            Some(server) => server.uuid,
361            None => {
362                return Err(anyhow::anyhow!(
363                    "This server mount does not have a server attached, cannot delete"
364                ));
365            }
366        };
367
368        self.run_delete_handlers(&options, state, transaction)
369            .await?;
370
371        sqlx::query(
372            r#"
373            DELETE FROM server_mounts
374            WHERE server_mounts.server_uuid = $1 AND server_mounts.mount_uuid = $2
375            "#,
376        )
377        .bind(server_uuid)
378        .bind(self.mount.uuid)
379        .execute(&mut **transaction)
380        .await?;
381
382        self.run_after_delete_handlers(&options, state, transaction)
383            .await?;
384
385        Ok(())
386    }
387}
388
389#[schema_extension_derive::extendible]
390#[init_args(ServerMount, crate::State)]
391#[hook_args(crate::State)]
392#[derive(ToSchema, Serialize)]
393#[schema(title = "ServerMount")]
394pub struct ApiServerMount {
395    pub uuid: uuid::Uuid,
396
397    pub name: compact_str::CompactString,
398    pub description: Option<compact_str::CompactString>,
399
400    pub target: compact_str::CompactString,
401    pub read_only: bool,
402
403    pub created: Option<chrono::DateTime<chrono::Utc>>,
404}
405
406#[derive(ToSchema, Serialize)]
407#[schema(title = "AdminServerServerMount")]
408pub struct AdminApiServerServerMount {
409    pub server: super::server::AdminApiServer,
410
411    pub created: chrono::DateTime<chrono::Utc>,
412}
413
414#[schema_extension_derive::extendible]
415#[init_args(ServerMount, crate::State)]
416#[hook_args(crate::State)]
417#[derive(ToSchema, Serialize)]
418#[schema(title = "AdminServerMount")]
419pub struct AdminApiServerMount {
420    pub mount: super::mount::AdminApiMount,
421
422    pub created: Option<chrono::DateTime<chrono::Utc>>,
423}