1
0
mirror of https://github.com/rm-dr/servable.git synced 2025-12-07 09:49:29 -08:00

v0.0.5
All checks were successful
CI / Check links (push) Successful in 12s
CI / Check typos (push) Successful in 18s
CI / Clippy (push) Successful in 1m42s
CI / Build and test (push) Successful in 5m25s

This commit is contained in:
2025-12-04 14:59:34 -08:00
committed by GitHub
parent 8674bd9c85
commit 7a13bd0cda
11 changed files with 43 additions and 863 deletions

3
Cargo.lock generated
View File

@@ -1274,12 +1274,13 @@ dependencies = [
[[package]] [[package]]
name = "servable" name = "servable"
version = "0.0.4" version = "0.0.5"
dependencies = [ dependencies = [
"axum", "axum",
"chrono", "chrono",
"image", "image",
"maud", "maud",
"mime",
"rand", "rand",
"serde", "serde",
"serde_urlencoded", "serde_urlencoded",

View File

@@ -5,7 +5,7 @@ resolver = "2"
[workspace.package] [workspace.package]
rust-version = "1.90.0" rust-version = "1.90.0"
edition = "2024" edition = "2024"
version = "0.0.4" version = "0.0.5"
license = "GPL-3.0" license = "GPL-3.0"
repository = "https://github.com/rm-dr/servable" repository = "https://github.com/rm-dr/servable"
readme = "README.md" readme = "README.md"
@@ -44,7 +44,7 @@ mutex_atomic = "deny"
needless_raw_strings = "deny" needless_raw_strings = "deny"
str_to_string = "deny" str_to_string = "deny"
string_add = "deny" string_add = "deny"
string_to_string = "deny" implicit_clone = "deny"
use_debug = "allow" use_debug = "allow"
verbose_file_reads = "deny" verbose_file_reads = "deny"
large_types_passed_by_value = "deny" large_types_passed_by_value = "deny"
@@ -74,6 +74,7 @@ axum = "0.8"
chrono = "0.4" chrono = "0.4"
image = "0.25" image = "0.25"
maud = "0.27" maud = "0.27"
mime = "0.3"
rand = "0.9" rand = "0.9"
serde = { version = "1.0", features = ["derive"] } serde = { version = "1.0", features = ["derive"] }
serde_urlencoded = "0.7" serde_urlencoded = "0.7"

View File

@@ -23,6 +23,7 @@ serde_urlencoded = { workspace = true }
tower = { workspace = true } tower = { workspace = true }
tracing = { workspace = true } tracing = { workspace = true }
rand = { workspace = true } rand = { workspace = true }
mime = { workspace = true }
tokio = { workspace = true, optional = true } tokio = { workspace = true, optional = true }
image = { workspace = true, optional = true } image = { workspace = true, optional = true }

View File

@@ -32,7 +32,7 @@ async fn main() {
"/hello", "/hello",
StaticAsset { StaticAsset {
bytes: b"Hello, World!", 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: - `StaticAsset`, for static files like CSS, JavaScript, images, or plain bytes:
```rust ```rust
use servable::{StaticAsset, mime::MimeType}; use servable::{StaticAsset};
let asset = StaticAsset { let asset = StaticAsset {
bytes: b"body { color: red; }", bytes: b"body { color: red; }",
mime: MimeType::Css, mime: mime::TEXT_CSS,
ttl: StaticAsset::DEFAULT_TTL 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: 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 ```rust
# use servable::{ServableRouter, StaticAsset, mime::MimeType}; # use servable::{ServableRouter, StaticAsset};
# let home_page = StaticAsset { bytes: b"home", mime: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL}; # let home_page = StaticAsset { bytes: b"home", mime: mime::TEXT_HTML, ttl: StaticAsset::DEFAULT_TTL};
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::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: MimeType::Css, 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: MimeType::Html, ttl: StaticAsset::DEFAULT_TTL }; # let custom_404_page = StaticAsset { bytes: b"404", mime: mime::TEXT_HTML, ttl: StaticAsset::DEFAULT_TTL };
let route = ServableRouter::new() let route = ServableRouter::new()
.add_page("/", home_page) .add_page("/", home_page)
.add_page("/about", about_page) .add_page("/about", about_page)
@@ -121,13 +121,13 @@ let route = ServableRouter::new()
When `image` is enabled, the image below... When `image` is enabled, the image below...
```rust ```rust
# use servable::{ServableRouter, StaticAsset, mime::MimeType}; # use servable::{ServableRouter, StaticAsset};
let route = ServableRouter::new() let route = ServableRouter::new()
.add_page( .add_page(
"/image.png", "/image.png",
StaticAsset { StaticAsset {
bytes: b"fake image data", bytes: b"fake image data",
mime: MimeType::Png, mime: mime::IMAGE_PNG,
ttl: StaticAsset::DEFAULT_TTL ttl: StaticAsset::DEFAULT_TTL
} }
); );
@@ -184,13 +184,12 @@ whenever the server is restarted:
```rust ```rust
use chrono::TimeDelta; use chrono::TimeDelta;
use servable::{HtmlPage, CACHE_BUST_STR, ServableWithRoute, StaticAsset, ServableRouter}; use servable::{HtmlPage, CACHE_BUST_STR, ServableWithRoute, StaticAsset, ServableRouter};
use servable::mime::MimeType;
pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new( pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|| format!("/{}/main.css", *CACHE_BUST_STR), || format!("/{}/main.css", *CACHE_BUST_STR),
StaticAsset { StaticAsset {
bytes: "div{}".as_bytes(), bytes: "div{}".as_bytes(),
mime: MimeType::Css, mime: mime::TEXT_CSS,
ttl: StaticAsset::DEFAULT_TTL, ttl: StaticAsset::DEFAULT_TTL,
}, },
); );

View File

@@ -4,8 +4,6 @@
// and needs a different relative path than cargo build. // and needs a different relative path than cargo build.
// https://github.com/rust-lang/cargo/issues/13309 // https://github.com/rust-lang/cargo/issues/13309
pub mod mime;
mod types; mod types;
use rand::{Rng, distr::Alphanumeric}; use rand::{Rng, distr::Alphanumeric};
@@ -47,7 +45,7 @@ pub static CACHE_BUST_STR: std::sync::LazyLock<String> = std::sync::LazyLock::ne
#[cfg(feature = "htmx-2.0.8")] #[cfg(feature = "htmx-2.0.8")]
pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset { pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(), bytes: include_str!("../htmx/htmx-2.0.8.min.js").as_bytes(),
mime: mime::MimeType::Javascript, mime: mime::TEXT_JAVASCRIPT,
ttl: StaticAsset::DEFAULT_TTL, ttl: StaticAsset::DEFAULT_TTL,
}; };
@@ -57,6 +55,6 @@ pub const HTMX_2_0_8: servable::StaticAsset = servable::StaticAsset {
#[cfg(feature = "htmx-2.0.8")] #[cfg(feature = "htmx-2.0.8")]
pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset { pub const EXT_JSON_1_19_12: servable::StaticAsset = servable::StaticAsset {
bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(), bytes: include_str!("../htmx/json-enc-1.9.12.js").as_bytes(),
mime: mime::MimeType::Javascript, mime: mime::TEXT_JAVASCRIPT,
ttl: StaticAsset::DEFAULT_TTL, ttl: StaticAsset::DEFAULT_TTL,
}; };

View File

@@ -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::Schema> {
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<S>(&self, serializer: S) -> Result<S::Ok, S::Error>
where
S: Serializer,
{
serializer.serialize_str(&self.to_string())
}
}
impl<'de> Deserialize<'de> for MimeType {
fn deserialize<D>(deserializer: D) -> Result<Self, D::Error>
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<String> 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<Self, <Self as FromStr>::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<Self, Self::Err> {
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<Self> {
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,
}
}
}

View File

@@ -19,7 +19,6 @@ use tracing::trace;
use crate::{ use crate::{
ClientInfo, RenderContext, Rendered, RenderedBody, ClientInfo, RenderContext, Rendered, RenderedBody,
mime::MimeType,
servable::{Servable, ServableWithRoute}, servable::{Servable, ServableWithRoute},
}; };
@@ -36,7 +35,7 @@ impl Servable for Default404 {
body: (), body: (),
ttl: Some(TimeDelta::days(1)), ttl: Some(TimeDelta::days(1)),
headers: HeaderMap::new(), headers: HeaderMap::new(),
mime: Some(MimeType::Html), mime: Some(mime::TEXT_HTML),
private: false, private: false,
}; };
}) })
@@ -54,7 +53,7 @@ impl Servable for Default404 {
/// ///
/// Use as follows: /// Use as follows:
/// ```rust /// ```rust
/// use servable::{ServableRouter, StaticAsset, mime::MimeType}; /// use servable::{ServableRouter, StaticAsset};
/// use axum::Router; /// use axum::Router;
/// use tower_http::compression::{CompressionLayer, predicate::DefaultPredicate}; /// use tower_http::compression::{CompressionLayer, predicate::DefaultPredicate};
/// ///
@@ -72,7 +71,7 @@ impl Servable for Default404 {
/// "/page", /// "/page",
/// StaticAsset { /// StaticAsset {
/// bytes: "I am a page".as_bytes(), /// bytes: "I am a page".as_bytes(),
/// mime: MimeType::Text, /// mime: mime::TEXT_PLAIN,
/// ttl: StaticAsset::DEFAULT_TTL /// ttl: StaticAsset::DEFAULT_TTL
/// }, /// },
/// ); /// );
@@ -273,7 +272,7 @@ impl Service<Request<Body>> for ServableRouter {
#[expect(clippy::unwrap_used)] #[expect(clippy::unwrap_used)]
rend.headers.insert( rend.headers.insert(
header::CONTENT_TYPE, header::CONTENT_TYPE,
HeaderValue::from_str(&mime.to_string()).unwrap(), HeaderValue::from_str(mime.as_ref()).unwrap(),
); );
} }
} }

View File

@@ -1,8 +1,9 @@
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use chrono::TimeDelta; use chrono::TimeDelta;
use mime::Mime;
use std::pin::Pin; 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 /// A static blob of bytes
pub struct StaticAsset { pub struct StaticAsset {
@@ -10,8 +11,7 @@ pub struct StaticAsset {
pub bytes: &'static [u8], pub bytes: &'static [u8],
/// The type of `bytes` /// The type of `bytes`
pub mime: MimeType, pub mime: Mime,
/// How long to cache this response. /// How long to cache this response.
/// If None, never cache /// If None, never cache
pub ttl: Option<TimeDelta>, pub ttl: Option<TimeDelta>,

View File

@@ -4,7 +4,7 @@ use maud::{DOCTYPE, Markup, PreEscaped, html};
use serde::Deserialize; use serde::Deserialize;
use std::{hash::Hash, pin::Pin, sync::Arc}; 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)] #[expect(missing_docs)]
#[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)] #[derive(Debug, Clone, Hash, PartialEq, Eq, Deserialize)]
@@ -225,7 +225,7 @@ impl Servable for HtmlPage {
ttl: self.ttl, ttl: self.ttl,
private: self.private, private: self.private,
headers: HeaderMap::new(), headers: HeaderMap::new(),
mime: Some(MimeType::Html), mime: Some(mime::TEXT_HTML),
}; };
}) })
} }

