@@ -19,15 +19,18 @@ use tracing::{instrument, warn};
1919
2020const PREFIX_CRATES : & str = "crates" ;
2121const PREFIX_READMES : & str = "readmes" ;
22+ const PREFIX_OG_IMAGES : & str = "og-images" ;
2223const DEFAULT_REGION : & str = "us-west-1" ;
2324const CONTENT_TYPE_CRATE : & str = "application/gzip" ;
2425const CONTENT_TYPE_GZIP : & str = "application/gzip" ;
2526const CONTENT_TYPE_ZIP : & str = "application/zip" ;
2627const CONTENT_TYPE_INDEX : & str = "text/plain" ;
2728const CONTENT_TYPE_README : & str = "text/html" ;
29+ const CONTENT_TYPE_OG_IMAGE : & str = "image/png" ;
2830const CACHE_CONTROL_IMMUTABLE : & str = "public,max-age=31536000,immutable" ;
2931const CACHE_CONTROL_INDEX : & str = "public,max-age=600" ;
3032const CACHE_CONTROL_README : & str = "public,max-age=604800" ;
33+ const CACHE_CONTROL_OG_IMAGE : & str = "public,max-age=86400" ;
3134
3235type StdPath = std:: path:: Path ;
3336
@@ -209,6 +212,13 @@ impl Storage {
209212 apply_cdn_prefix ( & self . cdn_prefix , & readme_path ( name, version) ) . replace ( '+' , "%2B" )
210213 }
211214
215+ /// Returns the URL of an uploaded crate's Open Graph image.
216+ ///
217+ /// The function doesn't check for the existence of the file.
218+ pub fn og_image_location ( & self , name : & str ) -> String {
219+ apply_cdn_prefix ( & self . cdn_prefix , & og_image_path ( name) )
220+ }
221+
212222 /// Returns the URL of an uploaded RSS feed.
213223 pub fn feed_url ( & self , feed_id : & FeedId < ' _ > ) -> String {
214224 apply_cdn_prefix ( & self . cdn_prefix , & feed_id. into ( ) ) . replace ( '+' , "%2B" )
@@ -240,6 +250,13 @@ impl Storage {
240250 self . store . delete ( & path) . await
241251 }
242252
253+ /// Deletes the Open Graph image for the given crate.
254+ #[ instrument( skip( self ) ) ]
255+ pub async fn delete_og_image ( & self , name : & str ) -> Result < ( ) > {
256+ let path = og_image_path ( name) ;
257+ self . store . delete ( & path) . await
258+ }
259+
243260 #[ instrument( skip( self ) ) ]
244261 pub async fn delete_feed ( & self , feed_id : & FeedId < ' _ > ) -> Result < ( ) > {
245262 let path = feed_id. into ( ) ;
@@ -270,6 +287,19 @@ impl Storage {
270287 Ok ( ( ) )
271288 }
272289
290+ /// Uploads an Open Graph image for the given crate.
291+ #[ instrument( skip( self , bytes) ) ]
292+ pub async fn upload_og_image ( & self , name : & str , bytes : Bytes ) -> Result < ( ) > {
293+ let path = og_image_path ( name) ;
294+ let attributes = self . attrs ( [
295+ ( Attribute :: ContentType , CONTENT_TYPE_OG_IMAGE ) ,
296+ ( Attribute :: CacheControl , CACHE_CONTROL_OG_IMAGE ) ,
297+ ] ) ;
298+ let opts = attributes. into ( ) ;
299+ self . store . put_opts ( & path, bytes. into ( ) , opts) . await ?;
300+ Ok ( ( ) )
301+ }
302+
273303 #[ instrument( skip( self , channel) ) ]
274304 pub async fn upload_feed (
275305 & self ,
@@ -385,6 +415,10 @@ fn readme_path(name: &str, version: &str) -> Path {
385415 format ! ( "{PREFIX_READMES}/{name}/{name}-{version}.html" ) . into ( )
386416}
387417
418+ fn og_image_path ( name : & str ) -> Path {
419+ format ! ( "{PREFIX_OG_IMAGES}/{name}.png" ) . into ( )
420+ }
421+
388422fn apply_cdn_prefix ( cdn_prefix : & Option < String > , path : & Path ) -> String {
389423 match cdn_prefix {
390424 Some ( cdn_prefix) if !cdn_prefix. starts_with ( "https://" ) => {
@@ -484,6 +518,17 @@ mod tests {
484518 for ( name, version, expected) in readme_tests {
485519 assert_eq ! ( storage. readme_location( name, version) , expected) ;
486520 }
521+
522+ let og_image_tests = vec ! [
523+ ( "foo" , "https://static.crates.io/og-images/foo.png" ) ,
524+ (
525+ "some-long-crate-name" ,
526+ "https://static.crates.io/og-images/some-long-crate-name.png" ,
527+ ) ,
528+ ] ;
529+ for ( name, expected) in og_image_tests {
530+ assert_eq ! ( storage. og_image_location( name) , expected) ;
531+ }
487532 }
488533
489534 #[ test]
@@ -661,4 +706,39 @@ mod tests {
661706 let expected_files = vec ! [ target] ;
662707 assert_eq ! ( stored_files( & s. store) . await , expected_files) ;
663708 }
709+
710+ #[ tokio:: test]
711+ async fn upload_og_image ( ) {
712+ let s = Storage :: from_config ( & StorageConfig :: in_memory ( ) ) ;
713+
714+ let bytes = Bytes :: from_static ( b"fake png data" ) ;
715+ s. upload_og_image ( "foo" , bytes. clone ( ) ) . await . unwrap ( ) ;
716+
717+ let expected_files = vec ! [ "og-images/foo.png" ] ;
718+ assert_eq ! ( stored_files( & s. store) . await , expected_files) ;
719+
720+ s. upload_og_image ( "some-long-crate-name" , bytes)
721+ . await
722+ . unwrap ( ) ;
723+
724+ let expected_files = vec ! [ "og-images/foo.png" , "og-images/some-long-crate-name.png" ] ;
725+ assert_eq ! ( stored_files( & s. store) . await , expected_files) ;
726+ }
727+
728+ #[ tokio:: test]
729+ async fn delete_og_image ( ) {
730+ let s = Storage :: from_config ( & StorageConfig :: in_memory ( ) ) ;
731+
732+ let bytes = Bytes :: from_static ( b"fake png data" ) ;
733+ s. upload_og_image ( "foo" , bytes. clone ( ) ) . await . unwrap ( ) ;
734+ s. upload_og_image ( "bar" , bytes) . await . unwrap ( ) ;
735+
736+ let expected_files = vec ! [ "og-images/bar.png" , "og-images/foo.png" ] ;
737+ assert_eq ! ( stored_files( & s. store) . await , expected_files) ;
738+
739+ s. delete_og_image ( "foo" ) . await . unwrap ( ) ;
740+
741+ let expected_files = vec ! [ "og-images/bar.png" ] ;
742+ assert_eq ! ( stored_files( & s. store) . await , expected_files) ;
743+ }
664744}
0 commit comments