buckets-core 1.0.9

Buckets media upload types
Documentation
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 {
    /// Create a new [`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(),
            },
        }
    }

    /// Get the path to the fs file for this upload (without bucket).
    pub(crate) fn legacy_path(&self, directory: &str) -> PathBufD {
        PathBufD::current().extend(&[
            directory,
            &format!("{}.{}", self.id, self.metadata.what.extension()),
        ])
    }

    /// Get the path to the fs file for this upload (with bucket).
    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()
            ),
        ])
    }

    /// Get the path to the fs file for this upload.
    ///
    /// Uses path with bucket unless legacy path exists.
    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)
    }

    /// Write to this upload in the file system.
    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())),
        }
    }

    /// Delete this upload in the file system.
    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>;
}