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
29void SaveMeta(clients::http::Headers& headers, const ClientImpl::Meta& meta) {
30 for (const auto& [header, value] : meta) {
31 headers[kMeta + header] = value;
32 }
33}
34
35void ReadMeta(const clients::http::Headers& headers, ClientImpl::Meta& meta) {
36 for (const auto& [header, value] : headers) {
37 if (boost::istarts_with(header, kMeta)) {
38 meta[header.substr(kMeta.length())] = value;
39 }
40 }
41}
42
43void SaveTags(clients::http::Headers& headers, const std::vector<ClientImpl::Tag>& tags) {
44 size_t size = 0;
45 for (const auto& [key, value] : tags) {
46 size += key.size() + value.size() + 2;
47 }
48 std::string tag_values;
49 tag_values.reserve(size);
50 for (const auto& [key, value] : tags) {
51 tag_values.append(key);
52 tag_values.append("=");
53 tag_values.append(value);
54 tag_values.append("&");
55 }
56 if (!tag_values.empty()) {
57 // pop last &
58 tag_values.pop_back();
59 }
60 headers[kTagging] = std::move(tag_values);
61}
62
63void AddQueryParamsToPresignedUrl(
64 std::ostringstream& generated_url,
65 time_t expires_at,
66 const Request& req,
67 std::shared_ptr<authenticators::Authenticator> authenticator
68) {
69 if (!req.req.empty()) {
70 generated_url << "/" + req.req;
71 }
72
73 auto params = authenticator->Sign(req, expires_at);
74
75 if (!params.empty()) {
76 generated_url << "?";
77 generated_url << USERVER_NAMESPACE::http::MakeQuery(params);
78 }
79}
80
81std::string GeneratePresignedUrl(
82 const Request& request,
83 std::string_view host,
84 std::string_view protocol,
85 const std::chrono::system_clock::time_point& expires_at,
86 std::shared_ptr<authenticators::Authenticator> authenticator
87) {
88 std::ostringstream generated_url;
89 // both internal (s3.mds(t)) and private (s3-private)
90 // balancers support virtual host addressing and https
91 generated_url << protocol << request.bucket << "." << host;
92 const auto expires_at_time_t = std::chrono::system_clock::to_time_t(expires_at);
93 AddQueryParamsToPresignedUrl(generated_url, expires_at_time_t, request, authenticator);
94 return generated_url.str();
95}
96
97std::vector<ObjectMeta> ParseS3ListResponse(std::string_view s3_response) {
98 std::vector<ObjectMeta> result;
99 pugi::xml_document xml;
100 pugi::xml_parse_result parse_result = xml.load_string(s3_response.data());
101 if (parse_result.status != pugi::status_ok) {
102 throw ListBucketError(fmt::format(
103 "Failed to parse S3 list response as xml, error: {}, response: {}", parse_result.description(), s3_response
104 ));
105 }
106 try {
107 const auto items = xml.child("ListBucketResult").children("Contents");
108 for (const auto& item : items) {
109 const auto key = item.child("Key").child_value();
110 const auto size = std::stoull(item.child("Size").child_value());
111 const auto last_modified = item.child("LastModified").child_value();
112 result.push_back(ObjectMeta{key, size, last_modified});
113 }
114 } catch (const pugi::xpath_exception& ex) {
115 throw ListBucketError(
116 fmt::format("Bad xml structure for S3 list response, error: {}, response: {}", ex.what(), s3_response)
117 );
118 }
119 return result;
120}
121
122std::vector<std::string> ParseS3DirectoriesListResponse(std::string_view s3_response) {
123 std::vector<std::string> result;
124 pugi::xml_document xml;
125 pugi::xml_parse_result parse_result = xml.load_string(s3_response.data());
126 if (parse_result.status != pugi::status_ok) {
127 throw ListBucketError(fmt::format(
128 "Failed to parse S3 directories list response as xml, error: {}, "
129 "response: {}",
130 parse_result.description(),
131 s3_response
132 ));
133 }
134 try {
135 const auto items = xml.child("ListBucketResult").children("CommonPrefixes");
136 for (const auto& item : items) {
137 result.push_back(item.child("Prefix").child_value());
138 }
139 } catch (const pugi::xpath_exception& ex) {
140 throw ListBucketError(fmt::format(
141 "Bad xml structure for S3 directories list response, error: {}, "
142 "response: {}",
143 ex.what(),
144 s3_response
145 ));
146 }
147 return result;
148}
149
150} // namespace
151
152void ClientImpl::UpdateConfig(ConnectionCfg&& config) { conn_->UpdateConfig(std::move(config)); }
153
154ClientImpl::ClientImpl(
155 std::shared_ptr<S3Connection> s3conn,
156 std::shared_ptr<authenticators::Authenticator> authenticator,
157 std::string bucket
158)
159 : conn_(std::move(s3conn)), authenticator_{std::move(authenticator)}, bucket_(std::move(bucket)) {}
160
161ClientImpl::ClientImpl(
162 std::shared_ptr<S3Connection> s3conn,
163 std::shared_ptr<authenticators::AccessKey> authenticator,
164 std::string bucket
165)
166 : ClientImpl(
167 std::move(s3conn),
168 std::static_pointer_cast<authenticators::Authenticator>(std::move(authenticator)),
169 std::move(bucket)
170 ) {}
171
172std::string_view ClientImpl::GetBucketName() const { return bucket_; }
173
174std::string ClientImpl::PutObject(
175 std::string_view path, //
176 std::string data, //
177 const std::optional<Meta>& meta, //
178 std::string_view content_type, //
179 const std::optional<std::string>& content_disposition,
180 const std::optional<std::vector<Tag>>& tags
181) const {
182 auto req = api_methods::PutObject( //
183 bucket_, //
184 path, //
185 std::move(data), //
186 content_type, //
187 content_disposition //
188 );
189
190 if (meta.has_value()) {
191 SaveMeta(req.headers, meta.value());
192 }
193 if (tags.has_value()) {
194 SaveTags(req.headers, tags.value());
195 }
196 return RequestApi(req, "put_object");
197}
198
199void ClientImpl::DeleteObject(std::string_view path) const {
200 auto req = api_methods::DeleteObject(bucket_, path);
201 RequestApi(req, "delete_object");
202}
203
204std::optional<std::string> ClientImpl::GetObject(
205 std::string_view path,
206 std::optional<std::string> version,
207 HeadersDataResponse* headers_data,
208 const HeaderDataRequest& headers_request
209) const {
210 try {
211 return std::make_optional(TryGetObject(path, std::move(version), headers_data, headers_request));
212 } catch (const clients::http::HttpException& e) {
213 if (e.code() == 404) {
214 LOG_INFO() << "Can't get object with path: " << path << ", object not found:" << e.what();
215 } else {
216 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
217 }
218 return std::nullopt;
219 } catch (const std::exception& e) {
220 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
221 return std::nullopt;
222 }
223}
224
225std::string ClientImpl::TryGetObject(
226 std::string_view path,
227 std::optional<std::string> version,
228 HeadersDataResponse* headers_data,
229 const HeaderDataRequest& headers_request
230) const {
231 auto req = api_methods::GetObject(bucket_, path, std::move(version));
232 return RequestApi(req, "get_object", headers_data, headers_request);
233}
234
235std::optional<std::string> ClientImpl::GetPartialObject(
236 std::string_view path,
237 std::string_view range,
238 std::optional<std::string> version,
239 HeadersDataResponse* headers_data,
240 const HeaderDataRequest& headers_request
241) const {
242 try {
243 return std::make_optional(TryGetPartialObject(path, range, std::move(version), headers_data, headers_request));
244 } catch (const clients::http::HttpException& e) {
245 if (e.code() == 404) {
246 LOG_INFO() << "Can't get object with path: " << path << ", object not found:" << e.what();
247 } else {
248 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
249 }
250 return std::nullopt;
251 } catch (const std::exception& e) {
252 LOG_ERROR() << "Can't get object with path: " << path << ", unknown error:" << e.what();
253 return std::nullopt;
254 }
255}
256
257std::string ClientImpl::TryGetPartialObject(
258 std::string_view path,
259 std::string_view range,
260 std::optional<std::string> version,
261 HeadersDataResponse* headers_data,
262 const HeaderDataRequest& headers_request
263) const {
264 auto req = api_methods::GetObject(bucket_, path, std::move(version));
265 api_methods::SetRange(req, range);
266 return RequestApi(req, "get_object", headers_data, headers_request);
267}
268
269std::optional<ClientImpl::HeadersDataResponse>
270ClientImpl::GetObjectHead(std::string_view path, const HeaderDataRequest& headers_request) const {
271 HeadersDataResponse headers_data;
272 auto req = api_methods::GetObjectHead(bucket_, path);
273 try {
274 RequestApi(req, "get_object_head", &headers_data, headers_request);
275 } catch (const std::exception& e) {
276 LOG_INFO() << "Can't get object with path: " << path << ", error:" << e.what();
277 return std::nullopt;
278 }
279 return std::make_optional(std::move(headers_data));
280}
281
282[[deprecated]] std::string ClientImpl::GenerateDownloadUrl(std::string_view path, time_t expires_at, bool use_ssl)
283 const {
284 auto req = api_methods::GetObject(bucket_, path);
285
286 std::ostringstream generated_url;
287 auto host = conn_->GetHost();
288 if (host.find("://") == std::string::npos) {
289 generated_url << (use_ssl ? "https" : "http") << "://";
290 }
291 generated_url << host;
292
293 if (!req.bucket.empty()) {
294 generated_url << "/" + req.bucket;
295 }
296 AddQueryParamsToPresignedUrl(generated_url, expires_at, req, authenticator_);
297 return generated_url.str();
298}
299
300std::string ClientImpl::GenerateDownloadUrlVirtualHostAddressing(
301 std::string_view path,
302 const std::chrono::system_clock::time_point& expires_at,
303 std::string_view protocol
304) const {
305 auto req = api_methods::GetObject(bucket_, path);
306 if (req.bucket.empty()) {
307 throw NoBucketError("presigned url for empty bucket string");
308 }
309 return GeneratePresignedUrl(req, conn_->GetHost(), protocol, expires_at, authenticator_);
310}
311
312std::string ClientImpl::GenerateUploadUrlVirtualHostAddressing(
313 std::string_view data,
314 std::string_view content_type,
315 std::string_view path,
316 const std::chrono::system_clock::time_point& expires_at,
317 std::string_view protocol
318) const {
319 auto req = api_methods::PutObject(bucket_, path, std::string{data}, content_type);
320 if (req.bucket.empty()) {
321 throw NoBucketError("presigned url for empty bucket string");
322 }
323 return GeneratePresignedUrl(req, conn_->GetHost(), protocol, expires_at, authenticator_);
324}
325
326void ClientImpl::Auth(Request& request) const {
327 if (!authenticator_) {
328 // anonymous request
329 return;
330 }
331
332 auto auth_headers = authenticator_->Auth(request);
333
334 {
335 auto it = std::find_if(
336 auth_headers.cbegin(),
337 auth_headers.cend(),
338 [&request](const decltype(auth_headers)::value_type& header) {
339 return request.headers.count(std::get<0>(header));
340 }
341 );
342
343 if (it != auth_headers.cend()) {
344 throw AuthHeaderConflictError{std::string{"Conflict with auth header: "} + it->first};
345 }
346 }
347
348 request.headers.insert(
349 std::make_move_iterator(std::begin(auth_headers)), std::make_move_iterator(std::end(auth_headers))
350 );
351}
352
353std::string ClientImpl::RequestApi(
354 Request& request,
355 std::string_view method_name,
356 HeadersDataResponse* headers_data,
357 const HeaderDataRequest& headers_request
358) const {
359 Auth(request);
360
361 auto response = conn_->RequestApi(request, method_name);
362
363 if (headers_data) {
364 if (headers_request.need_meta) {
365 headers_data->meta.emplace();
366 ReadMeta(response->headers(), *headers_data->meta);
367 }
368
369 if (headers_request.headers) {
370 headers_data->headers.emplace();
371 for (const auto& header : *headers_request.headers) {
372 if (auto it = response->headers().find(header); it != response->headers().end()) {
373 headers_data->headers->emplace(it->first, it->second);
374 }
375 }
376 }
377 }
378
379 return response->body();
380}
381
382std::optional<std::string>
383ClientImpl::ListBucketContents(std::string_view path, int max_keys, std::string marker, std::string delimiter) const {
384 auto req = api_methods::ListBucketContents(bucket_, path, max_keys, marker, delimiter);
385 std::string reply = RequestApi(req, "list_bucket_contents");
386 if (reply.empty()) {
387 return std::nullopt;
388 }
389 return std::optional<std::string>{std::move(reply)};
390}
391
392std::vector<ObjectMeta> ClientImpl::ListBucketContentsParsed(std::string_view path_prefix) const {
393 std::vector<ObjectMeta> result;
394 // S3 doc: specifies the key to start with when listing objects in a bucket
395 std::string marker = "";
396 bool is_finished = false;
397 while (!is_finished) {
398 auto response = ListBucketContents(path_prefix, kMaxS3Keys, marker, {});
399 if (!response) {
400 LOG_WARNING() << "Empty S3 bucket listing response for path prefix " << path_prefix;
401 break;
402 }
403 auto response_result = ParseS3ListResponse(*response);
404 if (response_result.empty()) {
405 break;
406 }
407 if (response_result.size() < kMaxS3Keys) {
408 is_finished = true;
409 }
410 result.insert(
411 result.end(),
412 std::make_move_iterator(response_result.begin()),
413 std::make_move_iterator(response_result.end())
414 );
415 marker = result.back().key;
416 }
417 return result;
418}
419
420std::vector<std::string> ClientImpl::ListBucketDirectories(std::string_view path_prefix) const {
421 std::vector<std::string> result;
422 // S3 doc: specifies the key to start with when listing objects in a bucket
423 std::string marker = "";
424 bool is_finished = false;
425 while (!is_finished) {
426 auto response = ListBucketContents(path_prefix, kMaxS3Keys, marker, "/");
427 if (!response) {
428 LOG_WARNING() << "Empty S3 directory bucket listing response "
429 "for path prefix "
430 << path_prefix;
431 break;
432 }
433 auto response_result = ParseS3DirectoriesListResponse(*response);
434 if (response_result.empty()) {
435 break;
436 }
437 if (response_result.size() < kMaxS3Keys) {
438 is_finished = true;
439 }
440 result.insert(
441 result.end(),
442 std::make_move_iterator(response_result.begin()),
443 std::make_move_iterator(response_result.end())
444 );
445 marker = result.back();
446 }
447
448 return result;
449}
450
451std::string ClientImpl::CopyObject(
452 std::string_view key_from,
453 std::string_view bucket_to,
454 std::string_view key_to,
455 const std::optional<Meta>& meta
456) {
457 const auto object_head = [&] {
458 HeaderDataRequest header_request;
459 header_request.headers.emplace();
460 header_request.headers->emplace(USERVER_NAMESPACE::http::headers::kContentType);
461 header_request.need_meta = false;
462 return GetObjectHead(key_from, header_request);
463 }();
464 if (!object_head) {
465 USERVER_NAMESPACE::utils::LogErrorAndThrow("S3Api : Failed to get object head");
466 }
467
468 const auto content_type = [&object_head]() -> std::optional<std::string> {
469 if (!object_head->headers) {
470 return std::nullopt;
471 }
472
473 return USERVER_NAMESPACE::utils::FindOptional(
474 *object_head->headers, USERVER_NAMESPACE::http::headers::kContentType
475 );
476 }();
477 if (!content_type) {
478 USERVER_NAMESPACE::utils::LogErrorAndThrow("S3Api : Object head is missing `content-type` header");
479 }
480
481 auto req = api_methods::CopyObject(bucket_, key_from, bucket_to, key_to, *content_type);
482 if (meta) {
483 SaveMeta(req.headers, *meta);
484 }
485 return RequestApi(req, "copy_object");
486}
487
488std::string
489ClientImpl::CopyObject(std::string_view key_from, std::string_view key_to, const std::optional<Meta>& meta) {
490 return CopyObject(key_from, bucket_, key_to, meta);
491}
492
493ClientPtr GetS3Client(
494 std::shared_ptr<S3Connection> s3conn,
495 std::shared_ptr<authenticators::AccessKey> authenticator,
496 std::string bucket
497) {
498 return GetS3Client(s3conn, std::static_pointer_cast<authenticators::Authenticator>(authenticator), bucket);
499}
500
501ClientPtr GetS3Client(
502 std::shared_ptr<S3Connection> s3conn,
503 std::shared_ptr<authenticators::Authenticator> authenticator,
504 std::string bucket
505) {
506 return std::static_pointer_cast<Client>(std::make_shared<ClientImpl>(s3conn, authenticator, bucket));
507}
508
509} // namespace s3api
510
511USERVER_NAMESPACE_END