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}