1use chrono::{DateTime, Utc};
2use serde::Serialize;
3use sqlx::{postgres::PgQueryResult, prelude::FromRow};
4use utoipa::ToSchema;
5use uuid::Uuid;
6
7use super::{
8 document_box::DocumentBoxScopeRaw,
9 folder::FolderId,
10 user::{User, UserId},
11};
12use crate::{
13 DbExecutor, DbResult,
14 models::{
15 document_box::DocumentBoxScopeRawRef,
16 shared::{
17 CountResult, DocboxInputPair, FolderPathSegment, TotalSizeResult, WithFullPath,
18 WithFullPathScope,
19 },
20 },
21};
22
23pub type FileId = Uuid;
24
25#[derive(Debug, Clone, FromRow, Serialize, sqlx::Type, ToSchema)]
26#[sqlx(type_name = "docbox_file")]
27pub struct File {
28 #[schema(value_type = Uuid)]
30 pub id: FileId,
31 pub name: String,
33 pub mime: String,
35 #[schema(value_type = Uuid)]
37 pub folder_id: FolderId,
38 #[schema(value_type = Option<Uuid>)]
41 pub parent_id: Option<FileId>,
42 pub hash: String,
44 pub size: i32,
46 pub encrypted: bool,
48 pub pinned: bool,
50 #[serde(skip)]
52 pub file_key: String,
53 pub created_at: DateTime<Utc>,
55 #[serde(skip)]
57 pub created_by: Option<UserId>,
58}
59
60impl Eq for File {}
61
62impl PartialEq for File {
63 fn eq(&self, other: &Self) -> bool {
64 self.id.eq(&other.id)
65 && self.name.eq(&other.name)
66 && self.mime.eq(&other.mime)
67 && self.folder_id.eq(&other.folder_id)
68 && self.parent_id.eq(&other.parent_id)
69 && self.hash.eq(&other.hash)
70 && self.size.eq(&other.size)
71 && self.encrypted.eq(&other.encrypted)
72 && self.pinned.eq(&other.pinned)
73 && self.file_key.eq(&other.file_key)
74 && self.created_by.eq(&self.created_by)
75 && self
78 .created_at
79 .timestamp_millis()
80 .eq(&other.created_at.timestamp_millis())
81 }
82}
83
84#[derive(Debug, FromRow, Serialize)]
85pub struct FileWithScope {
86 #[sqlx(flatten)]
87 pub file: File,
88 pub scope: String,
89}
90
91#[derive(Debug, Clone, FromRow, Serialize, ToSchema)]
93pub struct FileWithExtra {
94 #[serde(flatten)]
95 pub file: File,
96 #[schema(nullable, value_type = User)]
97 pub created_by: Option<User>,
98 #[schema(nullable, value_type = User)]
99 pub last_modified_by: Option<User>,
100 pub last_modified_at: Option<DateTime<Utc>>,
102}
103
104#[derive(Debug, FromRow, Serialize, ToSchema)]
106pub struct ResolvedFileWithExtra {
107 #[serde(flatten)]
108 #[sqlx(flatten)]
109 pub file: FileWithExtra,
110 pub full_path: Vec<FolderPathSegment>,
111}
112
113#[derive(Debug, Default, Clone)]
114pub struct CreateFile {
115 pub id: FileId,
117
118 pub parent_id: Option<FileId>,
121
122 pub name: String,
123 pub mime: String,
124 pub folder_id: FolderId,
125 pub hash: String,
126 pub size: i32,
127 pub file_key: String,
128 pub created_by: Option<UserId>,
129 pub created_at: DateTime<Utc>,
130 pub encrypted: bool,
131}
132
133impl File {
134 pub async fn create(
135 db: impl DbExecutor<'_>,
136 CreateFile {
137 id,
138 parent_id,
139 name,
140 mime,
141 folder_id,
142 hash,
143 size,
144 file_key,
145 created_by,
146 created_at,
147 encrypted,
148 }: CreateFile,
149 ) -> DbResult<File> {
150 sqlx::query(
151 r#"INSERT INTO "docbox_files" (
152 "id", "name", "mime", "folder_id", "hash", "size",
153 "encrypted", "file_key", "created_by", "created_at",
154 "parent_id"
155 )
156 VALUES ($1, $2, $3, $4, $5, $6, $7, $8, $9, $10, $11)
157 "#,
158 )
159 .bind(id)
160 .bind(name.as_str())
161 .bind(mime.as_str())
162 .bind(folder_id)
163 .bind(hash.as_str())
164 .bind(size)
165 .bind(encrypted)
166 .bind(file_key.as_str())
167 .bind(created_by.as_ref())
168 .bind(created_at)
169 .bind(parent_id)
170 .execute(db)
171 .await?;
172
173 Ok(File {
174 id,
175 name,
176 mime,
177 folder_id,
178 hash,
179 size,
180 encrypted,
181 file_key,
182 created_by,
183 created_at,
184 parent_id,
185 pinned: false,
186 })
187 }
188
189 pub async fn all(
190 db: impl DbExecutor<'_>,
191 offset: u64,
192 page_size: u64,
193 ) -> DbResult<Vec<FileWithScope>> {
194 sqlx::query_as(
195 r#"
196 SELECT
197 "file".*,
198 "folder"."document_box" AS "scope"
199 FROM "docbox_files" "file"
200 INNER JOIN "docbox_folders" "folder" ON "file"."folder_id" = "folder"."id"
201 ORDER BY "created_at" ASC
202 OFFSET $1
203 LIMIT $2
204 "#,
205 )
206 .bind(offset as i64)
207 .bind(page_size as i64)
208 .fetch_all(db)
209 .await
210 }
211
212 pub async fn move_to_folder(
213 mut self,
214 db: impl DbExecutor<'_>,
215 folder_id: FolderId,
216 ) -> DbResult<File> {
217 sqlx::query(r#"UPDATE "docbox_files" SET "folder_id" = $1 WHERE "id" = $2"#)
218 .bind(folder_id)
219 .bind(self.id)
220 .execute(db)
221 .await?;
222
223 self.folder_id = folder_id;
224
225 Ok(self)
226 }
227
228 pub async fn rename(mut self, db: impl DbExecutor<'_>, name: String) -> DbResult<File> {
229 sqlx::query(r#"UPDATE "docbox_files" SET "name" = $1 WHERE "id" = $2"#)
230 .bind(name.as_str())
231 .bind(self.id)
232 .execute(db)
233 .await?;
234
235 self.name = name;
236
237 Ok(self)
238 }
239
240 pub async fn set_pinned(mut self, db: impl DbExecutor<'_>, pinned: bool) -> DbResult<File> {
242 sqlx::query(r#"UPDATE "docbox_files" SET "pinned" = $1 WHERE "id" = $2"#)
243 .bind(pinned)
244 .bind(self.id)
245 .execute(db)
246 .await?;
247
248 self.pinned = pinned;
249
250 Ok(self)
251 }
252
253 pub async fn set_encrypted(
255 mut self,
256 db: impl DbExecutor<'_>,
257 encrypted: bool,
258 ) -> DbResult<File> {
259 sqlx::query(r#"UPDATE "docbox_files" SET "encrypted" = $1 WHERE "id" = $2"#)
260 .bind(encrypted)
261 .bind(self.id)
262 .execute(db)
263 .await?;
264
265 self.encrypted = encrypted;
266
267 Ok(self)
268 }
269
270 pub async fn set_mime(mut self, db: impl DbExecutor<'_>, mime: String) -> DbResult<File> {
272 sqlx::query(r#"UPDATE "docbox_files" SET "mime" = $1 WHERE "id" = $2"#)
273 .bind(&mime)
274 .bind(self.id)
275 .execute(db)
276 .await?;
277
278 self.mime = mime;
279
280 Ok(self)
281 }
282
283 pub async fn all_by_mime(
284 db: impl DbExecutor<'_>,
285 mime: &str,
286 offset: u64,
287 page_size: u64,
288 ) -> DbResult<Vec<FileWithScope>> {
289 sqlx::query_as(
290 r#"
291 SELECT
292 "file".*,
293 "folder"."document_box" AS "scope"
294 FROM "docbox_files" "file"
295 INNER JOIN "docbox_folders" "folder" ON "file"."folder_id" = "folder"."id"
296 WHERE "file"."mime" = $1
297 ORDER BY "created_at" ASC
298 OFFSET $2
299 LIMIT $3
300 "#,
301 )
302 .bind(mime)
303 .bind(offset as i64)
304 .bind(page_size as i64)
305 .fetch_all(db)
306 .await
307 }
308
309 pub async fn all_by_mimes(
310 db: impl DbExecutor<'_>,
311 mimes: &[&str],
312 offset: u64,
313 page_size: u64,
314 ) -> DbResult<Vec<FileWithScope>> {
315 sqlx::query_as(
316 r#"
317 SELECT
318 "file".*,
319 "folder"."document_box" AS "scope"
320 FROM "docbox_files" AS "file"
321 INNER JOIN "docbox_folders" "folder" ON "file"."folder_id" = "folder"."id"
322 WHERE "mime" = ANY($1) AND "file"."encrypted" = FALSE
323 ORDER BY "file"."created_at" ASC
324 OFFSET $2
325 LIMIT $3
326 "#,
327 )
328 .bind(mimes)
329 .bind(offset as i64)
330 .bind(page_size as i64)
331 .fetch_all(db)
332 .await
333 }
334
335 pub async fn find(
337 db: impl DbExecutor<'_>,
338 scope: &DocumentBoxScopeRaw,
339 file_id: FileId,
340 ) -> DbResult<Option<File>> {
341 sqlx::query_as(
342 r#"
343 SELECT "file".*
344 FROM "docbox_files" AS "file"
345 INNER JOIN "docbox_folders" "folder" ON "file"."folder_id" = "folder"."id"
346 WHERE "file"."id" = $1 AND "folder"."document_box" = $2
347 "#,
348 )
349 .bind(file_id)
350 .bind(scope)
351 .fetch_optional(db)
352 .await
353 }
354
355 pub async fn resolve_path(
358 db: impl DbExecutor<'_>,
359 file_id: FileId,
360 ) -> DbResult<Vec<FolderPathSegment>> {
361 sqlx::query_as(r#"SELECT "id", "name" FROM resolve_file_path($1)"#)
362 .bind(file_id)
363 .fetch_all(db)
364 .await
365 }
366
367 pub async fn find_by_parent(
368 db: impl DbExecutor<'_>,
369 parent_id: FolderId,
370 ) -> DbResult<Vec<File>> {
371 sqlx::query_as(r#"SELECT * FROM "docbox_files" WHERE "folder_id" = $1"#)
372 .bind(parent_id)
373 .fetch_all(db)
374 .await
375 }
376
377 pub async fn delete(&self, db: impl DbExecutor<'_>) -> DbResult<PgQueryResult> {
379 sqlx::query(r#"DELETE FROM "docbox_files" WHERE "id" = $1"#)
380 .bind(self.id)
381 .execute(db)
382 .await
383 }
384
385 pub async fn resolve_with_extra(
388 db: impl DbExecutor<'_>,
389 scope: &DocumentBoxScopeRaw,
390 file_ids: Vec<Uuid>,
391 ) -> DbResult<Vec<WithFullPath<FileWithExtra>>> {
392 if file_ids.is_empty() {
393 return Ok(Vec::new());
394 }
395
396 sqlx::query_as(r#"SELECT * FROM resolve_files_with_extra($1, $2)"#)
397 .bind(scope)
398 .bind(file_ids)
399 .fetch_all(db)
400 .await
401 }
402
403 pub async fn resolve_with_extra_mixed_scopes(
406 db: impl DbExecutor<'_>,
407 files_scope_with_id: Vec<DocboxInputPair<'_>>,
408 ) -> DbResult<Vec<WithFullPathScope<FileWithExtra>>> {
409 if files_scope_with_id.is_empty() {
410 return Ok(Vec::new());
411 }
412
413 sqlx::query_as(
414 r#"SELECT * FROM resolve_files_with_extra_mixed_scopes($1::docbox_input_pair[])"#,
415 )
416 .bind(files_scope_with_id)
417 .fetch_all(db)
418 .await
419 }
420
421 pub async fn find_with_extra(
425 db: impl DbExecutor<'_>,
426 scope: &DocumentBoxScopeRaw,
427 file_id: FileId,
428 ) -> DbResult<Option<FileWithExtra>> {
429 sqlx::query_as(r#"SELECT * FROM resolve_file_by_id_with_extra($1, $2)"#)
430 .bind(scope)
431 .bind(file_id)
432 .fetch_optional(db)
433 .await
434 }
435
436 pub async fn find_by_parent_folder_with_extra(
437 db: impl DbExecutor<'_>,
438 parent_id: FolderId,
439 ) -> DbResult<Vec<FileWithExtra>> {
440 sqlx::query_as(r#"SELECT * FROM resolve_files_by_parent_folder_with_extra($1)"#)
441 .bind(parent_id)
442 .fetch_all(db)
443 .await
444 }
445
446 pub async fn find_by_parent_file_with_extra(
447 db: impl DbExecutor<'_>,
448 parent_id: FileId,
449 ) -> DbResult<Vec<FileWithExtra>> {
450 sqlx::query_as(r#"SELECT * FROM resolve_files_by_parent_file_with_extra($1)"#)
451 .bind(parent_id)
452 .fetch_all(db)
453 .await
454 }
455
456 pub async fn total_count(db: impl DbExecutor<'_>) -> DbResult<i64> {
458 let count_result: CountResult =
459 sqlx::query_as(r#"SELECT COUNT(*) AS "count" FROM "docbox_files""#)
460 .fetch_one(db)
461 .await?;
462
463 Ok(count_result.count)
464 }
465
466 pub async fn total_size(db: impl DbExecutor<'_>) -> DbResult<i64> {
469 let size_result: TotalSizeResult = sqlx::query_as(
470 r#"
471 SELECT COALESCE(SUM("file"."size"), 0) AS "total_size"
472 FROM "docbox_files" "file";
473 "#,
474 )
475 .fetch_one(db)
476 .await?;
477
478 Ok(size_result.total_size)
479 }
480
481 pub async fn total_size_within_scope(
484 db: impl DbExecutor<'_>,
485 scope: DocumentBoxScopeRawRef<'_>,
486 ) -> DbResult<i64> {
487 let size_result: TotalSizeResult = sqlx::query_as(
488 r#"
489 SELECT COALESCE(SUM("file"."size"), 0) AS "total_size"
490 FROM "docbox_files" "file"
491 INNER JOIN "docbox_folders" "folder" ON "file"."folder_id" = "folder"."id"
492 WHERE "folder"."document_box" = $1;
493 "#,
494 )
495 .bind(scope)
496 .fetch_one(db)
497 .await?;
498
499 Ok(size_result.total_size)
500 }
501}