Talon/src/icons.rs
ThetaDev c94915e351
All checks were successful
ci/woodpecker/push/woodpecker Pipeline was successful
feat: add last-modified date to all GET responses
feat: add last-version tag to PageInfoModal
fix: set icon size to 48px
2023-04-02 16:52:50 +02:00

110 lines
2.8 KiB
Rust

use std::{
io::{BufReader, Read, Seek},
path::PathBuf,
};
use image::io::Reader as ImageReader;
use poem::{
error::ResponseError,
handler,
http::StatusCode,
web::{Data, Path, StaticFileRequest, StaticFileResponse},
FromRequest, IntoResponse, Request, Response,
};
use crate::Talon;
pub struct Icons {
path: PathBuf,
}
#[derive(thiserror::Error, Debug)]
pub enum ImagesError {
#[error("io error: {0}")]
Io(#[from] std::io::Error),
#[error("image error: {0}")]
Image(#[from] image::ImageError),
#[error("image for `{0}` not found")]
NotFound(String),
}
impl ResponseError for ImagesError {
fn status(&self) -> StatusCode {
match self {
ImagesError::NotFound(_) => StatusCode::NOT_FOUND,
ImagesError::Io(_) | ImagesError::Image(_) => StatusCode::INTERNAL_SERVER_ERROR,
}
}
}
const IMAGE_SIZE: u32 = 48;
const MAX_IMAGE_SIZE: u32 = 4000;
type Result<T> = std::result::Result<T, ImagesError>;
impl Icons {
/// Create a new image storage
pub fn new<P: Into<PathBuf>>(path: P) -> Self {
Self { path: path.into() }
}
fn icon_path(&self, subdomain: &str) -> PathBuf {
self.path.join(format!("{subdomain}.png"))
}
pub fn insert_icon(&self, reader: impl Read + Seek, subdomain: &str) -> Result<()> {
let mut img_reader = ImageReader::new(BufReader::new(reader));
let mut limits = image::io::Limits::default();
limits.max_image_height = Some(MAX_IMAGE_SIZE);
limits.max_image_width = Some(MAX_IMAGE_SIZE);
img_reader.limits(limits);
let img = img_reader.with_guessed_format()?.decode()?;
let img = img.resize(
IMAGE_SIZE,
IMAGE_SIZE,
image::imageops::FilterType::Lanczos3,
);
img.save(self.icon_path(subdomain))?;
Ok(())
}
pub fn delete_icon(&self, subdomain: &str) -> Result<()> {
let path = self.get_icon(subdomain)?;
std::fs::remove_file(path)?;
Ok(())
}
pub fn get_icon(&self, subdomain: &str) -> Result<PathBuf> {
let path = self.icon_path(subdomain);
if path.is_file() {
Ok(path)
} else {
Err(ImagesError::NotFound(subdomain.to_owned()))
}
}
pub async fn get_icon_response(
&self,
subdomain: &str,
request: &Request,
) -> poem::Result<StaticFileResponse> {
let path = self.icon_path(subdomain);
Ok(StaticFileRequest::from_request_without_body(request)
.await?
.create_response(path, false)?)
}
}
#[handler]
pub async fn icon(
request: &Request,
talon: Data<&Talon>,
subdomain: Path<String>,
) -> poem::Result<Response> {
talon
.icons
.get_icon_response(&subdomain, request)
.await
.map(|r| r.into_response())
}