Skip to content

Commit 33caffc

Browse files
authored
Merge pull request #14075 from Turbo87/cache-tags-uploads
storage: Add CDN cache-tag metadata on uploads
2 parents 583d4df + 6d3f88a commit 33caffc

4 files changed

Lines changed: 139 additions & 10 deletions

File tree

src/config/features.rs

Lines changed: 7 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -11,16 +11,23 @@ pub struct FeaturesConfig {
1111
///
1212
/// Read from the `ZIP_ARCHIVES_ENABLED` environment variable.
1313
pub zip_archives_enabled: bool,
14+
15+
/// Emit CDN cache tags as storage metadata on uploads.
16+
///
17+
/// Read from the `CACHE_TAGS_ENABLED` environment variable.
18+
pub cache_tags_enabled: bool,
1419
}
1520

1621
impl FeaturesConfig {
1722
pub fn from_env() -> anyhow::Result<Self> {
1823
let index_include_pubtime = var_parsed("INDEX_INCLUDE_PUBTIME")?.unwrap_or(false);
1924
let zip_archives_enabled = var_parsed("ZIP_ARCHIVES_ENABLED")?.unwrap_or(false);
25+
let cache_tags_enabled = var_parsed("CACHE_TAGS_ENABLED")?.unwrap_or(false);
2026

2127
Ok(Self {
2228
index_include_pubtime,
2329
zip_archives_enabled,
30+
cache_tags_enabled,
2431
})
2532
}
2633
}

src/config/server.rs

Lines changed: 5 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -111,7 +111,10 @@ impl Server {
111111

112112
let max_blocking_threads = var_parsed("SERVER_THREADS")?;
113113

114-
let storage = StorageConfig::from_environment();
114+
let features = FeaturesConfig::from_env()?;
115+
116+
let mut storage = StorageConfig::from_environment();
117+
storage.cache_tags_enabled = features.cache_tags_enabled;
115118

116119
let domain_name = dotenvy::var("DOMAIN_NAME").unwrap_or_else(|_| "crates.io".into());
117120
let trustpub_audience = var("TRUSTPUB_AUDIENCE")?.unwrap_or_else(|| domain_name.clone());
@@ -147,7 +150,7 @@ impl Server {
147150
trustpub_audience,
148151
disable_token_creation,
149152
banner_message,
150-
features: FeaturesConfig::from_env()?,
153+
features,
151154
index_archive_url: var_parsed("GIT_ARCHIVE_REPO_URL")?,
152155
postgres_bin_dir: var_parsed("POSTGRES_BIN_DIR")?,
153156
})

src/storage.rs

Lines changed: 125 additions & 8 deletions
Original file line numberDiff line numberDiff line change
@@ -31,6 +31,7 @@ const CACHE_CONTROL_OG_IMAGE: &str = "public,max-age=86400";
3131
pub 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

115120
impl 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;

src/tests/util/test_app.rs

Lines changed: 2 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -564,6 +564,7 @@ fn simple_config() -> config::Server {
564564

565565
let mut storage = StorageConfig::in_memory();
566566
storage.cdn_prefix = Some("static.crates.io".to_string());
567+
storage.cache_tags_enabled = true;
567568

568569
config::Server {
569570
base,
@@ -614,6 +615,7 @@ fn simple_config() -> config::Server {
614615
features: FeaturesConfig {
615616
index_include_pubtime: false,
616617
zip_archives_enabled: true,
618+
cache_tags_enabled: true,
617619
},
618620
index_archive_url: None,
619621
postgres_bin_dir: None,

0 commit comments

Comments
 (0)