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}