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::{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, 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
56#[derive(Debug, Clone, Deserialize, Serialize)]
57#[serde(tag = "status")]
58pub enum PresignedTaskStatus {
59    Pending,
60    Completed { file_id: FileId },
61    Failed { error: String },
62}
63
64/// Required data to create a presigned upload task
65pub struct CreatePresignedUploadTask {
66    pub name: String,
67    pub mime: String,
68    pub document_box: DocumentBoxScopeRaw,
69    pub folder_id: FolderId,
70    pub size: i32,
71    pub file_key: String,
72    pub created_by: Option<UserId>,
73    pub expires_at: DateTime<Utc>,
74    pub parent_id: Option<FileId>,
75    pub processing_config: Option<serde_json::Value>,
76}
77
78impl PresignedUploadTask {
79    /// Create a new presigned upload task
80    pub async fn create(
81        db: impl DbExecutor<'_>,
82        create: CreatePresignedUploadTask,
83    ) -> DbResult<PresignedUploadTask> {
84        let id = Uuid::new_v4();
85        let created_at = Utc::now();
86
87        let task = PresignedUploadTask {
88            id,
89            status: PresignedTaskStatus::Pending,
90            //
91            name: create.name,
92            mime: create.mime,
93            size: create.size,
94            //
95            document_box: create.document_box,
96            folder_id: create.folder_id,
97            file_key: create.file_key,
98            //
99            created_at,
100            expires_at: create.expires_at,
101            created_by: create.created_by,
102
103            parent_id: create.parent_id,
104            processing_config: create.processing_config.map(Json),
105        };
106
107        let status_json =
108            serde_json::to_value(&task.status).map_err(|err| DbErr::Encode(Box::new(err)))?;
109        let processing_config_json = task.processing_config.clone();
110
111        sqlx::query(
112            r#"
113            INSERT INTO "docbox_presigned_upload_tasks" (
114                "id",
115                "status",
116                "name",
117                "mime",
118                "size",
119                "document_box",
120                "folder_id",
121                "file_key",
122                "created_at",
123                "expires_at",
124                "created_by",
125                "parent_id",
126                "processing_config"
127            ) VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11, $12, $13)
128            "#,
129        )
130        .bind(task.id)
131        .bind(status_json)
132        .bind(task.name.as_str())
133        .bind(task.mime.clone())
134        .bind(task.size)
135        .bind(task.document_box.as_str())
136        .bind(task.folder_id)
137        .bind(task.file_key.as_str())
138        .bind(task.created_at)
139        .bind(task.expires_at)
140        .bind(task.created_by.clone())
141        .bind(task.parent_id)
142        .bind(processing_config_json)
143        .execute(db)
144        .await?;
145
146        Ok(task)
147    }
148
149    pub async fn set_status(
150        &mut self,
151        db: impl DbExecutor<'_>,
152        status: PresignedTaskStatus,
153    ) -> DbResult<()> {
154        let status_json =
155            serde_json::to_value(&status).map_err(|err| DbErr::Encode(Box::new(err)))?;
156
157        sqlx::query(r#"UPDATE "docbox_presigned_upload_tasks" SET "status" = $1 WHERE "id" = $2"#)
158            .bind(status_json)
159            .bind(self.id)
160            .execute(db)
161            .await?;
162
163        self.status = status;
164        Ok(())
165    }
166
167    /// Find a specific presigned upload task
168    pub async fn find(
169        db: impl DbExecutor<'_>,
170        scope: &DocumentBoxScopeRaw,
171        task_id: PresignedUploadTaskId,
172    ) -> DbResult<Option<PresignedUploadTask>> {
173        sqlx::query_as(
174            r#"SELECT * FROM "docbox_presigned_upload_tasks" 
175            WHERE "id" = $1 AND "document_box" = $2"#,
176        )
177        .bind(task_id)
178        .bind(scope)
179        .fetch_optional(db)
180        .await
181    }
182
183    /// Finds all presigned uploads that have expired based on the current date
184    pub async fn find_expired(
185        db: impl DbExecutor<'_>,
186        current_date: DateTime<Utc>,
187    ) -> DbResult<Vec<PresignedUploadTask>> {
188        sqlx::query_as(r#"SELECT * FROM "docbox_presigned_upload_tasks" WHERE "expires_at" < $1"#)
189            .bind(current_date)
190            .fetch_all(db)
191            .await
192    }
193
194    /// Find a specific presigned upload task
195    pub async fn find_by_file_key(
196        db: impl DbExecutor<'_>,
197        file_key: &str,
198    ) -> DbResult<Option<PresignedUploadTask>> {
199        sqlx::query_as(r#"SELECT * FROM "docbox_presigned_upload_tasks" WHERE "file_key" = $1"#)
200            .bind(file_key)
201            .fetch_optional(db)
202            .await
203    }
204
205    /// Delete a specific presigned upload task
206    pub async fn delete(db: impl DbExecutor<'_>, task_id: PresignedUploadTaskId) -> DbResult<()> {
207        sqlx::query(r#"DELETE FROM "docbox_presigned_upload_tasks" WHERE "id" = $1"#)
208            .bind(task_id)
209            .execute(db)
210            .await?;
211
212        Ok(())
213    }
214}