Skip to main content

sofie_storage/
variants.rs

1use bytes::Bytes;
2use ring::digest::{SHA256, digest};
3use sofie_core::{Error, Result};
4
5use crate::{ContentType, Storage, StorageKey};
6
7/// Read whole into memory for the resizer, so larger originals are refused.
8pub 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
24/// The app's: the framework decodes no images, so it brings no image library.
25pub 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    /// Found again without being stored, and still no name a caller chose.
43    #[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    /// A variant already kept is not made again, so a retried job finishes the rest.
66    /// `'static`, since resizing runs on the blocking pool rather than stall an async worker.
67    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}