diff --git a/Cargo.lock b/Cargo.lock index 6eda29d..dad89f4 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -1274,12 +1274,13 @@ dependencies = [ [[package]] name = "servable" -version = "0.0.4" +version = "0.0.5" dependencies = [ "axum", "chrono", "image", "maud", + "mime", "rand", "serde", "serde_urlencoded", diff --git a/Cargo.toml b/Cargo.toml index 21ff215..3ed32e7 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -5,7 +5,7 @@ resolver = "2" [workspace.package] rust-version = "1.90.0" edition = "2024" -version = "0.0.4" +version = "0.0.5" license = "GPL-3.0" repository = "https://github.com/rm-dr/servable" readme = "README.md" @@ -44,7 +44,7 @@ mutex_atomic = "deny" needless_raw_strings = "deny" str_to_string = "deny" string_add = "deny" -string_to_string = "deny" +implicit_clone = "deny" use_debug = "allow" verbose_file_reads = "deny" large_types_passed_by_value = "deny" @@ -74,6 +74,7 @@ axum = "0.8" chrono = "0.4" image = "0.25" maud = "0.27" +mime = "0.3" rand = "0.9" serde = { version = "1.0", features = ["derive"] } serde_urlencoded = "0.7" diff --git a/crates/servable/Cargo.toml b/crates/servable/Cargo.toml index 81822fe..8fdd6c7 100644 --- a/crates/servable/Cargo.toml +++ b/crates/servable/Cargo.toml @@ -23,6 +23,7 @@ serde_urlencoded = { workspace = true } tower = { workspace = true } tracing = { workspace = true } rand = { workspace = true } +mime = { workspace = true } tokio = { workspace = true, optional = true } image = { workspace = true, optional = true } diff --git a/crates/servable/README.md b/crates/servable/README.md index 240cb5c..291eded 100644 --- a/crates/servable/README.md +++ b/crates/servable/README.md @@ -32,7 +32,7 @@ async fn main() { "/hello", StaticAsset { bytes: b"Hello, World!", - mime: MimeType::Text, + mime: mime::TEXT_PLAIN }, ); @@ -56,11 +56,11 @@ The `Servable` trait is the foundation of this stack. \ - `StaticAsset`, for static files like CSS, JavaScript, images, or plain bytes: ```rust - use servable::{StaticAsset, mime::MimeType}; + use servable::{StaticAsset}; let asset = StaticAsset { bytes: b"body { color: red; }", - mime: MimeType::Css, + mime: mime::TEXT_CSS, ttl: StaticAsset::DEFAULT_TTL }; ``` @@ -102,11 +102,11 @@ The `Servable` trait is the foundation of this stack. \ A `ServableRouter` exposes a collection of `Servable`s under different routes. It implements `tower`'s `Service` trait, and can be easily be converted into an Axum `Router`. Construct one as follows: ```rust -# use servable::{ServableRouter, StaticAsset, mime::MimeType}; -# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL}; -# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL }; -# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css, ttl: StaticAsset::DEFAULT_TTL }; -# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL }; +# use servable::{ServableRouter, StaticAsset}; +# let home_page = StaticAsset { bytes: b"home", mime: mime::TEXT_HTML, ttl: StaticAsset::DEFAULT_TTL}; +# let about_page = StaticAsset { bytes: b"about", mime: mime::TEXT_HTML, ttl: StaticAsset::DEFAULT_TTL }; +# let stylesheet = StaticAsset { bytes: b"css", mime: mime::TEXT_CSS, ttl: StaticAsset::DEFAULT_TTL }; +# let custom_404_page = StaticAsset { bytes: b"404", mime: mime::TEXT_HTML, ttl: StaticAsset::DEFAULT_TTL }; let route = ServableRouter::new() .add_page("/", home_page) .add_page("/about", about_page) @@ -121,13 +121,13 @@ let route = ServableRouter::new() When `image` is enabled, the image below... ```rust - # use servable::{ServableRouter, StaticAsset, mime::MimeType}; + # use servable::{ServableRouter, StaticAsset}; let route = ServableRouter::new() .add_page( "/image.png", StaticAsset { bytes: b"fake image data", - mime: MimeType::Png, + mime: mime::IMAGE_PNG, ttl: StaticAsset::DEFAULT_TTL } ); @@ -184,13 +184,12 @@ whenever the server is restarted: ```rust use chrono::TimeDelta; use servable::{HtmlPage, CACHE_BUST_STR, ServableWithRoute, StaticAsset, ServableRouter}; -use servable::mime::MimeType; pub static HTMX: ServableWithRoute = ServableWithRoute::new( || format!("/{}/main.css", *CACHE_BUST_STR), StaticAsset { bytes: "div{}".as_bytes(), - mime: MimeType::Css, + mime: mime::TEXT_CSS, ttl: StaticAsset::DEFAULT_TTL, }, ); diff --git a/crates/servable/src/lib.rs b/crates/servable/src/lib.rs index a4ae06f..3b25e6e 100644 --- a/crates/servable/src/lib.rs +++ b/crates/servable/src/lib.rs @@ -4,8 +4,6 @@ // and needs a different relative path than cargo build. // https://github.com/rust-lang/cargo/issues/13309 -pub mod mime; - mod types; use rand::{Rng, distr::Alphanumeric}; @@ -47,7 +45,7 @@ pub static CACHE_BUST_STR: std::sync::LazyLock = std::sync::LazyLock::ne #[cfg(feature = "htmx-2.0.8")] pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset { bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(), - mime: mime::MimeType::Javascript, + mime: mime::TEXT_JAVASCRIPT, ttl: StaticAsset::DEFAULT_TTL, }; @@ -57,6 +55,6 @@ pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset { #[cfg(feature = "htmx-2.0.8")] pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset { bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(), - mime: mime::MimeType::Javascript, + mime: mime::TEXT_JAVASCRIPT, ttl: StaticAsset::DEFAULT_TTL, }; diff --git a/crates/servable/src/mime.rs b/crates/servable/src/mime.rs deleted file mode 100644 index 13958f9..0000000 --- a/crates/servable/src/mime.rs +++ /dev/null @@ -1,818 +0,0 @@ -//! Strongly-typed MIME types via [MimeType]. - -use serde::{Deserialize, Deserializer, Serialize, Serializer}; -use std::{fmt::Display, str::FromStr}; -use tracing::debug; - -/// A media type, conveniently parsed -#[derive(Debug, PartialEq, Eq, Clone)] -pub enum MimeType { - /// A mimetype we didn't recognize - Other(String), - - /// An unstructured binary blob (application/octet-stream) - Blob, - - // MARK: Audio - /// AAC audio file (audio/aac) - Aac, - /// FLAC audio file (audio/flac) - Flac, - /// MIDI audio file (audio/midi) - Midi, - /// MP3 audio file (audio/mpeg) - Mp3, - /// OGG audio file (audio/ogg) - Oga, - /// Opus audio file in Ogg container (audio/ogg) - Opus, - /// Waveform Audio Format (audio/wav) - Wav, - /// WEBM audio file (audio/webm) - Weba, - - // MARK: Video - /// AVI: Audio Video Interleave (video/x-msvideo) - Avi, - /// MP4 video file (video/mp4) - Mp4, - /// MPEG video file (video/mpeg) - Mpeg, - /// OGG video file (video/ogg) - Ogv, - /// MPEG transport stream (video/mp2t) - Ts, - /// WEBM video file (video/webm) - WebmVideo, - /// 3GPP audio/video container (video/3gpp) - ThreeGp, - /// 3GPP2 audio/video container (video/3gpp2) - ThreeG2, - - // MARK: Images - /// Animated Portable Network Graphics (image/apng) - Apng, - /// AVIF image (image/avif) - Avif, - /// Windows OS/2 Bitmap Graphics (image/bmp) - Bmp, - /// Graphics Interchange Format (image/gif) - Gif, - /// Icon format (image/vnd.microsoft.icon) - Ico, - /// JPEG image (image/jpeg) - Jpg, - /// Portable Network Graphics (image/png) - Png, - /// Quite ok Image Format - Qoi, - /// Scalable Vector Graphics (image/svg+xml) - Svg, - /// Tagged Image File Format (image/tiff) - Tiff, - /// WEBP image (image/webp) - Webp, - - // MARK: Text - /// Plain text (text/plain) - Text, - /// Cascading Style Sheets (text/css) - Css, - /// Comma-separated values (text/csv) - Csv, - /// HyperText Markup Language (text/html) - Html, - /// JavaScript (text/javascript) - Javascript, - /// JSON format (application/json) - Json, - /// JSON-LD format (application/ld+json) - JsonLd, - /// XML (application/xml) - Xml, - - // MARK: Documents - /// Adobe Portable Document Format (application/pdf) - Pdf, - /// Rich Text Format (application/rtf) - Rtf, - - // MARK: Archives - /// Archive document, multiple files embedded (application/x-freearc) - Arc, - /// BZip archive (application/x-bzip) - Bz, - /// BZip2 archive (application/x-bzip2) - Bz2, - /// GZip Compressed Archive (application/gzip) - Gz, - /// Java Archive (application/java-archive) - Jar, - /// OGG (application/ogg) - Ogg, - /// RAR archive (application/vnd.rar) - Rar, - /// 7-zip archive (application/x-7z-compressed) - SevenZ, - /// Tape Archive (application/x-tar) - Tar, - /// ZIP archive (application/zip) - Zip, - - // MARK: Fonts - /// MS Embedded OpenType fonts (application/vnd.ms-fontobject) - Eot, - /// OpenType font (font/otf) - Otf, - /// TrueType Font (font/ttf) - Ttf, - /// Web Open Font Format (font/woff) - Woff, - /// Web Open Font Format 2 (font/woff2) - Woff2, - - // MARK: Applications - /// AbiWord document (application/x-abiword) - Abiword, - /// Amazon Kindle eBook format (application/vnd.amazon.ebook) - Azw, - /// CD audio (application/x-cdf) - Cda, - /// C-Shell script (application/x-csh) - Csh, - /// Microsoft Word (application/msword) - Doc, - /// Microsoft Word OpenXML (application/vnd.openxmlformats-officedocument.wordprocessingml.document) - Docx, - /// Electronic publication (application/epub+zip) - Epub, - /// iCalendar format (text/calendar) - Ics, - /// Apple Installer Package (application/vnd.apple.installer+xml) - Mpkg, - /// OpenDocument presentation (application/vnd.oasis.opendocument.presentation) - Odp, - /// OpenDocument spreadsheet (application/vnd.oasis.opendocument.spreadsheet) - Ods, - /// OpenDocument text document (application/vnd.oasis.opendocument.text) - Odt, - /// Hypertext Preprocessor (application/x-httpd-php) - Php, - /// Microsoft PowerPoint (application/vnd.ms-powerpoint) - Ppt, - /// Microsoft PowerPoint OpenXML (application/vnd.openxmlformats-officedocument.presentationml.presentation) - Pptx, - /// Bourne shell script (application/x-sh) - Sh, - /// Microsoft Visio (application/vnd.visio) - Vsd, - /// XHTML (application/xhtml+xml) - Xhtml, - /// Microsoft Excel (application/vnd.ms-excel) - Xls, - /// Microsoft Excel OpenXML (application/vnd.openxmlformats-officedocument.spreadsheetml.sheet) - Xlsx, - /// XUL (application/vnd.mozilla.xul+xml) - Xul, -} - -// MARK: ser/de - -/* -impl utoipa::ToSchema for MimeType { - fn name() -> std::borrow::Cow<'static, str> { - std::borrow::Cow::Borrowed("MimeType") - } -} -impl utoipa::PartialSchema for MimeType { - fn schema() -> utoipa::openapi::RefOr { - utoipa::openapi::Schema::Object( - utoipa::openapi::schema::ObjectBuilder::new() - .schema_type(utoipa::openapi::schema::SchemaType::Type(Type::String)) - .description(Some( - "A media type string (e.g., 'application/json', 'text/plain')", - )) - .examples(Some("application/json")) - .build(), - ) - .into() - } -} -*/ - -impl Serialize for MimeType { - fn serialize(&self, serializer: S) -> Result - where - S: Serializer, - { - serializer.serialize_str(&self.to_string()) - } -} - -impl<'de> Deserialize<'de> for MimeType { - fn deserialize(deserializer: D) -> Result - where - D: Deserializer<'de>, - { - let s = String::deserialize(deserializer)?; - Ok(MimeType::from_str(&s).unwrap()) - } -} - -// -// MARK: misc -// - -impl Default for MimeType { - fn default() -> Self { - Self::const_default() - } -} - -impl MimeType { - /// [Default::default], but const - pub const fn const_default() -> Self { - Self::Blob - } -} - -impl From for MimeType { - fn from(value: String) -> Self { - Self::from_str(&value).unwrap() - } -} - -impl From<&str> for MimeType { - fn from(value: &str) -> Self { - Self::from_str(value).unwrap() - } -} - -impl From<&MimeType> for String { - fn from(value: &MimeType) -> Self { - value.to_string() - } -} - -// -// MARK: fromstr -// - -impl MimeType { - /// Parse a mimetype from a string that may contain - /// whitespace or ";" parameters. - /// - /// Parameters are discarded, write your own parser if you need them. - pub fn from_header(s: &str) -> Result::Err> { - let s = s.trim(); - let semi = s.find(';').unwrap_or(s.len()); - let space = s.find(' ').unwrap_or(s.len()); - let limit = semi.min(space); - let s = &s[0..limit]; - let s = s.trim(); - - return Self::from_str(s); - } -} - -impl FromStr for MimeType { - type Err = std::convert::Infallible; - - // Must match `display` below, but may provide other alternatives. - fn from_str(s: &str) -> Result { - Ok(match s { - "application/octet-stream" => Self::Blob, - - // Audio - "audio/aac" => Self::Aac, - "audio/flac" => Self::Flac, - "audio/midi" | "audio/x-midi" => Self::Midi, - "audio/mpeg" => Self::Mp3, - "audio/ogg" => Self::Oga, - "audio/wav" => Self::Wav, - "audio/webm" => Self::Weba, - - // Video - "video/x-msvideo" => Self::Avi, - "video/mp4" => Self::Mp4, - "video/mpeg" => Self::Mpeg, - "video/ogg" => Self::Ogv, - "video/mp2t" => Self::Ts, - "video/webm" => Self::WebmVideo, - "video/3gpp" => Self::ThreeGp, - "video/3gpp2" => Self::ThreeG2, - - // Images - "image/apng" => Self::Apng, - "image/avif" => Self::Avif, - "image/bmp" => Self::Bmp, - "image/gif" => Self::Gif, - "image/vnd.microsoft.icon" => Self::Ico, - "image/jpeg" | "image/jpg" => Self::Jpg, - "image/png" => Self::Png, - "image/svg+xml" => Self::Svg, - "image/tiff" => Self::Tiff, - "image/webp" => Self::Webp, - "image/qoi" => Self::Qoi, - - // Text - "text/plain" => Self::Text, - "text/css" => Self::Css, - "text/csv" => Self::Csv, - "text/html" => Self::Html, - "text/javascript" => Self::Javascript, - "application/json" => Self::Json, - "application/ld+json" => Self::JsonLd, - "application/xml" | "text/xml" => Self::Xml, - - // Documents - "application/pdf" => Self::Pdf, - "application/rtf" => Self::Rtf, - - // Archives - "application/x-freearc" => Self::Arc, - "application/x-bzip" => Self::Bz, - "application/x-bzip2" => Self::Bz2, - "application/gzip" | "application/x-gzip" => Self::Gz, - "application/java-archive" => Self::Jar, - "application/ogg" => Self::Ogg, - "application/vnd.rar" => Self::Rar, - "application/x-7z-compressed" => Self::SevenZ, - "application/x-tar" => Self::Tar, - "application/zip" | "application/x-zip-compressed" => Self::Zip, - - // Fonts - "application/vnd.ms-fontobject" => Self::Eot, - "font/otf" => Self::Otf, - "font/ttf" => Self::Ttf, - "font/woff" => Self::Woff, - "font/woff2" => Self::Woff2, - - // Applications - "application/x-abiword" => Self::Abiword, - "application/vnd.amazon.ebook" => Self::Azw, - "application/x-cdf" => Self::Cda, - "application/x-csh" => Self::Csh, - "application/msword" => Self::Doc, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" => Self::Docx, - "application/epub+zip" => Self::Epub, - "text/calendar" => Self::Ics, - "application/vnd.apple.installer+xml" => Self::Mpkg, - "application/vnd.oasis.opendocument.presentation" => Self::Odp, - "application/vnd.oasis.opendocument.spreadsheet" => Self::Ods, - "application/vnd.oasis.opendocument.text" => Self::Odt, - "application/x-httpd-php" => Self::Php, - "application/vnd.ms-powerpoint" => Self::Ppt, - "application/vnd.openxmlformats-officedocument.presentationml.presentation" => { - Self::Pptx - } - "application/x-sh" => Self::Sh, - "application/vnd.visio" => Self::Vsd, - "application/xhtml+xml" => Self::Xhtml, - "application/vnd.ms-excel" => Self::Xls, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" => Self::Xlsx, - "application/vnd.mozilla.xul+xml" => Self::Xul, - - _ => { - debug!(message = "Encountered unknown mimetype", mime_string = s); - Self::Other(s.into()) - } - }) - } -} - -// -// MARK: display -// - -impl Display for MimeType { - /// Get a string representation of this mimetype. - /// - /// The following always holds: - /// ```rust - /// # use servable::mime::MimeType; - /// # let x = MimeType::Blob; - /// assert_eq!(MimeType::from(x.to_string()), x); - /// ``` - /// - /// The following might not hold: - /// ```rust - /// # use servable::mime::MimeType; - /// # let y = "application/custom"; - /// // MimeType::from(y).to_string() may not equal y - /// ``` - fn fmt(&self, f: &mut std::fmt::Formatter<'_>) -> std::fmt::Result { - match self { - Self::Blob => write!(f, "application/octet-stream"), - - // Audio - Self::Aac => write!(f, "audio/aac"), - Self::Flac => write!(f, "audio/flac"), - Self::Midi => write!(f, "audio/midi"), - Self::Mp3 => write!(f, "audio/mpeg"), - Self::Oga => write!(f, "audio/ogg"), - Self::Opus => write!(f, "audio/ogg"), - Self::Wav => write!(f, "audio/wav"), - Self::Weba => write!(f, "audio/webm"), - - // Video - Self::Avi => write!(f, "video/x-msvideo"), - Self::Mp4 => write!(f, "video/mp4"), - Self::Mpeg => write!(f, "video/mpeg"), - Self::Ogv => write!(f, "video/ogg"), - Self::Ts => write!(f, "video/mp2t"), - Self::WebmVideo => write!(f, "video/webm"), - Self::ThreeGp => write!(f, "video/3gpp"), - Self::ThreeG2 => write!(f, "video/3gpp2"), - - // Images - Self::Apng => write!(f, "image/apng"), - Self::Avif => write!(f, "image/avif"), - Self::Bmp => write!(f, "image/bmp"), - Self::Gif => write!(f, "image/gif"), - Self::Ico => write!(f, "image/vnd.microsoft.icon"), - Self::Jpg => write!(f, "image/jpeg"), - Self::Png => write!(f, "image/png"), - Self::Svg => write!(f, "image/svg+xml"), - Self::Tiff => write!(f, "image/tiff"), - Self::Webp => write!(f, "image/webp"), - Self::Qoi => write!(f, "image/qoi"), - - // Text - Self::Text => write!(f, "text/plain"), - Self::Css => write!(f, "text/css"), - Self::Csv => write!(f, "text/csv"), - Self::Html => write!(f, "text/html"), - Self::Javascript => write!(f, "text/javascript"), - Self::Json => write!(f, "application/json"), - Self::JsonLd => write!(f, "application/ld+json"), - Self::Xml => write!(f, "application/xml"), - - // Documents - Self::Pdf => write!(f, "application/pdf"), - Self::Rtf => write!(f, "application/rtf"), - - // Archives - Self::Arc => write!(f, "application/x-freearc"), - Self::Bz => write!(f, "application/x-bzip"), - Self::Bz2 => write!(f, "application/x-bzip2"), - Self::Gz => write!(f, "application/gzip"), - Self::Jar => write!(f, "application/java-archive"), - Self::Ogg => write!(f, "application/ogg"), - Self::Rar => write!(f, "application/vnd.rar"), - Self::SevenZ => write!(f, "application/x-7z-compressed"), - Self::Tar => write!(f, "application/x-tar"), - Self::Zip => write!(f, "application/zip"), - - // Fonts - Self::Eot => write!(f, "application/vnd.ms-fontobject"), - Self::Otf => write!(f, "font/otf"), - Self::Ttf => write!(f, "font/ttf"), - Self::Woff => write!(f, "font/woff"), - Self::Woff2 => write!(f, "font/woff2"), - - // Applications - Self::Abiword => write!(f, "application/x-abiword"), - Self::Azw => write!(f, "application/vnd.amazon.ebook"), - Self::Cda => write!(f, "application/x-cdf"), - Self::Csh => write!(f, "application/x-csh"), - Self::Doc => write!(f, "application/msword"), - Self::Docx => write!( - f, - "application/vnd.openxmlformats-officedocument.wordprocessingml.document" - ), - Self::Epub => write!(f, "application/epub+zip"), - Self::Ics => write!(f, "text/calendar"), - Self::Mpkg => write!(f, "application/vnd.apple.installer+xml"), - Self::Odp => write!(f, "application/vnd.oasis.opendocument.presentation"), - Self::Ods => write!(f, "application/vnd.oasis.opendocument.spreadsheet"), - Self::Odt => write!(f, "application/vnd.oasis.opendocument.text"), - Self::Php => write!(f, "application/x-httpd-php"), - Self::Ppt => write!(f, "application/vnd.ms-powerpoint"), - Self::Pptx => write!( - f, - "application/vnd.openxmlformats-officedocument.presentationml.presentation" - ), - Self::Sh => write!(f, "application/x-sh"), - Self::Vsd => write!(f, "application/vnd.visio"), - Self::Xhtml => write!(f, "application/xhtml+xml"), - Self::Xls => write!(f, "application/vnd.ms-excel"), - Self::Xlsx => write!( - f, - "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet" - ), - Self::Xul => write!(f, "application/vnd.mozilla.xul+xml"), - - Self::Other(x) => write!(f, "{x}"), - } - } -} - -impl MimeType { - // - // MARK: from extension - // - - /// Try to guess a file's mime type from its extension. - /// `ext` should NOT start with a dot. - pub fn from_extension(ext: &str) -> Option { - Some(match ext { - // Audio - "aac" => Self::Aac, - "flac" => Self::Flac, - "mid" | "midi" => Self::Midi, - "mp3" => Self::Mp3, - "oga" => Self::Oga, - "opus" => Self::Opus, - "wav" => Self::Wav, - "weba" => Self::Weba, - - // Video - "avi" => Self::Avi, - "mp4" => Self::Mp4, - "mpeg" => Self::Mpeg, - "ogv" => Self::Ogv, - "ts" => Self::Ts, - "webm" => Self::WebmVideo, - "3gp" => Self::ThreeGp, - "3g2" => Self::ThreeG2, - - // Images - "apng" => Self::Apng, - "avif" => Self::Avif, - "bmp" => Self::Bmp, - "gif" => Self::Gif, - "ico" => Self::Ico, - "jpg" | "jpeg" => Self::Jpg, - "png" => Self::Png, - "svg" => Self::Svg, - "tif" | "tiff" => Self::Tiff, - "webp" => Self::Webp, - "qoi" => Self::Qoi, - - // Text - "txt" => Self::Text, - "css" => Self::Css, - "csv" => Self::Csv, - "htm" | "html" => Self::Html, - "js" | "mjs" => Self::Javascript, - "json" => Self::Json, - "jsonld" => Self::JsonLd, - "xml" => Self::Xml, - - // Documents - "pdf" => Self::Pdf, - "rtf" => Self::Rtf, - - // Archives - "arc" => Self::Arc, - "bz" => Self::Bz, - "bz2" => Self::Bz2, - "gz" => Self::Gz, - "jar" => Self::Jar, - "ogx" => Self::Ogg, - "rar" => Self::Rar, - "7z" => Self::SevenZ, - "tar" => Self::Tar, - "zip" => Self::Zip, - - // Fonts - "eot" => Self::Eot, - "otf" => Self::Otf, - "ttf" => Self::Ttf, - "woff" => Self::Woff, - "woff2" => Self::Woff2, - - // Applications - "abw" => Self::Abiword, - "azw" => Self::Azw, - "cda" => Self::Cda, - "csh" => Self::Csh, - "doc" => Self::Doc, - "docx" => Self::Docx, - "epub" => Self::Epub, - "ics" => Self::Ics, - "mpkg" => Self::Mpkg, - "odp" => Self::Odp, - "ods" => Self::Ods, - "odt" => Self::Odt, - "php" => Self::Php, - "ppt" => Self::Ppt, - "pptx" => Self::Pptx, - "sh" => Self::Sh, - "vsd" => Self::Vsd, - "xhtml" => Self::Xhtml, - "xls" => Self::Xls, - "xlsx" => Self::Xlsx, - "xul" => Self::Xul, - - _ => return None, - }) - } - - // - // MARK: to extension - // - - /// Get the extension we use for files with this type. - /// Never includes a dot. - pub fn extension(&self) -> Option<&'static str> { - match self { - Self::Blob => None, - Self::Other(_) => None, - - // Audio - Self::Aac => Some("aac"), - Self::Flac => Some("flac"), - Self::Midi => Some("midi"), - Self::Mp3 => Some("mp3"), - Self::Oga => Some("oga"), - Self::Opus => Some("opus"), - Self::Wav => Some("wav"), - Self::Weba => Some("weba"), - - // Video - Self::Avi => Some("avi"), - Self::Mp4 => Some("mp4"), - Self::Mpeg => Some("mpeg"), - Self::Ogv => Some("ogv"), - Self::Ts => Some("ts"), - Self::WebmVideo => Some("webm"), - Self::ThreeGp => Some("3gp"), - Self::ThreeG2 => Some("3g2"), - - // Images - Self::Apng => Some("apng"), - Self::Avif => Some("avif"), - Self::Bmp => Some("bmp"), - Self::Gif => Some("gif"), - Self::Ico => Some("ico"), - Self::Jpg => Some("jpg"), - Self::Png => Some("png"), - Self::Svg => Some("svg"), - Self::Tiff => Some("tiff"), - Self::Webp => Some("webp"), - Self::Qoi => Some("qoi"), - - // Text - Self::Text => Some("txt"), - Self::Css => Some("css"), - Self::Csv => Some("csv"), - Self::Html => Some("html"), - Self::Javascript => Some("js"), - Self::Json => Some("json"), - Self::JsonLd => Some("jsonld"), - Self::Xml => Some("xml"), - - // Documents - Self::Pdf => Some("pdf"), - Self::Rtf => Some("rtf"), - - // Archives - Self::Arc => Some("arc"), - Self::Bz => Some("bz"), - Self::Bz2 => Some("bz2"), - Self::Gz => Some("gz"), - Self::Jar => Some("jar"), - Self::Ogg => Some("ogx"), - Self::Rar => Some("rar"), - Self::SevenZ => Some("7z"), - Self::Tar => Some("tar"), - Self::Zip => Some("zip"), - - // Fonts - Self::Eot => Some("eot"), - Self::Otf => Some("otf"), - Self::Ttf => Some("ttf"), - Self::Woff => Some("woff"), - Self::Woff2 => Some("woff2"), - - // Applications - Self::Abiword => Some("abw"), - Self::Azw => Some("azw"), - Self::Cda => Some("cda"), - Self::Csh => Some("csh"), - Self::Doc => Some("doc"), - Self::Docx => Some("docx"), - Self::Epub => Some("epub"), - Self::Ics => Some("ics"), - Self::Mpkg => Some("mpkg"), - Self::Odp => Some("odp"), - Self::Ods => Some("ods"), - Self::Odt => Some("odt"), - Self::Php => Some("php"), - Self::Ppt => Some("ppt"), - Self::Pptx => Some("pptx"), - Self::Sh => Some("sh"), - Self::Vsd => Some("vsd"), - Self::Xhtml => Some("xhtml"), - Self::Xls => Some("xls"), - Self::Xlsx => Some("xlsx"), - Self::Xul => Some("xul"), - } - } - - // - // MARK: is_text - // - - /// Returns true if this MIME type is always plain text. - pub fn is_text(&self) -> bool { - match self { - // Text types - Self::Text => true, - Self::Css => true, - Self::Csv => true, - Self::Html => true, - Self::Javascript => true, - Self::Json => true, - Self::JsonLd => true, - Self::Xml => true, - Self::Svg => true, - Self::Ics => true, - Self::Xhtml => true, - - // Script types - Self::Csh => true, - Self::Php => true, - Self::Sh => true, - - // All other types are not plain text - Self::Other(_) => false, - Self::Blob => false, - - // Audio - Self::Aac => false, - Self::Flac => false, - Self::Midi => false, - Self::Mp3 => false, - Self::Oga => false, - Self::Opus => false, - Self::Wav => false, - Self::Weba => false, - - // Video - Self::Avi => false, - Self::Mp4 => false, - Self::Mpeg => false, - Self::Ogv => false, - Self::Ts => false, - Self::WebmVideo => false, - Self::ThreeGp => false, - Self::ThreeG2 => false, - - // Images - Self::Apng => false, - Self::Avif => false, - Self::Bmp => false, - Self::Gif => false, - Self::Ico => false, - Self::Jpg => false, - Self::Png => false, - Self::Qoi => false, - Self::Tiff => false, - Self::Webp => false, - - // Documents - Self::Pdf => false, - Self::Rtf => false, - - // Archives - Self::Arc => false, - Self::Bz => false, - Self::Bz2 => false, - Self::Gz => false, - Self::Jar => false, - Self::Ogg => false, - Self::Rar => false, - Self::SevenZ => false, - Self::Tar => false, - Self::Zip => false, - - // Fonts - Self::Eot => false, - Self::Otf => false, - Self::Ttf => false, - Self::Woff => false, - Self::Woff2 => false, - - // Applications - Self::Abiword => false, - Self::Azw => false, - Self::Cda => false, - Self::Doc => false, - Self::Docx => false, - Self::Epub => false, - Self::Mpkg => false, - Self::Odp => false, - Self::Ods => false, - Self::Odt => false, - Self::Ppt => false, - Self::Pptx => false, - Self::Vsd => false, - Self::Xls => false, - Self::Xlsx => false, - Self::Xul => false, - } - } -} diff --git a/crates/servable/src/router.rs b/crates/servable/src/router.rs index 3a57a89..81627b2 100644 --- a/crates/servable/src/router.rs +++ b/crates/servable/src/router.rs @@ -19,7 +19,6 @@ use tracing::trace; use crate::{ ClientInfo, RenderContext, Rendered, RenderedBody, - mime::MimeType, servable::{Servable, ServableWithRoute}, }; @@ -36,7 +35,7 @@ impl Servable for Default404 { body: (), ttl: Some(TimeDelta::days(1)), headers: HeaderMap::new(), - mime: Some(MimeType::Html), + mime: Some(mime::TEXT_HTML), private: false, }; }) @@ -54,7 +53,7 @@ impl Servable for Default404 { /// /// Use as follows: /// ```rust -/// use servable::{ServableRouter, StaticAsset, mime::MimeType}; +/// use servable::{ServableRouter, StaticAsset}; /// use axum::Router; /// use tower_http::compression::{CompressionLayer, predicate::DefaultPredicate}; /// @@ -72,7 +71,7 @@ impl Servable for Default404 { /// "/page", /// StaticAsset { /// bytes: "I am a page".as_bytes(), -/// mime: MimeType::Text, +/// mime: mime::TEXT_PLAIN, /// ttl: StaticAsset::DEFAULT_TTL /// }, /// ); @@ -273,7 +272,7 @@ impl Service> for ServableRouter { #[expect(clippy::unwrap_used)] rend.headers.insert( header::CONTENT_TYPE, - HeaderValue::from_str(&mime.to_string()).unwrap(), + HeaderValue::from_str(mime.as_ref()).unwrap(), ); } } diff --git a/crates/servable/src/servable/asset.rs b/crates/servable/src/servable/asset.rs index a629bfe..abc114e 100644 --- a/crates/servable/src/servable/asset.rs +++ b/crates/servable/src/servable/asset.rs @@ -1,8 +1,9 @@ use axum::http::{HeaderMap, StatusCode}; use chrono::TimeDelta; +use mime::Mime; use std::pin::Pin; -use crate::{RenderContext, Rendered, RenderedBody, mime::MimeType, servable::Servable}; +use crate::{RenderContext, Rendered, RenderedBody, servable::Servable}; /// A static blob of bytes pub struct StaticAsset { @@ -10,8 +11,7 @@ pub struct StaticAsset { pub bytes: &'static [u8], /// The type of `bytes` - pub mime: MimeType, - + pub mime: Mime, /// How long to cache this response. /// If None, never cache pub ttl: Option, diff --git a/crates/servable/src/servable/html.rs b/crates/servable/src/servable/html.rs index 70a9f4b..ad2a329 100644 --- a/crates/servable/src/servable/html.rs +++ b/crates/servable/src/servable/html.rs @@ -4,7 +4,7 @@ use maud::{DOCTYPE, Markup, PreEscaped, html}; use serde::Deserialize; use std::{hash::Hash, pin::Pin, sync::Arc}; -use crate::{RenderContext, Rendered, RenderedBody, mime::MimeType, servable::Servable}; +use crate::{RenderContext, Rendered, RenderedBody, servable::Servable}; #[expect(missing_docs)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)] @@ -225,7 +225,7 @@ impl Servable for HtmlPage { ttl: self.ttl, private: self.private, headers: HeaderMap::new(), - mime: Some(MimeType::Html), + mime: Some(mime::TEXT_HTML), }; }) } diff --git a/crates/servable/src/transform/chain.rs b/crates/servable/src/transform/chain.rs index 59f9155..574b9c5 100644 --- a/crates/servable/src/transform/chain.rs +++ b/crates/servable/src/transform/chain.rs @@ -1,10 +1,10 @@ use image::{DynamicImage, ImageFormat}; +use mime::Mime; use serde::{Deserialize, Deserializer, de}; use std::{fmt::Display, hash::Hash, io::Cursor, str::FromStr}; use thiserror::Error; use super::transformers::{ImageTransformer, TransformerEnum}; -use crate::mime::MimeType; #[expect(missing_docs)] #[derive(Debug, Error)] @@ -28,7 +28,7 @@ pub struct TransformerChain { impl TransformerChain { /// Returns `true` if `mime` is a type that can be transformed #[inline(always)] - pub fn mime_is_image(mime: &MimeType) -> bool { + pub fn mime_is_image(mime: &Mime) -> bool { ImageFormat::from_mime_type(mime.to_string()).is_some() } @@ -50,12 +50,14 @@ impl TransformerChain { /// with type `input_mime`. If this returns `None`, the input mime /// cannot be transformed. #[inline(always)] - pub fn output_mime(&self, input_mime: &MimeType) -> Option { + pub fn output_mime(&self, input_mime: &Mime) -> Option { let mime = self .steps .last() .and_then(|x| match x { - TransformerEnum::Format { format } => Some(MimeType::from(format.to_mime_type())), + TransformerEnum::Format { format } => Some( + Mime::from_str(format.to_mime_type()).unwrap_or(mime::APPLICATION_OCTET_STREAM), + ), _ => None, }) .unwrap_or(input_mime.clone()); @@ -72,8 +74,8 @@ impl TransformerChain { pub fn transform_bytes( &self, image_bytes: &[u8], - image_format: Option<&MimeType>, - ) -> Result<(MimeType, Vec), TransformBytesError> { + image_format: Option<&Mime>, + ) -> Result<(Mime, Vec), TransformBytesError> { let format: ImageFormat = match image_format { Some(x) => ImageFormat::from_mime_type(x.to_string()) .ok_or(TransformBytesError::NotAnImage(x.to_string()))?, @@ -92,7 +94,8 @@ impl TransformerChain { let img = image::load_from_memory_with_format(image_bytes, format)?; let img = self.transform_image(img); - let out_mime = MimeType::from(out_format.to_mime_type()); + let out_mime = + Mime::from_str(out_format.to_mime_type()).unwrap_or(mime::APPLICATION_OCTET_STREAM); let mut out_bytes = Cursor::new(Vec::new()); img.write_to(&mut out_bytes, *out_format)?; diff --git a/crates/servable/src/types.rs b/crates/servable/src/types.rs index f9ae86a..7a487a4 100644 --- a/crates/servable/src/types.rs +++ b/crates/servable/src/types.rs @@ -1,9 +1,8 @@ use axum::http::{HeaderMap, StatusCode}; use chrono::TimeDelta; +use mime::Mime; use std::collections::BTreeMap; -use crate::mime::MimeType; - // // MARK: rendered // @@ -52,7 +51,7 @@ pub struct Rendered { pub body: T, /// The type of `self.body` - pub mime: Option, + pub mime: Option, /// How long to cache this response. /// If none, don't cache. @@ -93,20 +92,17 @@ pub struct RenderContext { /// The type of device that requested a page #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] +#[derive(Default)] pub enum DeviceType { /// This is a mobile device, like a phone. Mobile, /// This is a device with a large screen /// and a mouse, like a laptop. - Desktop, + #[default] + Desktop, } -impl Default for DeviceType { - fn default() -> Self { - Self::Desktop - } -} /// Inferred information about the client /// that requested a certain route.