Skip to main content

docbox_database/models/
presigned_upload_task.rs

1//! # Presigned Upload Task
2//!
3//! Background uploading task, handles storing data about pending
4//! pre-signed S3 file uploads. Used to track completion and uploads
5//! that were cancelled or failed
6
7use chrono::{DateTime, Utc};
8use serde::{Deserialize, Serialize};
9use sqlx::{postgres::PgQueryResult, prelude::FromRow, types::Json};
10use uuid::Uuid;
11
12use super::{document_box::DocumentBoxScopeRaw, file::FileId, folder::FolderId, user::UserId};
13use crate::{DbErr, DbExecutor, DbResult};
14
15pub type PresignedUploadTaskId = Uuid;
16
17/// Task storing the details for a presigned upload task
18#[derive(Debug, Clone, FromRow, Serialize)]
19pub struct PresignedUploadTask {
20    /// ID of the upload task
21    pub id: PresignedUploadTaskId,
22    /// File created from the outcome of this task
23    #[sqlx(json)]
24    pub status: PresignedTaskStatus,
25
26    /// Name of the file being uploaded
27    pub name: String,
28    /// Mime type of the file being uploaded
29    pub mime: String,
30    /// Expected size in bytes of the file
31    pub size: i32,
32
33    /// ID of the document box the folder belongs to
34    pub document_box: DocumentBoxScopeRaw,
35    /// Target folder to store the file in
36    pub folder_id: FolderId,
37    /// S3 key where the file should be stored
38    pub file_key: String,
39
40    /// Creation timestamp of the upload task
41    pub created_at: DateTime<Utc>,
42    /// Timestamp of when the presigned URL will expire
43    /// (Used as the date for background cleanup, to ensure that all files)
44    pub expires_at: DateTime<Utc>,
45    /// User who created the file
46    pub created_by: Option<UserId>,
47
48    /// Optional file to make the parent of this file
49    pub parent_id: Option<FileId>,
50
51    /// Config that can be used when processing for additional
52    /// configuration to how the file is processed
53    pub processing_config: Option<Json<serde_json::Value>>,
54}
55
56impl Eq for PresignedUploadTask {}
57
58impl PartialEq for PresignedUploadTask {
59    fn eq(&self, other: &Self) -> bool {
60        self.id.eq(&other.id)
61            && self.status.eq(&other.status)
62            && self.name.eq(&other.name)
63            && self.mime.eq(&other.mime)
64            && self.size.eq(&other.size)
65            && self.document_box.eq(&other.document_box)
66            && self.folder_id.eq(&other.folder_id)
67            && self.file_key.eq(&other.file_key)
68            // Reduce precision when checking creation timestamp
69            // (Database does not store the full precision)
70            && self
71                .created_at
72                .timestamp_millis()
73                .eq(&other.created_at.timestamp_millis())
74            // Reduce precision when checking creation timestamp
75            // (Database does not store the full precision)
76            && self
77                .expires_at
78                .timestamp_millis()
79                .eq(&other.expires_at.timestamp_millis())
80            && self.created_by.eq(&self.created_by)
81            && self.parent_id.eq(&other.parent_id)
82            && self.processing_config.eq(&other.processing_config)
83    }
84}
85
86#[derive(Debug, Clone, Deserialize, Serialize, PartialEq, Eq)]
87#[serde(tag = "status")]
88pub enum PresignedTaskStatus {
89    Pending,
90    Completed { file_id: FileId },
91    Failed { error: String },
92}
93
94/// Required data to create a presigned upload task
95#[derive(Default)]
96pub struct CreatePresignedUploadTask {
97    pub name: String,
98    pub mime: String,
99    pub document_box: DocumentBoxScopeRaw,
100    pub folder_id: FolderId,
101    pub size: i32,
102    pub file_key: String,
103    pub created_by: Option<UserId>,
104    pub expires_at: DateTime<Utc>,
105    pub parent_id: Option<FileId>,
106    pub processing_config: Option<serde_json::Value>,
107}
108
109impl PresignedUploadTask {
110    /// Create a new presigned upload task
111    pub async fn create(
112        db: impl DbExecutor<'_>,
113        create: CreatePresignedUploadTask,
114    ) -> DbResult<PresignedUploadTask> {
115        let id = Uuid::new_v4();
116        let created_at = Utc::now();
117
118        let task = PresignedUploadTask {
119            id,
120            status: PresignedTaskStatus::Pending,
121            //
122            name: create.name,
123            mime: create.mime,
124            size: create.size,
125            //
126            document_box: create.document_box,
127            folder_id: create.folder_id,
128            file_key: create.file_key,
129            //
130            created_at,
131            expires_at: create.expires_at,
132            created_by: create.created_by,
133
134            parent_id: create.parent_id,
135            processing_config: create.processing_config.map(Json),
136        };
137
138        let status_json =
139            serde_json::to_value(&task.status).map_err(|err| DbErr::Encode(Box::new(err)))?;
140        let processing_config_json = task.processing_config.clone();
141
142        sqlx::query(
143            r#"
144            INSERT INTO "docbox_presigned_upload_tasks" (
145                "id",
146                "status",
147                "name",
148                "mime",
149                "size",
150                "document_box",
151                "folder_id",
152                "file_key",
153                "created_at",
154                "expires_at",
155                "created_by",
156                "parent_id",
157                "processing_config"
158            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
159            "#,
160        )
161        .bind(task.id)
162        .bind(status_json)
163        .bind(task.name.as_str())
164        .bind(task.mime.clone())
165        .bind(task.size)
166        .bind(task.document_box.as_str())
167        .bind(task.folder_id)
168        .bind(task.file_key.as_str())
169        .bind(task.created_at)
170        .bind(task.expires_at)
171        .bind(task.created_by.clone())
172        .bind(task.parent_id)
173        .bind(processing_config_json)
174        .execute(db)
175        .await?;
176
177        Ok(task)
178    }
179
180    pub async fn set_status(
181        &mut self,
182        db: impl DbExecutor<'_>,
183        status: PresignedTaskStatus,
184    ) -> DbResult<()> {
185        let status_json =
186            serde_json::to_value(&status).map_err(|err| DbErr::Encode(Box::new(err)))?;
187
188        sqlx::query(r#"UPDATE "docbox_presigned_upload_tasks" SET "status" = $1 WHERE "id" = $2"#)
189            .bind(status_json)
190            .bind(self.id)
191            .execute(db)
192            .await?;
193
194        self.status = status;
195        Ok(())
196    }
197
198    /// Find a specific presigned upload task
199    pub async fn find(
200        db: impl DbExecutor<'_>,
201        scope: &DocumentBoxScopeRaw,
202        task_id: PresignedUploadTaskId,
203    ) -> DbResult<Option<PresignedUploadTask>> {
204        sqlx::query_as(
205            r#"SELECT * FROM "docbox_presigned_upload_tasks"
206            WHERE "id" = $1 AND "document_box" = $2"#,
207        )
208        .bind(task_id)
209        .bind(scope)
210        .fetch_optional(db)
211        .await
212    }
213
214    /// Finds all presigned uploads that have expired based on the current date
215    pub async fn find_expired(
216        db: impl DbExecutor<'_>,
217        current_date: DateTime<Utc>,
218    ) -> DbResult<Vec<PresignedUploadTask>> {
219        sqlx::query_as(r#"SELECT * FROM "docbox_presigned_upload_tasks" WHERE "expires_at" < $1"#)
220            .bind(current_date)
221            .fetch_all(db)
222            .await
223    }
224
225    /// Find a specific presigned upload task
226    pub async fn find_by_file_key(
227        db: impl DbExecutor<'_>,
228        file_key: &str,
229    ) -> DbResult<Option<PresignedUploadTask>> {
230        sqlx::query_as(r#"SELECT * FROM "docbox_presigned_upload_tasks" WHERE "file_key" = $1"#)
231            .bind(file_key)
232            .fetch_optional(db)
233            .await
234    }
235
236    /// Delete a specific presigned upload task
237    pub async fn delete(
238        db: impl DbExecutor<'_>,
239        task_id: PresignedUploadTaskId,
240    ) -> DbResult<PgQueryResult> {
241        sqlx::query(r#"DELETE FROM "docbox_presigned_upload_tasks" WHERE "id" = $1"#)
242            .bind(task_id)
243            .execute(db)
244            .await
245    }
246}