diff options
| author | Dan Engelbrecht <[email protected]> | 2026-04-20 17:03:23 +0200 |
|---|---|---|
| committer | GitHub Enterprise <[email protected]> | 2026-04-20 17:03:23 +0200 |
| commit | d38156989508e63ee370998508267dc2cebd616c (patch) | |
| tree | 173fe07a5bf882efbcadf7f1c1ccad96b2101f3b /src/zenutil/cloud/s3client.cpp | |
| parent | zen history command (#987) (diff) | |
| download | archived-zen-d38156989508e63ee370998508267dc2cebd616c.tar.xz archived-zen-d38156989508e63ee370998508267dc2cebd616c.zip | |
s3 dehydration touch cas (#977)
* add Touch() function to s3 client
* touch all used cas files in s3 dehydration path
Diffstat (limited to 'src/zenutil/cloud/s3client.cpp')
| -rw-r--r-- | src/zenutil/cloud/s3client.cpp | 92 |
1 files changed, 91 insertions, 1 deletions
diff --git a/src/zenutil/cloud/s3client.cpp b/src/zenutil/cloud/s3client.cpp index 83238f5cc..3d6dca562 100644 --- a/src/zenutil/cloud/s3client.cpp +++ b/src/zenutil/cloud/s3client.cpp @@ -286,7 +286,11 @@ S3Client::GetSigningKey(std::string_view DateStamp) } HttpClient::KeyValueMap -S3Client::SignRequest(std::string_view Method, std::string_view Path, std::string_view CanonicalQueryString, std::string_view PayloadHash) +S3Client::SignRequest(std::string_view Method, + std::string_view Path, + std::string_view CanonicalQueryString, + std::string_view PayloadHash, + std::span<const std::pair<std::string, std::string>> ExtraSignedHeaders) { SigV4Credentials Credentials = GetCurrentCredentials(); @@ -294,6 +298,7 @@ S3Client::SignRequest(std::string_view Method, std::string_view Path, std::strin // Build sorted headers to sign (must be sorted by lowercase name) std::vector<std::pair<std::string, std::string>> HeadersToSign; + HeadersToSign.reserve(4 + ExtraSignedHeaders.size()); HeadersToSign.emplace_back("host", m_Host); HeadersToSign.emplace_back("x-amz-content-sha256", std::string(PayloadHash)); HeadersToSign.emplace_back("x-amz-date", AmzDate); @@ -301,6 +306,10 @@ S3Client::SignRequest(std::string_view Method, std::string_view Path, std::strin { HeadersToSign.emplace_back("x-amz-security-token", Credentials.SessionToken); } + for (const auto& [K, V] : ExtraSignedHeaders) + { + HeadersToSign.emplace_back(K, V); + } std::sort(HeadersToSign.begin(), HeadersToSign.end()); std::string_view DateStamp(AmzDate.data(), 8); @@ -317,6 +326,10 @@ S3Client::SignRequest(std::string_view Method, std::string_view Path, std::strin { Result->emplace("x-amz-security-token", Credentials.SessionToken); } + for (const auto& [K, V] : ExtraSignedHeaders) + { + Result->emplace(K, V); + } return Result; } @@ -443,6 +456,47 @@ S3Client::DeleteObject(std::string_view Key) return {}; } +S3Result +S3Client::Touch(std::string_view Key) +{ + std::string Path = KeyToPath(Key); + + // x-amz-copy-source is always "/bucket/key" regardless of addressing style. + // Key must be URI-encoded except for '/' separators. When source and destination + // are identical, REPLACE is required; COPY is rejected with InvalidRequest. + const std::array<std::pair<std::string, std::string>, 2> ExtraSigned{{ + {"x-amz-copy-source", fmt::format("/{}/{}", m_BucketName, AwsUriEncode(Key, false))}, + {"x-amz-metadata-directive", "REPLACE"}, + }}; + + HttpClient::KeyValueMap Headers = SignRequest("PUT", Path, "", EmptyPayloadHash, ExtraSigned); + + HttpClient::Response Response = m_HttpClient.Put(Path, IoBuffer{}, Headers); + if (!Response.IsSuccess()) + { + std::string Err = Response.ErrorMessage("S3 Touch failed"); + ZEN_WARN("S3 Touch '{}' failed: {}", Key, Err); + return S3Result{std::move(Err)}; + } + + // Copy operations can return HTTP 200 with an error in the XML body. + std::string_view ResponseBody = Response.AsText(); + if (ResponseBody.find("<Error>") != std::string_view::npos) + { + std::string_view ErrorCode = ExtractXmlValue(ResponseBody, "Code"); + std::string_view ErrorMessage = ExtractXmlValue(ResponseBody, "Message"); + std::string Err = fmt::format("S3 Touch '{}' returned error: {} - {}", Key, ErrorCode, ErrorMessage); + ZEN_WARN("{}", Err); + return S3Result{std::move(Err)}; + } + + if (m_Verbose) + { + ZEN_INFO("S3 Touch '{}' succeeded", Key); + } + return {}; +} + S3HeadObjectResult S3Client::HeadObject(std::string_view Key) { @@ -983,6 +1037,42 @@ TEST_CASE("s3client.minio_integration") CHECK(HeadRes2.Status == HeadObjectResult::NotFound); } + // -- touch ---------------------------------------------------------------- + { + std::string_view TestData = "touch-me"sv; + IoBuffer Content = IoBufferBuilder::MakeFromMemory(MakeMemoryView(TestData)); + S3Result PutRes = Client.PutObject("touch/obj.txt", std::move(Content)); + REQUIRE(PutRes.IsSuccess()); + + S3HeadObjectResult Before = Client.HeadObject("touch/obj.txt"); + REQUIRE(Before.IsSuccess()); + REQUIRE(Before.Status == HeadObjectResult::Found); + + // S3 LastModified has second precision; sleep past the second boundary so + // the touched timestamp is strictly greater. + Sleep(1100); + + S3Result TouchRes = Client.Touch("touch/obj.txt"); + REQUIRE(TouchRes.IsSuccess()); + + S3HeadObjectResult After = Client.HeadObject("touch/obj.txt"); + REQUIRE(After.IsSuccess()); + REQUIRE(After.Status == HeadObjectResult::Found); + CHECK(After.Info.Size == Before.Info.Size); + CHECK(After.Info.LastModified != Before.Info.LastModified); + + // Content must be unchanged by a self-copy. + S3GetObjectResult GetRes = Client.GetObject("touch/obj.txt"); + REQUIRE(GetRes.IsSuccess()); + CHECK(GetRes.AsText() == TestData); + + // Touching a missing key must fail. + S3Result MissRes = Client.Touch("touch/does-not-exist.txt"); + CHECK_FALSE(MissRes.IsSuccess()); + + Client.DeleteObject("touch/obj.txt"); + } + // -- head_not_found ------------------------------------------------------- { S3HeadObjectResult Res = Client.HeadObject("nonexistent/key.dat"); |