use pathbufd::PathBufD;
use serde::{Deserialize, Serialize};
use std::{
collections::HashMap,
fs::{exists, remove_file, write},
};
use tetratto_shared::{snow::Snowflake, unix_epoch_timestamp};
#[derive(Serialize, Deserialize, PartialEq, Eq)]
pub enum MediaType {
#[serde(alias = "image/webp")]
Webp,
#[serde(alias = "image/avif")]
Avif,
#[serde(alias = "image/png")]
Png,
#[serde(alias = "image/jpg")]
Jpg,
#[serde(alias = "image/gif")]
Gif,
#[serde(alias = "image/carpgraph")]
Carpgraph,
}
impl MediaType {
pub fn extension(&self) -> &str {
match self {
Self::Webp => "webp",
Self::Avif => "avif",
Self::Png => "png",
Self::Jpg => "jpg",
Self::Gif => "gif",
Self::Carpgraph => "carpgraph",
}
}
pub fn mime(&self) -> String {
format!("image/{}", self.extension())
}
}
#[derive(Serialize, Deserialize)]
pub struct UploadMetadata {
pub what: MediaType,
#[serde(default)]
pub alt: String,
#[serde(default)]
pub kv: HashMap<String, String>,
}
impl UploadMetadata {
pub fn validate_kv(&self) -> result::Result<()> {
for x in &self.kv {
if x.0.len() > 32 {
return Err(result::Error::DataTooLong("key".to_string()));
}
if x.1.len() > 128 {
return Err(result::Error::DataTooLong("value".to_string()));
}
}
Ok(())
}
}
#[derive(Serialize, Deserialize)]
pub struct MediaUpload {
pub id: usize,
pub created: usize,
pub owner: usize,
pub bucket: String,
pub metadata: UploadMetadata,
}
impl MediaUpload {
pub fn new(what: MediaType, owner: usize, bucket: String) -> Self {
Self {
id: Snowflake::new().to_string().parse::<usize>().unwrap(),
created: unix_epoch_timestamp(),
owner,
bucket,
metadata: UploadMetadata {
alt: String::new(),
what,
kv: HashMap::new(),
},
}
}
pub(crate) fn legacy_path(&self, directory: &str) -> PathBufD {
PathBufD::current().extend(&[
directory,
&format!("{}.{}", self.id, self.metadata.what.extension()),
])
}
pub fn full_path(&self, directory: &str) -> PathBufD {
PathBufD::current().extend(&[
directory,
&format!(
"{}{}.{}",
if self.bucket != "" {
format!("{}.", self.bucket)
} else {
String::new()
},
self.id,
self.metadata.what.extension()
),
])
}
pub fn path(&self, directory: &str) -> PathBufD {
let legacy = self.legacy_path(directory);
if std::fs::exists(&legacy).unwrap() {
return legacy;
}
self.full_path(directory)
}
pub fn write(&self, directory: &str, bytes: &[u8]) -> result::Result<()> {
match write(self.path(directory), bytes) {
Ok(_) => Ok(()),
Err(e) => Err(result::Error::MiscError(e.to_string())),
}
}
pub fn remove(&self, directory: &str) -> result::Result<()> {
let path = self.path(directory);
if !exists(&path).unwrap() {
return Ok(());
}
match remove_file(path) {
Ok(_) => Ok(()),
Err(e) => Err(result::Error::MiscError(e.to_string())),
}
}
}
pub mod result {
use serde::{Deserialize, Serialize};
use std::fmt::Display;
#[derive(Serialize, Deserialize)]
pub struct ApiReturn<T>
where
T: Serialize,
{
pub ok: bool,
pub message: String,
pub payload: T,
}
#[derive(Debug)]
pub enum Error {
MiscError(String),
DatabaseConnection(String),
GeneralNotFound(String),
DatabaseError(String),
NotAllowed,
DataTooLong(String),
DataTooShort(String),
FileTooLarge,
FileTooSmall,
Unknown,
}
impl Display for Error {
fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result {
f.write_str(&match self {
Self::MiscError(msg) => msg.to_owned(),
Self::DatabaseConnection(msg) => msg.to_owned(),
Self::DatabaseError(msg) => format!("Database error: {msg}"),
Self::GeneralNotFound(name) => format!("Unable to find requested {name}"),
Self::NotAllowed => "You are not allowed to do this".to_string(),
Self::DataTooLong(name) => format!("Given {name} is too long!"),
Self::DataTooShort(name) => format!("Given {name} is too short!"),
Self::FileTooLarge => "Given file is too large".to_string(),
Self::FileTooSmall => "Given file is too small".to_string(),
_ => format!("An unknown error as occurred: ({:?})", self),
})
}
}
impl<T> From<Error> for ApiReturn<T>
where
T: Default + Serialize,
{
fn from(val: Error) -> Self {
ApiReturn {
ok: false,
message: val.to_string(),
payload: T::default(),
}
}
}
pub type Result<T> = std::result::Result<T, Error>;
}