Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Add metadata_only attribute #241

Merged
merged 4 commits into from
Apr 16, 2024
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Jump to
Jump to file
Failed to load files.
Diff view
Diff view
55 changes: 47 additions & 8 deletions impl/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -16,6 +16,7 @@ use syn::{parse_macro_input, Data, DeriveInput, Expr, ExprLit, Fields, Lit, Meta

fn embedded(
ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String],
metadata_only: bool,
) -> syn::Result<TokenStream2> {
extern crate rust_embed_utils;

Expand All @@ -25,7 +26,10 @@ fn embedded(
let includes: Vec<&str> = includes.iter().map(AsRef::as_ref).collect();
let excludes: Vec<&str> = excludes.iter().map(AsRef::as_ref).collect();
for rust_embed_utils::FileEntry { rel_path, full_canonical_path } in rust_embed_utils::get_files(absolute_folder_path.clone(), &includes, &excludes) {
match_values.insert(rel_path.clone(), embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path)?);
match_values.insert(
rel_path.clone(),
embed_file(relative_folder_path, ident, &rel_path, &full_canonical_path, metadata_only)?,
);

list_values.push(if let Some(prefix) = prefix {
format!("{}{}", prefix, rel_path)
Expand Down Expand Up @@ -103,7 +107,7 @@ fn embedded(
})
}

fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String]) -> TokenStream2 {
fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includes: &[String], excludes: &[String], metadata_only: bool) -> TokenStream2 {
let (handle_prefix, map_iter) = if let ::std::option::Option::Some(prefix) = prefix {
(
quote! { let file_path = file_path.strip_prefix(#prefix)?; },
Expand All @@ -121,6 +125,12 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
const EXCLUDES: &[&str] = &[#(#excludes),*];
};

// In metadata_only mode, we still need to read file contents to generate the file hash, but
// then we drop the file data.
let strip_contents = metadata_only.then_some(quote! {
.map(|mut file| { file.data = ::std::default::Default::default(); file })
});

let canonical_folder_path = Path::new(&folder_path).canonicalize().expect("folder path must resolve to an absolute path");
let canonical_folder_path = canonical_folder_path.to_str().expect("absolute folder path must be valid unicode");

Expand Down Expand Up @@ -154,7 +164,7 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ
}

if rust_embed::utils::is_path_included(&rel_file_path, INCLUDES, EXCLUDES) {
rust_embed::utils::read_file_from_fs(&canonical_file_path).ok()
rust_embed::utils::read_file_from_fs(&canonical_file_path).ok() #strip_contents
pyrossh marked this conversation as resolved.
Show resolved Hide resolved
} else {
::std::option::Option::None
}
Expand Down Expand Up @@ -187,6 +197,7 @@ fn dynamic(ident: &syn::Ident, folder_path: String, prefix: Option<&str>, includ

fn generate_assets(
ident: &syn::Ident, relative_folder_path: Option<&str>, absolute_folder_path: String, prefix: Option<String>, includes: Vec<String>, excludes: Vec<String>,
metadata_only: bool,
) -> syn::Result<TokenStream2> {
let embedded_impl = embedded(
ident,
Expand All @@ -195,20 +206,21 @@ fn generate_assets(
prefix.as_deref(),
&includes,
&excludes,
metadata_only,
);
if cfg!(feature = "debug-embed") {
return embedded_impl;
}
let embedded_impl = embedded_impl?;
let dynamic_impl = dynamic(ident, absolute_folder_path, prefix.as_deref(), &includes, &excludes);
let dynamic_impl = dynamic(ident, absolute_folder_path, prefix.as_deref(), &includes, &excludes, metadata_only);

Ok(quote! {
#embedded_impl
#dynamic_impl
})
}

fn embed_file(folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str) -> syn::Result<TokenStream2> {
fn embed_file(folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, full_canonical_path: &str, metadata_only: bool) -> syn::Result<TokenStream2> {
let file = rust_embed_utils::read_file_from_fs(Path::new(full_canonical_path)).expect("File should be readable");
let hash = file.metadata.sha256_hash();
let last_modified = match file.metadata.last_modified() {
Expand All @@ -227,7 +239,11 @@ fn embed_file(folder_path: Option<&str>, ident: &syn::Ident, rel_path: &str, ful
#[cfg(not(feature = "mime-guess"))]
let mimetype_tokens = TokenStream2::new();

let embedding_code = if cfg!(feature = "compression") {
let embedding_code = if metadata_only {
quote! {
const BYTES: &'static [u8] = &[];
}
} else if cfg!(feature = "compression") {
let folder_path = folder_path.ok_or(syn::Error::new(ident.span(), "`folder` must be provided under `compression` feature."))?;
// Print some debugging information
let full_relative_path = PathBuf::from_iter([folder_path, rel_path]);
Expand Down Expand Up @@ -273,6 +289,20 @@ fn find_attribute_values(ast: &syn::DeriveInput, attr_name: &str) -> Vec<String>
.collect()
}

fn find_bool_attribute(ast: &syn::DeriveInput, attr_name: &str) -> Option<bool> {
ast
.attrs
.iter()
.find(|value| value.path().is_ident(attr_name))
.and_then(|attr| match &attr.meta {
Meta::NameValue(MetaNameValue {
value: Expr::Lit(ExprLit { lit: Lit::Bool(val), .. }),
..
}) => Some(val.value()),
_ => None,
})
}

fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
match ast.data {
Data::Struct(ref data) => match data.fields {
Expand All @@ -294,6 +324,7 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
let prefix = find_attribute_values(ast, "prefix").into_iter().next();
let includes = find_attribute_values(ast, "include");
let excludes = find_attribute_values(ast, "exclude");
let metadata_only = find_bool_attribute(ast, "metadata_only").unwrap_or(false);

#[cfg(not(feature = "include-exclude"))]
if !includes.is_empty() || !excludes.is_empty() {
Expand Down Expand Up @@ -340,10 +371,18 @@ fn impl_rust_embed(ast: &syn::DeriveInput) -> syn::Result<TokenStream2> {
return Err(syn::Error::new_spanned(ast, message));
};

generate_assets(&ast.ident, relative_path.as_deref(), absolute_folder_path, prefix, includes, excludes)
generate_assets(
&ast.ident,
relative_path.as_deref(),
absolute_folder_path,
prefix,
includes,
excludes,
metadata_only,
)
}

#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude))]
#[proc_macro_derive(RustEmbed, attributes(folder, prefix, include, exclude, metadata_only))]
pub fn derive_input_object(input: TokenStream) -> TokenStream {
let ast = parse_macro_input!(input as DeriveInput);
match impl_rust_embed(&ast) {
Expand Down
8 changes: 7 additions & 1 deletion readme.md
Original file line number Diff line number Diff line change
Expand Up @@ -75,12 +75,18 @@ If the feature `debug-embed` is enabled or the binary compiled in release mode a

Otherwise the files are listed from the file system on each call.

## The `prefix` attribute
## Attributes
### `prefix`

You can add `#[prefix = "my_prefix/"]` to the `RustEmbed` struct to add a prefix
to all of the file paths. This prefix will be required on `get` calls, and will
be included in the file paths returned by `iter`.

### `metadata_only`

You can add `#[metadata_only = true]` to the `RustEmbed` struct to exclude file contents from the
binary. Only file paths and metadata will be embedded.

## Features

### `debug-embed`
Expand Down
12 changes: 12 additions & 0 deletions tests/metadata_only.rs
Original file line number Diff line number Diff line change
@@ -0,0 +1,12 @@
use rust_embed::{EmbeddedFile, RustEmbed};

#[derive(RustEmbed)]
#[folder = "examples/public/"]
#[metadata_only = true]
struct Asset;

#[test]
fn file_is_empty() {
let index_file: EmbeddedFile = Asset::get("index.html").expect("index.html exists");
assert_eq!(index_file.data.len(), 0);
}