use std::path::{Path, PathBuf};
use bytes::Bytes;
use uuid::Uuid;
use crate::error::Error;
const RENDERS_SUBDIR: &str = "renders";
const LOGOS_SUBDIR: &str = "logos";
pub const IMAGE_SUFFIX: &str = "image";
pub const IMAGE_WITHOUT_ROUTE_SUFFIX: &str = "image-without-route";
#[must_use]
pub fn ext_for_content_type(content_type: &str) -> &'static str {
match content_type {
"image/png" => "png",
"image/jpeg" => "jpg",
"image/webp" => "webp",
_ => "bin",
}
}
#[must_use]
pub fn render_filename(render_id: Uuid, suffix: &str, ext: &str) -> String {
format!("{render_id}-{suffix}.{ext}")
}
#[must_use]
pub fn render_path(storage_dir: &Path, filename: &str) -> PathBuf {
storage_dir.join(RENDERS_SUBDIR).join(filename)
}
pub fn ensure_layout(storage_dir: &Path) -> Result<(), Error> {
fs_err::create_dir_all(storage_dir.join(RENDERS_SUBDIR))?;
fs_err::create_dir_all(storage_dir.join(LOGOS_SUBDIR))?;
Ok(())
}
pub async fn write_render_file(
storage_dir: &Path,
render_id: Uuid,
suffix: &str,
ext: &str,
bytes: Bytes,
) -> Result<String, Error> {
let filename = render_filename(render_id, suffix, ext);
let path = render_path(storage_dir, &filename);
tokio::task::spawn_blocking(move || fs_err::write(&path, &bytes))
.await
.map_err(|err| Error::Io(std::io::Error::other(err)))??;
Ok(filename)
}
pub async fn read_render_file(storage_dir: &Path, filename: &str) -> Result<Bytes, Error> {
let path = render_path(storage_dir, filename);
let filename_for_err = filename.to_owned();
let bytes = tokio::task::spawn_blocking(move || fs_err::read(&path))
.await
.map_err(|err| Error::Io(std::io::Error::other(err)))?;
match bytes {
Ok(b) => Ok(Bytes::from(b)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Err(Error::NotFound(format!("render file `{filename_for_err}`")))
}
Err(err) => Err(Error::Io(err)),
}
}
pub fn try_delete_render_file(storage_dir: &Path, filename: &str) -> Result<(), Error> {
let path = render_path(storage_dir, filename);
match fs_err::remove_file(&path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(Error::Io(err)),
}
}
pub fn list_render_files(storage_dir: &Path) -> Result<Vec<String>, Error> {
let dir = storage_dir.join(RENDERS_SUBDIR);
let mut out = Vec::new();
let entries = fs_err::read_dir(&dir)?;
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
if let Some(name) = entry.file_name().to_str() {
out.push(name.to_owned());
}
}
Ok(out)
}
#[must_use]
pub fn parse_render_id_from_filename(filename: &str) -> Option<Uuid> {
const UUID_LEN: usize = 36;
let prefix: String = filename.chars().take(UUID_LEN).collect();
if prefix.chars().count() != UUID_LEN {
return None;
}
Uuid::parse_str(&prefix).ok()
}
#[must_use]
pub fn logo_filename(logo_id: Uuid, ext: &str) -> String {
format!("{logo_id}.{ext}")
}
#[must_use]
pub fn logo_path(storage_dir: &Path, filename: &str) -> PathBuf {
storage_dir.join(LOGOS_SUBDIR).join(filename)
}
pub async fn write_logo_file(
storage_dir: &Path,
logo_id: Uuid,
ext: &str,
bytes: Bytes,
) -> Result<String, Error> {
let filename = logo_filename(logo_id, ext);
let path = logo_path(storage_dir, &filename);
tokio::task::spawn_blocking(move || fs_err::write(&path, &bytes))
.await
.map_err(|err| Error::Io(std::io::Error::other(err)))??;
Ok(filename)
}
pub async fn read_logo_file(storage_dir: &Path, filename: &str) -> Result<Bytes, Error> {
let path = logo_path(storage_dir, filename);
let filename_for_err = filename.to_owned();
let bytes = tokio::task::spawn_blocking(move || fs_err::read(&path))
.await
.map_err(|err| Error::Io(std::io::Error::other(err)))?;
match bytes {
Ok(b) => Ok(Bytes::from(b)),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => {
Err(Error::NotFound(format!("logo file `{filename_for_err}`")))
}
Err(err) => Err(Error::Io(err)),
}
}
pub fn try_delete_logo_file(storage_dir: &Path, filename: &str) -> Result<(), Error> {
let path = logo_path(storage_dir, filename);
match fs_err::remove_file(&path) {
Ok(()) => Ok(()),
Err(err) if err.kind() == std::io::ErrorKind::NotFound => Ok(()),
Err(err) => Err(Error::Io(err)),
}
}
pub fn list_logo_files(storage_dir: &Path) -> Result<Vec<String>, Error> {
let dir = storage_dir.join(LOGOS_SUBDIR);
let mut out = Vec::new();
let entries = fs_err::read_dir(&dir)?;
for entry in entries {
let entry = entry?;
if !entry.file_type()?.is_file() {
continue;
}
if let Some(name) = entry.file_name().to_str() {
out.push(name.to_owned());
}
}
Ok(out)
}
#[must_use]
pub fn parse_logo_id_from_filename(filename: &str) -> Option<Uuid> {
const UUID_LEN: usize = 36;
let prefix: String = filename.chars().take(UUID_LEN).collect();
if prefix.chars().count() != UUID_LEN {
return None;
}
Uuid::parse_str(&prefix).ok()
}