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}