backbone_bucket/application/service/
document_preview_service.rs1use std::sync::Arc;
6
7use chrono::Utc;
8use uuid::Uuid;
9
10use super::error::{ServiceError, ServiceResult};
11use crate::domain::entity::{
12 ProcessingJob, ProcessingJobType, JobStatus, Thumbnail, ThumbnailSize,
13};
14use crate::infrastructure::persistence::{
15 ProcessingJobRepository, ThumbnailRepository, StoredFileRepository,
16};
17
18const DOCUMENT_MIME_TYPES: &[&str] = &[
19 "application/pdf",
20 "application/msword",
21 "application/vnd.openxmlformats-officedocument.wordprocessingml.document",
22 "application/vnd.ms-excel",
23 "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet",
24 "application/vnd.ms-powerpoint",
25 "application/vnd.openxmlformats-officedocument.presentationml.presentation",
26 "text/plain",
27 "text/csv",
28 "text/markdown",
29];
30
31const DEFAULT_PREVIEW_SIZES: &[(ThumbnailSize, i32, i32)] = &[
32 (ThumbnailSize::Medium, 128, 128),
33 (ThumbnailSize::Large, 256, 256),
34 (ThumbnailSize::Xlarge, 512, 512),
35];
36
37pub struct DocumentPreviewService {
38 job_repo: Arc<ProcessingJobRepository>,
39 thumb_repo: Arc<ThumbnailRepository>,
40 file_repo: Arc<StoredFileRepository>,
41}
42
43impl DocumentPreviewService {
44 pub fn new(
45 job_repo: Arc<ProcessingJobRepository>,
46 thumb_repo: Arc<ThumbnailRepository>,
47 file_repo: Arc<StoredFileRepository>,
48 ) -> Self {
49 Self { job_repo, thumb_repo, file_repo }
50 }
51
52 pub async fn enqueue(
53 &self,
54 file_id: Uuid,
55 priority: Option<i32>,
56 pages: Option<Vec<i32>>,
57 ) -> ServiceResult<ProcessingJob> {
58 let file = self.file_repo
59 .find_by_id(&file_id.to_string())
60 .await
61 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
62 .ok_or(ServiceError::NotFound)?;
63
64 if !is_document(&file.mime_type) {
65 return Err(ServiceError::Validation(format!(
66 "File {} is not a document (mime: {})", file_id, file.mime_type
67 )));
68 }
69
70 let _ = &ProcessingJobType::DocumentPreview;
72
73 let mut input = serde_json::json!({
74 "mime_type": file.mime_type,
75 "sizes": DEFAULT_PREVIEW_SIZES.iter().map(|(size, w, h)| {
76 serde_json::json!({ "size": format!("{:?}", size), "width": w, "height": h })
77 }).collect::<Vec<_>>(),
78 });
79
80 if let Some(page_list) = pages {
81 input["pages"] = serde_json::json!(page_list);
82 }
83
84 let job = ProcessingJob::builder()
85 .file_id(file_id)
86 .job_type(ProcessingJobType::DocumentPreview)
87 .status(JobStatus::Pending)
88 .priority(priority.unwrap_or(0))
89 .input_data(input)
90 .retry_count(0)
91 .max_retries(3)
92 .build()
93 .map_err(|e| ServiceError::Validation(e))?;
94
95 let created = self.job_repo
96 .create(&job)
97 .await
98 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
99
100 Ok(created)
101 }
102
103 pub async fn record_preview(
104 &self,
105 file_id: Uuid,
106 size: ThumbnailSize,
107 width: i32,
108 height: i32,
109 storage_key: &str,
110 size_bytes: i64,
111 generation_time_ms: Option<i32>,
112 ) -> ServiceResult<Thumbnail> {
113 let thumb = Thumbnail::builder()
114 .file_id(file_id)
115 .size(size)
116 .width(width)
117 .height(height)
118 .storage_key(storage_key.to_string())
119 .mime_type("image/webp".to_string())
120 .format("webp".to_string())
121 .quality(85)
122 .size_bytes(size_bytes)
123 .generated_at(Utc::now())
124 .generation_time_ms(generation_time_ms.unwrap_or(0))
125 .source_version(1)
126 .is_stale(false)
127 .build()
128 .map_err(|e| ServiceError::Validation(e))?;
129
130 let created = self.thumb_repo
131 .create(&thumb)
132 .await
133 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?;
134
135 Ok(created)
136 }
137
138 pub async fn complete_job(
139 &self,
140 job_id: Uuid,
141 _file_id: Uuid,
142 page_count: Option<i32>,
143 ) -> ServiceResult<ProcessingJob> {
144 let mut job = self.find_job(job_id).await?;
145
146 let mut result = serde_json::json!({
147 "completed_at": Utc::now().to_rfc3339(),
148 "previews_generated": true,
149 });
150
151 if let Some(count) = page_count {
152 result["page_count"] = serde_json::json!(count);
153 }
154
155 job.status = JobStatus::Completed;
156 job.result_data = Some(result);
157 job.completed_at = Some(Utc::now());
158 job.metadata.touch();
159
160 let id_str = job.id.to_string();
161 let updated = self.job_repo
162 .update(&id_str, &job)
163 .await
164 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
165 .ok_or(ServiceError::NotFound)?;
166
167 Ok(updated)
170 }
171
172 pub async fn fail_job(
173 &self,
174 job_id: Uuid,
175 error_message: &str,
176 ) -> ServiceResult<ProcessingJob> {
177 let mut job = self.find_job(job_id).await?;
178
179 job.status = JobStatus::Failed;
180 job.error_message = Some(error_message.to_string());
181 job.completed_at = Some(Utc::now());
182 job.metadata.touch();
183
184 let id_str = job.id.to_string();
185 self.job_repo
186 .update(&id_str, &job)
187 .await
188 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
189 .ok_or(ServiceError::NotFound)
190 }
191
192 pub async fn get_previews(&self, _file_id: Uuid) -> ServiceResult<Vec<Thumbnail>> {
193 Ok(vec![])
195 }
196
197 pub async fn regenerate(&self, file_id: Uuid) -> ServiceResult<ProcessingJob> {
198 self.enqueue(file_id, Some(1), None).await
200 }
201
202 async fn find_job(&self, job_id: Uuid) -> ServiceResult<ProcessingJob> {
203 self.job_repo
204 .find_by_id(&job_id.to_string())
205 .await
206 .map_err(|e| ServiceError::Repository(backbone_core::RepositoryError::DatabaseError(e.to_string())))?
207 .ok_or(ServiceError::NotFound)
208 }
209}
210
211fn is_document(mime_type: &str) -> bool {
212 DOCUMENT_MIME_TYPES.contains(&mime_type)
213}