1use std::path::Path;
2
3use anyhow::Context;
4use chrono::Duration;
5use headless_lms_utils::file_store::{FileStore, GenericPayload};
6use sqlx::{Acquire, Postgres};
7
8use crate::prelude::*;
9
10pub async fn insert(
13 conn: &mut PgConnection,
14 name: &str,
15 path: &str,
16 mime: &str,
17 uploader: Option<Uuid>,
18 size_bytes: Option<i64>,
19) -> ModelResult<Uuid> {
20 let id = Uuid::new_v4();
21 insert_with_id(conn, id, name, path, mime, uploader, size_bytes).await?;
22 Ok(id)
23}
24
25#[derive(Debug, Clone, Copy)]
27pub struct NewUpload<'a> {
28 pub path: &'a Path,
30 pub name: &'a str,
32 pub mime: &'a str,
33 pub uploader: Option<Uuid>,
34 pub size_bytes: Option<i64>,
36}
37
38pub async fn store_and_insert<'c>(
42 db: impl Acquire<'c, Database = Postgres>,
43 file_store: &dyn FileStore,
44 upload: NewUpload<'_>,
45 contents: GenericPayload,
46) -> ModelResult<Uuid> {
47 let NewUpload {
48 path,
49 name,
50 mime,
51 uploader,
52 size_bytes,
53 } = upload;
54 let path_string = path.to_str().context("the upload path isn't valid UTF-8")?;
55 file_store.upload_stream(path, contents, mime).await?;
56 let inserted = async {
57 let mut conn = db.acquire().await?;
58 insert(&mut conn, name, path_string, mime, uploader, size_bytes).await
59 }
60 .await;
61 if inserted.is_err() && file_store.delete(path).await.is_err() {
62 warn!("Couldn't delete an uploaded file whose row wasn't written: {path_string}");
63 }
64 inserted
65}
66
67pub async fn insert_with_id(
72 conn: &mut PgConnection,
73 id: Uuid,
74 name: &str,
75 path: &str,
76 mime: &str,
77 uploader: Option<Uuid>,
78 size_bytes: Option<i64>,
79) -> ModelResult<()> {
80 sqlx::query!(
81 r#"
82INSERT INTO file_uploads(id, path, name, mime, uploaded_by_user, size_bytes)
83VALUES ($1, $2, $3, $4, $5, $6)
84"#,
85 id,
86 path,
87 name,
88 mime,
89 uploader,
90 size_bytes
91 )
92 .execute(conn)
93 .await?;
94 Ok(())
95}
96
97#[derive(Debug, Clone, PartialEq, Eq)]
99pub struct FileUploadRef {
100 pub id: Uuid,
101 pub name: String,
102 pub path: String,
103}
104
105pub async fn get_many(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<Vec<FileUploadRef>> {
108 let res = sqlx::query_as!(
109 FileUploadRef,
110 "
111SELECT id,
112 name,
113 path
114FROM file_uploads
115WHERE id = ANY($1)
116 AND deleted_at IS NULL
117",
118 ids
119 )
120 .fetch_all(conn)
121 .await?;
122 Ok(res)
123}
124
125pub struct StoredFileDetails {
126 pub name: String,
127 pub mime: String,
128}
129
130pub async fn get_details_by_path(
132 conn: &mut PgConnection,
133 path: &str,
134) -> ModelResult<StoredFileDetails> {
135 let res = sqlx::query_as!(
136 StoredFileDetails,
137 r#"
138SELECT name,
139 mime
140FROM file_uploads
141WHERE path = $1
142 AND deleted_at IS NULL
143"#,
144 path,
145 )
146 .fetch_one(conn)
147 .await?;
148 Ok(res)
149}
150
151pub async fn delete_and_fetch_path(conn: &mut PgConnection, id: Uuid) -> ModelResult<String> {
152 let res = sqlx::query!(
153 "
154UPDATE file_uploads
155SET deleted_at = now()
156WHERE id = $1
157AND deleted_at IS NULL
158RETURNING *
159",
160 id
161 )
162 .fetch_one(conn)
163 .await?;
164 Ok(res.path)
165}
166
167pub async fn backdate(conn: &mut PgConnection, id: Uuid, age: Duration) -> ModelResult<()> {
172 sqlx::query!(
173 "UPDATE file_uploads SET created_at = now() - $2::interval WHERE id = $1",
174 id,
175 age as Duration
176 )
177 .execute(conn)
178 .await?;
179 Ok(())
180}