709 lines
23 KiB
Rust
709 lines
23 KiB
Rust
|
use super::Result;
|
||
|
use itertools::Itertools;
|
||
|
use std::ascii::escape_default;
|
||
|
use std::collections::BTreeMap;
|
||
|
use std::fmt::{self, Display};
|
||
|
use std::fs::{read_dir, File};
|
||
|
use std::io::{Read, Write};
|
||
|
use std::path::{Path, PathBuf};
|
||
|
|
||
|
/// Handler for static files.
|
||
|
///
|
||
|
/// Apart from handling templates for dynamic content, ructe also
|
||
|
/// helps with constants for static content.
|
||
|
///
|
||
|
/// Most sites that need HTML templates also needs some static resources.
|
||
|
/// Maybe one or several CSS files, some javascript, and / or pictures.
|
||
|
/// A good way to reduce network round-trips is to use a far expires
|
||
|
/// header to tell the browser it can cache those files and don't need
|
||
|
/// to check if they have changed.
|
||
|
/// But what if the files do change?
|
||
|
/// Then pretty much the only way to make sure the browser gets the
|
||
|
/// updated file is to change the URL to the file as well.
|
||
|
///
|
||
|
/// Ructe can create content-dependent file names for static files.
|
||
|
/// If you have an `image.png`, ructe may call it `image-SomeHash.png`
|
||
|
/// where `SomeHash` is 8 url-safe base64 characters encoding 48 bits
|
||
|
/// of a md5 sum of the file.
|
||
|
///
|
||
|
/// Each static file will be available as a
|
||
|
/// [`StaticFile`](templates/statics/index.html) struct instance in
|
||
|
/// your `templates::statics` module.
|
||
|
/// Also, the const `STATICS` array in the same module will contain a
|
||
|
/// reference to each of those instances.
|
||
|
///
|
||
|
/// Actually serving the file is a job for a web framework like
|
||
|
/// [iron](https://github.com/iron/iron),
|
||
|
/// [nickel](https://github.com/nickel-org/nickel.rs) or
|
||
|
/// [rocket](https://rocket.rs/), but ructe helps by packing the file
|
||
|
/// contents into a constant struct that you can access from rust
|
||
|
/// code.
|
||
|
///
|
||
|
/// # Overview
|
||
|
///
|
||
|
/// This section describes how to set up your project to serve
|
||
|
/// static content using ructe.
|
||
|
///
|
||
|
/// To do this, the first step is to add a line in `build.rs` telling
|
||
|
/// ructe to find and transpile your static files:
|
||
|
///
|
||
|
/// ```no_run
|
||
|
/// # use ructe::{Ructe, RucteError};
|
||
|
/// # fn main() -> Result<(), RucteError> {
|
||
|
/// let mut ructe = Ructe::from_env()?;
|
||
|
/// ructe.statics()?.add_files("static")?;
|
||
|
/// # Ok(())
|
||
|
/// # }
|
||
|
/// ```
|
||
|
///
|
||
|
/// Then you need to link to the encoded file.
|
||
|
/// For an image, you probably want to link it from an `<img>` tag in
|
||
|
/// a template. That can be done like this:
|
||
|
///
|
||
|
/// ```html
|
||
|
/// @use super::statics::image_png;
|
||
|
/// @()
|
||
|
/// <img alt="Something" src="/static/@image_png.name">
|
||
|
/// ```
|
||
|
///
|
||
|
/// So, what has happened here?
|
||
|
/// First, assuming the `static` directory in your
|
||
|
/// `$CARGO_MANIFEST_DIR` contained a file name `image.png`, your
|
||
|
/// `templates::statics` module (which is reachable as `super::statics`
|
||
|
/// from inside a template) will contain a
|
||
|
/// `pub static image_png: StaticFile` which can be imported and used
|
||
|
/// in both templates and rust code.
|
||
|
/// A `StaticFile` has a field named `name` which is a `&'static str`
|
||
|
/// containing the name with the generated hash, `image-SomeHash.png`.
|
||
|
///
|
||
|
/// The next step is that a browser actually sends a request for
|
||
|
/// `/static/image-SomeHash.png` and your server needs to deliver it.
|
||
|
/// Here, things depend on your web framework, so we start with some
|
||
|
/// pseudo code.
|
||
|
/// Full examples for [warp], [gotham], [nickel], and [iron] is
|
||
|
/// available [in the ructe repository].
|
||
|
///
|
||
|
/// [warp]: https://crates.rs/crates/warp
|
||
|
/// [gotham]: https://crates.rs/crates/gotham
|
||
|
/// [nickel]: https://crates.rs/crates/nickel
|
||
|
/// [iron]: https://crates.rs/crates/iron
|
||
|
/// [in the ructe repository]: https://github.com/kaj/ructe/tree/master/examples
|
||
|
///
|
||
|
/// ```ignore
|
||
|
/// /// A hypothetical web framework calls this each /static/... request,
|
||
|
/// /// with the name component of the URL as the name argument.
|
||
|
/// fn serve_static(name: &str) -> Response {
|
||
|
/// if let Some(data) = StaticFile::get(name) {
|
||
|
/// Response::Ok(data.content)
|
||
|
/// } else {
|
||
|
/// Response::NotFound
|
||
|
/// }
|
||
|
/// }
|
||
|
/// ```
|
||
|
///
|
||
|
/// The `StaticFile::get` function returns the `&'static StaticFile`
|
||
|
/// for a given file name if the file exists.
|
||
|
/// This is a reference to the same struct that we used by the name
|
||
|
/// `image_png` in the template.
|
||
|
/// Besides the `name` field (which will be equal to the argument, or
|
||
|
/// `get` would not have returned this `StaticFile`), there is a
|
||
|
/// `content: &'static [u8]` field which contains the actual file
|
||
|
/// data.
|
||
|
///
|
||
|
/// # Content-types
|
||
|
///
|
||
|
/// How to get the content type of static files.
|
||
|
///
|
||
|
/// Ructe has support for making the content-type of each static
|
||
|
/// file availiable using the
|
||
|
/// [mime](https://crates.io/crates/mime) crate.
|
||
|
/// Since mime version 0.3.0 was a breaking change of how the
|
||
|
/// `mime::Mime` type was implemented, and both Nickel and Iron
|
||
|
/// currently require the old version (0.2.x), ructe provides
|
||
|
/// support for both mime 0.2.x and mime 0.3.x with separate
|
||
|
/// feature flags.
|
||
|
///
|
||
|
/// # Mime 0.2.x
|
||
|
///
|
||
|
/// To use the mime 0.2.x support, enable the `mime02` feature and
|
||
|
/// add mime 0.2.x as a dependency:
|
||
|
///
|
||
|
/// ```toml
|
||
|
/// [build-dependencies]
|
||
|
/// ructe = { version = "^0.3.2", features = ["mime02"] }
|
||
|
///
|
||
|
/// [dependencies]
|
||
|
/// mime = "~0.2"
|
||
|
/// ```
|
||
|
///
|
||
|
/// A `Mime` as implemented in `mime` version 0.2.x cannot be
|
||
|
/// created statically, so instead a `StaticFile` provides
|
||
|
/// `pub fn mime(&self) -> Mime`.
|
||
|
///
|
||
|
/// ```
|
||
|
/// # // Test and doc even without the feature, so mock functionality.
|
||
|
/// # pub mod templates { pub mod statics {
|
||
|
/// # pub struct FakeFile;
|
||
|
/// # impl FakeFile { pub fn mime(&self) -> &'static str { "image/png" } }
|
||
|
/// # pub static image_png: FakeFile = FakeFile;
|
||
|
/// # }}
|
||
|
/// use templates::statics::image_png;
|
||
|
///
|
||
|
/// # fn main() {
|
||
|
/// assert_eq!(format!("Type is {}", image_png.mime()),
|
||
|
/// "Type is image/png");
|
||
|
/// # }
|
||
|
/// ```
|
||
|
///
|
||
|
/// # Mime 0.3.x
|
||
|
///
|
||
|
/// To use the mime 0.3.x support, enable the `mime3` feature and
|
||
|
/// add mime 0.3.x as a dependency:
|
||
|
///
|
||
|
/// ```toml
|
||
|
/// [build-dependencies]
|
||
|
/// ructe = { version = "^0.3.2", features = ["mime03"] }
|
||
|
///
|
||
|
/// [dependencies]
|
||
|
/// mime = "~0.3"
|
||
|
/// ```
|
||
|
///
|
||
|
/// From version 0.3, the `mime` crates supports creating const
|
||
|
/// static `Mime` objects, so with this feature, a `StaticFile`
|
||
|
/// simply has a `pub mime: &'static Mime` field.
|
||
|
///
|
||
|
/// ```
|
||
|
/// # // Test and doc even without the feature, so mock functionality.
|
||
|
/// # pub mod templates { pub mod statics {
|
||
|
/// # pub struct FakeFile { pub mime: &'static str }
|
||
|
/// # pub static image_png: FakeFile = FakeFile { mime: "image/png", };
|
||
|
/// # }}
|
||
|
/// use templates::statics::image_png;
|
||
|
///
|
||
|
/// # fn main() {
|
||
|
/// assert_eq!(format!("Type is {}", image_png.mime),
|
||
|
/// "Type is image/png");
|
||
|
/// # }
|
||
|
/// ```
|
||
|
pub struct StaticFiles {
|
||
|
/// Rust source file `statics.rs` beeing written.
|
||
|
src: Vec<u8>,
|
||
|
/// Path for writing the file `statics.rs`.
|
||
|
src_path: PathBuf,
|
||
|
/// Base path for finding static files with relative paths
|
||
|
base_path: PathBuf,
|
||
|
/// Maps rust names to public names (foo_jpg -> foo-abc123.jpg)
|
||
|
names: BTreeMap<String, String>,
|
||
|
/// Maps public names to rust names (foo-abc123.jpg -> foo_jpg)
|
||
|
names_r: BTreeMap<String, String>,
|
||
|
}
|
||
|
|
||
|
impl StaticFiles {
|
||
|
pub(crate) fn for_template_dir(
|
||
|
outdir: &Path,
|
||
|
base_path: &Path,
|
||
|
) -> Result<Self> {
|
||
|
let mut src = Vec::with_capacity(512);
|
||
|
if cfg!(feature = "mime03") {
|
||
|
src.write_all(b"use mime::Mime;\n\n")?;
|
||
|
}
|
||
|
if cfg!(feature = "tide013") {
|
||
|
src.write_all(b"use tide::http::mime::{self, Mime};\n\n")?;
|
||
|
} else if cfg!(feature = "http-types") {
|
||
|
src.write_all(b"use http_types::mime::{self, Mime};\n\n")?;
|
||
|
}
|
||
|
src.write_all(
|
||
|
b"/// 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 = "mime02") {
|
||
|
src.write_all(b" _mime: &'static str,\n")?;
|
||
|
}
|
||
|
if cfg!(feature = "mime03") {
|
||
|
src.write_all(b" pub mime: &'static Mime,\n")?;
|
||
|
}
|
||
|
if cfg!(feature = "http-types") {
|
||
|
src.write_all(b" pub mime: &'static Mime,\n")?;
|
||
|
}
|
||
|
src.write_all(
|
||
|
b"}
|
||
|
#[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}
|
||
|
}
|
||
|
}
|
||
|
",
|
||
|
)?;
|
||
|
if cfg!(feature = "mime02") {
|
||
|
src.write_all(
|
||
|
b"use mime::Mime;
|
||
|
impl StaticFile {
|
||
|
/// Get the mime type of this static file.
|
||
|
///
|
||
|
/// Currently, this method parses a (static) string every time.
|
||
|
/// A future release of `mime` may support statically created
|
||
|
/// `Mime` structs, which will make this nicer.
|
||
|
#[allow(unused)]
|
||
|
pub fn mime(&self) -> Mime {
|
||
|
self._mime.parse().unwrap()
|
||
|
}
|
||
|
}
|
||
|
",
|
||
|
)?;
|
||
|
}
|
||
|
Ok(StaticFiles {
|
||
|
src,
|
||
|
src_path: outdir.join("statics.rs"),
|
||
|
base_path: base_path.into(),
|
||
|
names: BTreeMap::new(),
|
||
|
names_r: BTreeMap::new(),
|
||
|
})
|
||
|
}
|
||
|
|
||
|
// Should the return type be some kind of cow path?
|
||
|
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()
|
||
|
}
|
||
|
}
|
||
|
|
||
|
/// Add all files from a specific directory, `indir`, as static files.
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
/// Add all files from a specific directory, `indir`, as static files.
|
||
|
///
|
||
|
/// The `to` string is used as a directory path of the resulting
|
||
|
/// urls, the file names are taken as is, without adding any hash.
|
||
|
/// This is usefull for resources used by preexisting javascript
|
||
|
/// packages, where it might be hard to change the used urls.
|
||
|
///
|
||
|
/// Note that some way of changing the url when the content
|
||
|
/// changes is still needed if you serve the files with far
|
||
|
/// expire, and using this method makes that your responsibility
|
||
|
/// rather than ructes.
|
||
|
/// Either the file may have hashed names as is, or you may use
|
||
|
/// the version number of a 3:rd party package as part of the `to`
|
||
|
/// parameter.
|
||
|
///
|
||
|
/// The `to` parameter may be an empty string.
|
||
|
/// In that case, no extra slash is added.
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
/// Add one specific file as a static file.
|
||
|
///
|
||
|
/// Create a name to use in the url like `name-hash.ext` where
|
||
|
/// name and ext are the name and extension from `path` and has is
|
||
|
/// a few url-friendly bytes from a hash of the file content.
|
||
|
///
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
/// Add one specific file as a static file.
|
||
|
///
|
||
|
/// Use `url_name` in the url without adding any hash characters.
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
/// Add a resource by its name and content, without reading an actual file.
|
||
|
///
|
||
|
/// The `path` parameter is used only to create a file name, the actual
|
||
|
/// content of the static file will be the `data` parameter.
|
||
|
/// A hash will be added to the file name, just as for
|
||
|
/// file-sourced statics.
|
||
|
///
|
||
|
/// # Examples
|
||
|
///
|
||
|
/// With the folloing code in `build.rs`:
|
||
|
/// ````
|
||
|
/// # use ructe::{Result, Ructe, StaticFiles};
|
||
|
/// # use std::fs::create_dir_all;
|
||
|
/// # use std::path::PathBuf;
|
||
|
/// # use std::vec::Vec;
|
||
|
/// # fn main() -> Result<()> {
|
||
|
/// # let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("test-tmp").join("add-file");
|
||
|
/// # create_dir_all(&p);
|
||
|
/// # let mut ructe = Ructe::new(p)?;
|
||
|
/// let mut statics = ructe.statics()?;
|
||
|
/// statics.add_file_data("black.css", b"body{color:black}\n");
|
||
|
/// # Ok(())
|
||
|
/// # }
|
||
|
/// ````
|
||
|
///
|
||
|
/// A `StaticFile` named `black_css` will be defined in the
|
||
|
/// `templates::statics` module of your crate:
|
||
|
///
|
||
|
/// ````
|
||
|
/// # mod statics {
|
||
|
/// # use ructe::templates::StaticFile;
|
||
|
/// # pub static black_css: StaticFile = StaticFile {
|
||
|
/// # content: b"body{color:black}\n",
|
||
|
/// # name: "black-r3rltVhW.css",
|
||
|
/// # #[cfg(feature = "mime03")]
|
||
|
/// # mime: &mime::TEXT_CSS,
|
||
|
/// # };
|
||
|
/// # }
|
||
|
/// assert_eq!(statics::black_css.name, "black-r3rltVhW.css");
|
||
|
/// ````
|
||
|
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)
|
||
|
}
|
||
|
|
||
|
/// Compile a sass file and add the resulting css.
|
||
|
///
|
||
|
/// If `src` is `"somefile.sass"`, then that file will be copiled
|
||
|
/// with [rsass] (using the `Comressed` output style).
|
||
|
/// The result will be added as if if was an existing
|
||
|
/// `"somefile.css"` file.
|
||
|
///
|
||
|
/// While handling the scss input, rsass is extended with a
|
||
|
/// `static_name` sass function that takes a file name as given to
|
||
|
/// [`add_file()`][Self::add_file] (or simliar) and returns the
|
||
|
/// `name-hash.ext` filename that ructe creates for it.
|
||
|
/// Note that only files that are added to the `StaticFiles`
|
||
|
/// _before_ the call to `add_sass_files` are supported by the
|
||
|
/// `static_name` function.
|
||
|
///
|
||
|
/// This method is only available when ructe is built with the
|
||
|
/// "sass" feature.
|
||
|
#[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('-', "_").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)
|
||
|
}
|
||
|
|
||
|
/// Get a mapping of names, from without hash to with.
|
||
|
///
|
||
|
/// ````
|
||
|
/// # use ructe::{Result, Ructe, StaticFiles};
|
||
|
/// # use std::fs::create_dir_all;
|
||
|
/// # use std::path::PathBuf;
|
||
|
/// # use std::vec::Vec;
|
||
|
/// # fn main() -> Result<()> {
|
||
|
/// # let p = PathBuf::from(env!("CARGO_MANIFEST_DIR")).join("target").join("test-tmp").join("get-names");
|
||
|
/// # create_dir_all(&p);
|
||
|
/// # let mut ructe = Ructe::new(p)?;
|
||
|
/// let mut statics = ructe.statics()?;
|
||
|
/// statics.add_file_data("black.css", b"body{color:black}\n");
|
||
|
/// statics.add_file_data("blue.css", b"body{color:blue}\n");
|
||
|
/// assert_eq!(
|
||
|
/// statics.get_names().iter()
|
||
|
/// .map(|(a, b)| format!("{} -> {}", a, b))
|
||
|
/// .collect::<Vec<_>>(),
|
||
|
/// vec!["black_css -> black-r3rltVhW.css".to_string(),
|
||
|
/// "blue_css -> blue-GZGxfXag.css".to_string()],
|
||
|
/// );
|
||
|
/// # Ok(())
|
||
|
/// # }
|
||
|
/// ````
|
||
|
pub fn get_names(&self) -> &BTreeMap<String, String> {
|
||
|
&self.names
|
||
|
}
|
||
|
}
|
||
|
|
||
|
impl Drop for StaticFiles {
|
||
|
/// Write the ending of the statics source code, declaring the
|
||
|
/// `STATICS` variable.
|
||
|
fn drop(&mut self) {
|
||
|
// Ignore a possible write failure, rather than a panic in drop.
|
||
|
let _ = writeln!(
|
||
|
self.src,
|
||
|
"\npub static STATICS: &[&StaticFile] \
|
||
|
= &[{}];",
|
||
|
self.names_r
|
||
|
.iter()
|
||
|
.map(|s| format!("&{}", s.1))
|
||
|
.format(", "),
|
||
|
);
|
||
|
let _ = super::write_if_changed(&self.src_path, &self.src);
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct FileContent<'a>(&'a Path);
|
||
|
|
||
|
impl<'a> Display for FileContent<'a> {
|
||
|
fn fmt(&self, out: &mut fmt::Formatter) -> fmt::Result {
|
||
|
write!(out, "include_bytes!({:?})", self.0)
|
||
|
}
|
||
|
}
|
||
|
|
||
|
struct ByteString<'a>(&'a [u8]);
|
||
|
|
||
|
impl<'a> Display for ByteString<'a> {
|
||
|
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(any(
|
||
|
all(feature = "mime03", feature = "http-types"),
|
||
|
all(feature = "mime02", feature = "http-types"),
|
||
|
all(feature = "mime02", feature = "mime03"),
|
||
|
))]
|
||
|
compile_error!(
|
||
|
r#"Only one of these features "http-types", "mime02" or "mime03" must be enabled at a time."#
|
||
|
);
|
||
|
|
||
|
/// A short and url-safe checksum string from string data.
|
||
|
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])
|
||
|
}
|
||
|
#[cfg(not(feature = "mime02"))]
|
||
|
#[cfg(not(feature = "mime03"))]
|
||
|
#[cfg(not(feature = "http-types"))]
|
||
|
fn mime_arg(_: &str) -> String {
|
||
|
"".to_string()
|
||
|
}
|
||
|
#[cfg(feature = "mime02")]
|
||
|
fn mime_arg(suffix: &str) -> String {
|
||
|
format!(" _mime: {:?},\n", mime_from_suffix(suffix))
|
||
|
}
|
||
|
|
||
|
#[cfg(feature = "mime02")]
|
||
|
fn mime_from_suffix(suffix: &str) -> &'static str {
|
||
|
match suffix.to_lowercase().as_ref() {
|
||
|
"bmp" => "image/bmp",
|
||
|
"css" => "text/css",
|
||
|
"eot" => "application/vnd.ms-fontobject",
|
||
|
"gif" => "image/gif",
|
||
|
"jpg" | "jpeg" => "image/jpeg",
|
||
|
"js" | "jsonp" => "application/javascript",
|
||
|
"json" => "application/json",
|
||
|
"png" => "image/png",
|
||
|
"svg" => "image/svg+xml",
|
||
|
"woff" => "font/woff",
|
||
|
"woff2" => "font/woff2",
|
||
|
_ => "application/octet-stream",
|
||
|
}
|
||
|
}
|
||
|
|
||
|
#[cfg(any(feature = "mime03", feature = "http-types"))]
|
||
|
fn mime_arg(suffix: &str) -> String {
|
||
|
format!(" mime: &mime::{},\n", mime_from_suffix(suffix))
|
||
|
}
|
||
|
|
||
|
#[cfg(feature = "mime03")]
|
||
|
fn mime_from_suffix(suffix: &str) -> &'static str {
|
||
|
// This is limited to the constants that is defined in mime 0.3.
|
||
|
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",
|
||
|
}
|
||
|
}
|