archivist 0.4.5

Store files in a time or index based directory hierarchy, automatically deleting the oldest files if the size limit is reached
Documentation
use anyhow::{Context, Result};
use flate2::{read::GzEncoder, Compression};
use image::ImageFormat;
use std::{
    cell::RefCell,
    io::{BufRead, Cursor, Read, Seek},
    path::Path,
    pin::Pin,
};
use tokio::io;

pub trait Store {
    fn store(&self, path: &Path) -> Result<Pin<Box<dyn io::AsyncBufRead + Send>>>;
}

impl Store for String {
    fn store(&self, _path: &Path) -> Result<Pin<Box<dyn io::AsyncBufRead + Send>>> {
        Ok(Box::pin(Cursor::new(self.clone())))
    }
}

impl Store for image::DynamicImage {
    fn store(&self, path: &Path) -> Result<Pin<Box<dyn io::AsyncBufRead + Send>>> {
        let mut cursor = match ImageFormat::from_path(path).with_context(|| {
            format!(
                "Can't determine an image format from path: {}",
                path.display()
            )
        })? {
            #[cfg(feature = "turbojpeg")]
            // use faster turbojpeg lib here
            ImageFormat::Jpeg => {
                use image::GenericImageView;
                use turbojpeg::{Compressor, Image, PixelFormat, Subsamp};

                let (format, subsampling) = match &self {
                    Self::ImageLuma8(_) => (PixelFormat::GRAY, Subsamp::Gray),
                    Self::ImageRgb8(_) => (PixelFormat::RGB, Subsamp::None),
                    Self::ImageRgba8(_) => (PixelFormat::RGBA, Subsamp::None),
                    c => {
                        anyhow::bail!(
                            "Invalid pixel DynamicImage pixel format for Jpeg codec: {c:?}",
                        )
                    }
                };
                let (width, height) = self.dimensions();

                let image = Image {
                    pixels: self.as_bytes(),
                    width: width as usize,
                    pitch: format.size() * width as usize,
                    height: height as usize,
                    format,
                };

                let mut compressor = Compressor::new()?;
                compressor.set_quality(75)?;
                compressor.set_subsamp(subsampling)?;
                let v = compressor.compress_to_owned(image.as_deref())?;
                Cursor::new(v.to_vec())
            }
            fmt => {
                let v = Vec::new();
                let mut cursor = Cursor::new(v);
                self.write_to(&mut cursor, fmt)
                    .with_context(|| format!("Can't encode image for path: {}", path.display()))?;
                cursor
            }
        };

        // vital to rewind the cursor here to make the reader start from the beginning
        cursor.rewind()?;
        Ok(Box::pin(cursor))
    }
}

pub struct GzStore<R: BufRead>(RefCell<GzEncoder<R>>);

impl<R: BufRead> GzStore<R> {
    pub fn new(r: R) -> Self {
        GzStore(RefCell::new(GzEncoder::new(r, Compression::fast())))
    }
}

impl<R: BufRead> Store for GzStore<R> {
    fn store(&self, _path: &Path) -> Result<Pin<Box<dyn io::AsyncBufRead + Send>>> {
        let mut buff = Vec::new();
        self.0.borrow_mut().read_to_end(&mut buff)?;
        Ok(Box::pin(Cursor::new(buff)))
    }
}