View File

@@ -1,10 +1,10 @@
use image::{DynamicImage, ImageFormat}; use image::{DynamicImage, ImageFormat};
use mime::Mime;
use serde::{Deserialize, Deserializer, de}; use serde::{Deserialize, Deserializer, de};
use std::{fmt::Display, hash::Hash, io::Cursor, str::FromStr}; use std::{fmt::Display, hash::Hash, io::Cursor, str::FromStr};
use thiserror::Error; use thiserror::Error;
use super::transformers::{ImageTransformer, TransformerEnum}; use super::transformers::{ImageTransformer, TransformerEnum};
use crate::mime::MimeType;
#[expect(missing_docs)] #[expect(missing_docs)]
#[derive(Debug, Error)] #[derive(Debug, Error)]
@@ -28,7 +28,7 @@ pub struct TransformerChain {
impl TransformerChain { impl TransformerChain {
/// Returns `true` if `mime` is a type that can be transformed /// Returns `true` if `mime` is a type that can be transformed
#[inline(always)] #[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() 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 /// with type `input_mime`. If this returns `None`, the input mime
/// cannot be transformed. /// cannot be transformed.
#[inline(always)] #[inline(always)]
pub fn output_mime(&self, input_mime: &MimeType) -> Option<MimeType> { pub fn output_mime(&self, input_mime: &Mime) -> Option<Mime> {
let mime = self let mime = self
.steps .steps
.last() .last()
.and_then(|x| match x { .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, _ => None,
}) })
.unwrap_or(input_mime.clone()); .unwrap_or(input_mime.clone());
@@ -72,8 +74,8 @@ impl TransformerChain {
pub fn transform_bytes( pub fn transform_bytes(
&self, &self,
image_bytes: &[u8], image_bytes: &[u8],
image_format: Option<&MimeType>, image_format: Option<&Mime>,
) -> Result<(MimeType, Vec<u8>), TransformBytesError> { ) -> Result<(Mime, Vec<u8>), TransformBytesError> {
let format: ImageFormat = match image_format { let format: ImageFormat = match image_format {
Some(x) => ImageFormat::from_mime_type(x.to_string()) Some(x) => ImageFormat::from_mime_type(x.to_string())
.ok_or(TransformBytesError::NotAnImage(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 = image::load_from_memory_with_format(image_bytes, format)?;
let img = self.transform_image(img); 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()); let mut out_bytes = Cursor::new(Vec::new());
img.write_to(&mut out_bytes, *out_format)?; img.write_to(&mut out_bytes, *out_format)?;

View File

@@ -1,9 +1,8 @@
use axum::http::{HeaderMap, StatusCode}; use axum::http::{HeaderMap, StatusCode};
use chrono::TimeDelta; use chrono::TimeDelta;
use mime::Mime;
use std::collections::BTreeMap; use std::collections::BTreeMap;
use crate::mime::MimeType;
// //
// MARK: rendered // MARK: rendered
// //
@@ -52,7 +51,7 @@ pub struct Rendered<T: RenderedBodyType> {
pub body: T, pub body: T,
/// The type of `self.body` /// The type of `self.body`
pub mime: Option<MimeType>, pub mime: Option<Mime>,
/// How long to cache this response. /// How long to cache this response.
/// If none, don't cache. /// If none, don't cache.
@@ -93,20 +92,17 @@ pub struct RenderContext {
/// The type of device that requested a page /// The type of device that requested a page
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)] #[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
#[derive(Default)]
pub enum DeviceType { pub enum DeviceType {
/// This is a mobile device, like a phone. /// This is a mobile device, like a phone.
Mobile, Mobile,
/// This is a device with a large screen /// This is a device with a large screen
/// and a mouse, like a laptop. /// and a mouse, like a laptop.
Desktop, #[default]
Desktop,
} }
impl Default for DeviceType {
fn default() -> Self {
Self::Desktop
}
}
/// Inferred information about the client /// Inferred information about the client
/// that requested a certain route. /// that requested a certain route.