diff --git a/Cargo.lock b/Cargo.lock index 64ac49f6a..29261540e 100644 --- a/Cargo.lock +++ b/Cargo.lock @@ -4210,7 +4210,7 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "39cab71617ae0d63f51a36d69f866391735b51691dbda63cf6f96d042b63efeb" dependencies = [ "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -5668,7 +5668,7 @@ checksum = "3640c1c38b8e4e43584d8df18be5fc6b0aa314ce6ebf51b53313d4306cca8e46" dependencies = [ "hermit-abi", "libc", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -6986,9 +6986,9 @@ checksum = "a3c00a0c9600379bd32f8972de90676a7672cba3bf4886986bc05902afc1e093" [[package]] name = "nvml-wrapper" -version = "0.12.1" +version = "0.13.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "f049ae562349fefb8e837eb15443da1e7c6dcbd8a11f52a228f92220c2e5c85e" +checksum = "d164abbde0b3c03edb9edb9cb8d31a7f5b79015c692b7c771f6e0840e9106b9f" dependencies = [ "bitflags 2.13.1", "libloading", @@ -7000,9 +7000,9 @@ dependencies = [ [[package]] name = "nvml-wrapper-sys" -version = "0.9.1" +version = "0.10.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "6b4d594420fcda43b1c2c4bd44d48974aa3c7a9ab2cbf10dc18e35265767bf0b" +checksum = "5d2079f4c9b6d2170bfb71c6355734ead6c47da75c179847395c31f9f2f66ede" dependencies = [ "libloading", ] @@ -8610,7 +8610,7 @@ dependencies = [ "once_cell", "socket2", "tracing", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -9512,7 +9512,7 @@ dependencies = [ "tokio-util", "tonic", "tower", - "tower-http 0.7.0", + "tower-http 0.7.1", "tracing", "tracing-opentelemetry", "tracing-subscriber", @@ -10943,7 +10943,7 @@ dependencies = [ "errno", "libc", "linux-raw-sys", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -11016,7 +11016,7 @@ dependencies = [ "security-framework", "security-framework-sys", "webpki-root-certs", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -11073,7 +11073,7 @@ checksum = "9774ba4a74de5f7b1c1451ed6cd5285a32eddb5cccb8cc655a4e50009e06477f" [[package]] name = "s3s" version = "0.15.0" -source = "git+https://github.com/rustfs/s3s.git?rev=9c4690d8e73fc8d184031a19b2c4539ebc77d180#9c4690d8e73fc8d184031a19b2c4539ebc77d180" +source = "git+https://github.com/rustfs/s3s.git?rev=c8f575e3b8c7ec9962a722e197bb929efc12a07a#c8f575e3b8c7ec9962a722e197bb929efc12a07a" dependencies = [ "arc-swap", "arrayvec", @@ -11104,6 +11104,7 @@ dependencies = [ "pin-project-lite", "quick-xml", "regex", + "s3s-rfc2047", "s3s-sigv2", "s3s-sigv4", "serde", @@ -11127,28 +11128,45 @@ dependencies = [ "zeroize", ] +[[package]] +name = "s3s-rfc2047" +version = "0.16.0-alpha.1" +source = "git+https://github.com/rustfs/s3s.git?rev=c8f575e3b8c7ec9962a722e197bb929efc12a07a#c8f575e3b8c7ec9962a722e197bb929efc12a07a" +dependencies = [ + "base64-simd", + "thiserror 2.0.20", +] + [[package]] name = "s3s-sigv2" version = "0.16.0-alpha.1" -source = "git+https://github.com/rustfs/s3s.git?rev=9c4690d8e73fc8d184031a19b2c4539ebc77d180#9c4690d8e73fc8d184031a19b2c4539ebc77d180" +source = "git+https://github.com/rustfs/s3s.git?rev=c8f575e3b8c7ec9962a722e197bb929efc12a07a#c8f575e3b8c7ec9962a722e197bb929efc12a07a" dependencies = [ + "base64-simd", + "hmac 0.13.0", "jiff", + "sha1 0.11.0", + "smallvec", "thiserror 2.0.20", ] [[package]] name = "s3s-sigv4" version = "0.16.0-alpha.1" -source = "git+https://github.com/rustfs/s3s.git?rev=9c4690d8e73fc8d184031a19b2c4539ebc77d180#9c4690d8e73fc8d184031a19b2c4539ebc77d180" +source = "git+https://github.com/rustfs/s3s.git?rev=c8f575e3b8c7ec9962a722e197bb929efc12a07a#c8f575e3b8c7ec9962a722e197bb929efc12a07a" dependencies = [ "arrayvec", "base64-simd", "hex-simd", + "hmac 0.13.0", "jiff", "nom 8.0.0", "serde", + "sha2 0.11.0", "smallvec", + "std-next", "thiserror 2.0.20", + "zeroize", ] [[package]] @@ -12040,9 +12058,9 @@ checksum = "13c2bddecc57b384dee18652358fb23172facb8a2c51ccc10d74c157bdea3292" [[package]] name = "suppaftp" -version = "10.0.2" +version = "11.0.0" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "821001051ea3d12a60fb790b8c7cb9a6f5f8698dcfdca4cd533a025fefb0b5b8" +checksum = "46c5095831abc0d7944a2d50d6ec6abcd75b9d165d9377deb3e45798cae2343a" dependencies = [ "async-trait", "chrono", @@ -12233,10 +12251,10 @@ source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "32497e9a4c7b38532efcdebeef879707aa9f794296a4f0244f6f69e9bc8574bd" dependencies = [ "fastrand", - "getrandom 0.3.4", + "getrandom 0.4.3", "once_cell", "rustix", - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] @@ -12729,9 +12747,9 @@ dependencies = [ [[package]] name = "tower-http" -version = "0.7.0" +version = "0.7.1" source = "registry+https://github.com/rust-lang/crates.io-index" -checksum = "b11f75e912b0c2be01b63d8cf8057b8c3f97cf34abb3d431a3a4c8675498e233" +checksum = "08a05a66a4fdd61cbbe0a1d755ffe0ca6aba159dd4820936a0ff8a8278245b9c" dependencies = [ "async-compression", "bitflags 2.13.1", @@ -13355,7 +13373,7 @@ version = "0.1.11" source = "registry+https://github.com/rust-lang/crates.io-index" checksum = "c2a7b1c03c876122aa43f3020e6c3c3ee5c05081c9a00739faf7503aeba10d22" dependencies = [ - "windows-sys 0.52.0", + "windows-sys 0.61.2", ] [[package]] diff --git a/Cargo.toml b/Cargo.toml index 12df2b640..4fa938fb1 100644 --- a/Cargo.toml +++ b/Cargo.toml @@ -174,7 +174,7 @@ tonic = { version = "0.14.6" } tonic-prost = { version = "0.14.6" } tonic-prost-build = { version = "0.14.6" } tower = { version = "0.5.3" } -tower-http = { version = "0.7.0" } +tower-http = { version = "0.7.1" } # Serialization and Data Formats apache-avro = { version = "0.22.0", features = ["snappy", "zstandard"] } @@ -283,7 +283,7 @@ mime_guess = "2.0.5" moka = { version = "0.12.16" } netif = "0.1.6" num_cpus = { version = "1.17.0" } -nvml-wrapper = "0.12.1" +nvml-wrapper = "0.13.0" parking_lot = "0.12.5" path-absolutize = "4.0.1" percent-encoding = "2.3.2" @@ -305,7 +305,7 @@ rustify = { version = "0.7", default-features = false } rustix = { version = "1.1.4" } rust-embed = { version = "8.12.0" } rustc-hash = { version = "2.1.3" } -s3s = { git = "https://github.com/rustfs/s3s.git", rev = "9c4690d8e73fc8d184031a19b2c4539ebc77d180", version = "0.15.0", features = ["minio"] } +s3s = { git = "https://github.com/rustfs/s3s.git", rev = "c8f575e3b8c7ec9962a722e197bb929efc12a07a", version = "0.15.0", features = ["minio"] } serial_test = "4.0.1" shadow-rs = { default-features = false, version = "2.0.0" } siphasher = "1.0.3" @@ -355,7 +355,7 @@ pyroscope = { version = "2.1.1" } # FTP and SFTP libunftp = { version = "0.23.0" } unftp-core = "0.1.0" -suppaftp = { version = "10.0.2" } +suppaftp = { version = "11.0.0" } rcgen = { version = "0.14.10", default-features = false, features = ["aws_lc_rs", "crypto", "pem"] } russh = { version = "0.63.1" } russh-sftp = "2.4.0" diff --git a/docs/architecture/compat-cleanup-register.md b/docs/architecture/compat-cleanup-register.md index 26813f836..dfaffb92f 100644 --- a/docs/architecture/compat-cleanup-register.md +++ b/docs/architecture/compat-cleanup-register.md @@ -31,6 +31,7 @@ for later deletion. - `ns-scanner-rpc-v3` namespace scanner capability and activity handshake: old peers and legacy internode transports lack the authenticated startup-epoch handshake. The oldest peers send an empty activity request and receive a field-empty protocol-0 response. Protocol v4 binds the challenge and response topology but cannot authenticate distributed dirty-usage state. Protocol v5 binds the request version, acknowledgement target and generation, and the response dirty-usage state, but predates set-scoped scanner cache locks. Protocol v6 additionally fences scanner cache lock-domain changes. Current protocol v7 binds the storage-owned movement generation and publication-blocked state, so distributed scanner cycles publish usage only after every peer reports a complete v7 activity proof; v6 responses remain readable but are treated as unverified for publication. Servers retain protocol-0, protocol-v4, and protocol-v6 codecs alongside the current v7 codec for rolling upgrades, while protocol-v5 peers are treated as previous-version peers that cannot safely participate in the new cache lock domain. Scanner selection treats HTTP 404/405/426 and the legacy MethodNotAllowed default as an explicit lack of remote scanner v3 support and assigns those disks to coordinator-driven workers; transient capability failures remain incomplete and do not activate the fallback. Remove the coordinator fallback after the minimum supported RustFS peer version implements namespace scanner protocol v3, remove protocol-0 activity requests and responses after every supported peer implements authenticated scanner activity protocol v4, remove the protocol-v4 activity codec after every supported peer implements protocol v5, and remove protocol-v5 previous-version rejection after every supported peer implements protocol v6; future protocol revisions must keep the same dual-version server/codec window before changing the advertised version. - `#4648` walk-dir stream completion capability: old clients can append fallback output to an already-used metacache writer after a terminal body error, so servers emit terminal walk errors only to clients that sign the `walk_dir_stream_completion=error-v1` query capability and its request-body digest. Remove the legacy clean-EOF path after the minimum supported RustFS peer version always advertises this capability. - `heal-rpc-auth-v2` internode gRPC authentication: servers temporarily accept legacy prefix signatures so old peers remain available during rolling upgrades. Remove the legacy fallback after the minimum supported RustFS peer version sends v2 authentication on every internode gRPC request. +- `put-file-auth-epoch-strict` internode put_file epoch compatibility: rc.2 peers can cache a remote put_file capability before that remote node restarts, then continue sending v1 authenticated uploads with the old server epoch; those peers cannot recover from the 409 conflict used by newer clients to trigger a re-probe. Servers temporarily accept signed, non-nil stale put_file epochs while legacy put_file auth remains non-strict so mixed-version rolling upgrades can finish multipart/object writes. Remove the stale-epoch fallback after the minimum supported RustFS peer version re-probes put_file capability after server-epoch conflicts and legacy put_file auth is no longer accepted. - `disk-mutation-body-digest` internode mutating disk RPCs: servers temporarily accept mutating disk RPCs (RenameData, DeleteVersion, DeleteVersions, WriteMetadata, UpdateMetadata, WriteAll, Delete, DeletePaths, RenameFile, RenamePart, DeleteVolume, MakeVolume, MakeVolumes) that carry no signature-bound canonical body digest, so peers from releases that predate body-digest signing remain available during rolling upgrades. Accepted digestless mutations increment the internode body-digest fallback counter; that counter must read zero fleet-wide across a release window before RUSTFS_INTERNODE_RPC_BODY_DIGEST_STRICT is enabled. Because body-bound requests now consume replay-cache nonces on the receiver, deploy the raised RUSTFS_INTERNODE_RPC_REPLAY_CACHE_CAPACITY default fleet-wide before enabling strict mode, and watch the internode replay-cache overflow counter for undersized capacity during the rollout. Remove the digestless fallback after the minimum supported RustFS peer version body-binds every mutating disk RPC. - `heal-status-rpc-v1` node heal status capability: new peers treat an unimplemented BackgroundHealStatus RPC as an explicitly incomplete rolling-upgrade response. Remove the fallback after the minimum supported RustFS peer version implements BackgroundHealStatus. - `backlog-1316` legacy encrypted multipart range seek: the feature remains opt-in until every server that can initiate, write, or complete multipart uploads supports the candidate-to-final marker protocol and uploadId commit lock, and pre-upgrade multipart uploads have drained. Remove the RUSTFS_ENCRYPTED_RANGE_SEEK switch after the minimum supported release does so; keep the quorum marker and malformed-layout full-read guards permanently. diff --git a/rustfs/src/admin/router.rs b/rustfs/src/admin/router.rs index 272a93386..661795244 100644 --- a/rustfs/src/admin/router.rs +++ b/rustfs/src/admin/router.rs @@ -1013,7 +1013,7 @@ fn build_get_object_response_headers(output: &GetObjectOutput, base_headers: &He insert_string_header(&mut headers, http::header::LAST_MODIFIED, format_timestamp_http_date(last_modified)?)?; } if let Some(expires) = &output.expires { - insert_string_header(&mut headers, http::header::EXPIRES, format_timestamp_http_date(expires)?)?; + insert_string_header(&mut headers, http::header::EXPIRES, expires.clone())?; } if let Some(version_id) = &output.version_id { insert_string_header(&mut headers, HeaderName::from_static("x-amz-version-id"), version_id.clone())?; diff --git a/rustfs/src/app/metadata_route.rs b/rustfs/src/app/metadata_route.rs index 3a49a41e7..6f6cf90c4 100644 --- a/rustfs/src/app/metadata_route.rs +++ b/rustfs/src/app/metadata_route.rs @@ -333,7 +333,7 @@ mod tests { #[test] fn metadata_operation_matches_virtual_hosted_bucket_root() { - let host = MultiDomain::new(["example.com", "example.com:9000"]).expect("valid test host domain"); + let host = MultiDomain::new(["example.com:9000"]).expect("valid test host domain"); let mut headers = HeaderMap::new(); headers.insert(HOST, "demo-bucket.example.com:9000".parse().expect("valid host header")); @@ -351,7 +351,7 @@ mod tests { #[test] fn metadata_operation_matches_unconfigured_host_fallbacks() { - let host = MultiDomain::new(["s3.example.com", "s3.example.com:9000"]).expect("valid test host domain"); + let host = MultiDomain::new(["s3.example.com:9000"]).expect("valid test host domain"); let mut path_style_headers = HeaderMap::new(); path_style_headers.insert(HOST, "localhost:9000".parse().expect("valid host header")); diff --git a/rustfs/src/app/object/copy.rs b/rustfs/src/app/object/copy.rs index a2c6df010..cb9c04ea7 100644 --- a/rustfs/src/app/object/copy.rs +++ b/rustfs/src/app/object/copy.rs @@ -265,6 +265,7 @@ impl DefaultObjectUsecase { )); } }; + let expires_timestamp = parse_expires_header(expires.as_deref())?; let replacement_metadata = if replaces_metadata { validate_archive_content_encoding(&key, content_type.as_deref(), content_encoding.as_deref())?; let mut replacement_metadata = metadata.unwrap_or_default(); @@ -276,7 +277,7 @@ impl DefaultObjectUsecase { content_encoding.as_deref(), content_language.as_deref(), content_type.as_deref(), - expires.as_ref(), + expires_timestamp.as_ref(), website_redirect_location.as_deref(), )?; Some(replacement_metadata) @@ -609,7 +610,7 @@ impl DefaultObjectUsecase { user_defined = replacement_metadata; src_info.content_type = content_type.clone(); src_info.content_encoding = content_encoding.as_deref().and_then(normalize_content_encoding_for_storage); - src_info.expires = expires.map(OffsetDateTime::from); + src_info.expires = expires_timestamp.map(OffsetDateTime::from); } else if metadata_directive.is_some() || website_redirect_location.is_some() { user_defined.retain(|key, _| !key.eq_ignore_ascii_case(AMZ_WEBSITE_REDIRECT_LOCATION)); if let Some(website_redirect_location) = website_redirect_location { @@ -923,6 +924,7 @@ mod tests { sse_algorithm: ServerSideEncryption::from(String::from("garbage")), kms_master_key_id: None, }), + blocked_encryption_types: None, bucket_key_enabled: None, }], }; diff --git a/rustfs/src/app/object/head.rs b/rustfs/src/app/object/head.rs index 7c6055773..7294eb088 100644 --- a/rustfs/src/app/object/head.rs +++ b/rustfs/src/app/object/head.rs @@ -311,7 +311,11 @@ impl DefaultObjectUsecase { let content_disposition = metadata_map.get("content-disposition").cloned(); let content_language = metadata_map.get("content-language").cloned(); let website_redirect_location = metadata_map.get(AMZ_WEBSITE_REDIRECT_LOCATION).cloned(); - let expires = info.expires.map(Timestamp::from); + let expires = info + .expires + .map(Timestamp::from) + .map(|expires| format_expires_header(&expires)) + .transpose()?; // Calculate tag count from user_tags already in ObjectInfo // This avoids an additional API call since user_tags is already populated by get_object_info diff --git a/rustfs/src/app/object/put.rs b/rustfs/src/app/object/put.rs index 5de84e2f2..62cea9b39 100644 --- a/rustfs/src/app/object/put.rs +++ b/rustfs/src/app/object/put.rs @@ -829,12 +829,13 @@ pub(super) fn apply_put_request_metadata( content_encoding: Option, content_language: Option, content_type: Option, - expires: Option, + expires: Option, website_redirect_location: Option, tagging: Option, storage_class: Option, ) -> S3Result<()> { namespace_reserved_user_metadata(metadata); + let expires = parse_expires_header(expires.as_deref())?; apply_standard_object_metadata( metadata, cache_control.as_deref(), @@ -2399,6 +2400,7 @@ mod tests { sse_algorithm: ServerSideEncryption::from_static(algorithm), kms_master_key_id: kms_key_id.map(|id| SSEKMSKeyId::from(id.to_string())), }), + blocked_encryption_types: None, bucket_key_enabled: None, }], }; diff --git a/rustfs/src/app/object/shared.rs b/rustfs/src/app/object/shared.rs index 17f7b89a2..8698bb908 100644 --- a/rustfs/src/app/object/shared.rs +++ b/rustfs/src/app/object/shared.rs @@ -475,13 +475,25 @@ pub(super) fn expected_current_version_id(headers: &HeaderMap) -> S3Result) -> S3Result> { + expires + .map(|expires| { + Timestamp::parse(TimestampFormat::HttpDate, expires).map_err(|_| s3_error!(InvalidArgument, "Invalid Expires header")) + }) + .transpose() +} + +pub(super) fn format_expires_header(expires: &Timestamp) -> S3Result { + let mut formatted = Vec::new(); + expires + .format(TimestampFormat::HttpDate, &mut formatted) + .map_err(|e| ApiError::from(StorageError::other(format!("Invalid expires timestamp: {e}"))))?; + Ok(String::from_utf8_lossy(&formatted).into_owned()) +} + pub(super) fn insert_expires_metadata(metadata: &mut HashMap, expires: Option<&Timestamp>) -> S3Result<()> { if let Some(expires) = expires { - let mut formatted = Vec::new(); - expires - .format(TimestampFormat::HttpDate, &mut formatted) - .map_err(|e| ApiError::from(StorageError::other(format!("Invalid expires timestamp: {e}"))))?; - metadata.insert("expires".to_string(), String::from_utf8_lossy(&formatted).into_owned()); + metadata.insert("expires".to_string(), format_expires_header(expires)?); } Ok(()) } @@ -802,6 +814,22 @@ mod tests { assert_eq!(throttle.claim(IO_QUEUE_CONGESTION_WARN_INTERVAL_MS + 1), None); } + #[test] + fn parse_expires_header_accepts_http_date() { + let expires = parse_expires_header(Some("Wed, 21 Oct 2015 07:28:00 GMT")) + .expect("valid Expires header should parse") + .expect("header should be present"); + + assert_eq!(format_expires_header(&expires).unwrap(), "Wed, 21 Oct 2015 07:28:00 GMT"); + } + + #[test] + fn parse_expires_header_rejects_invalid_http_date() { + let err = parse_expires_header(Some("not-a-date")).expect_err("invalid Expires header should fail"); + + assert_eq!(err.code(), &S3ErrorCode::InvalidArgument); + } + // classify_response_checksums is the single point that splits decrypted checksum // pairs into the five s3s-typed fields and the additional-algorithm `extra` // headers, replacing five copies of the loop. Lock its behaviour (#1252). @@ -938,6 +966,7 @@ mod tests { sse_algorithm: ServerSideEncryption::from(String::from(algorithm)), kms_master_key_id: kms_key_id.map(|id| SSEKMSKeyId::from(id.to_string())), }), + blocked_encryption_types: None, bucket_key_enabled: None, }], } diff --git a/rustfs/src/kms_deletion_gate.rs b/rustfs/src/kms_deletion_gate.rs index 47ce9e38a..247e75e47 100644 --- a/rustfs/src/kms_deletion_gate.rs +++ b/rustfs/src/kms_deletion_gate.rs @@ -184,6 +184,7 @@ mod tests { sse_algorithm: ServerSideEncryption::from_static(ServerSideEncryption::AWS_KMS), kms_master_key_id: key_id.map(str::to_string), }), + blocked_encryption_types: None, bucket_key_enabled: None, }], } diff --git a/rustfs/src/storage/rpc/http_service.rs b/rustfs/src/storage/rpc/http_service.rs index 052dbaff5..d910ff42e 100644 --- a/rustfs/src/storage/rpc/http_service.rs +++ b/rustfs/src/storage/rpc/http_service.rs @@ -370,6 +370,18 @@ fn put_file_server_epoch_matches(query: &PutFileQuery) -> bool { query.put_file_server_epoch == Some(*PUT_FILE_CAPABILITY_SERVER_EPOCH) } +fn put_file_server_epoch_accepted(query: &PutFileQuery, strict: bool) -> bool { + if put_file_server_epoch_matches(query) { + return true; + } + if strict { + return false; + } + + // RUSTFS_COMPAT_TODO(put-file-auth-epoch-strict): accept signed, non-nil stale epochs because rc.2 peers can cache a server epoch before a rolling restart and cannot recover from the v1 409. Remove after the minimum supported RustFS peer version re-probes put_file capability after server-epoch conflicts and legacy put_file auth is no longer accepted. + query.put_file_server_epoch.is_some_and(|epoch| !epoch.is_nil()) +} + impl Service> for InternodeRpcService where S: Service, Response = Response> + Clone + Send + 'static, @@ -1346,7 +1358,7 @@ async fn handle_put_file(req: Request, require_auth: bool) -> Response if require_auth && auth_nonce.is_none() { return response_with_status(StatusCode::FORBIDDEN, "invalid put_file auth: put_file auth required"); } - if require_auth && !put_file_server_epoch_matches(&query) { + if require_auth && !put_file_server_epoch_accepted(&query, *PUT_FILE_AUTH_STRICT) { return response_with_status(StatusCode::CONFLICT, "put_file capability server epoch changed"); } if let Some(nonce) = auth_nonce @@ -1690,8 +1702,8 @@ mod tests { READ_FILE_STREAM_PATH, WALK_DIR_BODY_SHA256_QUERY, WALK_DIR_PATH, WalkDirQuery, append_walk_dir_completion, internode_http_operation, internode_rpc_subsystem, is_internode_rpc_path, ns_scanner_response_body, ns_scanner_server_epoch_matches, put_body_size_mismatch, put_file_auth_nonce, put_file_capability_response, - put_file_server_epoch_matches, put_file_stage_error_message, put_file_target_lock, read_file_body_stream, - read_file_stream_buffer_size, remote_scanner_claim_rejection, response_with_disk_error, + put_file_server_epoch_accepted, put_file_server_epoch_matches, put_file_stage_error_message, put_file_target_lock, + read_file_body_stream, read_file_stream_buffer_size, remote_scanner_claim_rejection, response_with_disk_error, supports_walk_dir_stream_completion, validate_walk_dir_completion_request, verify_internode_rpc_signature, verify_ns_scanner_body_digest, verify_walk_dir_body_digest, walk_dir_response_body, write_authenticated_put_file, write_body_chunks_to_writer, write_put_file_body_chunks_to_writer, @@ -1832,7 +1844,7 @@ mod tests { for (server_epoch, expected_status) in [ (None, StatusCode::CONFLICT), - (Some(uuid::Uuid::new_v4()), StatusCode::CONFLICT), + (Some(uuid::Uuid::new_v4()), StatusCode::BAD_REQUEST), (Some(*super::PUT_FILE_CAPABILITY_SERVER_EPOCH), StatusCode::BAD_REQUEST), ] { let nonce = uuid::Uuid::new_v4(); @@ -2251,14 +2263,23 @@ mod tests { assert_eq!(put_file_auth_nonce(&query).expect("v1 auth should parse"), Some(nonce)); assert!(put_file_server_epoch_matches(&query)); + assert!(put_file_server_epoch_accepted(&query, false)); + assert!(put_file_server_epoch_accepted(&query, true)); let mut stale_epoch = query.clone(); stale_epoch.put_file_server_epoch = Some(uuid::Uuid::new_v4()); assert!(!put_file_server_epoch_matches(&stale_epoch)); + assert!(put_file_server_epoch_accepted(&stale_epoch, false)); + assert!(!put_file_server_epoch_accepted(&stale_epoch, true)); let mut missing_epoch = query.clone(); missing_epoch.put_file_server_epoch = None; assert!(!put_file_server_epoch_matches(&missing_epoch)); + assert!(!put_file_server_epoch_accepted(&missing_epoch, false)); + + let mut nil_epoch = query.clone(); + nil_epoch.put_file_server_epoch = Some(uuid::Uuid::nil()); + assert!(!put_file_server_epoch_accepted(&nil_epoch, false)); let mut append = query.clone(); append.append = true;