use proc_macro2::{Ident, Span};
use std::ffi::OsStr;
use std::fmt;
use std::path::{Path, PathBuf};
use utils;
use utils::Filter;
use utils::Filter::*;
use utils::FilterListType;
use utils::FilterListType::*;
use walkdir::WalkDir;
use Pipeline;
struct AssetInfo {
path: String,
clean_path: String,
has_gz: bool,
has_br: bool,
}
enum CompressionType {
GZIP,
BROTLI,
}
pub struct WebAssets {
ident: String,
prefix: String,
path: PathBuf,
filters: Vec<Filter>,
filter_list_type: FilterListType,
brotli: bool,
gzip: bool,
}
impl WebAssets {
pub fn new<S: Into<String>, P: Into<PathBuf>>(identifier: S, path: P) -> Self {
WebAssets {
ident: identifier.into(),
prefix: "/".to_string(),
path: path.into(),
filters: Vec::new(),
filter_list_type: Blacklist,
brotli: true,
gzip: true,
}
}
pub fn filter(mut self, filter: Filter) -> Self {
self.filters.push(filter);
self
}
pub fn prefix<S: Into<String>>(mut self, prefix: S) -> Self {
self.prefix = prefix.into();
self
}
pub fn set_path<P: Into<PathBuf>>(mut self, path: P) -> Self {
self.path = path.into();
self
}
pub fn blacklist(mut self) -> Self {
self.filter_list_type = Blacklist;
self
}
pub fn whitelist(mut self) -> Self {
self.filter_list_type = Whitelist;
self
}
pub fn build(self) -> Box<Self> {
Box::new(self)
}
pub fn brotli(mut self, brotli: bool) -> Self {
self.brotli = brotli;
self
}
pub fn gzip(mut self, gzip: bool) -> Self {
self.gzip = gzip;
self
}
}
impl fmt::Display for WebAssets {
fn fmt(&self, f: &mut fmt::Formatter) -> fmt::Result {
let mut entries = Vec::new();
for maybe_entry in WalkDir::new(&self.path) {
let entry = maybe_entry.unwrap();
if entry.file_type().is_dir() {
utils::watch_path(entry.path());
continue;
}
if self.filters.is_empty() {
match self.filter_list_type {
Whitelist => break,
Blacklist => {
if !skip_compressed(&self, entry.path().extension()) {
utils::watch_path(entry.path());
entries.push(PathBuf::from(entry.path()));
}
continue;
}
}
}
let mut matched = true;
for filter in &self.filters {
if !filter.matches(entry.path()) {
continue;
}
if skip_compressed(&self, entry.path().extension()) {
matched = false;
}
if let Exclude(_) = filter {
matched = false;
}
break;
}
if matched {
utils::watch_path(entry.path());
entries.push(PathBuf::from(entry.path()));
}
}
let asset_info: Vec<AssetInfo> = entries
.iter()
.map(|p| AssetInfo {
path: utils::path_to_string(p),
clean_path: normalize_path(p, &self.path, &self.prefix),
has_gz: compressed_exists(&p, CompressionType::GZIP),
has_br: compressed_exists(&p, CompressionType::BROTLI),
}).collect();
let code = generate_asset_const(&self.ident, asset_info);
write!(f, "{}", code)
}
}
impl Pipeline for WebAssets {}
fn compressed_exists(path: &Path, compression: CompressionType) -> bool {
let ext = match compression {
CompressionType::GZIP => ".gz",
CompressionType::BROTLI => ".br",
};
let f = path.file_name().unwrap();
let new_f = format!("{}{}", f.to_str().unwrap(), ext);
let mut p = PathBuf::from(path);
p.set_file_name(new_f);
Path::exists(&p)
}
fn skip_compressed(builder: &WebAssets, ext: Option<&OsStr>) -> bool {
if builder.gzip && ext == Some("gz".as_ref()) {
return true;
}
if builder.brotli && ext == Some("br".as_ref()) {
return true;
}
false
}
fn normalize_path(path: &Path, dir: &Path, prefix: &str) -> String {
let path = path.strip_prefix(&dir).unwrap();
let path = PathBuf::from("/").join(prefix).join(&path);
if path.file_name().unwrap() == "index.html" {
path.parent().unwrap().to_str().unwrap().to_owned()
} else {
path.to_str().unwrap().to_owned()
}
}
fn generate_asset_const(ident_str: &str, raw_assets: Vec<AssetInfo>) -> String {
let len = raw_assets.len();
let mut structs = Vec::new();
for AssetInfo {
path,
clean_path,
has_gz,
has_br,
} in raw_assets
{
let gz = if has_gz {
let path_gz = path.clone() + ".gz";
quote! {Some(include_bytes!(#path_gz))}
} else {
quote! {None}
};
let br = if has_br {
let path_br = path.clone() + ".br";
quote! {Some(include_bytes!(#path_br))}
} else {
quote! {None}
};
structs.push(quote! {
WebAsset {
uri: #clean_path,
data: include_bytes!(#path),
data_gz: #gz,
data_br: #br,
}
});
}
let ident = Ident::new(ident_str, Span::call_site());
let tokens = quote! {
const #ident: [WebAsset; #len] = [#(#structs),*];
};
format!("{}\n", tokens)
}
#[cfg(test)]
mod tests {
#[test]
fn it_works() {
assert_eq!(1 + 1, 2);
}
}