Skip to content

Commit 6fa1a1e

Browse files
committed
HeadBucket and HeadObject supported
1 parent 765035e commit 6fa1a1e

5 files changed

Lines changed: 375 additions & 23 deletions

File tree

README.md

Lines changed: 15 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -1,7 +1,18 @@
11
# s3cpp
22

33
> [!WARNING]
4-
> **WIP** Currently supporting ListObjectsV2, CreateBucket, DeleteBucket, GetObject, PutObject and DeleteObject on MinIO instances
4+
> **WIP** Currently supporting:
5+
>
6+
> - ListObjectsV2
7+
> - CreateBucket
8+
> - DeleteBucket
9+
> - HeadBucket
10+
> - GetObject
11+
> - PutObject
12+
> - DeleteObject
13+
> - HeadObject
14+
>
15+
> On MinIO instances
516
617
A lightweight C++ client library for AWS S3, with zero 3rd party C++ dependencies (only libcurl and OpenSSL). Inspired by the AWS SDK for Go.
718

@@ -96,19 +107,8 @@ Checking if a bucket exists:
96107
#include <s3cpp/s3.h>
97108

98109
bool BucketExists(S3Client& client, const std::string& bucketName) {
99-
auto result = client.ListObjects(bucketName, {.MaxKeys = 1});
100-
101-
if (!result) {
102-
// Check the Resource Error field
103-
if (result.error().Resource == "/Does-not-exist") {
104-
return false;
105-
}
106-
// Other errors
107-
std::println("Error checking bucket: {}", result.error().Message);
108-
return false;
109-
}
110-
111-
return true;
110+
auto result = client.HeadBucket(bucketName);
111+
return result.has_value();
112112
}
113113

