@@ -31,6 +31,7 @@ const CACHE_CONTROL_OG_IMAGE: &str = "public,max-age=86400";
3131pub struct StorageConfig {
3232 backend : StorageBackend ,
3333 pub cdn_prefix : Option < String > ,
34+ pub cache_tags_enabled : bool ,
3435}
3536
3637#[ derive( Debug ) ]
@@ -54,6 +55,7 @@ impl StorageConfig {
5455 Self {
5556 backend : StorageBackend :: InMemory ,
5657 cdn_prefix : None ,
58+ cache_tags_enabled : false ,
5759 }
5860 }
5961
@@ -87,6 +89,7 @@ impl StorageConfig {
8789 return Self {
8890 backend,
8991 cdn_prefix,
92+ cache_tags_enabled : false ,
9093 } ;
9194 }
9295
@@ -101,6 +104,7 @@ impl StorageConfig {
101104 Self {
102105 backend,
103106 cdn_prefix : None ,
107+ cache_tags_enabled : false ,
104108 }
105109 }
106110}
@@ -110,6 +114,7 @@ pub struct Storage {
110114 store : Arc < dyn ObjectStore > ,
111115 index_store : Arc < dyn ObjectStore > ,
112116 supports_attributes : bool ,
117+ cache_tags_enabled : bool ,
113118}
114119
115120impl Storage {
@@ -145,6 +150,7 @@ impl Storage {
145150 store : Arc :: new ( store) ,
146151 index_store : Arc :: new ( index_store) ,
147152 supports_attributes : true ,
153+ cache_tags_enabled : config. cache_tags_enabled ,
148154 }
149155 }
150156
@@ -173,6 +179,7 @@ impl Storage {
173179 store,
174180 index_store,
175181 supports_attributes : false ,
182+ cache_tags_enabled : config. cache_tags_enabled ,
176183 }
177184 }
178185
@@ -185,6 +192,7 @@ impl Storage {
185192 store : store. clone ( ) ,
186193 index_store : Arc :: new ( PrefixStore :: new ( store, "index" ) ) ,
187194 supports_attributes : true ,
195+ cache_tags_enabled : config. cache_tags_enabled ,
188196 }
189197 }
190198 }
@@ -227,10 +235,7 @@ impl Storage {
227235 /// the key's intended attributes.
228236 #[ instrument( skip( self , payload) ) ]
229237 pub async fn upload ( & self , key : & StorageKey < ' _ > , payload : PutPayload ) -> Result < ( ) > {
230- let attributes = self
231- . supports_attributes
232- . then ( || key. attributes ( ) )
233- . unwrap_or_default ( ) ;
238+ let attributes = self . attributes ( key) ;
234239
235240 let opts = attributes. into ( ) ;
236241 self . store . put_opts ( & key. path ( ) , payload, opts) . await ?;
@@ -245,10 +250,7 @@ impl Storage {
245250 key : & StorageKey < ' _ > ,
246251 mut reader : impl AsyncRead + Unpin ,
247252 ) -> anyhow:: Result < ( ) > {
248- let attributes = self
249- . supports_attributes
250- . then ( || key. attributes ( ) )
251- . unwrap_or_default ( ) ;
253+ let attributes = self . attributes ( key) ;
252254
253255 // Set up a streaming upload
254256 let mut writer = object_store:: buffered:: BufWriter :: new ( self . store . clone ( ) , key. path ( ) )
@@ -267,6 +269,22 @@ impl Storage {
267269 Ok ( ( ) )
268270 }
269271
272+ fn attributes ( & self , key : & StorageKey < ' _ > ) -> Attributes {
273+ if !self . supports_attributes {
274+ return Attributes :: new ( ) ;
275+ }
276+
277+ let mut attributes = key. attributes ( ) ;
278+
279+ if self . cache_tags_enabled
280+ && let Some ( cache_tags) = key. cache_tags ( )
281+ {
282+ attributes. insert ( Attribute :: Metadata ( "cache-tags" . into ( ) ) , cache_tags. into ( ) ) ;
283+ }
284+
285+ attributes
286+ }
287+
270288 #[ instrument( skip( self ) ) ]
271289 pub async fn download_stream (
272290 & self ,
@@ -437,6 +455,31 @@ impl<'a> StorageKey<'a> {
437455 }
438456 }
439457
458+ /// The CDN cache tags the file should be stored with, rendered as a single
459+ /// comma-separated metadata value, or `None` for untagged objects.
460+ ///
461+ /// Version-scoped objects carry both a `crate:{name}` and a
462+ /// `release:{name}@{version}` tag, so a release purge drops one version,
463+ /// and a crate purge drops the whole crate. Crate-scoped objects carry
464+ /// only `crate:{name}`. Global objects are untagged and rely on URL purge.
465+ pub fn cache_tags ( & self ) -> Option < String > {
466+ match self {
467+ StorageKey :: CrateFile { name, version }
468+ | StorageKey :: CrateZip { name, version }
469+ | StorageKey :: CrateZipManifest { name, version }
470+ | StorageKey :: Readme { name, version } => {
471+ Some ( format ! ( "crate:{name},release:{name}@{version}" ) )
472+ }
473+ StorageKey :: OgImage { name } | StorageKey :: CrateFeed { name } => {
474+ Some ( format ! ( "crate:{name}" ) )
475+ }
476+ StorageKey :: CratesFeed
477+ | StorageKey :: UpdatesFeed
478+ | StorageKey :: DbDumpTar
479+ | StorageKey :: DbDumpZip => None ,
480+ }
481+ }
482+
440483 /// The intended attribute set (content-type + cache-control) for the file.
441484 pub fn attributes ( & self ) -> Attributes {
442485 let mut attributes = Attributes :: new ( ) ;
@@ -596,6 +639,80 @@ mod tests {
596639 ) ;
597640 }
598641
642+ #[ test]
643+ fn cache_tags ( ) {
644+ use claims:: { assert_none, assert_some_eq} ;
645+
646+ let name = "Some_Crate-Name" ;
647+ let version = "1.0.0-beta.1+build.2" ;
648+
649+ let version_scoped = [
650+ StorageKey :: for_crate_file ( name, version) ,
651+ StorageKey :: for_crate_zip ( name, version) ,
652+ StorageKey :: for_crate_zip_manifest ( name, version) ,
653+ StorageKey :: for_readme ( name, version) ,
654+ ] ;
655+ for key in version_scoped {
656+ assert_some_eq ! (
657+ key. cache_tags( ) ,
658+ "crate:Some_Crate-Name,release:Some_Crate-Name@1.0.0-beta.1+build.2"
659+ ) ;
660+ }
661+
662+ let crate_scoped = [
663+ StorageKey :: for_og_image ( name) ,
664+ StorageKey :: CrateFeed { name } ,
665+ ] ;
666+ for key in crate_scoped {
667+ assert_some_eq ! ( key. cache_tags( ) , "crate:Some_Crate-Name" ) ;
668+ }
669+
670+ let untagged = [
671+ StorageKey :: CratesFeed ,
672+ StorageKey :: UpdatesFeed ,
673+ StorageKey :: DbDumpTar ,
674+ StorageKey :: DbDumpZip ,
675+ ] ;
676+ for key in untagged {
677+ assert_none ! ( key. cache_tags( ) ) ;
678+ }
679+ }
680+
681+ async fn cache_tags_metadata ( storage : & Storage , key : & StorageKey < ' _ > ) -> Option < String > {
682+ let result = storage. store . get ( & key. path ( ) ) . await . unwrap ( ) ;
683+ result
684+ . attributes
685+ . get ( & Attribute :: Metadata ( "cache-tags" . into ( ) ) )
686+ . map ( |value| value. as_ref ( ) . to_string ( ) )
687+ }
688+
689+ #[ tokio:: test]
690+ async fn upload_sets_cache_tags_metadata ( ) {
691+ use claims:: { assert_none, assert_some_eq} ;
692+
693+ let mut config = StorageConfig :: in_memory ( ) ;
694+ config. cache_tags_enabled = true ;
695+ let s = Storage :: from_config ( & config) ;
696+
697+ let key = StorageKey :: for_crate_file ( "foo" , "1.2.3" ) ;
698+ s. upload ( & key, Bytes :: new ( ) . into ( ) ) . await . unwrap ( ) ;
699+ assert_some_eq ! (
700+ cache_tags_metadata( & s, & key) . await ,
701+ "crate:foo,release:foo@1.2.3"
702+ ) ;
703+
704+ let key = StorageKey :: for_crate_zip ( "foo" , "1.2.3" ) ;
705+ s. upload_stream ( & key, & b"fake zip data" [ ..] ) . await . unwrap ( ) ;
706+ assert_some_eq ! (
707+ cache_tags_metadata( & s, & key) . await ,
708+ "crate:foo,release:foo@1.2.3"
709+ ) ;
710+
711+ let key = StorageKey :: DbDumpTar ;
712+ s. upload_stream ( & key, & b"fake db dump" [ ..] ) . await . unwrap ( ) ;
713+ assert_none ! ( cache_tags_metadata( & s, & key) . await ) ;
714+ }
715+
599716 #[ tokio:: test]
600717 async fn delete_all_crate_files ( ) {
601718 let storage = prepare ( ) . await ;
0 commit comments