Skip to content

Commit d9569f6

Browse files
committed
storage: Implement OG image storage functionality
1 parent 5616b95 commit d9569f6

File tree

1 file changed

+80
-0
lines changed

1 file changed

+80
-0
lines changed

src/storage.rs

Lines changed: 80 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -19,15 +19,18 @@ use tracing::{instrument, warn};
1919

2020
const PREFIX_CRATES: &str = "crates";
2121
const PREFIX_READMES: &str = "readmes";
22+
const PREFIX_OG_IMAGES: &str = "og-images";
2223
const DEFAULT_REGION: &str = "us-west-1";
2324
const CONTENT_TYPE_CRATE: &str = "application/gzip";
2425
const CONTENT_TYPE_GZIP: &str = "application/gzip";
2526
const CONTENT_TYPE_ZIP: &str = "application/zip";
2627
const CONTENT_TYPE_INDEX: &str = "text/plain";
2728
const CONTENT_TYPE_README: &str = "text/html";
29+
const CONTENT_TYPE_OG_IMAGE: &str = "image/png";
2830
const CACHE_CONTROL_IMMUTABLE: &str = "public,max-age=31536000,immutable";
2931
const CACHE_CONTROL_INDEX: &str = "public,max-age=600";
3032
const CACHE_CONTROL_README: &str = "public,max-age=604800";
33+
const CACHE_CONTROL_OG_IMAGE: &str = "public,max-age=86400";
3134

3235
type 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+
388422
fn 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

Comments
 (0)