@@ -19,15 +19,18 @@ use tracing::{instrument, warn};
19
19
20
20
const PREFIX_CRATES : & str = "crates" ;
21
21
const PREFIX_READMES : & str = "readmes" ;
22
+ const PREFIX_OG_IMAGES : & str = "og-images" ;
22
23
const DEFAULT_REGION : & str = "us-west-1" ;
23
24
const CONTENT_TYPE_CRATE : & str = "application/gzip" ;
24
25
const CONTENT_TYPE_GZIP : & str = "application/gzip" ;
25
26
const CONTENT_TYPE_ZIP : & str = "application/zip" ;
26
27
const CONTENT_TYPE_INDEX : & str = "text/plain" ;
27
28
const CONTENT_TYPE_README : & str = "text/html" ;
29
+ const CONTENT_TYPE_OG_IMAGE : & str = "image/png" ;
28
30
const CACHE_CONTROL_IMMUTABLE : & str = "public,max-age=31536000,immutable" ;
29
31
const CACHE_CONTROL_INDEX : & str = "public,max-age=600" ;
30
32
const CACHE_CONTROL_README : & str = "public,max-age=604800" ;
33
+ const CACHE_CONTROL_OG_IMAGE : & str = "public,max-age=86400" ;
31
34
32
35
type StdPath = std:: path:: Path ;
33
36
@@ -209,6 +212,13 @@ impl Storage {
209
212
apply_cdn_prefix ( & self . cdn_prefix , & readme_path ( name, version) ) . replace ( '+' , "%2B" )
210
213
}
211
214
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
+
212
222
/// Returns the URL of an uploaded RSS feed.
213
223
pub fn feed_url ( & self , feed_id : & FeedId < ' _ > ) -> String {
214
224
apply_cdn_prefix ( & self . cdn_prefix , & feed_id. into ( ) ) . replace ( '+' , "%2B" )
@@ -240,6 +250,13 @@ impl Storage {
240
250
self . store . delete ( & path) . await
241
251
}
242
252
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
+
243
260
#[ instrument( skip( self ) ) ]
244
261
pub async fn delete_feed ( & self , feed_id : & FeedId < ' _ > ) -> Result < ( ) > {
245
262
let path = feed_id. into ( ) ;
@@ -270,6 +287,19 @@ impl Storage {
270
287
Ok ( ( ) )
271
288
}
272
289
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
+
273
303
#[ instrument( skip( self , channel) ) ]
274
304
pub async fn upload_feed (
275
305
& self ,
@@ -385,6 +415,10 @@ fn readme_path(name: &str, version: &str) -> Path {
385
415
format ! ( "{PREFIX_READMES}/{name}/{name}-{version}.html" ) . into ( )
386
416
}
387
417
418
+ fn og_image_path ( name : & str ) -> Path {
419
+ format ! ( "{PREFIX_OG_IMAGES}/{name}.png" ) . into ( )
420
+ }
421
+
388
422
fn apply_cdn_prefix ( cdn_prefix : & Option < String > , path : & Path ) -> String {
389
423
match cdn_prefix {
390
424
Some ( cdn_prefix) if !cdn_prefix. starts_with ( "https://" ) => {
@@ -484,6 +518,17 @@ mod tests {
484
518
for ( name, version, expected) in readme_tests {
485
519
assert_eq ! ( storage. readme_location( name, version) , expected) ;
486
520
}
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
+ }
487
532
}
488
533
489
534
#[ test]
@@ -661,4 +706,39 @@ mod tests {
661
706
let expected_files = vec ! [ target] ;
662
707
assert_eq ! ( stored_files( & s. store) . await , expected_files) ;
663
708
}
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
+ }
664
744
}
0 commit comments