diff --git a/README.md b/README.md index 8ce8821..06e8f85 100644 --- a/README.md +++ b/README.md @@ -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 @@ -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) diff --git a/static-serve-macro/src/error.rs b/static-serve-macro/src/error.rs index fb40771..8997794 100644 --- a/static-serve-macro/src/error.rs +++ b/static-serve-macro/src/error.rs @@ -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")] diff --git a/static-serve-macro/src/lib.rs b/static-serve-macro/src/lib.rs index 21a4b6d..7f5b6b7 100644 --- a/static-serve-macro/src/lib.rs +++ b/static-serve-macro/src/lib.rs @@ -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, @@ -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; @@ -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()?; @@ -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`", )); } } @@ -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![])); @@ -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, @@ -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, @@ -302,44 +302,38 @@ impl Parse for AssetsDir { } } -struct IgnoreDirs(Vec); +struct IgnorePaths(Vec); -struct IgnoreDirsWithSpan(Vec<(PathBuf, Span)>); +struct IgnorePathsWithSpan(Vec<(PathBuf, Span)>); -impl Parse for IgnoreDirsWithSpan { +impl Parse for IgnorePathsWithSpan { fn parse(input: ParseStream) -> syn::Result { 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 { - let mut valid_ignore_dirs = Vec::new(); - for (dir, span) in ignore_dirs.0 { +) -> syn::Result { + 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) ), @@ -347,7 +341,7 @@ fn validate_ignore_dirs( } } } - Ok(IgnoreDirs(valid_ignore_dirs)) + Ok(IgnorePaths(valid_ignore_paths)) } struct ShouldCompress(LitBool); @@ -452,7 +446,7 @@ fn parse_dirs(input: ParseStream) -> syn::Result> { 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, @@ -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::, _>>()?; let canon_cache_busted_dirs = cache_busted_paths .dirs @@ -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; } diff --git a/static-serve/tests/tests.rs b/static-serve/tests/tests.rs index 4511a5d..b3c74b0 100644 --- a/static-serve/tests/tests.rs +++ b/static-serve/tests/tests.rs @@ -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()); @@ -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 @@ -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()); +}