Skip to content

Commit 0dbc530

Browse files
feat!: make headers for cache-busted paths an optional feature
1 parent daa65d7 commit 0dbc530

File tree

3 files changed

+221
-53
lines changed

3 files changed

+221
-53
lines changed

static-serve-macro/src/error.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -37,6 +37,8 @@ pub(crate) enum Error {
3737
Glob(#[source] GlobError),
3838
#[error("Cannot get entry metadata")]
3939
CannotGetMetadata(#[source] io::Error),
40+
#[error("Cannot canonicalize directory for cache-busting")]
41+
CannotCanonicalizeCacheBustedDir(#[source] io::Error),
4042
}
4143

4244
struct UnknownFileExtension<'a>(Option<&'a OsStr>);

static-serve-macro/src/lib.rs

Lines changed: 157 additions & 25 deletions
Original file line numberDiff line numberDiff line change
@@ -40,6 +40,7 @@ pub fn embed_asset(input: proc_macro::TokenStream) -> proc_macro::TokenStream {
4040
struct EmbedAsset {
4141
asset_file: AssetFile,
4242
should_compress: ShouldCompress,
43+
cache_busted: IsCacheBusted,
4344
}
4445

4546
struct AssetFile(LitStr);
@@ -48,37 +49,51 @@ impl Parse for EmbedAsset {
4849
fn parse(input: ParseStream) -> syn::Result<Self> {
4950
let asset_file: AssetFile = input.parse()?;
5051

51-
// Default to no compression
52+
// Default to no compression, no cache-busting
5253
let mut maybe_should_compress = None;
54+
let mut maybe_is_cache_busted = None;
5355

5456
while !input.is_empty() {
5557
input.parse::<Token![,]>()?;
5658
let key: Ident = input.parse()?;
5759
input.parse::<Token![=]>()?;
5860

59-
if matches!(key.to_string().as_str(), "compress") {
60-
let value = input.parse()?;
61-
maybe_should_compress = Some(value);
62-
} else {
63-
return Err(syn::Error::new(
61+
match key.to_string().as_str() {
62+
"compress" => {
63+
let value = input.parse()?;
64+
maybe_should_compress = Some(value);
65+
}
66+
"cache_bust" => {
67+
let value = input.parse()?;
68+
maybe_is_cache_busted = Some(value);
69+
}
70+
_ => {
71+
return Err(syn::Error::new(
6472
key.span(),
6573
format!(
66-
"Unknown key in `embed_asset!` macro. Expected `compress` but got {key}"
74+
"Unknown key in `embed_asset!` macro. Expected `compress` or `cache_bust` but got {key}"
6775
),
6876
));
77+
}
6978
}
7079
}
71-
7280
let should_compress = maybe_should_compress.unwrap_or_else(|| {
7381
ShouldCompress(LitBool {
7482
value: false,
7583
span: Span::call_site(),
7684
})
7785
});
86+
let cache_busted = maybe_is_cache_busted.unwrap_or_else(|| {
87+
IsCacheBusted(LitBool {
88+
value: false,
89+
span: Span::call_site(),
90+
})
91+
});
7892

7993
Ok(Self {
8094
asset_file,
8195
should_compress,
96+
cache_busted,
8297
})
8398
}
8499
}
@@ -120,8 +135,9 @@ impl ToTokens for EmbedAsset {
120135
fn to_tokens(&self, tokens: &mut TokenStream) {
121136
let AssetFile(asset_file) = &self.asset_file;
122137
let ShouldCompress(should_compress) = &self.should_compress;
138+
let IsCacheBusted(cache_busted) = &self.cache_busted;
123139

124-
let result = generate_static_handler(asset_file, should_compress);
140+
let result = generate_static_handler(asset_file, should_compress, cache_busted);
125141

126142
match result {
127143
Ok(value) => {
@@ -142,6 +158,7 @@ struct EmbedAssets {
142158
validated_ignore_dirs: IgnoreDirs,
143159
should_compress: ShouldCompress,
144160
should_strip_html_ext: ShouldStripHtmlExt,
161+
cache_busted_paths: CacheBustedPaths,
145162
}
146163

147164
impl Parse for EmbedAssets {
@@ -152,6 +169,7 @@ impl Parse for EmbedAssets {
152169
let mut maybe_should_compress = None;
153170
let mut maybe_ignore_dirs = None;
154171
let mut maybe_should_strip_html_ext = None;
172+
let mut maybe_cache_busted_paths = None;
155173

156174
while !input.is_empty() {
157175
input.parse::<Token![,]>()?;
@@ -171,10 +189,14 @@ impl Parse for EmbedAssets {
171189
let value = input.parse()?;
172190
maybe_should_strip_html_ext = Some(value);
173191
}
192+
"cache_busted_paths" => {
193+
let value = input.parse()?;
194+
maybe_cache_busted_paths = Some(value);
195+
}
174196
_ => {
175197
return Err(syn::Error::new(
176198
key.span(),
177-
"Unknown key in embed_assets! macro. Expected `compress`, `ignore_dirs`, or `strip_html_ext`",
199+
"Unknown key in embed_assets! macro. Expected `compress`, `ignore_dirs`, `strip_html_ext`, or `cache_busted_paths`",
178200
));
179201
}
180202
}
@@ -197,11 +219,17 @@ impl Parse for EmbedAssets {
197219
let ignore_dirs_with_span = maybe_ignore_dirs.unwrap_or(IgnoreDirsWithSpan(vec![]));
198220
let validated_ignore_dirs = validate_ignore_dirs(ignore_dirs_with_span, &assets_dir.0)?;
199221

222+
let maybe_cache_busted_paths =
223+
maybe_cache_busted_paths.unwrap_or(CacheBustedPathsWithSpan(vec![]));
224+
let cache_busted_paths =
225+
validate_cache_busted_paths(maybe_cache_busted_paths, &assets_dir.0)?;
226+
200227
Ok(Self {
201228
assets_dir,
202229
validated_ignore_dirs,
203230
should_compress,
204231
should_strip_html_ext,
232+
cache_busted_paths,
205233
})
206234
}
207235
}
@@ -212,12 +240,14 @@ impl ToTokens for EmbedAssets {
212240
let ignore_dirs = &self.validated_ignore_dirs;
213241
let ShouldCompress(should_compress) = &self.should_compress;
214242
let ShouldStripHtmlExt(should_strip_html_ext) = &self.should_strip_html_ext;
243+
let cache_busted_paths = &self.cache_busted_paths;
215244

216245
let result = generate_static_routes(
217246
assets_dir,
218247
ignore_dirs,
219248
should_compress,
220249
should_strip_html_ext,
250+
cache_busted_paths,
221251
);
222252

223253
match result {
@@ -278,20 +308,7 @@ struct IgnoreDirsWithSpan(Vec<(PathBuf, Span)>);
278308

279309
impl Parse for IgnoreDirsWithSpan {
280310
fn parse(input: ParseStream) -> syn::Result<Self> {
281-
let inner_content;
282-
bracketed!(inner_content in input);
283-
284-
let mut dirs = Vec::new();
285-
while !inner_content.is_empty() {
286-
let directory_span = inner_content.span();
287-
let directory_str = inner_content.parse::<LitStr>()?;
288-
let path = PathBuf::from(directory_str.value());
289-
dirs.push((path, directory_span));
290-
291-
if !inner_content.is_empty() {
292-
inner_content.parse::<Token![,]>()?;
293-
}
294-
}
311+
let dirs = parse_dirs(input)?;
295312

296313
Ok(IgnoreDirsWithSpan(dirs))
297314
}
@@ -351,11 +368,94 @@ impl Parse for ShouldStripHtmlExt {
351368
}
352369
}
353370

371+
struct IsCacheBusted(LitBool);
372+
373+
impl Parse for IsCacheBusted {
374+
fn parse(input: ParseStream) -> syn::Result<Self> {
375+
let lit = input.parse()?;
376+
Ok(IsCacheBusted(lit))
377+
}
378+
}
379+
380+
struct CacheBustedPaths {
381+
dirs: Vec<PathBuf>,
382+
files: Vec<PathBuf>,
383+
}
384+
struct CacheBustedPathsWithSpan(Vec<(PathBuf, Span)>);
385+
386+
impl Parse for CacheBustedPathsWithSpan {
387+
fn parse(input: ParseStream) -> syn::Result<Self> {
388+
let dirs = parse_dirs(input)?;
389+
Ok(CacheBustedPathsWithSpan(dirs))
390+
}
391+
}
392+
393+
fn validate_cache_busted_paths(
394+
tuples: CacheBustedPathsWithSpan,
395+
assets_dir: &LitStr,
396+
) -> syn::Result<CacheBustedPaths> {
397+
let mut valid_dirs = Vec::new();
398+
let mut valid_files = Vec::new();
399+
for (dir, span) in tuples.0 {
400+
let full_path = PathBuf::from(assets_dir.value()).join(&dir);
401+
match fs::metadata(&full_path) {
402+
Ok(meta) => {
403+
if meta.is_dir() {
404+
valid_dirs.push(full_path);
405+
} else {
406+
valid_files.push(full_path);
407+
}
408+
}
409+
Err(e) if matches!(e.kind(), std::io::ErrorKind::NotFound) => {
410+
return Err(syn::Error::new(
411+
span,
412+
"The specified directory for cache busting does not exist",
413+
))
414+
}
415+
Err(e) => {
416+
return Err(syn::Error::new(
417+
span,
418+
format!(
419+
"Error reading path {}: {}",
420+
dir.to_string_lossy(),
421+
DisplayFullError(&e)
422+
),
423+
))
424+
}
425+
}
426+
}
427+
Ok(CacheBustedPaths {
428+
dirs: valid_dirs,
429+
files: valid_files,
430+
})
431+
}
432+
433+
/// Helper function for turning an array of strs representing paths into
434+
/// a `Vec` containing tuples of each `PathBuf` and its `Span` in the `ParseStream`
435+
fn parse_dirs(input: ParseStream) -> syn::Result<Vec<(PathBuf, Span)>> {
436+
let inner_content;
437+
bracketed!(inner_content in input);
438+
439+
let mut dirs = Vec::new();
440+
while !inner_content.is_empty() {
441+
let directory_span = inner_content.span();
442+
let directory_str = inner_content.parse::<LitStr>()?;
443+
let path = PathBuf::from(directory_str.value());
444+
dirs.push((path, directory_span));
445+
446+
if !inner_content.is_empty() {
447+
inner_content.parse::<Token![,]>()?;
448+
}
449+
}
450+
Ok(dirs)
451+
}
452+
354453
fn generate_static_routes(
355454
assets_dir: &LitStr,
356455
ignore_dirs: &IgnoreDirs,
357456
should_compress: &LitBool,
358457
should_strip_html_ext: &LitBool,
458+
cache_busted_paths: &CacheBustedPaths,
359459
) -> Result<TokenStream, error::Error> {
360460
let assets_dir_abs = Path::new(&assets_dir.value())
361461
.canonicalize()
@@ -368,6 +468,19 @@ fn generate_static_routes(
368468
.iter()
369469
.map(|d| d.canonicalize().map_err(Error::CannotCanonicalizeIgnoreDir))
370470
.collect::<Result<Vec<_>, _>>()?;
471+
let canon_cache_busted_dirs = cache_busted_paths
472+
.dirs
473+
.iter()
474+
.map(|d| {
475+
d.canonicalize()
476+
.map_err(Error::CannotCanonicalizeCacheBustedDir)
477+
})
478+
.collect::<Result<Vec<_>, _>>()?;
479+
let canon_cache_busted_files = cache_busted_paths
480+
.files
481+
.iter()
482+
.map(|file| file.canonicalize().map_err(Error::CannotCanonicalizeFile))
483+
.collect::<Result<Vec<_>, _>>()?;
371484

372485
let mut routes = Vec::new();
373486
for entry in glob(&format!("{assets_dir_abs_str}/**/*")).map_err(Error::Pattern)? {
@@ -385,6 +498,15 @@ fn generate_static_routes(
385498
continue;
386499
}
387500

501+
let mut is_entry_cache_busted = false;
502+
if canon_cache_busted_dirs
503+
.iter()
504+
.any(|dir| entry.starts_with(dir))
505+
|| canon_cache_busted_files.contains(&entry)
506+
{
507+
is_entry_cache_busted = true;
508+
}
509+
388510
let entry = entry
389511
.canonicalize()
390512
.map_err(Error::CannotCanonicalizeFile)?;
@@ -396,11 +518,13 @@ fn generate_static_routes(
396518
lit_byte_str_contents,
397519
maybe_gzip,
398520
maybe_zstd,
521+
cache_busted,
399522
} = EmbeddedFileInfo::from_path(
400523
&entry,
401524
Some(assets_dir_abs_str),
402525
should_compress,
403526
should_strip_html_ext,
527+
is_entry_cache_busted,
404528
)?;
405529

406530
routes.push(quote! {
@@ -413,10 +537,11 @@ fn generate_static_routes(
413537
// Poor man's `tracked_path`
414538
// https://github.com/rust-lang/rust/issues/99515
415539
const _: &[u8] = include_bytes!(#entry_str);
416-
#lit_byte_str_contents
540+
#lit_byte_str_contents
417541
},
418542
#maybe_gzip,
419543
#maybe_zstd,
544+
#cache_busted
420545
);
421546
});
422547
}
@@ -434,6 +559,7 @@ fn generate_static_routes(
434559
fn generate_static_handler(
435560
asset_file: &LitStr,
436561
should_compress: &LitBool,
562+
cache_busted: &LitBool,
437563
) -> Result<TokenStream, error::Error> {
438564
let asset_file_abs = Path::new(&asset_file.value())
439565
.canonicalize()
@@ -447,6 +573,7 @@ fn generate_static_handler(
447573
lit_byte_str_contents,
448574
maybe_gzip,
449575
maybe_zstd,
576+
cache_busted,
450577
} = EmbeddedFileInfo::from_path(
451578
&asset_file_abs,
452579
None,
@@ -455,6 +582,7 @@ fn generate_static_handler(
455582
value: false,
456583
span: Span::call_site(),
457584
},
585+
cache_busted.value(),
458586
)?;
459587

460588
let route = quote! {
@@ -469,6 +597,7 @@ fn generate_static_handler(
469597
},
470598
#maybe_gzip,
471599
#maybe_zstd,
600+
#cache_busted
472601
)
473602
};
474603

@@ -496,6 +625,7 @@ struct EmbeddedFileInfo<'a> {
496625
lit_byte_str_contents: LitByteStr,
497626
maybe_gzip: OptionBytesSlice,
498627
maybe_zstd: OptionBytesSlice,
628+
cache_busted: bool,
499629
}
500630

501631
impl<'a> EmbeddedFileInfo<'a> {
@@ -504,6 +634,7 @@ impl<'a> EmbeddedFileInfo<'a> {
504634
assets_dir_abs_str: Option<&str>,
505635
should_compress: &LitBool,
506636
should_strip_html_ext: &LitBool,
637+
cache_busted: bool,
507638
) -> Result<Self, Error> {
508639
let contents = fs::read(pathbuf).map_err(Error::CannotReadEntryContents)?;
509640

@@ -548,6 +679,7 @@ impl<'a> EmbeddedFileInfo<'a> {
548679
lit_byte_str_contents,
549680
maybe_gzip,
550681
maybe_zstd,
682+
cache_busted,
551683
})
552684
}
553685
}

0 commit comments

Comments
 (0)