Skip to main content

headless_lms_models/
file_uploads.rs

1use crate::prelude::*;
2use chrono::Duration;
3
4/// Records a stored object under a fresh id. `size_bytes` is the byte count measured while
5/// receiving it; `None` where the upload path does not count bytes.
6pub async fn insert(
7    conn: &mut PgConnection,
8    name: &str,
9    path: &str,
10    mime: &str,
11    uploader: Option<Uuid>,
12    size_bytes: Option<i64>,
13) -> ModelResult<Uuid> {
14    let id = Uuid::new_v4();
15    insert_with_id(conn, id, name, path, mime, uploader, size_bytes).await?;
16    Ok(id)
17}
18
19/// Records a stored object under an id the caller has already chosen.
20///
21/// For paths that contain the id: the object reaches the store before this row exists, so a path
22/// built from the row's own id has to be settled first. Everything else wants [`insert`].
23pub async fn insert_with_id(
24    conn: &mut PgConnection,
25    id: Uuid,
26    name: &str,
27    path: &str,
28    mime: &str,
29    uploader: Option<Uuid>,
30    size_bytes: Option<i64>,
31) -> ModelResult<()> {
32    sqlx::query!(
33        r#"
34INSERT INTO file_uploads(id, path, name, mime, uploaded_by_user, size_bytes)
35VALUES ($1, $2, $3, $4, $5, $6)
36"#,
37        id,
38        path,
39        name,
40        mime,
41        uploader,
42        size_bytes
43    )
44    .execute(conn)
45    .await?;
46    Ok(())
47}
48
49/// A stored file's name and object-store path.
50#[derive(Debug, Clone, PartialEq, Eq)]
51pub struct FileUploadRef {
52    pub id: Uuid,
53    pub name: String,
54    pub path: String,
55}
56
57/// The named files, in unspecified order and omitting deleted ones. Callers that need a
58/// particular order must impose it themselves.
59pub async fn get_many(conn: &mut PgConnection, ids: &[Uuid]) -> ModelResult<Vec<FileUploadRef>> {
60    let res = sqlx::query_as!(
61        FileUploadRef,
62        "
63SELECT id,
64  name,
65  path
66FROM file_uploads
67WHERE id = ANY($1)
68  AND deleted_at IS NULL
69",
70        ids
71    )
72    .fetch_all(conn)
73    .await?;
74    Ok(res)
75}
76
77pub async fn get_filename(conn: &mut PgConnection, path: &str) -> ModelResult<String> {
78    let res = sqlx::query!(
79        r#"
80SELECT *
81FROM file_uploads
82WHERE path = $1
83"#,
84        path,
85    )
86    .fetch_one(conn)
87    .await?;
88    Ok(res.name)
89}
90
91pub async fn delete_and_fetch_path(conn: &mut PgConnection, id: Uuid) -> ModelResult<String> {
92    let res = sqlx::query!(
93        "
94UPDATE file_uploads
95SET deleted_at = now()
96WHERE id = $1
97AND deleted_at IS NULL
98RETURNING *
99",
100        id
101    )
102    .fetch_one(conn)
103    .await?;
104    Ok(res.path)
105}
106
107/// Moves an upload's creation time `age` into the past, to bring it within a retention window.
108///
109/// Shifts the row rather than the clock because the retention filters compare against Postgres
110/// `now()`, which no Rust-side clock reaches.
111pub async fn backdate(conn: &mut PgConnection, id: Uuid, age: Duration) -> ModelResult<()> {
112    sqlx::query!(
113        "UPDATE file_uploads SET created_at = now() - $2::interval WHERE id = $1",
114        id,
115        age as Duration
116    )
117    .execute(conn)
118    .await?;
119    Ok(())
120}