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