2021-04-18 14:06:40 -07:00
|
|
|
use std::fmt::Display;
|
2021-04-14 19:11:00 -07:00
|
|
|
use std::path::PathBuf;
|
2021-04-18 14:06:40 -07:00
|
|
|
use std::pin::Pin;
|
|
|
|
use std::str::FromStr;
|
|
|
|
use std::task::{Context, Poll};
|
2021-04-14 19:11:00 -07:00
|
|
|
|
|
|
|
use actix_web::http::HeaderValue;
|
|
|
|
use async_trait::async_trait;
|
|
|
|
use bytes::Bytes;
|
|
|
|
use chrono::{DateTime, FixedOffset};
|
2021-04-18 20:06:18 -07:00
|
|
|
use fs::ConcurrentFsStream;
|
2021-04-18 14:06:40 -07:00
|
|
|
use futures::{Stream, StreamExt};
|
2021-04-18 14:38:33 -07:00
|
|
|
use log::debug;
|
2021-04-18 14:06:40 -07:00
|
|
|
use thiserror::Error;
|
2021-04-14 19:11:00 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
pub use fs::UpstreamError;
|
2021-04-14 19:11:00 -07:00
|
|
|
pub use generational::GenerationalCache;
|
|
|
|
pub use low_mem::LowMemCache;
|
2021-04-18 20:06:18 -07:00
|
|
|
use tokio::fs::File;
|
|
|
|
use tokio_util::codec::{BytesCodec, FramedRead};
|
2021-04-14 19:11:00 -07:00
|
|
|
|
|
|
|
mod fs;
|
|
|
|
mod generational;
|
|
|
|
mod low_mem;
|
|
|
|
|
|
|
|
#[derive(PartialEq, Eq, Hash, Clone)]
|
|
|
|
pub struct CacheKey(pub String, pub String, pub bool);
|
|
|
|
|
|
|
|
impl Display for CacheKey {
|
|
|
|
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
|
|
|
|
if self.2 {
|
|
|
|
write!(f, "saver/{}/{}", self.0, self.1)
|
|
|
|
} else {
|
|
|
|
write!(f, "data/{}/{}", self.0, self.1)
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl From<CacheKey> for PathBuf {
|
|
|
|
#[inline]
|
|
|
|
fn from(key: CacheKey) -> Self {
|
|
|
|
key.to_string().into()
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
#[derive(Clone)]
|
2021-04-14 19:11:00 -07:00
|
|
|
pub struct CachedImage(pub Bytes);
|
|
|
|
|
2021-04-14 19:52:54 -07:00
|
|
|
#[derive(Copy, Clone)]
|
2021-04-14 19:11:00 -07:00
|
|
|
pub struct ImageMetadata {
|
2021-04-14 19:52:54 -07:00
|
|
|
pub content_type: Option<ImageContentType>,
|
2021-04-18 14:06:40 -07:00
|
|
|
// If we can guarantee a non-zero u32 here we can save 4 bytes
|
|
|
|
pub content_length: Option<u32>,
|
2021-04-14 19:11:00 -07:00
|
|
|
pub last_modified: Option<DateTime<FixedOffset>>,
|
|
|
|
}
|
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
// Confirmed by Ply to be these types: https://link.eddie.sh/ZXfk0
|
2021-04-14 19:52:54 -07:00
|
|
|
#[derive(Copy, Clone)]
|
|
|
|
pub enum ImageContentType {
|
|
|
|
Png,
|
|
|
|
Jpeg,
|
|
|
|
Gif,
|
|
|
|
}
|
|
|
|
|
|
|
|
pub struct InvalidContentType;
|
|
|
|
|
|
|
|
impl FromStr for ImageContentType {
|
|
|
|
type Err = InvalidContentType;
|
|
|
|
|
|
|
|
#[inline]
|
|
|
|
fn from_str(s: &str) -> Result<Self, Self::Err> {
|
|
|
|
match s {
|
|
|
|
"image/png" => Ok(Self::Png),
|
|
|
|
"image/jpeg" => Ok(Self::Jpeg),
|
|
|
|
"image/gif" => Ok(Self::Gif),
|
|
|
|
_ => Err(InvalidContentType),
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
|
|
|
impl AsRef<str> for ImageContentType {
|
|
|
|
#[inline]
|
|
|
|
fn as_ref(&self) -> &str {
|
|
|
|
match self {
|
|
|
|
Self::Png => "image/png",
|
|
|
|
Self::Jpeg => "image/jpeg",
|
|
|
|
Self::Gif => "image/gif",
|
|
|
|
}
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-14 19:11:00 -07:00
|
|
|
#[derive(Debug)]
|
|
|
|
pub enum ImageRequestError {
|
|
|
|
InvalidContentType,
|
|
|
|
InvalidContentLength,
|
|
|
|
InvalidLastModified,
|
|
|
|
}
|
|
|
|
|
|
|
|
impl ImageMetadata {
|
|
|
|
pub fn new(
|
|
|
|
content_type: Option<HeaderValue>,
|
|
|
|
content_length: Option<HeaderValue>,
|
|
|
|
last_modified: Option<HeaderValue>,
|
|
|
|
) -> Result<Self, ImageRequestError> {
|
|
|
|
Ok(Self {
|
|
|
|
content_type: content_type
|
2021-04-14 19:52:54 -07:00
|
|
|
.map(|v| match v.to_str() {
|
|
|
|
Ok(v) => ImageContentType::from_str(v),
|
|
|
|
Err(_) => Err(InvalidContentType),
|
|
|
|
})
|
2021-04-14 19:11:00 -07:00
|
|
|
.transpose()
|
|
|
|
.map_err(|_| ImageRequestError::InvalidContentType)?,
|
|
|
|
content_length: content_length
|
|
|
|
.map(|header_val| {
|
|
|
|
header_val
|
|
|
|
.to_str()
|
|
|
|
.map_err(|_| ImageRequestError::InvalidContentLength)?
|
|
|
|
.parse()
|
|
|
|
.map_err(|_| ImageRequestError::InvalidContentLength)
|
|
|
|
})
|
|
|
|
.transpose()?,
|
|
|
|
last_modified: last_modified
|
|
|
|
.map(|header_val| {
|
|
|
|
DateTime::parse_from_rfc2822(
|
|
|
|
header_val
|
|
|
|
.to_str()
|
|
|
|
.map_err(|_| ImageRequestError::InvalidLastModified)?,
|
|
|
|
)
|
|
|
|
.map_err(|_| ImageRequestError::InvalidLastModified)
|
|
|
|
})
|
|
|
|
.transpose()?,
|
|
|
|
})
|
|
|
|
}
|
|
|
|
}
|
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
type BoxedImageStream = Box<dyn Stream<Item = Result<Bytes, CacheError>> + Unpin + Send>;
|
|
|
|
|
|
|
|
#[derive(Error, Debug)]
|
|
|
|
pub enum CacheError {
|
|
|
|
#[error(transparent)]
|
|
|
|
Io(#[from] std::io::Error),
|
|
|
|
#[error(transparent)]
|
|
|
|
Reqwest(#[from] reqwest::Error),
|
|
|
|
#[error(transparent)]
|
|
|
|
Upstream(#[from] UpstreamError),
|
|
|
|
}
|
|
|
|
|
2021-04-14 19:11:00 -07:00
|
|
|
#[async_trait]
|
2021-04-14 20:44:13 -07:00
|
|
|
pub trait Cache: Send + Sync {
|
2021-04-18 14:06:40 -07:00
|
|
|
async fn get(
|
|
|
|
&mut self,
|
|
|
|
key: &CacheKey,
|
|
|
|
) -> Option<Result<(CacheStream, &ImageMetadata), CacheError>>;
|
2021-04-18 14:38:33 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
async fn put(
|
|
|
|
&mut self,
|
|
|
|
key: CacheKey,
|
|
|
|
image: BoxedImageStream,
|
|
|
|
metadata: ImageMetadata,
|
|
|
|
) -> Result<(CacheStream, &ImageMetadata), CacheError>;
|
2021-04-18 14:38:33 -07:00
|
|
|
|
|
|
|
async fn prune(&mut self) {
|
|
|
|
debug!("Would trim but cache does not implement trimming!");
|
|
|
|
}
|
2021-04-18 14:06:40 -07:00
|
|
|
}
|
2021-04-14 19:11:00 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
pub enum CacheStream {
|
2021-04-18 20:06:18 -07:00
|
|
|
Concurrent(ConcurrentFsStream),
|
2021-04-18 14:06:40 -07:00
|
|
|
Memory(MemStream),
|
2021-04-18 20:06:18 -07:00
|
|
|
Completed(FramedRead<File, BytesCodec>),
|
2021-04-18 14:06:40 -07:00
|
|
|
}
|
2021-04-14 19:11:00 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
impl From<CachedImage> for CacheStream {
|
|
|
|
fn from(image: CachedImage) -> Self {
|
|
|
|
Self::Memory(MemStream(image.0))
|
2021-04-14 19:11:00 -07:00
|
|
|
}
|
2021-04-18 14:06:40 -07:00
|
|
|
}
|
2021-04-14 19:11:00 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
type CacheStreamItem = Result<Bytes, UpstreamError>;
|
|
|
|
|
|
|
|
impl Stream for CacheStream {
|
|
|
|
type Item = CacheStreamItem;
|
|
|
|
|
|
|
|
fn poll_next(self: Pin<&mut Self>, cx: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
|
|
|
match self.get_mut() {
|
2021-04-18 20:06:18 -07:00
|
|
|
Self::Concurrent(stream) => stream.poll_next_unpin(cx),
|
2021-04-18 14:06:40 -07:00
|
|
|
Self::Memory(stream) => stream.poll_next_unpin(cx),
|
2021-04-18 20:06:18 -07:00
|
|
|
Self::Completed(stream) => stream
|
|
|
|
.poll_next_unpin(cx)
|
|
|
|
.map_ok(|v| v.freeze())
|
|
|
|
.map_err(|_| UpstreamError),
|
2021-04-18 14:06:40 -07:00
|
|
|
}
|
2021-04-14 19:11:00 -07:00
|
|
|
}
|
|
|
|
}
|
2021-04-14 20:44:13 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
pub struct MemStream(Bytes);
|
|
|
|
|
|
|
|
impl Stream for MemStream {
|
|
|
|
type Item = CacheStreamItem;
|
|
|
|
|
|
|
|
fn poll_next(mut self: Pin<&mut Self>, _: &mut Context<'_>) -> Poll<Option<Self::Item>> {
|
2021-04-18 20:06:18 -07:00
|
|
|
let new_bytes = self.0.split_to(1460);
|
|
|
|
if new_bytes.is_empty() {
|
|
|
|
Poll::Ready(None)
|
|
|
|
} else {
|
|
|
|
Poll::Ready(Some(Ok(new_bytes)))
|
|
|
|
}
|
2021-04-18 14:06:40 -07:00
|
|
|
}
|
|
|
|
}
|
2021-04-14 20:44:13 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
#[cfg(test)]
|
|
|
|
mod tests {
|
|
|
|
use super::*;
|
2021-04-14 20:44:13 -07:00
|
|
|
|
2021-04-18 14:06:40 -07:00
|
|
|
#[test]
|
|
|
|
fn metadata_size() {
|
|
|
|
assert_eq!(std::mem::size_of::<ImageMetadata>(), 32);
|
2021-04-14 20:44:13 -07:00
|
|
|
}
|
|
|
|
}
|