From 4697563be4a014c9ad57d95554ce80213a84b6d4 Mon Sep 17 00:00:00 2001 From: BryanFRD Date: Sat, 3 Oct 2026 09:46:33 +0200 Subject: [PATCH] fix(server): announce the bucket settings once at boot --- renovate.json | 5 ++- server/src/config.rs | 4 +- server/src/lib.rs | 93 +++++++++++++++++++++------------------- server/tests/boot_log.rs | 35 ++++++--------- 4 files changed, 68 insertions(+), 69 deletions(-) diff --git a/renovate.json b/renovate.json index 3a7722e5..abf3870b 100644 --- a/renovate.json +++ b/renovate.json @@ -65,7 +65,7 @@ ] }, { - "description": "Analog's Vite plugin reaches into @angular/build internals, so each only works with the release of the other it was built against: analog 2.7.5 needs a hash module that @angular/build 22.2 introduced, and @angular/build 22.2 asserts an initialisation analog 2.7.2 never does. Updated apart, both PRs fail the site build (LFSX#463, LFSX#465). One branch for both.", + "description": "Analog's Vite plugin reaches into @angular/build internals, so each only works with the release of the other it was built against: analog 2.7.5 needs a hash module that @angular/build 22.2 introduced, and @angular/build 22.2 asserts an initialisation analog 2.7.2 never does. Updated apart, both PRs fail the site build (LFSX#463, LFSX#465). One branch for both. Majors stay in the same branch too, since an Analog minor can be the one that needs the next @angular/build major.", "matchPackageNames": [ "@analogjs/**", "@angular/build", @@ -73,7 +73,8 @@ "@angular-devkit/**", "@schematics/angular" ], - "groupName": "Angular build and Analog" + "groupName": "Angular build and Analog", + "separateMajorMinor": false } ] } diff --git a/server/src/config.rs b/server/src/config.rs index fadec6cd..28969872 100644 --- a/server/src/config.rs +++ b/server/src/config.rs @@ -530,9 +530,7 @@ fn is_set(value: Option<&str>) -> bool { } fn allowed(value: Option<&str>) -> Option { - value - .filter(|value| is_set(Some(value))) - .map(|value| Namespaces::parse("LFSX_ALLOWED", Some(value))) + is_set(value).then(|| Namespaces::parse("LFSX_ALLOWED", value)) } // Both variables or neither. One without the other is a configuration that diff --git a/server/src/lib.rs b/server/src/lib.rs index af15731f..6e965de3 100644 --- a/server/src/lib.rs +++ b/server/src/lib.rs @@ -56,6 +56,7 @@ pub fn app(config: Config) -> Router { } announce_access(&config); + announce_storage(&config); let (store, locks) = backends(&config); let authorizer = Authorizer::new(&config.auth); let transfers = (config.max_concurrent_transfers > 0) @@ -322,6 +323,55 @@ fn announce_access(config: &Config) { } } +fn announce_storage(config: &Config) { + let crate::config::Storage::Bucket { presign, cache, .. } = &config.storage else { + return; + }; + + tracing::warn!( + "objects and locks are stored in a bucket: deduplication, rewriting and \ + verification answer 501, and the lfsx_objects_stored and lfsx_store_bytes \ + gauges are not measured: read capacity from the bucket itself" + ); + + if *presign { + if config.encryption_key.is_some() || config.compression.is_some() { + tracing::warn!( + "LFSX_S3_PRESIGN=true, but a codec is configured, so downloads keep \ + streaming through this server: what sits in the bucket is a frame under \ + the plaintext digest, and a client handed that directly would hash it \ + and reject the object" + ); + } else { + tracing::warn!( + "LFSX_S3_PRESIGN=true, downloads are redirected to the bucket, so \ + lfsx_downloaded_bytes stops counting them and the bucket serves the ranges" + ); + } + + if config.encryption_key.is_some() { + tracing::warn!( + "an encryption key is configured, so uploads keep coming through this \ + server rather than going straight to the bucket: an object a client \ + writes itself would arrive unencrypted" + ); + } else if config.compression.is_some() { + tracing::warn!( + "LFSX_COMPRESSION is set, and objects clients upload straight to the \ + bucket arrive uncompressed: only what passes through this server is \ + compressed" + ); + } + } + + if cache.is_some() && *presign { + tracing::warn!( + "LFSX_S3_CACHE_DIR is set with LFSX_S3_PRESIGN=true, so downloads go straight to the \ + bucket and the cache never sees them: the two settings pull in opposite directions" + ); + } +} + fn backends(config: &Config) -> (Store, LockStore) { // Refusing to start beats starting without it. A server that silently wrote // plaintext because a Secret failed to mount is the one failure this feature @@ -360,42 +410,6 @@ fn backends(config: &Config) -> (Store, LockStore) { // reaches into the other to get at them. let keys = keyspace(config).expect("a bucket keyspace for a bucket store"); - tracing::warn!( - "objects and locks are stored in a bucket: deduplication, rewriting and \ - verification answer 501, and the lfsx_objects_stored and lfsx_store_bytes \ - gauges are not measured: read capacity from the bucket itself" - ); - - if *presign { - if config.encryption_key.is_some() || config.compression.is_some() { - tracing::warn!( - "LFSX_S3_PRESIGN=true, but a codec is configured, so downloads keep \ - streaming through this server: what sits in the bucket is a frame under \ - the plaintext digest, and a client handed that directly would hash it \ - and reject the object" - ); - } else { - tracing::warn!( - "LFSX_S3_PRESIGN=true, downloads are redirected to the bucket, so \ - lfsx_downloaded_bytes stops counting them and the bucket serves the ranges" - ); - } - - if config.encryption_key.is_some() { - tracing::warn!( - "an encryption key is configured, so uploads keep coming through this \ - server rather than going straight to the bucket: an object a client \ - writes itself would arrive unencrypted" - ); - } else if config.compression.is_some() { - tracing::warn!( - "LFSX_COMPRESSION is set, and objects clients upload straight to the \ - bucket arrive uncompressed: only what passes through this server is \ - compressed" - ); - } - } - // The locks go with the objects. Left on the volume they would make // the bucket a half measure: capacity would be shared and the one // piece of state a second replica must agree on would not be. @@ -407,13 +421,6 @@ fn backends(config: &Config) -> (Store, LockStore) { .expect("the cache directory is not usable") }); - if disk.is_some() && *presign { - tracing::warn!( - "LFSX_S3_CACHE_DIR is set with LFSX_S3_PRESIGN=true, so downloads go straight to the \ - bucket and the cache never sees them: the two settings pull in opposite directions" - ); - } - ( Store::bucket(S3Store::new(keys.clone(), *presign), local).with_cache(disk), LockStore::bucket(keys).with_conditional_writes(*locking), diff --git a/server/tests/boot_log.rs b/server/tests/boot_log.rs index 57665ffa..575422a2 100644 --- a/server/tests/boot_log.rs +++ b/server/tests/boot_log.rs @@ -30,28 +30,21 @@ async fn a_boot_says_what_access_is_configured_once() { let (api_url, _forge) = forge().await; let config = config(&root, &api_url); - let mut logged = String::new(); - for _ in 0..5 { - let captured = Captured::default(); - { - let _guard = tracing::subscriber::set_default( - tracing_subscriber::fmt() - .with_writer(captured.clone()) - .with_max_level(tracing::Level::INFO) - .with_ansi(false) - .finish(), - ); - - lfsx_server::reclaim(&config).await; - lfsx_server::store(&config); - let _app = lfsx_server::app(config.clone()); - } - - logged = String::from_utf8(captured.0.lock().unwrap().clone()).unwrap(); - if !logged.is_empty() { - break; - } + let captured = Captured::default(); + { + let _guard = tracing::subscriber::set_default( + tracing_subscriber::fmt() + .with_writer(captured.clone()) + .with_max_level(tracing::Level::INFO) + .with_ansi(false) + .finish(), + ); + + lfsx_server::reclaim(&config).await; + lfsx_server::store(&config); + let _app = lfsx_server::app(config.clone()); } + let logged = String::from_utf8(captured.0.lock().unwrap().clone()).unwrap(); assert_eq!( logged.matches("LFSX_ALLOWED is unset").count(),