bundle UI files into the binary

This is optional but now enabled for release builds.

Why?

* It shrinks the release docker images a bit, as the binary
  includes only the gzipped version of files and uncompressed into RAM
  at startup (which should be fast).

* It's a step toward #160.
This commit is contained in:
Scott Lamb
2023-08-04 12:52:05 -05:00
parent 02ac1a5570
commit faba358925
13 changed files with 534 additions and 61 deletions

View File

@@ -17,6 +17,7 @@ use self::path::Path;
use crate::body::Body;
use crate::json;
use crate::mp4;
use crate::web::static_file::Ui;
use base::err;
use base::Error;
use base::ResultExt;
@@ -28,7 +29,6 @@ use db::{auth, recording};
use fnv::FnvHashMap;
use http::header::{self, HeaderValue};
use http::{status::StatusCode, Request, Response};
use http_serve::dir::FsDir;
use hyper::body::Bytes;
use std::net::IpAddr;
use std::sync::Arc;
@@ -162,7 +162,7 @@ fn require_csrf_if_session(caller: &Caller, csrf: Option<&str>) -> Result<(), ba
pub struct Config<'a> {
pub db: Arc<db::Database>,
pub ui_dir: Option<&'a std::path::Path>,
pub ui_dir: Option<&'a crate::cmds::run::config::UiDir>,
pub trust_forward_hdrs: bool,
pub time_zone_name: String,
pub allow_unauthenticated_permissions: Option<db::Permissions>,
@@ -171,7 +171,7 @@ pub struct Config<'a> {
pub struct Service {
db: Arc<db::Database>,
ui_dir: Option<Arc<FsDir>>,
ui: Ui,
dirs_by_stream_id: Arc<FnvHashMap<i32, Arc<SampleFileDir>>>,
time_zone_name: String,
allow_unauthenticated_permissions: Option<db::Permissions>,
@@ -195,19 +195,7 @@ enum CacheControl {
impl Service {
pub fn new(config: Config) -> Result<Self, Error> {
let mut ui_dir = None;
if let Some(d) = config.ui_dir {
match FsDir::builder().for_path(d) {
Err(e) => {
warn!(
"Unable to load ui dir {}; will serve no static files: {}",
d.display(),
e
);
}
Ok(d) => ui_dir = Some(d),
};
}
let ui_dir = config.ui_dir.map(Ui::from).unwrap_or(Ui::None);
let dirs_by_stream_id = {
let l = config.db.lock();
let mut d =
@@ -225,7 +213,7 @@ impl Service {
Ok(Service {
db: config.db,
dirs_by_stream_id,
ui_dir,
ui: ui_dir,
allow_unauthenticated_permissions: config.allow_unauthenticated_permissions,
trust_forward_hdrs: config.trust_forward_hdrs,
time_zone_name: config.time_zone_name,

View File

@@ -4,45 +4,104 @@
//! Static file serving.
use base::{bail, err, Error, ErrorKind, ResultExt};
use std::sync::Arc;
use base::{bail, err, ErrorKind, ResultExt};
use http::{header, HeaderValue, Request};
use http_serve::dir::FsDir;
use tracing::warn;
use crate::cmds::run::config::UiDir;
use super::{ResponseResult, Service};
pub enum Ui {
None,
FromFilesystem(Arc<FsDir>),
#[cfg(feature = "bundled-ui")]
Bundled(&'static crate::bundled_ui::Ui),
}
impl Ui {
pub fn from(cfg: &UiDir) -> Self {
match cfg {
UiDir::FromFilesystem(d) => match FsDir::builder().for_path(d) {
Err(err) => {
warn!(
%err,
"unable to load ui dir {}; will serve no static files",
d.display(),
);
Self::None
}
Ok(d) => Self::FromFilesystem(d),
},
#[cfg(feature = "bundled-ui")]
UiDir::Bundled(_) => Self::Bundled(crate::bundled_ui::Ui::get()),
#[cfg(not(feature = "bundled-ui"))]
UiDir::Bundled(_) => {
warn!("server compiled without bundled ui; will serve not static files");
Self::None
}
}
}
async fn serve(
&self,
path: &str,
req: &Request<hyper::Body>,
cache_control: &'static str,
content_type: &'static str,
) -> ResponseResult {
match self {
Ui::None => bail!(
NotFound,
msg("ui not configured or missing; no static files available")
),
Ui::FromFilesystem(d) => {
let node = d.clone().get(path, req.headers()).await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
err!(NotFound, msg("static file not found"))
} else {
err!(Internal, source(e))
}
})?;
let mut hdrs = http::HeaderMap::new();
node.add_encoding_headers(&mut hdrs);
hdrs.insert(
header::CACHE_CONTROL,
HeaderValue::from_static(cache_control),
);
hdrs.insert(header::CONTENT_TYPE, HeaderValue::from_static(content_type));
let e = node.into_file_entity(hdrs).err_kind(ErrorKind::Internal)?;
Ok(http_serve::serve(e, &req))
}
#[cfg(feature = "bundled-ui")]
Ui::Bundled(ui) => {
let Some(e) = ui.lookup(path, req.headers(), cache_control, content_type) else {
bail!(NotFound, msg("static file not found"));
};
Ok(http_serve::serve(e, &req))
}
}
}
}
impl Service {
/// Serves a static file if possible.
pub(super) async fn static_file(&self, req: Request<hyper::Body>) -> ResponseResult {
let Some(dir) = self.ui_dir.clone() else {
bail!(NotFound, msg("ui dir not configured or missing; no static files available"))
};
let Some(static_req) = StaticFileRequest::parse(req.uri().path()) else {
bail!(NotFound, msg("static file not found"));
};
let f = dir.get(static_req.path, req.headers());
let node = f.await.map_err(|e| {
if e.kind() == std::io::ErrorKind::NotFound {
err!(NotFound, msg("no such static file"))
} else {
Error::wrap(ErrorKind::Internal, e)
}
})?;
let mut hdrs = http::HeaderMap::new();
node.add_encoding_headers(&mut hdrs);
hdrs.insert(
header::CACHE_CONTROL,
HeaderValue::from_static(if static_req.immutable {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Caching_static_assets
"public, max-age=604800, immutable"
} else {
"public"
}),
);
hdrs.insert(
header::CONTENT_TYPE,
HeaderValue::from_static(static_req.mime),
);
let e = node.into_file_entity(hdrs).err_kind(ErrorKind::Internal)?;
Ok(http_serve::serve(e, &req))
let cache_control = if static_req.immutable {
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Headers/Cache-Control#Caching_static_assets
"public, max-age=604800, immutable"
} else {
"public"
};
self.ui
.serve(static_req.path, &req, cache_control, static_req.mime)
.await
}
}