always serve embedded frontend
This commit is contained in:
Generated
+1
-1
@@ -709,7 +709,7 @@ dependencies = [
|
|||||||
|
|
||||||
[[package]]
|
[[package]]
|
||||||
name = "ushort"
|
name = "ushort"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
dependencies = [
|
dependencies = [
|
||||||
"ctrlc",
|
"ctrlc",
|
||||||
"include_dir",
|
"include_dir",
|
||||||
|
|||||||
+1
-1
@@ -1,6 +1,6 @@
|
|||||||
[package]
|
[package]
|
||||||
name = "ushort"
|
name = "ushort"
|
||||||
version = "0.1.1"
|
version = "0.1.2"
|
||||||
edition = "2021"
|
edition = "2021"
|
||||||
rust-version = "1.86"
|
rust-version = "1.86"
|
||||||
description = "A compact, self-contained URL shortener"
|
description = "A compact, self-contained URL shortener"
|
||||||
|
|||||||
@@ -16,7 +16,3 @@ max_url_length = 2048
|
|||||||
max_retention_days = 3650
|
max_retention_days = 3650
|
||||||
rate_limit_requests = 60
|
rate_limit_requests = 60
|
||||||
rate_limit_window = 60
|
rate_limit_window = 60
|
||||||
|
|
||||||
# Keep false when the embedded frontend should be served by ushort.
|
|
||||||
# Set true only when a separate web server serves the frontend files.
|
|
||||||
production = false
|
|
||||||
|
|||||||
@@ -1,6 +1,6 @@
|
|||||||
services:
|
services:
|
||||||
ushort:
|
ushort:
|
||||||
image: sodium/ushort:0.1.1
|
image: sodium/ushort:0.1.2
|
||||||
container_name: ushort
|
container_name: ushort
|
||||||
command: ["/app/config.toml"]
|
command: ["/app/config.toml"]
|
||||||
ports:
|
ports:
|
||||||
|
|||||||
+1
-1
@@ -6,7 +6,7 @@ when a query string follows the slash.
|
|||||||
|
|
||||||
## Health and frontend
|
## Health and frontend
|
||||||
|
|
||||||
- `GET /` serves the embedded frontend when `production=false`.
|
- `GET /` serves the embedded frontend.
|
||||||
- `GET /api/health` returns HTTP 200:
|
- `GET /api/health` returns HTTP 200:
|
||||||
|
|
||||||
```json
|
```json
|
||||||
|
|||||||
@@ -24,9 +24,10 @@ ushort legacy-config.json
|
|||||||
| `max_retention_days` | no | `3650` | Maximum per-URL retention value. |
|
| `max_retention_days` | no | `3650` | Maximum per-URL retention value. |
|
||||||
| `rate_limit_requests` | no | `60` | Requests allowed per client/window. |
|
| `rate_limit_requests` | no | `60` | Requests allowed per client/window. |
|
||||||
| `rate_limit_window` | no | `60` | Sliding-window length in seconds. |
|
| `rate_limit_window` | no | `60` | Sliding-window length in seconds. |
|
||||||
| `production` | no | `false` | Disable embedded frontend routes when a separate server provides them. |
|
|
||||||
|
|
||||||
The legacy `short_length` option remains an alias for `min_short_length`.
|
The legacy `short_length` option remains an alias for `min_short_length`.
|
||||||
|
The removed legacy `production` key is ignored when present; ushort always
|
||||||
|
serves its embedded frontend and static assets.
|
||||||
|
|
||||||
## Public URL layouts
|
## Public URL layouts
|
||||||
|
|
||||||
|
|||||||
+4
-5
@@ -19,13 +19,12 @@ the server as `docker-compose.yml`. For repeatable releases, replace its image
|
|||||||
tag with the verified multi-architecture digest:
|
tag with the verified multi-architecture digest:
|
||||||
|
|
||||||
```yaml
|
```yaml
|
||||||
image: docker.io/sodium/ushort:0.1.1@sha256:<verified-index-digest>
|
image: docker.io/sodium/ushort:0.1.2@sha256:<verified-index-digest>
|
||||||
```
|
```
|
||||||
|
|
||||||
Set `db_path = "data/urlshort.db"` and normally keep `production = false` so
|
Set `db_path = "data/urlshort.db"`. The executable always serves its embedded
|
||||||
the executable serves its embedded frontend. Keep `config.toml` owned by the
|
frontend. Keep `config.toml` owned by the container identity (`1001:1001`) with
|
||||||
container identity (`1001:1001`) with mode `0400`; the `data` directory must be
|
mode `0400`; the `data` directory must be writable by the same identity.
|
||||||
writable by the same identity.
|
|
||||||
|
|
||||||
## Start and validate
|
## Start and validate
|
||||||
|
|
||||||
|
|||||||
+4
-3
@@ -8,9 +8,10 @@ actions, API-key administration, sorting, pagination, theme selection, and
|
|||||||
locally hosted fonts. Relative asset URLs and the browser-derived API prefix
|
locally hosted fonts. Relative asset URLs and the browser-derived API prefix
|
||||||
allow the same files to work at either a subdomain root or a nested path.
|
allow the same files to work at either a subdomain root or a nested path.
|
||||||
|
|
||||||
Set `production = false` for the normal self-contained deployment. Setting it
|
The frontend and static assets are always available. The former `production`
|
||||||
to `true` disables the embedded frontend routes for installations that serve
|
toggle was removed because disabling embedded assets conflicts with ushort's
|
||||||
those files separately.
|
self-contained deployment model. Old configuration files containing that key
|
||||||
|
remain loadable; its value is ignored.
|
||||||
|
|
||||||
The nginx examples apply `no-store` to the HTML entry point and `no-cache` to
|
The nginx examples apply `no-store` to the HTML entry point and `no-cache` to
|
||||||
static assets. Embedded static requests do not consume the API rate-limit
|
static assets. Embedded static requests do not consume the API rate-limit
|
||||||
|
|||||||
+28
-40
@@ -30,7 +30,6 @@ struct RawConfig {
|
|||||||
max_retention_days: i64,
|
max_retention_days: i64,
|
||||||
rate_limit_requests: usize,
|
rate_limit_requests: usize,
|
||||||
rate_limit_window: u64,
|
rate_limit_window: u64,
|
||||||
production: bool,
|
|
||||||
}
|
}
|
||||||
|
|
||||||
impl Default for RawConfig {
|
impl Default for RawConfig {
|
||||||
@@ -49,7 +48,6 @@ impl Default for RawConfig {
|
|||||||
max_retention_days: 3650,
|
max_retention_days: 3650,
|
||||||
rate_limit_requests: 60,
|
rate_limit_requests: 60,
|
||||||
rate_limit_window: 60,
|
rate_limit_window: 60,
|
||||||
production: false,
|
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -68,7 +66,6 @@ pub struct Config {
|
|||||||
pub max_retention_days: i64,
|
pub max_retention_days: i64,
|
||||||
pub rate_limit_requests: usize,
|
pub rate_limit_requests: usize,
|
||||||
pub rate_limit_window: u64,
|
pub rate_limit_window: u64,
|
||||||
pub production: bool,
|
|
||||||
pub base_path: String,
|
pub base_path: String,
|
||||||
}
|
}
|
||||||
|
|
||||||
@@ -152,7 +149,6 @@ impl Config {
|
|||||||
max_retention_days: raw.max_retention_days,
|
max_retention_days: raw.max_retention_days,
|
||||||
rate_limit_requests: raw.rate_limit_requests,
|
rate_limit_requests: raw.rate_limit_requests,
|
||||||
rate_limit_window: raw.rate_limit_window,
|
rate_limit_window: raw.rate_limit_window,
|
||||||
production: raw.production,
|
|
||||||
base_path,
|
base_path,
|
||||||
})
|
})
|
||||||
}
|
}
|
||||||
@@ -372,10 +368,7 @@ impl App {
|
|||||||
|
|
||||||
let raw_path = target_path(&request.target);
|
let raw_path = target_path(&request.target);
|
||||||
let bare_base = !self.config.base_path.is_empty() && raw_path == self.config.base_path;
|
let bare_base = !self.config.base_path.is_empty() && raw_path == self.config.base_path;
|
||||||
if matches!(request.method.as_str(), "GET" | "HEAD")
|
if matches!(request.method.as_str(), "GET" | "HEAD") && !bare_base {
|
||||||
&& !self.config.production
|
|
||||||
&& !bare_base
|
|
||||||
{
|
|
||||||
if let Some(path) = local_path(raw_path, &self.config.base_path) {
|
if let Some(path) = local_path(raw_path, &self.config.base_path) {
|
||||||
match path.as_str() {
|
match path.as_str() {
|
||||||
"/" | "/static" => return self.serve_static("index.html"),
|
"/" | "/static" => return self.serve_static("index.html"),
|
||||||
@@ -432,32 +425,14 @@ impl App {
|
|||||||
|
|
||||||
fn handle_get(&self, path: &str, request: &RequestData) -> ResponseData {
|
fn handle_get(&self, path: &str, request: &RequestData) -> ResponseData {
|
||||||
match path {
|
match path {
|
||||||
"/" => {
|
"/" => self.serve_static("index.html"),
|
||||||
if self.config.production {
|
|
||||||
health_response()
|
|
||||||
} else {
|
|
||||||
self.serve_static("index.html")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
"/api/health" => health_response(),
|
"/api/health" => health_response(),
|
||||||
"/api/shorten" => self.handle_shorten(request),
|
"/api/shorten" => self.handle_shorten(request),
|
||||||
"/api/urls" => self.handle_list_urls(request),
|
"/api/urls" => self.handle_list_urls(request),
|
||||||
"/api/lookup" => self.handle_lookup(request),
|
"/api/lookup" => self.handle_lookup(request),
|
||||||
"/static" => {
|
"/static" => self.serve_static("index.html"),
|
||||||
if self.config.production {
|
|
||||||
ResponseData::empty(404)
|
|
||||||
} else {
|
|
||||||
self.serve_static("index.html")
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ if path.starts_with("/api/urls/") => self.handle_get_url(&path["/api/urls/".len()..]),
|
_ if path.starts_with("/api/urls/") => self.handle_get_url(&path["/api/urls/".len()..]),
|
||||||
_ if path.starts_with("/static/") => {
|
_ if path.starts_with("/static/") => self.serve_static(&path["/static/".len()..]),
|
||||||
if self.config.production {
|
|
||||||
ResponseData::empty(404)
|
|
||||||
} else {
|
|
||||||
self.serve_static(&path["/static/".len()..])
|
|
||||||
}
|
|
||||||
}
|
|
||||||
_ => self.handle_redirect(path.trim_start_matches('/')),
|
_ => self.handle_redirect(path.trim_start_matches('/')),
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
@@ -1477,19 +1452,32 @@ mod tests {
|
|||||||
}
|
}
|
||||||
|
|
||||||
#[test]
|
#[test]
|
||||||
fn production_flag_keeps_legacy_backend_static_behavior() {
|
fn deprecated_production_key_is_ignored_and_frontend_remains_enabled() {
|
||||||
let path = temp_path("production");
|
let path = temp_path("deprecated-production");
|
||||||
let mut cfg = config(path.clone());
|
let config_path = std::env::temp_dir().join(format!(
|
||||||
cfg.production = true;
|
"ushort-deprecated-production-{}-{}.toml",
|
||||||
let app = App::new(cfg).unwrap();
|
std::process::id(),
|
||||||
|
rand::random::<u64>()
|
||||||
|
));
|
||||||
|
fs::write(
|
||||||
|
&config_path,
|
||||||
|
format!(
|
||||||
|
"base_url = \"https://example.test/s\"\n\
|
||||||
|
api_key = \"secret\"\n\
|
||||||
|
db_path = \"{}\"\n\
|
||||||
|
production = true\n",
|
||||||
|
path.display()
|
||||||
|
),
|
||||||
|
)
|
||||||
|
.unwrap();
|
||||||
|
let app = App::new(Config::load(&config_path).unwrap()).unwrap();
|
||||||
let root = app.handle(RequestData::new("GET", "/s/"));
|
let root = app.handle(RequestData::new("GET", "/s/"));
|
||||||
assert_eq!(
|
assert_eq!(root.status, 200);
|
||||||
root.body,
|
assert_eq!(root.body, include_bytes!("../static/index.html"));
|
||||||
br#"{"status": "ok", "service": "url-shortener"}"#
|
|
||||||
);
|
|
||||||
let asset = app.handle(RequestData::new("GET", "/s/static/app.js"));
|
let asset = app.handle(RequestData::new("GET", "/s/static/app.js"));
|
||||||
assert_eq!(asset.status, 404);
|
assert_eq!(asset.status, 200);
|
||||||
assert!(asset.body.is_empty());
|
assert_eq!(asset.body, include_bytes!("../static/app.js"));
|
||||||
|
let _ = fs::remove_file(config_path);
|
||||||
let _ = fs::remove_file(path);
|
let _ = fs::remove_file(path);
|
||||||
}
|
}
|
||||||
|
|
||||||
|
|||||||
@@ -49,8 +49,6 @@ fn main() {
|
|||||||
"Rate limit : {} req/{}s per IP",
|
"Rate limit : {} req/{}s per IP",
|
||||||
app.config.rate_limit_requests, app.config.rate_limit_window
|
app.config.rate_limit_requests, app.config.rate_limit_window
|
||||||
);
|
);
|
||||||
eprintln!("Production : {}", app.config.production);
|
|
||||||
|
|
||||||
for mut request in server.incoming_requests() {
|
for mut request in server.incoming_requests() {
|
||||||
let maximum_body = app.max_request_body_bytes();
|
let maximum_body = app.max_request_body_bytes();
|
||||||
let declared_too_large = request
|
let declared_too_large = request
|
||||||
|
|||||||
Reference in New Issue
Block a user