114114
int main() {
@@ -182,4 +182,4 @@ $ docker run -d -p 9000:9000 -p 9001:9001 \
182182
server /data --console-address ":9001"
183183
```
184184

185-
The full test suite contains 56 tests
185+
The full test suite contains 60 tests

src/s3cpp/s3.cpp

Lines changed: 232 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -329,6 +329,134 @@ std::expected<void, Error> S3Client::DeleteBucket(const std::string& bucket, con
329329
return std::unexpected<Error>(deserializeError(XMLBody));
330330
}
331331

332+
std::expected<HeadBucketResult, Error> S3Client::HeadBucket(const std::string& bucket, const HeadBucketInput& options) {
333+
std::string url = buildURL(bucket);
334+
335+
HttpRequest req = Client.head(url).header("Host", getHostHeader(bucket));
336+
337+
// opt headers
338+
if (options.ExpectedBucketOwner.has_value())
339+
req.header("x-amz-expected-bucket-owner", std::move(options.ExpectedBucketOwner.value()));
340+
341+
Signer.sign(req);
342+
HttpResponse res = req.execute();
343+
344+
if (res.status() == 200) {
345+
return deserializeHeadBucketResult(res.headers());
346+
}
347+
348+
// HEAD requests dont return error bodies, parse it from headers
349+
Error error;
350+
const auto& headers = res.headers();
351+
if (headers.contains("X-Minio-Error-Code")) {
352+
error.Code = headers.at("X-Minio-Error-Code");
353+
if (headers.contains("X-Minio-Error-Desc")) {
354+
error.Message = headers.at("X-Minio-Error-Desc");
355+
}
356+
} else if (headers.contains("x-amz-error-code")) {
357+
error.Code = headers.at("x-amz-error-code");
358+
if (headers.contains("x-amz-error-message")) {
359+
error.Message = headers.at("x-amz-error-message");
360+
}
361+
} else {
362+
error.Code = "UnknownError";
363+
error.Message = std::format("HTTP {}", res.status());
364+
}
365+
366+
return std::unexpected<Error>(error);
367+
}
368+
369+
std::expected<HeadObjectResult, Error> S3Client::HeadObject(const std::string& bucket, const std::string& key, const HeadObjectInput& options) {
370+
std::string url = buildURL(bucket) + std::format("/{}", key);
371+
372+
// Query params
373+
bool firstParam = true;
374+
if (options.partNumber.has_value()) {
375+
url += std::format("{}part-number={}", firstParam ? "?" : "&", options.partNumber.value());
376+
firstParam = false;
377+
}
378+
if (options.versionId.has_value()) {
379+
url += std::format("{}versionId={}", firstParam ? "?" : "&", options.versionId.value());
380+
firstParam = false;
381+
}
382+
if (options.response_cache_control.has_value()) {
383+
url += std::format("{}response-cache-control={}", firstParam ? "?" : "&", options.response_cache_control.value());
384+
firstParam = false;
385+
}
386+
if (options.response_content_disposition.has_value()) {
387+
url += std::format("{}response-content-disposition={}", firstParam ? "?" : "&", options.response_content_disposition.value());
388+
firstParam = false;
389+
}
390+
if (options.response_content_encoding.has_value()) {
391+
url += std::format("{}response-content-encoding={}", firstParam ? "?" : "&", options.response_content_encoding.value());
392+
firstParam = false;
393+
}
394+
if (options.response_content_language.has_value()) {
395+
url += std::format("{}response-content-language={}", firstParam ? "?" : "&", options.response_content_language.value());
396+
firstParam = false;
397+
}
398+
if (options.response_content_type.has_value()) {
399+
url += std::format("{}response-content-type={}", firstParam ? "?" : "&", options.response_content_type.value());
400+
firstParam = false;
401+
}
402+
if (options.response_expires.has_value()) {
403+
url += std::format("{}response-expires={}", firstParam ? "?" : "&", options.response_expires.value());
404+
firstParam = false;
405+
}
406+
407+
HttpRequest req = Client.head(url).header("Host", getHostHeader(bucket));
408+
409+
// opt headers
410+
if (options.If_Match.has_value())
411+
req.header("If-Match", options.If_Match.value());
412+
if (options.If_Modified_Since.has_value())
413+
req.header("If-Modified-Since", options.If_Modified_Since.value());
414+
if (options.If_None_Match.has_value())
415+
req.header("If-None-Match", options.If_None_Match.value());
416+
if (options.If_Unmodified_Since.has_value())
417+
req.header("If-Unmodified-Since", options.If_Unmodified_Since.value());
418+
if (options.Range.has_value())
419+
req.header("Range", options.Range.value());
420+
if (options.CheckSumMode.has_value())
421+
req.header("x-amz-checksum-mode", options.CheckSumMode.value());
422+
if (options.ExpectedBucketOwner.has_value())
423+
req.header("x-amz-expected-bucket-owner", options.ExpectedBucketOwner.value());
424+
if (options.RequestPayer.has_value())
425+
req.header("x-amz-request-payer", options.RequestPayer.value());
426+
if (options.SideEncryptionCustomerAlgorithm.has_value())
427+
req.header("x-amz-server-side-encryption-customer-algorithm", options.SideEncryptionCustomerAlgorithm.value());
428+
if (options.SideEncryptionCustomerKey.has_value())
429+
req.header("x-amz-server-side-encryption-customer-key", options.SideEncryptionCustomerKey.value());
430+
if (options.SideEncryptionCustomerKeyMD5.has_value())
431+
req.header("x-amz-server-side-encryption-customer-key-MD5", options.SideEncryptionCustomerKeyMD5.value());
432+
433+
Signer.sign(req);
434+
HttpResponse res = req.execute();
435+
436+
if (res.status() == 200) {
437+
return deserializeHeadObjectResult(res.headers());
438+
}
439+
440+
// HEAD requests dont return error bodies, parse it from headers
441+
Error error;
442+
const auto& headers = res.headers();
443+
if (headers.contains("X-Minio-Error-Code")) {
444+
error.Code = headers.at("X-Minio-Error-Code");
445+
if (headers.contains("X-Minio-Error-Desc")) {
446+
error.Message = headers.at("X-Minio-Error-Desc");
447+
}
448+
} else if (headers.contains("x-amz-error-code")) {
449+
error.Code = headers.at("x-amz-error-code");
450+
if (headers.contains("x-amz-error-message")) {
451+
error.Message = headers.at("x-amz-error-message");
452+
}
453+
} else {
454+
error.Code = "UnknownError";
455+
error.Message = std::format("HTTP {}", res.status());
456+
}
457+
return std::unexpected<Error>(error);
458+
}
459+
332460
Error S3Client::deserializeError(const std::vector<XMLNode>& nodes) {
333461
Error error;
334462

@@ -429,3 +557,107 @@ std::expected<CreateBucketResult, Error> S3Client::deserializeCreateBucketResult
429557
}
430558
return result;
431559
}
560+
561+
std::expected<HeadBucketResult, Error> S3Client::deserializeHeadBucketResult(const std::map<std::string, std::string, LowerCaseCompare>& headers) {
562+
HeadBucketResult result;
563+
for (const auto& [header, value] : headers) {
564+
if (header == "x-amz-bucket-arn")
565+
result.BucketARN = std::move(value);
566+
else if (header == "x-amz-bucket-location-type")
567+
result.BucketLocationType = std::move(value);
568+
else if (header == "x-amz-bucket-location-name")
569+
result.BucketLocationName = std::move(value);
570+
else if (header == "x-amz-bucket-region")
571+
result.BucketRegion = std::move(value);
572+
else if (header == "x-amz-access-point-alias")
573+
result.AccessPointAlias = std::move(value);
574+
else {
575+
continue;
576+
}
577+
}
578+
return result;
579+
}
580+
581+
std::expected<HeadObjectResult, Error> S3Client::deserializeHeadObjectResult(const std::map<std::string, std::string, LowerCaseCompare>& headers) {
582+
HeadObjectResult result;
583+
for (const auto& [header, value] : headers) {
584+
if (header == "x-amz-delete-marker")
585+
result.DeleteMarker = Parser.parseBool(value);
586+
else if (header == "accept-ranges")
587+
result.AcceptRanges = std::move(value);
588+
else if (header == "x-amz-expiration")
589+
result.Expiration = std::move(value);
590+
else if (header == "x-amz-restore")
591+
result.Restore = std::move(value);
592+
else if (header == "x-amz-archive-status")
593+
result.ArchiveStatus = std::move(value);
594+
else if (header == "Last-Modified")
595+
result.LastModified = std::move(value);
596+
else if (header == "Content-Length")
597+
result.ContentLength = Parser.parseNumber<int64_t>(value);
598+
else if (header == "x-amz-checksum-crc32")
599+
result.ChecksumCRC32 = std::move(value);
600+
else if (header == "x-amz-checksum-crc32c")
601+
result.ChecksumCRC32C = std::move(value);
602+
else if (header == "x-amz-checksum-crc64nvme")
603+
result.ChecksumCRC64NVME = std::move(value);
604+
else if (header == "x-amz-checksum-sha1")
605+
result.ChecksumSHA1 = std::move(value);
606+
else if (header == "x-amz-checksum-sha256")
607+
result.ChecksumSHA256 = std::move(value);
608+
else if (header == "x-amz-checksum-type")
609+
result.ChecksumType = std::move(value);
610+
else if (header == "ETag")
611+
result.ETag = std::move(value);
612+
else if (header == "x-amz-missing-meta")
613+
result.MissingMeta = Parser.parseNumber<int>(value);
614+
else if (header == "x-amz-version-id")
615+
result.VersionId = std::move(value);
616+
else if (header == "Cache-Control")
617+
result.CacheControl = std::move(value);
618+
else if (header == "Content-Disposition")
619+
result.ContentDisposition = std::move(value);
620+
else if (header == "Content-Encoding")
621+
result.ContentEncoding = std::move(value);
622+
else if (header == "Content-Language")
623+
result.ContentLanguage = std::move(value);
624+
else if (header == "Content-Type")
625+
result.ContentType = std::move(value);
626+
else if (header == "Content-Range")
627+
result.ContentRange = std::move(value);
628+
else if (header == "Expires")
629+
result.Expires = std::move(value);
630+
else if (header == "x-amz-website-redirect-location")
631+
result.WebsiteRedirectLocation = std::move(value);
632+
else if (header == "x-amz-server-side-encryption")
633+
result.ServerSideEncryption = std::move(value);
634+
else if (header == "x-amz-server-side-encryption-customer-algorithm")
635+
result.SSECustomerAlgorithm = std::move(value);
636+
else if (header == "x-amz-server-side-encryption-customer-key-MD5")
637+
result.SSECustomerKeyMD5 = std::move(value);
638+
else if (header == "x-amz-server-side-encryption-aws-kms-key-id")
639+
result.SSEKMSKeyId = std::move(value);
640+
else if (header == "x-amz-server-side-encryption-bucket-key-enabled")
641+
result.BucketKeyEnabled = Parser.parseBool(value);
642+
else if (header == "x-amz-storage-class")
643+
result.StorageClass = std::move(value);
644+
else if (header == "x-amz-request-charged")
645+
result.RequestCharged = std::move(value);
646+
else if (header == "x-amz-replication-status")
647+
result.ReplicationStatus = std::move(value);
648+
else if (header == "x-amz-mp-parts-count")
649+
result.PartsCount = Parser.parseNumber<int>(value);
650+
else if (header == "x-amz-tagging-count")
651+
result.TagCount = Parser.parseNumber<int>(value);
652+
else if (header == "x-amz-object-lock-mode")
653+
result.ObjectLockMode = std::move(value);
654+
else if (header == "x-amz-object-lock-retain-until-date")
655+
result.ObjectLockRetainUntilDate = std::move(value);
656+
else if (header == "x-amz-object-lock-legal-hold")
657+
result.ObjectLockLegalHoldStatus = std::move(value);
658+
else {
659+
continue;
660+
}
661+
}
662+
return result;
663+
}

src/s3cpp/s3.h

Lines changed: 7 additions & 2 deletions
Original file line numberDiff line numberDiff line change
@@ -30,14 +30,15 @@ class S3Client {
3030
, addressing_style_(style) {
3131
}
3232

33-
// S3 operations: Goal is to support CRUD
33+
// S3 operations: Goal is to support CRUD and stay minimal
3434
std::expected<ListObjectsResult, Error> ListObjects(const std::string& bucket, const ListObjectsInput& options = {});
3535
std::expected<std::string, Error> GetObject(const std::string& bucket, const std::string& key, const GetObjectInput& options = {});
3636
std::expected<PutObjectResult, Error> PutObject(const std::string& bucket, const std::string& key, const std::string& body, const PutObjectInput& options = {});
3737
std::expected<DeleteObjectResult, Error> DeleteObject(const std::string& bucket, const std::string& key, const DeleteObjectInput& options = {});
3838
std::expected<CreateBucketResult, Error> CreateBucket(const std::string& bucket, const CreateBucketConfiguration& configuration = {}, const CreateBucketInput& options = {});
3939
std::expected<void, Error> DeleteBucket(const std::string& bucket, const DeleteBucketInput& options = {});
40-
// - HeadBucket
40+
std::expected<HeadBucketResult, Error> HeadBucket(const std::string& bucket, const HeadBucketInput& options = {});
41+
std::expected<HeadObjectResult, Error> HeadObject(const std::string& bucket, const std::string& key, const HeadObjectInput& options = {});
4142
// - HeadObject
4243

4344
// S3 responses
@@ -49,11 +50,15 @@ class S3Client {
4950
*
5051
* That is; Can we have a single deserialize method that takes in a struct
5152
* and values and returns that?
53+
*
54+
* Otherwise; wait until C++26 to introduce reflection
5255
*/
5356
std::expected<ListObjectsResult, Error> deserializeListBucketResult(const std::vector<XMLNode>& nodes, const int maxKeys);
5457
std::expected<PutObjectResult, Error> deserializePutObjectResult(const std::map<std::string, std::string, LowerCaseCompare>& headers);
5558
std::expected<DeleteObjectResult, Error> deserializeDeleteObjectResult(const std::map<std::string, std::string, LowerCaseCompare>& headers);
5659
std::expected<CreateBucketResult, Error> deserializeCreateBucketResult(const std::map<std::string, std::string, LowerCaseCompare>& headers);
60+
std::expected<HeadBucketResult, Error> deserializeHeadBucketResult(const std::map<std::string, std::string, LowerCaseCompare>& headers);
61+
std::expected<HeadObjectResult, Error> deserializeHeadObjectResult(const std::map<std::string, std::string, LowerCaseCompare>& headers);
5762

5863
Error deserializeError(const std::vector<XMLNode>& nodes);
5964

0 commit comments

Comments
 (0)