Files
MyFSIO/crates/myfsio-server/src/services/s3_client.rs
kqjy 4d923df16c Fix S3 read-path response-header gaps and replication-loop regression
S3 read path (handlers/mod.rs):
- Add Last-Modified + x-amz-meta-* to Range 206 responses
- Add x-amz-server-side-encryption to HEAD/?partNumber= responses
- 304 Not Modified now carries ETag, Last-Modified, version-id, cache headers
- Treat If-Match/If-Unmodified-Since and If-None-Match/If-Modified-Since
  as RFC-9110 pairs on both GET and CopyObject (date header ignored when
  ETag header is present)

Website hosting (middleware/auth.rs):
- Add ETag, Last-Modified, x-amz-server-side-encryption, and x-amz-meta-*
  to website HEAD/200/206 responses so CDN cachers can revalidate

Replication (services/replication.rs, services/s3_client.rs,
middleware/auth.rs, services/site_registry.rs):
- Detect replicated incoming writes via the authenticated principal's
  access key against the site registry's peer_inbound_access_key set.
  The auth middleware inserts a ReplicationPeerRequest extension marker
  on matched requests; handlers skip trigger_replication when set.
  Replaces a forgeable User-Agent substring check.
- Replication retry preflight now probes HeadBucket on the actual target
  bucket (not ListBuckets) and treats any HTTP response as reachable, so
  bucket-scoped credentials no longer block valid retries
- Populate ReplicationFailure.last_error_code from SdkError metadata
- Health probes use a max_attempts=1 client (fast-fail) rather than the
  production retry budget
2026-04-27 23:41:30 +08:00

104 lines
3.2 KiB
Rust

use std::time::Duration;
use aws_config::BehaviorVersion;
use aws_credential_types::Credentials;
use aws_sdk_s3::config::{AppName, Region, SharedCredentialsProvider};
use aws_sdk_s3::Client;
use crate::stores::connections::RemoteConnection;
pub const REPLICATION_USER_AGENT_TAG: &str = "MyFSIO-Replication";
pub struct ClientOptions {
pub connect_timeout: Duration,
pub read_timeout: Duration,
pub max_attempts: u32,
}
impl Default for ClientOptions {
fn default() -> Self {
Self {
connect_timeout: Duration::from_secs(5),
read_timeout: Duration::from_secs(30),
max_attempts: 2,
}
}
}
pub fn build_client(connection: &RemoteConnection, options: &ClientOptions) -> Client {
let credentials = Credentials::new(
connection.access_key.clone(),
connection.secret_key.clone(),
None,
None,
"myfsio-replication",
);
let timeout_config = aws_smithy_types::timeout::TimeoutConfig::builder()
.connect_timeout(options.connect_timeout)
.read_timeout(options.read_timeout)
.build();
let retry_config =
aws_smithy_types::retry::RetryConfig::standard().with_max_attempts(options.max_attempts);
let mut builder = aws_sdk_s3::config::Builder::new()
.behavior_version(BehaviorVersion::latest())
.credentials_provider(SharedCredentialsProvider::new(credentials))
.region(Region::new(connection.region.clone()))
.endpoint_url(connection.endpoint_url.clone())
.force_path_style(true)
.timeout_config(timeout_config)
.retry_config(retry_config);
if let Ok(app_name) = AppName::new(REPLICATION_USER_AGENT_TAG) {
builder = builder.app_name(app_name);
}
Client::from_conf(builder.build())
}
pub fn build_health_client(connection: &RemoteConnection, options: &ClientOptions) -> Client {
let fast_fail = ClientOptions {
connect_timeout: options.connect_timeout,
read_timeout: options.read_timeout,
max_attempts: 1,
};
build_client(connection, &fast_fail)
}
pub async fn check_endpoint_health(client: &Client) -> bool {
match client.list_buckets().send().await {
Ok(_) => true,
Err(err) => {
tracing::warn!("Endpoint health check failed: {:?}", err);
false
}
}
}
pub async fn check_target_bucket_reachable(client: &Client, target_bucket: &str) -> bool {
match client.head_bucket().bucket(target_bucket).send().await {
Ok(_) => true,
Err(err) => match &err {
aws_sdk_s3::error::SdkError::ServiceError(_)
| aws_sdk_s3::error::SdkError::ResponseError(_) => {
tracing::debug!(
"Target-bucket reachability probe for {} got a server response; treating as reachable: {:?}",
target_bucket,
err
);
true
}
_ => {
tracing::warn!(
"Target-bucket reachability probe for {} failed at transport layer: {:?}",
target_bucket,
err
);
false
}
},
}
}