mirror of
https://github.com/rm-dr/servable.git
synced 2026-05-30 07:08:58 -07:00
Compare commits
4 Commits
1f26bd4938
..
v0.0.6
| Author | SHA1 | Date | |
|---|---|---|---|
| 5f1511df9d | |||
| 7a13bd0cda | |||
| 8674bd9c85 | |||
| e5926bccbf |
@@ -52,8 +52,6 @@ jobs:
|
|||||||
buildandtest:
|
buildandtest:
|
||||||
name: "Build and test"
|
name: "Build and test"
|
||||||
runs-on: ubuntu-latest
|
runs-on: ubuntu-latest
|
||||||
permissions:
|
|
||||||
contents: write
|
|
||||||
|
|
||||||
steps:
|
steps:
|
||||||
- uses: actions/checkout@v4
|
- uses: actions/checkout@v4
|
||||||
@@ -71,3 +69,6 @@ jobs:
|
|||||||
|
|
||||||
- name: Test
|
- name: Test
|
||||||
run: cargo test-all-features -- --release
|
run: cargo test-all-features -- --release
|
||||||
|
|
||||||
|
- name: Publish
|
||||||
|
run: cargo publish --dry-run
|
||||||
Generated
+2
-1
@@ -1274,12 +1274,13 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "servable"
|
name = "servable"
|
||||||
version = "0.0.2"
|
version = "0.0.6"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"axum",
|
"axum",
|
||||||
"chrono",
|
"chrono",
|
||||||
"image",
|
"image",
|
||||||
"maud",
|
"maud",
|
||||||
|
"mime",
|
||||||
"rand",
|
"rand",
|
||||||
"serde",
|
"serde",
|
||||||
"serde_urlencoded",
|
"serde_urlencoded",
|
||||||
|
|||||||
+16
-15
@@ -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.2"
|
version = "0.0.6"
|
||||||
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"
|
||||||
@@ -70,16 +70,17 @@ cargo_common_metadata = "deny"
|
|||||||
|
|
||||||
[workspace.dependencies]
|
[workspace.dependencies]
|
||||||
|
|
||||||
axum = "0.8.7"
|
axum = "0.8"
|
||||||
chrono = "0.4.42"
|
chrono = "0.4"
|
||||||
image = "0.25.9"
|
image = "0.25"
|
||||||
maud = "0.27.0"
|
maud = "0.27"
|
||||||
rand = "0.9.0"
|
mime = "0.3"
|
||||||
serde = { version = "1.0.228", features = ["derive"] }
|
rand = "0.9"
|
||||||
serde_urlencoded = "0.7.1"
|
serde = { version = "1.0", features = ["derive"] }
|
||||||
strum = { version = "0.27.2", features = ["derive"] }
|
serde_urlencoded = "0.7"
|
||||||
thiserror = "2.0.17"
|
strum = { version = "0.27", features = ["derive"] }
|
||||||
tokio = "1.48.0"
|
thiserror = "2.0"
|
||||||
tower = "0.5.2"
|
tokio = "1.48"
|
||||||
tower-http = { version = "0.6.7", features = ["compression-full"] }
|
tower = "0.5"
|
||||||
tracing = "0.1.41"
|
tower-http = { version = "0.6", features = ["compression-full"] }
|
||||||
|
tracing = "0.1"
|
||||||
|
|||||||
@@ -1,179 +0,0 @@
|
|||||||
# Servable: a simple web framework
|
|
||||||
|
|
||||||
[](https://github.com/rm-dr/servable/actions)
|
|
||||||
[](https://crates.io/crates/servable)
|
|
||||||
[](https://docs.rs/servable/)
|
|
||||||
|
|
||||||
A tiny, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz).
|
|
||||||
Inspired by the "MASH" stack described [here](https://yree.io/mash) and [here](https://emschwartz.me/building-a-fast-website-with-the-mash-stack-in-rust).
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
|
||||||
|
|
||||||
`servable` provides abstractions that implement common utilities needed by an http server. \
|
|
||||||
|
|
||||||
- response headers and cache-busting utilities
|
|
||||||
- client device detection (mobile / desktop)
|
|
||||||
- server-side image optimization (see the `image` feature below)
|
|
||||||
- ergonomic [htmx](https://htmx.org) integration (see `htmx-*` features below)
|
|
||||||
|
|
||||||
|
|
||||||
-------------------
|
|
||||||
|
|
||||||
|
|
||||||
## Quick Start
|
|
||||||
|
|
||||||
```rust,ignore
|
|
||||||
use servable::{ServableRouter, servable::StaticAsset, mime::MimeType};
|
|
||||||
|
|
||||||
#[tokio::main]
|
|
||||||
async fn main() {
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page(
|
|
||||||
"/hello",
|
|
||||||
StaticAsset {
|
|
||||||
bytes: b"Hello, World!",
|
|
||||||
mime: MimeType::Text,
|
|
||||||
},
|
|
||||||
);
|
|
||||||
|
|
||||||
// usual axum startup routine
|
|
||||||
let app = route.into_router();
|
|
||||||
let listener = tokio::net::TcpListener::bind("0.0.0.0:3000")
|
|
||||||
.await
|
|
||||||
.unwrap();
|
|
||||||
|
|
||||||
axum::serve(listener, app).await.unwrap();
|
|
||||||
}
|
|
||||||
```
|
|
||||||
|
|
||||||
# Core Concepts
|
|
||||||
|
|
||||||
## The `Servable` trait
|
|
||||||
|
|
||||||
The `Servable` trait is the foundation of this stack. \
|
|
||||||
`servable` provides implementations for a few common servables:
|
|
||||||
|
|
||||||
|
|
||||||
- `StaticAsset`, for static files like CSS, JavaScript, images, or plain bytes:
|
|
||||||
```rust
|
|
||||||
use servable::{StaticAsset, mime::MimeType};
|
|
||||||
|
|
||||||
let asset = StaticAsset {
|
|
||||||
bytes: b"body { color: red; }",
|
|
||||||
mime: MimeType::Css,
|
|
||||||
};
|
|
||||||
```
|
|
||||||
|
|
||||||
- `Redirect`, for simple http redirects:
|
|
||||||
```rust
|
|
||||||
use servable::Redirect;
|
|
||||||
|
|
||||||
let redirect = Redirect::new("/new-location").unwrap();
|
|
||||||
```
|
|
||||||
|
|
||||||
- `HtmlPage`, for dynamically-rendered HTML pages
|
|
||||||
```rust
|
|
||||||
use servable::{HtmlPage, PageMetadata};
|
|
||||||
use maud::html;
|
|
||||||
use std::pin::Pin;
|
|
||||||
|
|
||||||
let page = HtmlPage::default()
|
|
||||||
.with_meta(PageMetadata {
|
|
||||||
title: "My Page".into(),
|
|
||||||
description: Some("A great page".into()),
|
|
||||||
..Default::default()
|
|
||||||
})
|
|
||||||
.with_render(|_page, ctx| {
|
|
||||||
Box::pin(async move {
|
|
||||||
html! {
|
|
||||||
h1 { "Welcome!" }
|
|
||||||
p { "Route: " (ctx.route) }
|
|
||||||
}
|
|
||||||
})
|
|
||||||
});
|
|
||||||
```
|
|
||||||
`HtmlPage` automatically generates a `<head>` and wraps its rendered html in `<html><body>`.
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## `ServableRouter`
|
|
||||||
|
|
||||||
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 };
|
|
||||||
# let about_page = StaticAsset { bytes: b"about", mime: MimeType::Html };
|
|
||||||
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css };
|
|
||||||
# let custom_404_page = StaticAsset { bytes: b"404", mime: MimeType::Html };
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page("/", home_page)
|
|
||||||
.add_page("/about", about_page)
|
|
||||||
.add_page("/style.css", stylesheet)
|
|
||||||
.with_404(custom_404_page); // override default 404
|
|
||||||
```
|
|
||||||
|
|
||||||
# Features
|
|
||||||
- `image`: enable image transformation via query parameters. This makes `tokio` a dependency. \
|
|
||||||
When this is enabled, all `StaticAssets` with a valid mimetype can take an optional `t=` query parameter. \
|
|
||||||
See the `TransformerEnum` in this crate's documentation for details.
|
|
||||||
|
|
||||||
When `image` is enabled, the image below...
|
|
||||||
```rust
|
|
||||||
# use servable::{ServableRouter, StaticAsset, mime::MimeType};
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page(
|
|
||||||
"/image.png",
|
|
||||||
StaticAsset {
|
|
||||||
bytes: b"fake image data",
|
|
||||||
mime: MimeType::Png,
|
|
||||||
}
|
|
||||||
);
|
|
||||||
```
|
|
||||||
...may be accessed as follows:
|
|
||||||
|
|
||||||
```r
|
|
||||||
# Original image
|
|
||||||
GET /image.png
|
|
||||||
|
|
||||||
# Resize to max 800px on longest side
|
|
||||||
GET /image.png?t=maxdim(800)
|
|
||||||
|
|
||||||
# Crop to a 400x400 square at the center of the image
|
|
||||||
GET /image.png?t=crop(400,400,c)
|
|
||||||
|
|
||||||
# Chain transformations and transcode
|
|
||||||
GET /image.png?t=maxdim(800);crop(400,400);format(webp)
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
- `htmx-2.0.8`: Include htmx sources in the compiled executable. \
|
|
||||||
Use as follows:
|
|
||||||
```rust
|
|
||||||
# use servable::ServableRouter;
|
|
||||||
# #[cfg(feature = "htmx-2.0.8")]
|
|
||||||
let route = ServableRouter::new()
|
|
||||||
.add_page("/htmx.js", servable::HTMX_2_0_8)
|
|
||||||
.add_page("/htmx-json-enc.js", servable::EXT_JSON_1_19_12);
|
|
||||||
```
|
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Caching and cache-busting
|
|
||||||
|
|
||||||
Control caching behavior per servable:
|
|
||||||
|
|
||||||
```rust
|
|
||||||
use chrono::TimeDelta;
|
|
||||||
use servable::HtmlPage;
|
|
||||||
|
|
||||||
let page = HtmlPage::default()
|
|
||||||
.with_ttl(Some(TimeDelta::hours(1)))
|
|
||||||
.with_immutable(false);
|
|
||||||
```
|
|
||||||
|
|
||||||
Headers are automatically generated:
|
|
||||||
- `Cache-Control: public, max-age=3600`
|
|
||||||
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)
|
|
||||||
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "servable"
|
name = "servable"
|
||||||
description = "A tiny web stack featuring htmx, Axum, Rust, and Maud."
|
description = "A tiny web stack built around htmx, Axum, and Maud."
|
||||||
keywords = ["htmx", "web", "webui", "maud", "framework"]
|
keywords = ["htmx", "web", "webui", "maud", "framework"]
|
||||||
categories = ["web-programming", "web-programming::http-server"]
|
categories = ["web-programming", "web-programming::http-server"]
|
||||||
rust-version = { workspace = true }
|
rust-version = { workspace = true }
|
||||||
@@ -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 }
|
||||||
|
|||||||
+47
-20
@@ -4,14 +4,12 @@
|
|||||||
[](https://crates.io/crates/servable)
|
[](https://crates.io/crates/servable)
|
||||||
[](https://docs.rs/servable/)
|
[](https://docs.rs/servable/)
|
||||||
|
|
||||||
A tiny, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz).
|
A minimal, convenient web micro-framework built around [htmx](https://htmx.org), [Axum](https://github.com/tokio-rs/axum), and [Maud](https://maud.lambda.xyz). \
|
||||||
Inspired by the "MASH" stack described [here](https://yree.io/mash) and [here](https://emschwartz.me/building-a-fast-website-with-the-mash-stack-in-rust).
|
This powers [my homepage](https://betalupi.com). See example usage [here](https://git.betalupi.com/Mark/webpage/src/branch/main/crates/service/service-webpage/src/routes/mod.rs).
|
||||||
|
|
||||||
|
|
||||||
|
|
||||||
## Features
|
## Features
|
||||||
|
|
||||||
`servable` provides abstractions that implement common utilities needed by an http server. \
|
`servable` provides abstractions that implement common utilities needed by an http server.
|
||||||
|
|
||||||
- response headers and cache-busting utilities
|
- response headers and cache-busting utilities
|
||||||
- client device detection (mobile / desktop)
|
- client device detection (mobile / desktop)
|
||||||
@@ -34,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
|
||||||
},
|
},
|
||||||
);
|
);
|
||||||
|
|
||||||
@@ -58,11 +56,12 @@ 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
|
||||||
};
|
};
|
||||||
```
|
```
|
||||||
|
|
||||||
@@ -103,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 };
|
# 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 };
|
# let about_page = StaticAsset { bytes: b"about", mime: mime::TEXT_HTML, ttl: StaticAsset::DEFAULT_TTL };
|
||||||
# let stylesheet = StaticAsset { bytes: b"css", mime: MimeType::Css };
|
# 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 };
|
# 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)
|
||||||
@@ -122,13 +121,14 @@ 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
|
||||||
}
|
}
|
||||||
);
|
);
|
||||||
```
|
```
|
||||||
@@ -139,13 +139,13 @@ let route = ServableRouter::new()
|
|||||||
GET /image.png
|
GET /image.png
|
||||||
|
|
||||||
# Resize to max 800px on longest side
|
# Resize to max 800px on longest side
|
||||||
GET /image.png?t=maxdim(800)
|
GET /image.png?t=maxdim(800,800)
|
||||||
|
|
||||||
# Crop to a 400x400 square at the center of the image
|
# Crop to a 400x400 square at the center of the image
|
||||||
GET /image.png?t=crop(400,400,c)
|
GET /image.png?t=crop(400,400,c)
|
||||||
|
|
||||||
# Chain transformations and transcode
|
# Chain transformations and transcode
|
||||||
GET /image.png?t=maxdim(800);crop(400,400);format(webp)
|
GET /image.png?t=maxdim(800,800);crop(400,400);format(webp)
|
||||||
```
|
```
|
||||||
|
|
||||||
|
|
||||||
@@ -171,9 +171,36 @@ use servable::HtmlPage;
|
|||||||
|
|
||||||
let page = HtmlPage::default()
|
let page = HtmlPage::default()
|
||||||
.with_ttl(Some(TimeDelta::hours(1)))
|
.with_ttl(Some(TimeDelta::hours(1)))
|
||||||
.with_immutable(false);
|
.with_private(false);
|
||||||
```
|
```
|
||||||
|
|
||||||
Headers are automatically generated:
|
Headers are automatically generated:
|
||||||
- `Cache-Control: public, max-age=3600`
|
- `Cache-Control: public, max-age=3600` (default)
|
||||||
- `Cache-Control: immutable, public, max-age=31536000` (for immutable assets)
|
- `Cache-Control: private, max-age=31536000` (if `private` is true)
|
||||||
|
|
||||||
|
We also provide a static `CACHE_BUST_STR`, which may be formatted into urls to force cache refresh
|
||||||
|
whenever the server is restarted:
|
||||||
|
|
||||||
|
```rust
|
||||||
|
use chrono::TimeDelta;
|
||||||
|
use servable::{HtmlPage, CACHE_BUST_STR, ServableWithRoute, StaticAsset, ServableRouter};
|
||||||
|
|
||||||
|
pub static HTMX: ServableWithRoute<StaticAsset> = ServableWithRoute::new(
|
||||||
|
|| format!("/{}/main.css", *CACHE_BUST_STR),
|
||||||
|
StaticAsset {
|
||||||
|
bytes: "div{}".as_bytes(),
|
||||||
|
mime: mime::TEXT_CSS,
|
||||||
|
ttl: StaticAsset::DEFAULT_TTL,
|
||||||
|
},
|
||||||
|
);
|
||||||
|
|
||||||
|
|
||||||
|
let route = HTMX.route();
|
||||||
|
println!("Css is at {route}");
|
||||||
|
|
||||||
|
let router = ServableRouter::new()
|
||||||
|
.add_page_with_route(&HTMX);
|
||||||
|
```
|
||||||
|
|
||||||
|
## TODO:
|
||||||
|
- cache-busting fonts in css is not possible, we need to dynamic replace urls
|
||||||
|
|||||||
@@ -1,6 +1,8 @@
|
|||||||
#![doc = include_str!("../README.md")]
|
#![doc = include_str!("../README.md")]
|
||||||
|
// readme is symlinked to the root of this repo
|
||||||
pub mod mime;
|
// because `cargo publish` works from a different dir,
|
||||||
|
// and needs a different relative path than cargo build.
|
||||||
|
// https://github.com/rust-lang/cargo/issues/13309
|
||||||
|
|
||||||
mod types;
|
mod types;
|
||||||
|
|
||||||
@@ -43,7 +45,8 @@ 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,
|
||||||
};
|
};
|
||||||
|
|
||||||
/// HTMX json extension, 1.19.2.
|
/// HTMX json extension, 1.19.2.
|
||||||
@@ -52,5 +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,
|
||||||
};
|
};
|
||||||
|
|||||||
@@ -1,811 +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::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,
|
|
||||||
}
|
|
||||||
}
|
|
||||||
}
|
|
||||||
@@ -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},
|
||||||
};
|
};
|
||||||
|
|
||||||
@@ -35,9 +34,9 @@ impl Servable for Default404 {
|
|||||||
code: StatusCode::NOT_FOUND,
|
code: StatusCode::NOT_FOUND,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: Some(TimeDelta::days(1)),
|
ttl: Some(TimeDelta::days(1)),
|
||||||
immutable: true,
|
|
||||||
headers: HeaderMap::new(),
|
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:
|
/// 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,8 @@ 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
|
||||||
/// },
|
/// },
|
||||||
/// );
|
/// );
|
||||||
///
|
///
|
||||||
@@ -243,14 +243,15 @@ impl Service<Request<Body>> for ServableRouter {
|
|||||||
// Tweak headers
|
// Tweak headers
|
||||||
{
|
{
|
||||||
if !rend.headers.contains_key(header::CACHE_CONTROL) {
|
if !rend.headers.contains_key(header::CACHE_CONTROL) {
|
||||||
let max_age = rend.ttl.map(|x| x.num_seconds()).unwrap_or(1).max(1);
|
let max_age = rend.ttl.map(|x| x.num_seconds()).unwrap_or(0).max(0);
|
||||||
|
|
||||||
let mut value = String::new();
|
let mut value = String::new();
|
||||||
if rend.immutable {
|
|
||||||
value.push_str("immutable, ");
|
|
||||||
}
|
|
||||||
|
|
||||||
value.push_str("public, ");
|
value.push_str(match rend.private {
|
||||||
|
true => "private, ",
|
||||||
|
false => "public, ",
|
||||||
|
});
|
||||||
|
|
||||||
value.push_str(&format!("max-age={}, ", max_age));
|
value.push_str(&format!("max-age={}, ", max_age));
|
||||||
|
|
||||||
#[expect(clippy::unwrap_used)]
|
#[expect(clippy::unwrap_used)]
|
||||||
@@ -271,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(),
|
||||||
);
|
);
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -1,10 +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};
|
||||||
|
|
||||||
const TTL: Option<TimeDelta> = Some(TimeDelta::days(1));
|
|
||||||
|
|
||||||
/// A static blob of bytes
|
/// A static blob of bytes
|
||||||
pub struct StaticAsset {
|
pub struct StaticAsset {
|
||||||
@@ -12,7 +11,21 @@ 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.
|
||||||
|
/// If None, never cache
|
||||||
|
pub ttl: Option<TimeDelta>,
|
||||||
|
}
|
||||||
|
|
||||||
|
impl StaticAsset {
|
||||||
|
/// Default ttl of a [StaticAsset]
|
||||||
|
pub const DEFAULT_TTL: Option<TimeDelta> = Some(TimeDelta::days(14));
|
||||||
|
|
||||||
|
/// Set `self.ttl`
|
||||||
|
pub const fn with_ttl(mut self, ttl: Option<TimeDelta>) -> Self {
|
||||||
|
self.ttl = ttl;
|
||||||
|
self
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
#[cfg(feature = "image")]
|
#[cfg(feature = "image")]
|
||||||
@@ -36,8 +49,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::BAD_REQUEST,
|
code: StatusCode::BAD_REQUEST,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -51,8 +64,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(
|
mime: Some(
|
||||||
@@ -67,8 +80,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(self.mime.clone()),
|
mime: Some(self.mime.clone()),
|
||||||
@@ -99,8 +112,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::BAD_REQUEST,
|
code: StatusCode::BAD_REQUEST,
|
||||||
body: RenderedBody::String(err),
|
body: RenderedBody::String(err),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -131,7 +144,7 @@ impl Servable for StaticAsset {
|
|||||||
"Error while transforming image: {error:?}"
|
"Error while transforming image: {error:?}"
|
||||||
)),
|
)),
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -144,8 +157,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: RenderedBody::Bytes(bytes),
|
body: RenderedBody::Bytes(bytes),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(mime),
|
mime: Some(mime),
|
||||||
@@ -156,8 +169,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::INTERNAL_SERVER_ERROR,
|
code: StatusCode::INTERNAL_SERVER_ERROR,
|
||||||
body: RenderedBody::String(format!("{err}")),
|
body: RenderedBody::String(format!("{err}")),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: None,
|
mime: None,
|
||||||
@@ -170,8 +183,9 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: RenderedBody::Static(self.bytes),
|
body: RenderedBody::Static(self.bytes),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(self.mime.clone()),
|
mime: Some(self.mime.clone()),
|
||||||
};
|
};
|
||||||
@@ -191,8 +205,8 @@ impl Servable for StaticAsset {
|
|||||||
return Rendered {
|
return Rendered {
|
||||||
code: StatusCode::OK,
|
code: StatusCode::OK,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: TTL,
|
ttl: self.ttl,
|
||||||
immutable: true,
|
private: false,
|
||||||
|
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(self.mime.clone()),
|
mime: Some(self.mime.clone()),
|
||||||
|
|||||||
@@ -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)]
|
||||||
@@ -52,7 +52,7 @@ pub struct HtmlPage {
|
|||||||
pub meta: PageMetadata,
|
pub meta: PageMetadata,
|
||||||
|
|
||||||
/// If true, the contents of this page never change
|
/// If true, the contents of this page never change
|
||||||
pub immutable: bool,
|
pub private: bool,
|
||||||
|
|
||||||
/// How long this page's html may be cached.
|
/// How long this page's html may be cached.
|
||||||
/// This controls the maximum age of a page shown to the user.
|
/// This controls the maximum age of a page shown to the user.
|
||||||
@@ -94,7 +94,7 @@ impl Default for HtmlPage {
|
|||||||
HtmlPage {
|
HtmlPage {
|
||||||
// No cache by default
|
// No cache by default
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: false,
|
private: false,
|
||||||
|
|
||||||
meta: Default::default(),
|
meta: Default::default(),
|
||||||
render: Arc::new(|_, _| Box::pin(async { html!() })),
|
render: Arc::new(|_, _| Box::pin(async { html!() })),
|
||||||
@@ -132,10 +132,10 @@ impl HtmlPage {
|
|||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
/// Set `self.immutable`
|
/// Set `self.private`
|
||||||
#[inline(always)]
|
#[inline(always)]
|
||||||
pub fn with_immutable(mut self, immutable: bool) -> Self {
|
pub fn with_private(mut self, private: bool) -> Self {
|
||||||
self.immutable = immutable;
|
self.private = private;
|
||||||
self
|
self
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -223,9 +223,9 @@ impl Servable for HtmlPage {
|
|||||||
code: self.response_code,
|
code: self.response_code,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: self.ttl,
|
ttl: self.ttl,
|
||||||
immutable: self.immutable,
|
private: self.private,
|
||||||
headers: HeaderMap::new(),
|
headers: HeaderMap::new(),
|
||||||
mime: Some(MimeType::Html),
|
mime: Some(mime::TEXT_HTML),
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
|
|||||||
@@ -60,6 +60,12 @@ impl<S: Servable> ServableWithRoute<S> {
|
|||||||
pub fn route(&self) -> &str {
|
pub fn route(&self) -> &str {
|
||||||
&self.route
|
&self.route
|
||||||
}
|
}
|
||||||
|
|
||||||
|
/// Get the route associated with this resource,
|
||||||
|
/// with the given prefix
|
||||||
|
pub fn route_at(&self, prefix: &str) -> String {
|
||||||
|
format!("{prefix}/{}", &*self.route)
|
||||||
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
impl<S: Servable> Servable for ServableWithRoute<S> {
|
impl<S: Servable> Servable for ServableWithRoute<S> {
|
||||||
|
|||||||
@@ -60,7 +60,7 @@ impl Servable for Redirect {
|
|||||||
headers,
|
headers,
|
||||||
body: (),
|
body: (),
|
||||||
ttl: None,
|
ttl: None,
|
||||||
immutable: true,
|
private: false,
|
||||||
mime: None,
|
mime: None,
|
||||||
};
|
};
|
||||||
})
|
})
|
||||||
|
|||||||
@@ -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)?;
|
||||||
|
|
||||||
|
|||||||
@@ -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,14 +51,14 @@ 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.
|
||||||
pub ttl: Option<TimeDelta>,
|
pub ttl: Option<TimeDelta>,
|
||||||
|
|
||||||
/// If true, the data at this route will never change.
|
/// If true, this response sets `Cache-Control: private`
|
||||||
pub immutable: bool,
|
pub private: bool,
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Rendered<()> {
|
impl Rendered<()> {
|
||||||
@@ -71,7 +70,7 @@ impl Rendered<()> {
|
|||||||
body,
|
body,
|
||||||
mime: self.mime,
|
mime: self.mime,
|
||||||
ttl: self.ttl,
|
ttl: self.ttl,
|
||||||
immutable: self.immutable,
|
private: self.private,
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -92,22 +91,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, 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.
|
||||||
|
#[default]
|
||||||
Desktop,
|
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.
|
||||||
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
#[derive(Debug, Clone, Copy, PartialEq, Eq, Hash)]
|
||||||
|
|||||||
Reference in New Issue
Block a user