use super::Result;
use std::ascii::escape_default;
use std::collections::BTreeMap;
use std::fmt::{self, Display, Write};
use std::fs::{read_dir, File};
use std::io::Read;
use std::path::{Path, PathBuf};
pub struct StaticFiles {
src: String,
src_path: PathBuf,
base_path: PathBuf,
names: BTreeMap<String, String>,
names_r: BTreeMap<String, String>,
}
impl StaticFiles {
pub(crate) fn for_template_dir(
outdir: &Path,
base_path: &Path,
) -> Result<Self> {
let mut src = String::with_capacity(512);
if cfg!(feature = "mime03") {
src.write_str("use mime::Mime;\n\n")?;
}
if cfg!(feature = "tide013") {
src.write_str("use tide::http::mime::{self, Mime};\n\n")?;
} else if cfg!(feature = "http-types") {
src.write_str("use http_types::mime::{self, Mime};\n\n")?;
}
src.write_str(
"/// A static file has a name (so its url can be recognized) and the
/// actual file contents.
///
/// The name includes a short (48 bits as 8 base64 characters) hash of
/// the content, to enable long-time caching of static resourses in
/// the clients.
#[allow(dead_code)]
pub struct StaticFile {
pub content: &'static [u8],
pub name: &'static str,
")?;
if cfg!(feature = "mime03") {
src.write_str(" pub mime: &'static Mime,\n")?;
}
if cfg!(feature = "http-types") {
src.write_str(" pub mime: &'static Mime,\n")?;
}
src.write_str(
"}
#[allow(dead_code)]
impl StaticFile {
/// Get a single `StaticFile` by name, if it exists.
#[must_use]
pub fn get(name: &str) -> Option<&'static Self> {
if let Ok(pos) = STATICS.binary_search_by_key(&name, |s| s.name) {
Some(STATICS[pos])
} else {None}
}
}
",
)?;
Ok(StaticFiles {
src,
src_path: outdir.join("statics.rs"),
base_path: base_path.into(),
names: BTreeMap::new(),
names_r: BTreeMap::new(),
})
}
fn path_for(&self, path: impl AsRef<Path>) -> PathBuf {
let path = path.as_ref();
if path.is_relative() {
self.base_path.join(path)
} else {
path.into()
}
}
pub fn add_files(
&mut self,
indir: impl AsRef<Path>,
) -> Result<&mut Self> {
let indir = self.path_for(indir);
println!("cargo:rerun-if-changed={}", indir.display());
for entry in read_dir(indir)? {
let entry = entry?;
if entry.file_type()?.is_file() {
self.add_file(entry.path())?;
}
}
Ok(self)
}
pub fn add_files_as(
&mut self,
indir: impl AsRef<Path>,
to: &str,
) -> Result<&mut Self> {
for entry in read_dir(self.path_for(indir))? {
let entry = entry?;
let file_type = entry.file_type()?;
let to = if to.is_empty() {
entry.file_name().to_string_lossy().to_string()
} else {
format!("{}/{}", to, entry.file_name().to_string_lossy())
};
if file_type.is_file() {
self.add_file_as(entry.path(), &to)?;
} else if file_type.is_dir() {
self.add_files_as(entry.path(), &to)?;
}
}
Ok(self)
}
pub fn add_file(&mut self, path: impl AsRef<Path>) -> Result<&mut Self> {
let path = self.path_for(path);
if let Some((name, ext)) = name_and_ext(&path) {
println!("cargo:rerun-if-changed={}", path.display());
let mut input = File::open(&path)?;
let mut buf = Vec::new();
input.read_to_end(&mut buf)?;
let rust_name = format!("{name}_{ext}");
let url_name = format!("{name}-{}.{ext}", checksum_slug(&buf));
self.add_static(
&path,
&rust_name,
&url_name,
&FileContent(&path),
ext,
)?;
}
Ok(self)
}
pub fn add_file_as(
&mut self,
path: impl AsRef<Path>,
url_name: &str,
) -> Result<&mut Self> {
let path = &self.path_for(path);
let ext = name_and_ext(path).map_or("", |(_, e)| e);
println!("cargo:rerun-if-changed={}", path.display());
self.add_static(path, url_name, url_name, &FileContent(path), ext)?;
Ok(self)
}
pub fn add_file_data<P>(
&mut self,
path: P,
data: &[u8],
) -> Result<&mut Self>
where
P: AsRef<Path>,
{
let path = &self.path_for(path);
if let Some((name, ext)) = name_and_ext(path) {
let rust_name = format!("{name}_{ext}");
let url_name = format!("{name}-{}.{ext}", checksum_slug(data));
self.add_static(
path,
&rust_name,
&url_name,
&ByteString(data),
ext,
)?;
}
Ok(self)
}
#[cfg(feature = "sass")]
pub fn add_sass_file<P>(&mut self, src: P) -> Result<&mut Self>
where
P: AsRef<Path>,
{
use rsass::css::CssString;
use rsass::input::CargoContext;
use rsass::output::{Format, Style};
use rsass::sass::{CallError, FormalArgs};
use rsass::value::Quotes;
use rsass::*;
use std::sync::Arc;
let format = Format {
style: Style::Compressed,
precision: 4,
};
let src = self.path_for(src);
let (context, scss) =
CargoContext::for_path(&src).map_err(rsass::Error::from)?;
let mut context = context.with_format(format);
let existing_statics = self.get_names().clone();
context.get_scope().define_function(
"static_name".into(),
sass::Function::builtin(
"",
&"static_name".into(),
FormalArgs::new(vec![("name".into(), None)]),
Arc::new(move |s| {
let name: String = s.get("name".into())?;
let rname = name.replace(['-', '.'], "_");
existing_statics
.iter()
.find(|(n, _v)| *n == &rname)
.map(|(_n, v)| {
CssString::new(v.into(), Quotes::Double).into()
})
.ok_or_else(|| {
CallError::msg(format!(
"Static file {name:?} not found",
))
})
}),
),
);
let css = context.transform(scss)?;
self.add_file_data(src.with_extension("css"), &css)
}
fn add_static(
&mut self,
path: &Path,
rust_name: &str,
url_name: &str,
content: &impl Display,
suffix: &str,
) -> Result<&mut Self> {
let mut rust_name =
rust_name.replace(|c: char| !c.is_alphanumeric(), "_");
if rust_name
.as_bytes()
.first()
.map(|c| c.is_ascii_digit())
.unwrap_or(true)
{
rust_name.insert(0, 'n');
}
writeln!(
self.src,
"\n/// From {path:?}\
\n#[allow(non_upper_case_globals)]\
\npub static {rust_name}: StaticFile = StaticFile {{\
\n content: {content},\
\n name: \"{url_name}\",\
\n{mime}\
}};",
path = path,
rust_name = rust_name,
url_name = url_name,
content = content,
mime = mime_arg(suffix),
)?;
self.names.insert(rust_name.clone(), url_name.into());
self.names_r.insert(url_name.into(), rust_name);
Ok(self)
}
pub fn get_names(&self) -> &BTreeMap<String, String> {
&self.names
}
}
impl Drop for StaticFiles {
fn drop(&mut self) {
fn do_write(s: &mut StaticFiles) -> Result<()> {
write!(s.src, "\npub static STATICS: &[&StaticFile] = &[")?;
let mut q = s.names_r.values();
if let Some(a) = q.next() {
write!(s.src, "&{a}")?;
}
for a in q {
write!(s.src, ", &{a}")?;
}
writeln!(s.src, "];")?;
super::write_if_changed(&s.src_path, &s.src)?;
Ok(())
}
let _ = do_write(self);
}
}
struct FileContent<'a>(&'a Path);
impl Display for FileContent<'_> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
write!(out, "include_bytes!({:?})", self.0)
}
}
struct ByteString<'a>(&'a [u8]);
impl Display for ByteString<'_> {
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
out.write_str("b\"")?;
for byte in self.0 {
escape_default(*byte).fmt(out)?;
}
out.write_str("\"")
}
}
fn name_and_ext(path: &Path) -> Option<(&str, &str)> {
if let (Some(name), Some(ext)) = (path.file_name(), path.extension()) {
if let (Some(name), Some(ext)) = (name.to_str(), ext.to_str()) {
return Some((&name[..name.len() - ext.len() - 1], ext));
}
}
None
}
#[cfg(all(feature = "mime03", feature = "http-types"))]
compile_error!(
r#"Only one of the features "http-types" or "mime03" can be enabled at a time."#
);
fn checksum_slug(data: &[u8]) -> String {
use base64::prelude::{Engine, BASE64_URL_SAFE_NO_PAD};
BASE64_URL_SAFE_NO_PAD.encode(&md5::compute(data)[..6])
}
fn mime_arg(#[allow(unused)] suffix: &str) -> String {
#[cfg(not(any(feature = "mime03", feature = "http-types")))]
let result = String::new();
#[cfg(any(feature = "mime03", feature = "http-types"))]
let result = format!(" mime: &mime::{},\n", mime_from_suffix(suffix));
result
}
#[cfg(feature = "mime03")]
fn mime_from_suffix(suffix: &str) -> &'static str {
match suffix.to_lowercase().as_ref() {
"bmp" => "IMAGE_BMP",
"css" => "TEXT_CSS",
"gif" => "IMAGE_GIF",
"jpg" | "jpeg" => "IMAGE_JPEG",
"js" | "jsonp" => "TEXT_JAVASCRIPT",
"json" => "APPLICATION_JSON",
"png" => "IMAGE_PNG",
"svg" => "IMAGE_SVG",
"woff" => "FONT_WOFF",
"woff2" => "FONT_WOFF",
_ => "APPLICATION_OCTET_STREAM",
}
}
#[cfg(feature = "http-types")]
fn mime_from_suffix(suffix: &str) -> &'static str {
match suffix.to_lowercase().as_ref() {
"css" => "CSS",
"html" | "htm" => "CSS",
"ico" => "ICO",
"jpg" | "jpeg" => "JPEG",
"js" | "jsonp" => "JAVASCRIPT",
"json" => "JSON",
"png" => "PNG",
"svg" => "SVG",
"txt" => "PLAIN",
"wasm" => "WASM",
"xml" => "XML",
_ => "mime::BYTE_STREAM",
}
}