Skip to main content

backbone_bucket/application/service/
document_preview_service.rs

1//! Document Preview Generator Service
2//!
3//! Hand-written — NOT generated. This file is safe from regeneration.
4
5use 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        // TODO: job_repo.has_active_job — implement custom repository method
71        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        // TODO: file_repo.update_processing_status — implement custom method
168
169        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        // TODO: thumb_repo.find_by_file_id — implement custom repository method
194        Ok(vec![])
195    }
196
197    pub async fn regenerate(&self, file_id: Uuid) -> ServiceResult<ProcessingJob> {
198        // TODO: thumb_repo.mark_stale_by_file — implement custom method
199        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}