1use chrono::{SecondsFormat, Utc};
6use futures::TryStreamExt;
7use laterite_core::strata::*;
8use laterite_core::Db;
9use std::collections::HashMap;
10use std::sync::Arc;
11use tokio::sync::Mutex;
12
13use crate::image::{self, ImageError};
14use crate::ingest::blob_path;
15use crate::preset::Preset;
16use crate::record::{MediaRecord, RecordError};
17use crate::{Kind, StorageDriver, StorageError};
18
19#[derive(Iden)]
20pub enum MediaVariants {
21 Table,
22 Id,
23 MediaId,
24 Preset,
25 Path,
26 Width,
27 Height,
28 Size,
29 ContentType,
30 CreatedAt,
31}
32
33#[derive(Debug, Clone, PartialEq, Eq)]
35pub struct Variant {
36 pub id: i64,
37 pub media_id: i64,
38 pub preset: String,
39 pub path: String,
40 pub width: Option<i64>,
41 pub height: Option<i64>,
42 pub size: i64,
43 pub content_type: String,
44 pub created_at: String,
45}
46
47#[derive(Debug, thiserror::Error)]
48pub enum VariantError {
49 #[error(transparent)]
50 Record(#[from] RecordError),
51 #[error(transparent)]
52 Storage(#[from] StorageError),
53 #[error(transparent)]
54 Image(#[from] ImageError),
55 #[error("the file is not an image")]
56 NotImage,
57 #[error("the blob could not be read: {0}")]
58 Read(String),
59}
60
61const COLUMNS: [MediaVariants; 9] = [
62 MediaVariants::Id,
63 MediaVariants::MediaId,
64 MediaVariants::Preset,
65 MediaVariants::Path,
66 MediaVariants::Width,
67 MediaVariants::Height,
68 MediaVariants::Size,
69 MediaVariants::ContentType,
70 MediaVariants::CreatedAt,
71];
72
73fn from_row(r: &sqlx::any::AnyRow) -> Result<Variant, sqlx::Error> {
74 Ok(Variant {
75 id: r.get_int("id")?,
76 media_id: r.get_int("media_id")?,
77 preset: r.get_text("preset")?,
78 path: r.get_text("path")?,
79 width: r.get_int_opt("width")?,
80 height: r.get_int_opt("height")?,
81 size: r.get_int("size")?,
82 content_type: r.get_text("content_type")?,
83 created_at: r.get_text("created_at")?,
84 })
85}
86
87pub fn variant_path(hash: &str, preset: &str, extension: &str) -> String {
89 format!(
90 "variants/{}/{}/{hash}/{preset}.{extension}",
91 &hash[0..2],
92 &hash[2..4]
93 )
94}
95
96pub async fn find(db: &Db, media_id: i64, preset: &str) -> Result<Option<Variant>, RecordError> {
98 let (sql, values) = build(
99 db.backend,
100 Query::select()
101 .columns(COLUMNS)
102 .from(MediaVariants::Table)
103 .and_where(Expr::col(MediaVariants::MediaId).eq(media_id))
104 .and_where(Expr::col(MediaVariants::Preset).eq(preset))
105 .to_owned(),
106 );
107 let row = bind_values(sqlx::query(&sql), values)
108 .fetch_optional(&db.pool)
109 .await?;
110 Ok(row.as_ref().map(from_row).transpose()?)
111}
112
113pub async fn list_for(db: &Db, media_id: i64) -> Result<Vec<Variant>, RecordError> {
115 let (sql, values) = build(
116 db.backend,
117 Query::select()
118 .columns(COLUMNS)
119 .from(MediaVariants::Table)
120 .and_where(Expr::col(MediaVariants::MediaId).eq(media_id))
121 .order_by(MediaVariants::Id, Order::Asc)
122 .to_owned(),
123 );
124 let rows = bind_values(sqlx::query(&sql), values)
125 .fetch_all(&db.pool)
126 .await?;
127 Ok(rows.iter().map(from_row).collect::<Result<_, _>>()?)
128}
129
130pub async fn delete_for(db: &Db, media_id: i64) -> Result<u64, RecordError> {
132 let (sql, values) = build(
133 db.backend,
134 Query::delete()
135 .from_table(MediaVariants::Table)
136 .and_where(Expr::col(MediaVariants::MediaId).eq(media_id))
137 .to_owned(),
138 );
139 Ok(bind_values(sqlx::query(&sql), values)
140 .execute(&db.pool)
141 .await?
142 .rows_affected())
143}
144
145async fn insert(
146 db: &Db,
147 media_id: i64,
148 preset: &str,
149 path: &str,
150 rendered: &image::Rendered,
151) -> Result<Variant, RecordError> {
152 let now = Utc::now().to_rfc3339_opts(SecondsFormat::Micros, true);
153 let stmt = Query::insert()
154 .into_table(MediaVariants::Table)
155 .columns([
156 MediaVariants::MediaId,
157 MediaVariants::Preset,
158 MediaVariants::Path,
159 MediaVariants::Width,
160 MediaVariants::Height,
161 MediaVariants::Size,
162 MediaVariants::ContentType,
163 MediaVariants::CreatedAt,
164 ])
165 .values_panic([
166 media_id.into(),
167 preset.into(),
168 path.into(),
169 i64::from(rendered.width).into(),
170 i64::from(rendered.height).into(),
171 (rendered.bytes.len() as i64).into(),
172 rendered.content_type.into(),
173 now.clone().into(),
174 ])
175 .to_owned();
176 let id = insert_returning_id(db, stmt, MediaVariants::Id).await?;
177 Ok(Variant {
178 id,
179 media_id,
180 preset: preset.to_string(),
181 path: path.to_string(),
182 width: Some(i64::from(rendered.width)),
183 height: Some(i64::from(rendered.height)),
184 size: rendered.bytes.len() as i64,
185 content_type: rendered.content_type.to_string(),
186 created_at: now,
187 })
188}
189
190#[derive(Default)]
193pub struct Variants {
194 locks: Mutex<HashMap<String, Arc<Mutex<()>>>>,
195}
196
197impl Variants {
198 pub fn new() -> Self {
199 Self::default()
200 }
201
202 pub async fn ensure(
205 &self,
206 db: &Db,
207 disk: &dyn StorageDriver,
208 record: &MediaRecord,
209 preset: &Preset,
210 ) -> Result<Variant, VariantError> {
211 if record.kind() != Kind::Image || record.content_type == "image/svg+xml" {
212 return Err(VariantError::NotImage);
213 }
214 if let Some(v) = find(db, record.id, &preset.name).await? {
215 return Ok(v);
216 }
217 let key = format!("{}:{}:{}", record.disk, record.hash, preset.name);
218 let lock = {
219 let mut locks = self.locks.lock().await;
220 Arc::clone(locks.entry(key.clone()).or_default())
221 };
222 let result = {
223 let _held = lock.lock().await;
224 match find(db, record.id, &preset.name).await? {
225 Some(v) => Ok(v),
226 None => make(db, disk, record, preset).await,
227 }
228 };
229 let mut locks = self.locks.lock().await;
230 if Arc::strong_count(&lock) <= 2 {
231 locks.remove(&key);
232 }
233 result
234 }
235}
236
237async fn make(
238 db: &Db,
239 disk: &dyn StorageDriver,
240 record: &MediaRecord,
241 preset: &Preset,
242) -> Result<Variant, VariantError> {
243 let mut stream = disk.get(&blob_path(&record.hash)).await?;
244 let mut bytes: Vec<u8> = Vec::with_capacity(usize::try_from(record.size).unwrap_or(0));
245 while let Some(chunk) = stream
246 .try_next()
247 .await
248 .map_err(|e| VariantError::Read(e.to_string()))?
249 {
250 bytes.extend_from_slice(&chunk);
251 }
252 let orientation = record
253 .meta
254 .get("orientation")
255 .and_then(|o| o.as_u64())
256 .and_then(|o| u8::try_from(o).ok())
257 .unwrap_or(1);
258 let preset_owned = preset.clone();
259 let rendered =
260 tokio::task::spawn_blocking(move || image::render(&bytes, &preset_owned, orientation))
261 .await
262 .map_err(|e| VariantError::Read(e.to_string()))??;
263 let path = variant_path(&record.hash, &preset.name, rendered.extension);
264 disk.put(&path, crate::stream_of(rendered.bytes.clone()))
265 .await?;
266 Ok(insert(db, record.id, &preset.name, &path, &rendered).await?)
267}