userver: /data/code/userver/libraries/s3api/src/s3api/clients/client.cpp Source File
Loading...
Searching...
No Matches
client.cpp
1#include <s3api/clients/client.hpp>
2
3#include <sstream>
4
5#include <fmt/format.h>
6#include <boost/algorithm/string.hpp>
7#include <pugixml.hpp>
8
9#include <userver/http/common_headers.hpp>
10#include <userver/http/url.hpp>
11#include <userver/logging/log.hpp>
12#include <userver/utils/algo.hpp>
13#include <userver/utils/exception.hpp>
14
15#include <userver/s3api/authenticators/access_key.hpp>
16
17#include <s3api/s3_connection.hpp>
18#include <s3api/s3api_methods.hpp>
19
20USERVER_NAMESPACE_BEGIN
21
22namespace s3api {
23namespace {
24
25const std::string kMeta = "x-amz-meta-";
26const std::string kTagging = "X-Amz-Tagging";
27constexpr const std::size_t kMaxS3Keys = 1000;
28
29constexpr http::headers::PredefinedHeader kEtagHeader{"ETag"};
30
31void SaveMeta(clients::http::Headers& headers, const ClientImpl::Meta& meta) {
32 for (const auto& [header, value] : meta) {
33 headers[kMeta + header] = value;
34 }
35}
36
37void ReadMeta(const clients::http::Headers& headers, ClientImpl::Meta& meta) {
38 for (const auto& [header, value] : headers) {
39 if (boost::istarts_with(header, kMeta)) {
40 meta[header.substr(kMeta.length())] = value;
41 }
42 }
43}
44
45void SaveTags(clients::http::Headers& headers, const std::vector<ClientImpl::Tag>& tags) {
46 size_t size = 0;
47 for (const auto& [key, value] : tags) {
48 size += key.size() + value.size() + 2;
49 }
50 std::string tag_values;
51 tag_values.reserve(size);
52 for (const auto& [key, value] : tags) {
53 tag_values.append(key);
54 tag_values.append("=");
55 tag_values.append(value);
56 tag_values.append("&");
57 }
58 if (!tag_values.empty()) {
59 // pop last &
60 tag_values.pop_back();
61 }
62 headers[kTagging] = std::move(tag_values);
63}
64
65void AddQueryParamsToPresignedUrl(
66 std::ostringstream& generated_url,
67 time_t expires_at,
68 const Request& req,
69 std::shared_ptr<authenticators::Authenticator> authenticator
70) {
71 if (!req.req.empty()) {
72 generated_url << "/" + req.req;
73 }
74
75 auto params = authenticator->Sign(req, expires_at);
76
77 if (!params.empty()) {
78 generated_url << "?";
79 generated_url << USERVER_NAMESPACE::http::MakeQuery(params);
80 }
81}
82
83std::string GeneratePresignedUrl(
84 const Request& request,
85 std::string_view host,
86 std::string_view protocol,
87 const std::chrono::system_clock::time_point& expires_at,
88 std::shared_ptr<authenticators::Authenticator> authenticator
89) {
90 std::ostringstream generated_url;
91 // both internal (s3.mds(t)) and private (s3-private)
92 // balancers support virtual host addressing and https
93 generated_url << protocol << request.bucket << "." << host;
94 const auto expires_at_time_t = std::chrono::system_clock::to_time_t(expires_at);
95 AddQueryParamsToPresignedUrl(generated_url, expires_at_time_t, request, std::move(authenticator));
96 return generated_url.str();
97}
98
99bool IsS3ResponseTruncated(const pugi::xml_node& list_bucket_result) {
100 const auto is_truncated = list_bucket_result.child("IsTruncated").child_value();
101 return std::string_view{is_truncated} == "true";
102}
103
104std::vector<ObjectMeta> ParseS3ListResponse(utils::zstring_view s3_response, bool& is_truncated) {
105 std::vector<ObjectMeta> result;
106 pugi::xml_document xml;
107 const pugi::xml_parse_result parse_result = xml.load_string(s3_response.c_str());
108 if (parse_result.status != pugi::status_ok) {
109 throw ListBucketError(fmt::format(
110 "Failed to parse S3 list response as xml, error: {}, response: {}",
111 parse_result.description(),
112 s3_response
113 ));
114 }
115 try {
116 const auto list_bucket_result = xml.child("ListBucketResult");
117 is_truncated = IsS3ResponseTruncated(list_bucket_result);
118
119 const auto items = list_bucket_result.children("Contents");
120 for (const auto& item : items) {
121 const auto key = item.child("Key").child_value();
122 const auto size = std::stoull(item.child("Size").child_value());
123 const auto last_modified = item.child("LastModified").child_value();
124 result.push_back(ObjectMeta{key, size, last_modified});
125 }
126 } catch (const pugi::xpath_exception& ex) {
127 throw ListBucketError(
128 fmt::format("Bad xml structure for S3 list response, error: {}, response: {}", ex.what(), s3_response)
129 );
130 }
131 return result;
132}
133
134std::vector<std::string> ParseS3DirectoriesListResponse(utils::zstring_view s3_response, bool& is_truncated) {
135 std::vector<std::string> result;
136 pugi::xml_document xml;
137 const pugi::xml_parse_result parse_result = xml.load_string(s3_response.c_str());
138 if (parse_result.status != pugi::status_ok) {
139 throw ListBucketError(fmt::format(
140 "Failed to parse S3 directories list response as xml, error: {}, "
141 "response: {}",
142 parse_result.description(),
143 s3_response
144 ));
145 }
146 try {
147 const auto list_bucket_result = xml.child("ListBucketResult");
148 is_truncated = IsS3ResponseTruncated(list_bucket_result);
149
150 const auto items = list_bucket_result.children("CommonPrefixes");
151 for (const auto& item : items) {
152 result.push_back(item.child("Prefix").child_value());
153 }
154 } catch (const pugi::xpath_exception& ex) {
155 throw ListBucketError(fmt::format(
156 "Bad xml structure for S3 directories list response, error: {}, "
157 "response: {}",
158 ex.what(),
159 s3_response
160 ));
161 }
162 return result;
163}
164
165} // namespace
166
167void ClientImpl::UpdateConfig(ConnectionCfg&& config) { conn_->UpdateConfig(std::move(config)); }
168
169ClientImpl::ClientImpl(
170 std::shared_ptr<S3Connection> s3conn,
171 std::shared_ptr<authenticators::Authenticator> authenticator,
172 std::string bucket
173)
174 : conn_(std::move(s3conn)),
175 authenticator_{std::move(authenticator)},
176 bucket_(std::move(bucket))
177{}
178
179ClientImpl::ClientImpl(
180 std::shared_ptr<S3Connection> s3conn,
181 std::shared_ptr<authenticators::AccessKey> authenticator,
182 std::string bucket
183)
184 : ClientImpl(
185 std::move(s3conn),
186 std::static_pointer_cast<authenticators::Authenticator>(std::move(authenticator)),
187 std::move(bucket)
188 )
189{}
190
191std::string_view ClientImpl::GetBucketName() const { return bucket_; }
192
193std::string ClientImpl::PutObject(
194 std::string_view path, //
195 std::string data, //
196 const std::optional<Meta>& meta, //
197 std::string_view content_type, //
198 const std::optional<std::string>& content_disposition,
199 const std::optional<std::vector<Tag>>& tags
200) const {
201 auto req = api_methods::PutObject( //
202 bucket_, //
203 path, //
204 std::move(data), //
205 content_type, //
206 content_disposition //
207 );
208
209 if (meta.has_value()) {
210 SaveMeta(req.headers, meta.value());
211 }
212 if (tags.has_value()) {
213 SaveTags(req.headers, tags.value());
214 }
215 return RequestApi(req, "put_object");
216}
217
218void ClientImpl::DeleteObject(std::string_view path) const {
219 auto req = api_methods::DeleteObject(bucket_, path);
220 RequestApi(req, "delete_object");
221}
222
223std::optional<std::string> ClientImpl::GetObject(
224 std::string_view path,
225 std::optional<std::string> version,
226 HeadersDataResponse* headers_data,
227 const HeaderDataRequest& headers_request
228) const {
229 try {
230 return std::make_optional(TryGetObject(path, std::move(version), headers_data, headers_request));
231 } catch (const clients::http::HttpException& e) {
232 if (e.code() == 404) {
233 LOG_INFO() << "Can't get object with path: " << path << ", object not found:" << e.what();
234 } else {
235 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
236 }
237 return std::nullopt;
238 } catch (const std::exception& e) {
239 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
240 return std::nullopt;
241 }
242}
243
244std::string ClientImpl::TryGetObject(
245 std::string_view path,
246 std::optional<std::string> version,
247 HeadersDataResponse* headers_data,
248 const HeaderDataRequest& headers_request
249) const {
250 auto req = api_methods::GetObject(bucket_, path, std::move(version));
251 return RequestApi(req, "get_object", headers_data, headers_request);
252}
253
254std::optional<std::string> ClientImpl::GetPartialObject(
255 std::string_view path,
256 std::string_view range,
257 std::optional<std::string> version,
258 HeadersDataResponse* headers_data,
259 const HeaderDataRequest& headers_request
260) const {
261 try {
262 return std::make_optional(TryGetPartialObject(path, range, std::move(version), headers_data, headers_request));
263 } catch (const clients::http::HttpException& e) {
264 if (e.code() == 404) {
265 LOG_INFO() << "Can't get object with path: " << path << ", object not found:" << e.what();
266 } else {
267 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
268 }
269 return std::nullopt;
270 } catch (const std::exception& e) {
271 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
272 return std::nullopt;
273 }
274}
275
276std::string ClientImpl::TryGetPartialObject(
277 std::string_view path,
278 std::string_view range,
279 std::optional<std::string> version,
280 HeadersDataResponse* headers_data,
281 const HeaderDataRequest& headers_request
282) const {
283 auto req = api_methods::GetObject(bucket_, path, std::move(version));
284 api_methods::SetRange(req, range);
285 return RequestApi(req, "get_object", headers_data, headers_request);
286}
287
288std::optional<ClientImpl::HeadersDataResponse> ClientImpl::GetObjectHead(
289 std::string_view path,
290 const HeaderDataRequest& headers_request
291) const {
292 HeadersDataResponse headers_data;
293 auto req = api_methods::GetObjectHead(bucket_, path);
294 try {
295 RequestApi(req, "get_object_head", &headers_data, headers_request);
296 } catch (const std::exception& e) {
297 LOG_INFO() << "Can't get object with path: " << path << ", error:" << e.what();
298 return std::nullopt;
299 }
300 return std::make_optional(std::move(headers_data));
301}
302
303[[deprecated]] std::string ClientImpl::GenerateDownloadUrl(std::string_view path, time_t expires_at, bool use_ssl)
304 const {
305 auto req = api_methods::GetObject(bucket_, path);
306
307 std::ostringstream generated_url;
308 auto host = conn_->GetHost();
309 if (host.find("://") == std::string::npos) {
310 generated_url << (use_ssl ? "https" : "http") << "://";
311 }
312 generated_url << host;
313
314 if (!req.bucket.empty()) {
315 generated_url << "/" + req.bucket;
316 }
317 AddQueryParamsToPresignedUrl(generated_url, expires_at, req, authenticator_);
318 return generated_url.str();
319}
320
321std::string ClientImpl::GenerateDownloadUrlVirtualHostAddressing(
322 std::string_view path,
323 const std::chrono::system_clock::time_point& expires_at,
324 std::string_view protocol
325) const {
326 auto req = api_methods::GetObject(bucket_, path);
327 if (req.bucket.empty()) {
328 throw NoBucketError("presigned url for empty bucket string");
329 }
330 return GeneratePresignedUrl(req, conn_->GetHost(), protocol, expires_at, authenticator_);
331}
332
333std::string ClientImpl::GenerateUploadUrlVirtualHostAddressing(
334 std::string_view data,
335 std::string_view content_type,
336 std::string_view path,
337 const std::chrono::system_clock::time_point& expires_at,
338 std::string_view protocol
339) const {
340 auto req = api_methods::PutObject(bucket_, path, std::string{data}, content_type);
341 if (req.bucket.empty()) {
342 throw NoBucketError("presigned url for empty bucket string");
343 }
344 return GeneratePresignedUrl(req, conn_->GetHost(), protocol, expires_at, authenticator_);
345}
346
347void ClientImpl::Auth(Request& request) const {
348 if (!authenticator_) {
349 // anonymous request
350 return;
351 }
352
353 auto auth_headers = authenticator_->Auth(request);
354
355 {
356 auto it = std::ranges::find_if(auth_headers, [&request](const auto& header) {
357 return request.headers.contains(header.first);
358 });
359
360 if (it != auth_headers.cend()) {
361 throw AuthHeaderConflictError{std::string{"Conflict with auth header: "} + it->first};
362 }
363 }
364
365 request.headers
366 .insert(std::make_move_iterator(std::begin(auth_headers)), std::make_move_iterator(std::end(auth_headers)));
367}
368
369std::string ClientImpl::RequestApi(
370 Request& request,
371 std::string_view method_name,
372 HeadersDataResponse* headers_data,
373 const HeaderDataRequest& headers_request
374) const {
375 Auth(request);
376
377 auto response = conn_->RequestApi(request, method_name);
378
379 if (headers_data) {
380 if (headers_request.need_meta) {
381 headers_data->meta.emplace();
382 ReadMeta(response->headers(), *headers_data->meta);
383 }
384 if (headers_request.headers) {
385 headers_data->headers.emplace();
386 for (const auto& header : *headers_request.headers) {
387 if (auto it = response->headers().find(header); it != response->headers().end()) {
388 headers_data->headers->emplace(it->first, it->second);
389 }
390 }
391 }
392 }
393
394 return response->body();
395}
396
397std::optional<std::string> ClientImpl::ListBucketContents(
398 std::string_view path,
399 int max_keys,
400 std::string marker,
401 std::string delimiter
402) const {
403 auto req = api_methods::ListBucketContents(bucket_, path, max_keys, marker, delimiter);
404 std::string reply = RequestApi(req, "list_bucket_contents");
405 if (reply.empty()) {
406 return std::nullopt;
407 }
408 return std::optional<std::string>{std::move(reply)};
409}
410
411std::vector<ObjectMeta> ClientImpl::ListBucketContentsParsed(std::string_view path_prefix) const {
412 std::vector<ObjectMeta> result;
413 // S3 doc: specifies the key to start with when listing objects in a bucket
414 std::string marker{};
415 bool is_finished = false;
416 while (!is_finished) {
417 auto response = ListBucketContents(path_prefix, kMaxS3Keys, marker, {});
418 if (!response) {
419 LOG_WARNING() << "Empty S3 bucket listing response for path prefix " << path_prefix;
420 break;
421 }
422
423 bool is_truncated = false;
424 auto response_result = ParseS3ListResponse(*response, is_truncated);
425 if (response_result.empty()) {
426 break;
427 }
428 if (!is_truncated) {
429 is_finished = true;
430 }
431 result.insert(
432 result.end(),
433 std::make_move_iterator(response_result.begin()),
434 std::make_move_iterator(response_result.end())
435 );
436 marker = result.back().key;
437 }
438 return result;
439}
440
441std::vector<std::string> ClientImpl::ListBucketDirectories(std::string_view path_prefix) const {
442 std::vector<std::string> result;
443 // S3 doc: specifies the key to start with when listing objects in a bucket
444 std::string marker{};
445 bool is_finished = false;
446 while (!is_finished) {
447 auto response = ListBucketContents(path_prefix, kMaxS3Keys, marker, "/");
448 if (!response) {
449 LOG_WARNING() << "Empty S3 directory bucket listing response for path prefix " << path_prefix;
450 break;
451 }
452
453 bool is_truncated = false;
454 auto response_result = ParseS3DirectoriesListResponse(*response, is_truncated);
455 if (response_result.empty()) {
456 break;
457 }
458 if (!is_truncated) {
459 is_finished = true;
460 }
461 result.insert(
462 result.end(),
463 std::make_move_iterator(response_result.begin()),
464 std::make_move_iterator(response_result.end())
465 );
466 marker = result.back();
467 }
468
469 return result;
470}
471
472std::string ClientImpl::CopyObject(
473 std::string_view key_from,
474 std::string_view bucket_to,
475 std::string_view key_to,
476 const std::optional<Meta>& meta
477) {
478 const auto object_head = [&] {
479 HeaderDataRequest header_request;
480 header_request.headers.emplace();
481 header_request.headers->emplace(USERVER_NAMESPACE::http::headers::kContentType);
482 header_request.need_meta = false;
483 return GetObjectHead(key_from, header_request);
484 }();
485 if (!object_head) {
486 USERVER_NAMESPACE::utils::LogErrorAndThrow("S3Api : Failed to get object head");
487 }
488
489 const auto content_type = [&object_head]() -> std::optional<std::string> {
490 if (!object_head->headers) {
491 return std::nullopt;
492 }
493
494 return USERVER_NAMESPACE::utils::FindOptional(
495 *object_head->headers,
496 USERVER_NAMESPACE::http::headers::kContentType
497 );
498 }();
499 if (!content_type) {
500 USERVER_NAMESPACE::utils::LogErrorAndThrow("S3Api : Object head is missing `content-type` header");
501 }
502
503 auto req = api_methods::CopyObject(bucket_, key_from, bucket_to, key_to, *content_type);
504 if (meta) {
505 SaveMeta(req.headers, *meta);
506 }
507 return RequestApi(req, "copy_object");
508}
509
510std::string ClientImpl::CopyObject(
511 std::string_view key_from,
512 std::string_view key_to,
513 const std::optional<Meta>& meta
514) {
515 return CopyObject(key_from, bucket_, key_to, meta);
516}
517
519 const multipart_upload::CreateMultipartUploadRequest& request
520) const try
521{
522 auto api_request = api_methods::CreateInternalApiRequest(bucket_, request);
523 const auto api_response_body = RequestApi(api_request, "create_multipart_upload");
524 return multipart_upload::InitiateMultipartUploadResult::Parse(api_response_body);
525} catch (const ResponseParsingError& exc) {
527 fmt::format("failed to parse CreateMultipartUpload action response - {}; key: {}", exc.what(), request.key)
528 );
529}
530
531multipart_upload::UploadPartResult ClientImpl::UploadPart(const multipart_upload::UploadPartRequest& request) const try
532{
533 auto api_request = api_methods::CreateInternalApiRequest(bucket_, request);
534
535 const HeaderDataRequest expected_headers({{std::string(kEtagHeader)}}, false);
536 HeadersDataResponse response_headers_data;
537
538 RequestApi(api_request, "upload_part", &response_headers_data, expected_headers);
539 if (!response_headers_data.headers) {
540 throw ResponseParsingError("missing ETag header in response");
541 }
542 const auto iter = response_headers_data.headers->find(kEtagHeader);
543 if (iter == response_headers_data.headers->end()) {
544 throw ResponseParsingError("missing ETag header in response");
545 }
546 if (iter->second.empty()) {
547 throw ResponseParsingError("got empty ETag header value in response");
548 }
549 return {std::move(iter->second)};
550
551} catch (const ResponseParsingError& exc) {
552 throw MultipartUploadError(fmt::format(
553 "failed to parse UploadPart action response - {}; upload_id '{}'; key '{}'",
554 exc.what(),
555 request.upload_id,
556 request.key
557 ));
558}
559
561 const multipart_upload::CompleteMultipartUploadRequest& request
562) const try
563{
564 auto api_request = api_methods::CreateInternalApiRequest(bucket_, request);
565 const auto api_response_body = RequestApi(api_request, "complete_multipart_upload");
566 return multipart_upload::CompleteMultipartUploadResult::Parse(api_response_body);
567} catch (const ResponseParsingError& exc) {
568 throw MultipartUploadError(fmt::format(
569 "failed to parse CompleteMultipartUpload action response - {}; upload_id '{}'; key '{}'",
570 exc.what(),
571 request.upload_id,
572 request.key
573 ));
574}
575
576void ClientImpl::AbortMultipartUpload(const multipart_upload::AbortMultipartUploadRequest& request) const {
577 auto api_request = api_methods::CreateInternalApiRequest(bucket_, request);
578 RequestApi(api_request, "abort_multipart_upload");
579}
580
581multipart_upload::ListPartsResult ClientImpl::ListParts(const multipart_upload::ListPartsRequest& request) const try
582{
583 auto api_request = api_methods::CreateInternalApiRequest(bucket_, request);
584 const auto api_response_body = RequestApi(api_request, "list_parts");
585 return multipart_upload::ListPartsResult::Parse(api_response_body);
586} catch (const ResponseParsingError& exc) {
587 throw MultipartUploadError(fmt::format(
588 "failed to parse ListParts action response - {}; upload_id '{}'; key '{}'",
589 exc.what(),
590 request.upload_id,
591 request.key
592 ));
593}
594
596 const multipart_upload::ListMultipartUploadsRequest& request
597) const try
598{
599 auto api_request = api_methods::CreateInternalApiRequest(bucket_, request);
600 const auto api_response_body = RequestApi(api_request, "list_multipart_uploads");
601 return multipart_upload::ListMultipartUploadsResult::Parse(api_response_body);
602} catch (const ResponseParsingError& exc) {
603 throw MultipartUploadError(fmt::format("failed to parse ListMultipartUploads action response - {}", exc.what()));
604}
605
606ClientPtr GetS3Client(
607 std::shared_ptr<S3Connection> s3conn,
608 std::shared_ptr<authenticators::AccessKey> authenticator,
609 std::string bucket
610) {
611 return GetS3Client(
612 std::move(s3conn),
613 std::static_pointer_cast<authenticators::Authenticator>(authenticator),
614 std::move(bucket)
615 );
616}
617
618ClientPtr GetS3Client(
619 std::shared_ptr<S3Connection> s3conn,
620 std::shared_ptr<authenticators::Authenticator> authenticator,
621 std::string bucket
622) {
623 return std::static_pointer_cast<Client>(std::make_shared<ClientImpl>(s3conn, authenticator, bucket));
624}
625
626} // namespace s3api
627
628USERVER_NAMESPACE_END