Skip to content
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.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
6 changes: 3 additions & 3 deletions README.md
Original file line number Diff line number Diff line change
Expand Up @@ -31,13 +31,13 @@ Use the `embed_assets!` macro to create a `static_router()` function in scope wh
```rust
use static_serve::embed_assets;

embed_assets!("assets", compress = true, cache_busted_paths = ["immutable"]);
embed_assets!("assets", compress = true, ignore_paths = ["temp.txt","temp"], cache_busted_paths = ["immutable"]);
let router = static_router();
```

This will:

- Include all files from the `assets` directory
- Include all files from the `assets` directory except `temp.txt` and the `temp` directory
- Compress them using `gzip` and `zstd` (if beneficial)
- For only files in `assets/immutable`, add a `Cache-Control` header with `public, max-age=31536000, immutable` (since these are marked as cache-busted paths)
- Generate a `static_router()` function to serve these assets
Expand All @@ -50,7 +50,7 @@ This will:

- `compress = false` - compress static files with zstd and gzip, true or false (defaults to false)

- `ignore_dirs = ["my_ignore_dir", "other_ignore_dir"]` - a bracketed list of `&str`s of the paths/subdirectories inside the target directory, which should be ignored and not included. (If this parameter is missing, no subdirectories will be ignored)
- `ignore_paths = ["my_ignore_dir", "other_ignore_dir", "my_ignore_file.txt"]` - a bracketed list of `&str`s of paths/subdirectories/files inside the target directory, which should be ignored and not included. (If this parameter is missing, no paths/subdirectories/files will be ignored)

- `strip_html_ext = false` - strips the `.html` or `.htm` from all HTML files included. If the filename is `index.html` or `index.htm`, the `index` part will also be removed, leaving just the root (defaults to false)

