sofie_storage/
variants.rs1use bytes::Bytes;
2use ring::digest::{SHA256, digest};
3use sofie_core::{Error, Result};
4
5use crate::{ContentType, Storage, StorageKey};
6
7pub const MAX_ORIGINAL_BYTES: u64 = 32 * 1024 * 1024;
9const RESIZED: [&str; 4] = ["image/jpeg", "image/png", "image/webp", "image/gif"];
10
11#[derive(Clone, Copy, Debug, PartialEq, Eq)]
12pub struct Variant {
13 pub name: &'static str,
14 pub width: u32,
15}
16
17impl Variant {
18 #[must_use]
19 pub const fn new(name: &'static str, width: u32) -> Self {
20 Self { name, width }
21 }
22}
23
24pub trait Resizer: Send + Sync {
26 fn resize(&self, image: &[u8], content_type: &ContentType, width: u32) -> Result<Resized>;
27}
28
29pub struct Resized {
30 pub bytes: Bytes,
31 pub content_type: ContentType,
32}
33
34#[derive(Clone, Debug, PartialEq, Eq)]
35pub struct MadeVariant {
36 pub variant: Variant,
37 pub key: StorageKey,
38 pub content_type: ContentType,
39}
40
41impl StorageKey {
42 #[must_use]
44 pub fn variant(&self, variant: &Variant) -> Self {
45 let derived = digest(
46 &SHA256,
47 format!("{}/{}", self.as_str(), variant.name).as_bytes(),
48 );
49 Self::from_hex(sofie_core::encoding::hex::encode(&derived.as_ref()[..16]))
50 }
51}
52
53async fn resized(
54 resizer: &'static dyn Resizer,
55 image: Bytes,
56 content_type: ContentType,
57 variant: Variant,
58) -> Result<Resized> {
59 tokio::task::spawn_blocking(move || resizer.resize(&image, &content_type, variant.width))
60 .await
61 .map_err(|error| Error::internal("resizing an image", error))?
62}
63
64impl Storage {
65 pub async fn make_variants(
68 &self,
69 original: &StorageKey,
70 variants: &[Variant],
71 resizer: &'static dyn Resizer,
72 ) -> Result<Vec<MadeVariant>> {
73 let image = Bytes::from(self.read_image(original).await?);
74 let content_type = ContentType::sniff(&image)
75 .filter(|sniffed| RESIZED.contains(&sniffed.as_str()))
76 .ok_or_else(|| Error::invalid("image", "is not a JPEG, PNG, WebP or GIF image"))?;
77 let mut made = Vec::with_capacity(variants.len());
78 for variant in variants {
79 let key = original.variant(variant);
80 let content_type = if let Some(kept) = self.kept_type(&key).await? {
81 kept
82 } else {
83 let smaller =
84 resized(resizer, image.clone(), content_type.clone(), *variant).await?;
85 self.put_at(&key, smaller.bytes).await?;
86 smaller.content_type
87 };
88 made.push(MadeVariant {
89 variant: *variant,
90 key,
91 content_type,
92 });
93 }
94 Ok(made)
95 }
96
97 pub async fn delete_with_variants(
98 &self,
99 original: &StorageKey,
100 variants: &[Variant],
101 ) -> Result<()> {
102 for variant in variants {
103 self.delete(&original.variant(variant)).await?;
104 }
105 self.delete(original).await
106 }
107
108 async fn kept_type(&self, key: &StorageKey) -> Result<Option<ContentType>> {
109 let Some(mut content) = self.get(key).await? else {
110 return Ok(None);
111 };
112 let start = content.chunk().await?.unwrap_or_default();
113 Ok(Some(
114 ContentType::sniff(&start).unwrap_or_else(ContentType::octet_stream),
115 ))
116 }
117
118 async fn read_image(&self, key: &StorageKey) -> Result<Vec<u8>> {
119 let mut content = self.get(key).await?.ok_or(Error::NotFound)?;
120 let mut image = Vec::new();
121 while let Some(chunk) = content.chunk().await? {
122 image.extend_from_slice(&chunk);
123 if u64::try_from(image.len()).unwrap_or(u64::MAX) > MAX_ORIGINAL_BYTES {
124 return Err(Error::invalid(
125 "image",
126 "is larger than 32 MiB, the most a variant is made from",
127 ));
128 }
129 }
130 Ok(image)
131 }
132
133 async fn put_at(&self, key: &StorageKey, bytes: Bytes) -> Result<()> {
134 let mut writer = self.store.writer(key).await?;
135 if let Err(error) = writer.write(bytes).await {
136 writer.abort().await?;
137 return Err(error);
138 }
139 writer.finish().await
140 }
141}