docbox_database/models/
generated_file.rs

1use std::str::FromStr;
2
3use chrono::{DateTime, Utc};
4use serde::{Deserialize, Serialize};
5use sqlx::{postgres::PgQueryResult, prelude::FromRow};
6use utoipa::ToSchema;
7use uuid::Uuid;
8
9use super::{document_box::DocumentBoxScopeRaw, file::FileId};
10use crate::{DbExecutor, DbResult};
11
12pub type GeneratedFileId = Uuid;
13
14#[derive(
15    Debug,
16    Clone,
17    Copy,
18    strum::EnumString,
19    strum::Display,
20    Deserialize,
21    Serialize,
22    ToSchema,
23    PartialEq,
24    Eq,
25)]
26pub enum GeneratedFileType {
27    /// Conversion to PDF file
28    Pdf,
29    /// Full sized cover page render
30    CoverPage,
31    /// Small file sized thumbnail image
32    SmallThumbnail,
33    /// Larger thumbnail image, for a small preview tooltip
34    LargeThumbnail,
35    /// Text content extracted from the file
36    TextContent,
37    /// HTML content extracted from things like emails
38    HtmlContent,
39    /// JSON encoded metadata for the file
40    /// (Used by emails to store the email metadata in an accessible ways)
41    Metadata,
42}
43
44impl TryFrom<String> for GeneratedFileType {
45    type Error = strum::ParseError;
46    fn try_from(value: String) -> Result<Self, Self::Error> {
47        GeneratedFileType::from_str(&value)
48    }
49}
50
51/// File generated as an artifact of an uploaded file
52#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
53pub struct GeneratedFile {
54    /// Unique identifier for the file
55    #[schema(value_type = Uuid)]
56    pub id: GeneratedFileId,
57    /// File this generated file belongs  to
58    #[schema(value_type = Uuid)]
59    pub file_id: FileId,
60    /// Mime type of the generated file content
61    pub mime: String,
62    /// Type of the generated file
63    #[sqlx(rename = "type")]
64    #[serde(rename = "type")]
65    #[sqlx(try_from = "String")]
66    pub ty: GeneratedFileType,
67    /// Hash of the file this was generated from
68    pub hash: String,
69    /// S3 key pointing to the file
70    #[serde(skip)]
71    pub file_key: String,
72    /// When the file was created
73    pub created_at: DateTime<Utc>,
74}
75
76impl Eq for GeneratedFile {}
77
78impl PartialEq for GeneratedFile {
79    fn eq(&self, other: &Self) -> bool {
80        self.id.eq(&other.id)
81            && self.file_id.eq(&other.file_id)
82            && self.mime.eq(&other.mime)
83            && self.ty.eq(&other.ty)
84            && self.hash.eq(&other.hash)
85            && self.file_key.eq(&other.file_key)
86            // Reduce precision when checking creation timestamp
87            // (Database does not store the full precision)
88            && self
89                .created_at
90                .timestamp_millis()
91                .eq(&other.created_at.timestamp_millis())
92    }
93}
94
95#[derive(Debug)]
96pub struct CreateGeneratedFile {
97    pub id: Uuid,
98    pub file_id: FileId,
99    pub mime: String,
100    pub ty: GeneratedFileType,
101    pub hash: String,
102    pub file_key: String,
103    pub created_at: DateTime<Utc>,
104}
105
106impl GeneratedFile {
107    pub async fn create(
108        db: impl DbExecutor<'_>,
109        CreateGeneratedFile {
110            id,
111            file_id,
112            ty,
113            hash,
114            file_key,
115            mime,
116            created_at,
117        }: CreateGeneratedFile,
118    ) -> DbResult<GeneratedFile> {
119        sqlx::query(
120            r#"
121            INSERT INTO "docbox_generated_files"
122            ("id", "file_id", "mime", "type", "hash", "file_key", "created_at")
123            VALUES ($1, $2, $3, $4, $5, $6, $7)
124        "#,
125        )
126        .bind(id)
127        .bind(file_id)
128        .bind(mime.as_str())
129        .bind(ty.to_string())
130        .bind(hash.as_str())
131        .bind(file_key.as_str())
132        .bind(created_at)
133        .execute(db)
134        .await?;
135
136        Ok(GeneratedFile {
137            id,
138            file_id,
139            mime,
140            ty,
141            hash,
142            file_key,
143            created_at,
144        })
145    }
146
147    /// Deletes the generated file
148    pub async fn delete(self, db: impl DbExecutor<'_>) -> DbResult<PgQueryResult> {
149        sqlx::query(r#"DELETE FROM "docbox_generated_files" WHERE "id" = $1"#)
150            .bind(self.id)
151            .execute(db)
152            .await
153    }
154
155    pub async fn find_all(
156        db: impl DbExecutor<'_>,
157        file_id: FileId,
158    ) -> DbResult<Vec<GeneratedFile>> {
159        sqlx::query_as(r#"SELECT * FROM "docbox_generated_files" WHERE "file_id" = $1"#)
160            .bind(file_id)
161            .fetch_all(db)
162            .await
163    }
164
165    /// Finds a specific file using its full path scope -> folder -> file
166    pub async fn find(
167        db: impl DbExecutor<'_>,
168        scope: &DocumentBoxScopeRaw,
169        file_id: FileId,
170        ty: GeneratedFileType,
171    ) -> DbResult<Option<GeneratedFile>> {
172        sqlx::query_as(
173            r#"
174            SELECT "gen".*
175            FROM "docbox_generated_files" "gen"
176            -- Join on the file itself
177            INNER JOIN "docbox_files" "file" ON "gen".file_id = "file"."id"
178            -- Join to the file parent folder
179            INNER JOIN "docbox_folders" "folder" ON "file"."folder_id" = "folder"."id"
180            -- Only find the matching type for the specified file
181            WHERE "file"."id" = $1 AND "folder"."document_box" = $2 AND "gen"."type" = $3
182        "#,
183        )
184        .bind(file_id)
185        .bind(scope)
186        .bind(ty.to_string())
187        .fetch_optional(db)
188        .await
189    }
190}