Expand Down
4 changes: 2 additions & 2 deletions static-serve-macro/src/error.rs
Original file line number Diff line number Diff line change
Expand Up @@ -21,8 +21,8 @@ pub(crate) enum Error {
FilePathIsNotUtf8,
#[error("Invalid unicode in directory name")]
InvalidUnicodeInDirectoryName,
#[error("Cannot canonicalize ignore directory")]
CannotCanonicalizeIgnoreDir(#[source] io::Error),
#[error("Cannot canonicalize ignore path")]
CannotCanonicalizeIgnorePath(#[source] io::Error),
#[error("Invalid unicode in directory name")]
InvalidUnicodeInEntryName,
#[error("Error while compressing with gzip")]
Expand Down
67 changes: 32 additions & 35 deletions static-serve-macro/src/lib.rs
Original file line number Diff line number Diff line change
Expand Up @@ -155,7 +155,7 @@ impl ToTokens for EmbedAsset {

struct EmbedAssets {
assets_dir: AssetsDir,
validated_ignore_dirs: IgnoreDirs,
validated_ignore_paths: IgnorePaths,
should_compress: ShouldCompress,
should_strip_html_ext: ShouldStripHtmlExt,
cache_busted_paths: CacheBustedPaths,
Expand All @@ -167,7 +167,7 @@ impl Parse for EmbedAssets {

// Default to no compression
let mut maybe_should_compress = None;
let mut maybe_ignore_dirs = None;
let mut maybe_ignore_paths = None;
let mut maybe_should_strip_html_ext = None;
let mut maybe_cache_busted_paths = None;

Expand All @@ -181,9 +181,9 @@ impl Parse for EmbedAssets {
let value = input.parse()?;
maybe_should_compress = Some(value);
}
"ignore_dirs" => {
"ignore_paths" => {
let value = input.parse()?;
maybe_ignore_dirs = Some(value);
maybe_ignore_paths = Some(value);
}
"strip_html_ext" => {
let value = input.parse()?;
Expand All @@ -196,7 +196,7 @@ impl Parse for EmbedAssets {
_ => {
return Err(syn::Error::new(
key.span(),
"Unknown key in embed_assets! macro. Expected `compress`, `ignore_dirs`, `strip_html_ext`, or `cache_busted_paths`",
"Unknown key in embed_assets! macro. Expected `compress`, `ignore_paths`, `strip_html_ext`, or `cache_busted_paths`",
));
}
}
Expand All @@ -216,8 +216,8 @@ impl Parse for EmbedAssets {
})
});

let ignore_dirs_with_span = maybe_ignore_dirs.unwrap_or(IgnoreDirsWithSpan(vec![]));
let validated_ignore_dirs = validate_ignore_dirs(ignore_dirs_with_span, &assets_dir.0)?;
let ignore_paths_with_span = maybe_ignore_paths.unwrap_or(IgnorePathsWithSpan(vec![]));
let validated_ignore_paths = validate_ignore_paths(ignore_paths_with_span, &assets_dir.0)?;

let maybe_cache_busted_paths =
maybe_cache_busted_paths.unwrap_or(CacheBustedPathsWithSpan(vec![]));
Expand All @@ -226,7 +226,7 @@ impl Parse for EmbedAssets {

Ok(Self {
assets_dir,
validated_ignore_dirs,
validated_ignore_paths,
should_compress,
should_strip_html_ext,
cache_busted_paths,
Expand All @@ -237,14 +237,14 @@ impl Parse for EmbedAssets {
impl ToTokens for EmbedAssets {
fn to_tokens(&self, tokens: &mut TokenStream) {
let AssetsDir(assets_dir) = &self.assets_dir;
let ignore_dirs = &self.validated_ignore_dirs;
let ignore_paths = &self.validated_ignore_paths;
let ShouldCompress(should_compress) = &self.should_compress;
let ShouldStripHtmlExt(should_strip_html_ext) = &self.should_strip_html_ext;
let cache_busted_paths = &self.cache_busted_paths;

let result = generate_static_routes(
assets_dir,
ignore_dirs,
ignore_paths,
should_compress,
should_strip_html_ext,
cache_busted_paths,
Expand Down Expand Up @@ -302,52 +302,46 @@ impl Parse for AssetsDir {
}
}

struct IgnoreDirs(Vec<PathBuf>);
struct IgnorePaths(Vec<PathBuf>);

struct IgnoreDirsWithSpan(Vec<(PathBuf, Span)>);
struct IgnorePathsWithSpan(Vec<(PathBuf, Span)>);

impl Parse for IgnoreDirsWithSpan {
impl Parse for IgnorePathsWithSpan {
fn parse(input: ParseStream) -> syn::Result<Self> {
let dirs = parse_dirs(input)?;

Ok(IgnoreDirsWithSpan(dirs))
Ok(IgnorePathsWithSpan(dirs))
}
}

fn validate_ignore_dirs(
ignore_dirs: IgnoreDirsWithSpan,
fn validate_ignore_paths(
ignore_paths: IgnorePathsWithSpan,
assets_dir: &LitStr,
) -> syn::Result<IgnoreDirs> {
let mut valid_ignore_dirs = Vec::new();
for (dir, span) in ignore_dirs.0 {
) -> syn::Result<IgnorePaths> {
let mut valid_ignore_paths = Vec::new();
for (dir, span) in ignore_paths.0 {
let full_path = PathBuf::from(assets_dir.value()).join(&dir);
match fs::metadata(&full_path) {
Ok(meta) if !meta.is_dir() => {
return Err(syn::Error::new(
span,
"The specified ignored directory is not a directory",
));
}
Ok(_) => valid_ignore_dirs.push(full_path),
Ok(_) => valid_ignore_paths.push(full_path),
Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => {
return Err(syn::Error::new(
span,
"The specified ignored directory does not exist",
"The specified ignored path does not exist",
))
}
Err(e) => {
return Err(syn::Error::new(
span,
format!(
"Error reading ignored directory {}: {}",
"Error reading ignored path {}: {}",
dir.to_string_lossy(),
DisplayFullError(&e)
),
))
}
}
}
Ok(IgnoreDirs(valid_ignore_dirs))
Ok(IgnorePaths(valid_ignore_paths))
}

struct ShouldCompress(LitBool);
Expand Down Expand Up @@ -452,7 +446,7 @@ fn parse_dirs(input: ParseStream) -> syn::Result<Vec<(PathBuf, Span)>> {

fn generate_static_routes(
assets_dir: &LitStr,
ignore_dirs: &IgnoreDirs,
ignore_paths: &IgnorePaths,
should_compress: &LitBool,
should_strip_html_ext: &LitBool,
cache_busted_paths: &CacheBustedPaths,
Expand All @@ -463,10 +457,13 @@ fn generate_static_routes(
let assets_dir_abs_str = assets_dir_abs
.to_str()
.ok_or(Error::InvalidUnicodeInDirectoryName)?;
let canon_ignore_dirs = ignore_dirs
let canon_ignore_paths = ignore_paths
.0
.iter()
.map(|d| d.canonicalize().map_err(Error::CannotCanonicalizeIgnoreDir))
.map(|d| {
d.canonicalize()
.map_err(Error::CannotCanonicalizeIgnorePath)
})
.collect::<Result<Vec<_>, _>>()?;
let canon_cache_busted_dirs = cache_busted_paths
.dirs
Expand All @@ -490,10 +487,10 @@ fn generate_static_routes(
continue;
}

// Skip `entry`s which are located in ignored subdirectories
if canon_ignore_dirs
// Skip `entry`s which are located in ignored paths
if canon_ignore_paths
.iter()
.any(|ignore_dir| entry.starts_with(ignore_dir))
.any(|ignore_path| entry.starts_with(ignore_path))
{
continue;
}
Expand Down
66 changes: 62 additions & 4 deletions static-serve/tests/tests.rs
Original file line number Diff line number Diff line change
Expand Up @@ -208,8 +208,8 @@ async fn router_created_compressed_zstd_or_gzip_accepted() {
}

#[tokio::test]
async fn router_created_ignore_dirs_one() {
embed_assets!("../static-serve/test_assets", ignore_dirs = ["dist"]);
async fn router_created_ignore_paths_one() {
embed_assets!("../static-serve/test_assets", ignore_paths = ["dist"]);
let router: Router<()> = static_router();
assert!(router.has_routes());

Expand All @@ -230,10 +230,10 @@ async fn router_created_ignore_dirs_one() {
}

#[tokio::test]
async fn router_created_ignore_dirs_three() {
async fn router_created_ignore_paths_three() {
embed_assets!(
"../static-serve/test_assets",
ignore_dirs = ["big", "small", "dist", "with_html"]
ignore_paths = ["big", "small", "dist", "with_html"]
);
let router: Router<()> = static_router();
// all directories ignored, so router has no routes
Expand Down Expand Up @@ -1079,3 +1079,61 @@ async fn handles_dir_with_cache_control_on_filename_and_dir() {
let expected_body_bytes = include_bytes!("../../../static-serve/test_assets/small/app.js");
assert_eq!(*collected_body_bytes, *expected_body_bytes);
}

#[tokio::test]
async fn router_created_ignore_paths() {
embed_assets!(
"../static-serve/test_assets/small",
ignore_paths = ["app.js"]
);
let router: Router<()> = static_router();

// app.js should be ignored, but styles.css should be available
assert!(router.has_routes());

// Request for styles.css should succeed
let request = create_request("/styles.css", &Compression::None);
let response = get_response(router.clone(), request).await;
let (parts, _) = response.into_parts();
assert!(parts.status.is_success());

// Request for app.js should return 404 since it's ignored
let request = create_request("/app.js", &Compression::None);
let response = get_response(router, request).await;
let (parts, _) = response.into_parts();
assert_eq!(parts.status, StatusCode::NOT_FOUND);
}

#[tokio::test]
async fn router_created_ignore_multiple_files() {
embed_assets!(
"../static-serve/test_assets/big",
ignore_paths = ["app.js", "styles.css"]
);
let router: Router<()> = static_router();

// app.js and styles.css at root should be ignored, but files in /big/immutable should still be available
assert!(router.has_routes());

// Request for ignored files should return 404
let request = create_request("/app.js", &Compression::None);
let response = get_response(router.clone(), request).await;
let (parts, _) = response.into_parts();
assert_eq!(parts.status, StatusCode::NOT_FOUND);

let request = create_request("/styles.css", &Compression::None);
let response = get_response(router.clone(), request).await;
let (parts, _) = response.into_parts();
assert_eq!(parts.status, StatusCode::NOT_FOUND);

// Request for files in subdirectory should succeed
let request = create_request("/immutable/app.js", &Compression::None);
let response = get_response(router.clone(), request).await;
let (parts, _) = response.into_parts();
assert!(parts.status.is_success());

let request = create_request("/immutable/styles.css", &Compression::None);
let response = get_response(router, request).await;
let (parts, _) = response.into_parts();
assert!(parts.status.is_success());
}