be/src/service/http/http_client.cpp
Line | Count | Source |
1 | | // Licensed to the Apache Software Foundation (ASF) under one |
2 | | // or more contributor license agreements. See the NOTICE file |
3 | | // distributed with this work for additional information |
4 | | // regarding copyright ownership. The ASF licenses this file |
5 | | // to you under the Apache License, Version 2.0 (the |
6 | | // "License"); you may not use this file except in compliance |
7 | | // with the License. You may obtain a copy of the License at |
8 | | // |
9 | | // http://www.apache.org/licenses/LICENSE-2.0 |
10 | | // |
11 | | // Unless required by applicable law or agreed to in writing, |
12 | | // software distributed under the License is distributed on an |
13 | | // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
14 | | // KIND, either express or implied. See the License for the |
15 | | // specific language governing permissions and limitations |
16 | | // under the License. |
17 | | |
18 | | #include "service/http/http_client.h" |
19 | | |
20 | | #include <absl/strings/str_split.h> |
21 | | #include <glog/logging.h> |
22 | | #include <unistd.h> |
23 | | |
24 | | #include <memory> |
25 | | #include <ostream> |
26 | | |
27 | | #include "common/cast_set.h" |
28 | | #include "common/config.h" |
29 | | #include "common/status.h" |
30 | | #include "io/fs/local_file_system.h" |
31 | | #include "runtime/cluster_info.h" |
32 | | #include "runtime/exec_env.h" |
33 | | #include "service/http/http_headers.h" |
34 | | #include "util/security.h" |
35 | | #include "util/stack_util.h" |
36 | | |
37 | | namespace doris { |
38 | | class MultiFileSplitter { |
39 | | public: |
40 | | MultiFileSplitter(std::string local_dir, std::unordered_set<std::string> expected_files) |
41 | 1 | : _local_dir_path(std::move(local_dir)), _expected_files(std::move(expected_files)) {} |
42 | 1 | ~MultiFileSplitter() { |
43 | 1 | if (_fd >= 0) { |
44 | 0 | close(_fd); |
45 | 0 | } |
46 | | |
47 | 1 | if (!_status.ok() && !downloaded_files.empty()) { |
48 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << " failed, try remove the " |
49 | 0 | << downloaded_files.size() << " downloaded files"; |
50 | 0 | for (const auto& file : downloaded_files) { |
51 | 0 | remove(file.c_str()); |
52 | 0 | } |
53 | 0 | } |
54 | 1 | } |
55 | | |
56 | 661 | bool append(const char* data, size_t length) { |
57 | | // Already failed. |
58 | 661 | if (!_status.ok()) { |
59 | 0 | return false; |
60 | 0 | } |
61 | | |
62 | 661 | std::string buf; |
63 | 661 | if (!_buffer.empty()) { |
64 | 0 | buf.swap(_buffer); |
65 | 0 | buf.append(data, length); |
66 | 0 | data = buf.data(); |
67 | 0 | length = buf.size(); |
68 | 0 | } |
69 | 661 | return append_inner(data, length); |
70 | 661 | } |
71 | | |
72 | 1 | Status finish() { |
73 | 1 | if (_status.ok()) { |
74 | 1 | _status = finish_inner(); |
75 | 1 | } |
76 | | |
77 | 1 | return _status; |
78 | 1 | } |
79 | | |
80 | | private: |
81 | 661 | bool append_inner(const char* data, size_t length) { |
82 | 1.39k | while (length > 0) { |
83 | 729 | int consumed = 0; |
84 | 729 | if (_is_reading_header) { |
85 | 35 | consumed = parse_header(data, length); |
86 | 694 | } else { |
87 | 694 | consumed = append_file(data, length); |
88 | 694 | } |
89 | | |
90 | 729 | if (consumed < 0) { |
91 | 0 | return false; |
92 | 0 | } |
93 | | |
94 | 729 | DCHECK(consumed <= length); |
95 | 729 | data += consumed; |
96 | 729 | length -= consumed; |
97 | 729 | } |
98 | 661 | return true; |
99 | 661 | } |
100 | | |
101 | 35 | int parse_header(const char* data, size_t length) { |
102 | 35 | DCHECK(_fd < 0); |
103 | | |
104 | 35 | std::string_view buf(data, length); |
105 | 35 | size_t pos = buf.find("\r\n\r\n"); |
106 | 35 | if (pos == std::string::npos) { |
107 | 0 | _buffer.append(data, length); |
108 | 0 | return static_cast<int>(length); |
109 | 0 | } |
110 | | |
111 | | // header already read. |
112 | 35 | _is_reading_header = false; |
113 | | |
114 | 35 | bool has_file_name = false; |
115 | 35 | bool has_file_size = false; |
116 | 35 | std::string_view header = buf.substr(0, pos); |
117 | 35 | std::vector<std::string> headers = absl::StrSplit(header, "\r\n", absl::SkipWhitespace()); |
118 | 70 | for (auto& s : headers) { |
119 | 70 | size_t header_pos = s.find(':'); |
120 | 70 | if (header_pos == std::string::npos) { |
121 | 0 | continue; |
122 | 0 | } |
123 | 70 | std::string_view header_view(s); |
124 | 70 | std::string_view key = header_view.substr(0, header_pos); |
125 | 70 | std::string_view value = header_view.substr(header_pos + 1); |
126 | 70 | if (value.starts_with(' ')) { |
127 | 70 | value.remove_prefix(std::min(value.find_first_not_of(' '), value.size())); |
128 | 70 | } |
129 | 70 | if (key == "File-Name") { |
130 | 35 | _file_name = value; |
131 | 35 | has_file_name = true; |
132 | 35 | } else if (key == "Content-Length") { |
133 | 35 | auto res = std::from_chars(value.data(), value.data() + value.size(), _file_size); |
134 | 35 | if (res.ec != std::errc()) { |
135 | 0 | std::string error_msg = fmt::format("invalid content length: {}", value); |
136 | 0 | LOG(WARNING) << "download files to " << _local_dir_path |
137 | 0 | << "failed, err=" << error_msg; |
138 | 0 | _status = Status::HttpError(std::move(error_msg)); |
139 | 0 | return -1; |
140 | 0 | } |
141 | 35 | has_file_size = true; |
142 | 35 | } |
143 | 70 | } |
144 | | |
145 | 35 | if (!has_file_name || !has_file_size) { |
146 | 0 | std::string error_msg = |
147 | 0 | fmt::format("invalid multi part header, has file name: {}, has file size: {}", |
148 | 0 | has_file_name, has_file_size); |
149 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << "failed, err=" << error_msg; |
150 | 0 | _status = Status::HttpError(std::move(error_msg)); |
151 | 0 | return -1; |
152 | 0 | } |
153 | | |
154 | 35 | if (!_expected_files.contains(_file_name)) { |
155 | 0 | std::string error_msg = fmt::format("unexpected file: {}", _file_name); |
156 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << "failed, err=" << error_msg; |
157 | 0 | _status = Status::HttpError(std::move(error_msg)); |
158 | 0 | return -1; |
159 | 0 | } |
160 | | |
161 | 35 | VLOG_DEBUG << "receive file " << _file_name << ", size " << _file_size; |
162 | | |
163 | 35 | _written_size = 0; |
164 | 35 | _local_file_path = fmt::format("{}/{}", _local_dir_path, _file_name); |
165 | 35 | _fd = open(_local_file_path.c_str(), O_WRONLY | O_CREAT | O_TRUNC, 0644); |
166 | 35 | if (_fd < 0) { |
167 | 0 | std::string error_msg = "fail to open file to write: " + _local_file_path; |
168 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << "failed, err=" << error_msg; |
169 | 0 | _status = Status::IOError(std::move(error_msg)); |
170 | 0 | return -1; |
171 | 0 | } |
172 | 35 | downloaded_files.push_back(_local_file_path); |
173 | | |
174 | 35 | return static_cast<int>(pos + 4); |
175 | 35 | } |
176 | | |
177 | 694 | int append_file(const char* data, size_t length) { |
178 | 694 | DCHECK(_fd >= 0); |
179 | 694 | DCHECK(_file_size >= _written_size); |
180 | | |
181 | 694 | size_t write_size = std::min(length, _file_size - _written_size); |
182 | 694 | if (write_size > 0 && write(_fd, data, write_size) < 0) { |
183 | 0 | auto msg = fmt::format("write file failed, file={}, error={}", _local_file_path, |
184 | 0 | strerror(errno)); |
185 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << "failed, err=" << msg; |
186 | 0 | _status = Status::HttpError(std::move(msg)); |
187 | 0 | return -1; |
188 | 0 | } |
189 | | |
190 | 694 | _written_size += write_size; |
191 | 694 | if (_written_size == _file_size) { |
192 | | // This file has been downloaded, switch to the next one. |
193 | 34 | switch_to_next_file(); |
194 | 34 | } |
195 | | |
196 | 694 | return cast_set<int>(write_size); |
197 | 694 | } |
198 | | |
199 | 1 | Status finish_inner() { |
200 | 1 | if (!_is_reading_header && _written_size == _file_size) { |
201 | 1 | switch_to_next_file(); |
202 | 1 | } |
203 | | |
204 | 1 | if (_fd >= 0) { |
205 | | // This file is not completely downloaded. |
206 | 0 | close(_fd); |
207 | 0 | _fd = -1; |
208 | 0 | auto error_msg = fmt::format("file {} is not completely downloaded", _local_file_path); |
209 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << "failed, err=" << error_msg; |
210 | 0 | return Status::HttpError(std::move(error_msg)); |
211 | 0 | } |
212 | | |
213 | 1 | if (!_expected_files.empty()) { |
214 | 0 | auto error_msg = fmt::format("not all files are downloaded, {} missing files", |
215 | 0 | _expected_files.size()); |
216 | 0 | LOG(WARNING) << "download files to " << _local_dir_path << "failed, err=" << error_msg; |
217 | 0 | return Status::HttpError(std::move(error_msg)); |
218 | 0 | } |
219 | | |
220 | 1 | downloaded_files.clear(); |
221 | 1 | return Status::OK(); |
222 | 1 | } |
223 | | |
224 | 35 | void switch_to_next_file() { |
225 | 35 | DCHECK(_fd >= 0); |
226 | 35 | DCHECK(_written_size == _file_size); |
227 | | |
228 | 35 | close(_fd); |
229 | 35 | _fd = -1; |
230 | 35 | _expected_files.erase(_file_name); |
231 | 35 | _is_reading_header = true; |
232 | 35 | } |
233 | | |
234 | | const std::string _local_dir_path; |
235 | | std::string _buffer; |
236 | | std::unordered_set<std::string> _expected_files; |
237 | | Status _status; |
238 | | |
239 | | bool _is_reading_header = true; |
240 | | int _fd = -1; |
241 | | std::string _local_file_path; |
242 | | std::string _file_name; |
243 | | size_t _file_size = 0; |
244 | | size_t _written_size = 0; |
245 | | std::vector<std::string> downloaded_files; |
246 | | }; |
247 | | |
248 | 0 | static const char* header_error_msg(CURLHcode code) { |
249 | 0 | switch (code) { |
250 | 0 | case CURLHE_OK: |
251 | 0 | return "OK"; |
252 | 0 | case CURLHE_BADINDEX: |
253 | 0 | return "header exists but not with this index "; |
254 | 0 | case CURLHE_MISSING: |
255 | 0 | return "no such header exists"; |
256 | 0 | case CURLHE_NOHEADERS: |
257 | 0 | return "no headers at all exist (yet)"; |
258 | 0 | case CURLHE_NOREQUEST: |
259 | 0 | return "no request with this number was used"; |
260 | 0 | case CURLHE_OUT_OF_MEMORY: |
261 | 0 | return "out of memory while processing"; |
262 | 0 | case CURLHE_BAD_ARGUMENT: |
263 | 0 | return "a function argument was not okay"; |
264 | 0 | case CURLHE_NOT_BUILT_IN: |
265 | 0 | return "curl_easy_header() was disabled in the build"; |
266 | 0 | default: |
267 | 0 | return "unknown"; |
268 | 0 | } |
269 | 0 | } |
270 | | |
271 | 12.2k | HttpClient::HttpClient() = default; |
272 | | |
273 | 12.2k | HttpClient::~HttpClient() { |
274 | 12.2k | if (_curl != nullptr) { |
275 | 12.2k | curl_easy_cleanup(_curl); |
276 | 12.2k | _curl = nullptr; |
277 | 12.2k | } |
278 | 12.2k | if (_header_list != nullptr) { |
279 | 12.1k | curl_slist_free_all(_header_list); |
280 | 12.1k | _header_list = nullptr; |
281 | 12.1k | } |
282 | 12.2k | } |
283 | | |
284 | 12.2k | Status HttpClient::init(const std::string& url, bool set_fail_on_error) { |
285 | 12.2k | if (_curl == nullptr) { |
286 | 12.2k | _curl = curl_easy_init(); |
287 | 12.2k | if (_curl == nullptr) { |
288 | 0 | return Status::InternalError("fail to initialize curl"); |
289 | 0 | } |
290 | 12.2k | } else { |
291 | 1 | curl_easy_reset(_curl); |
292 | 1 | } |
293 | | |
294 | 12.2k | if (_header_list != nullptr) { |
295 | 0 | curl_slist_free_all(_header_list); |
296 | 0 | _header_list = nullptr; |
297 | 0 | } |
298 | | // set error_buf |
299 | 12.2k | _error_buf[0] = 0; |
300 | 12.2k | auto code = curl_easy_setopt(_curl, CURLOPT_ERRORBUFFER, _error_buf); |
301 | 12.2k | if (code != CURLE_OK) { |
302 | 0 | LOG(WARNING) << "fail to set CURLOPT_ERRORBUFFER, msg=" << _to_errmsg(code); |
303 | 0 | return Status::InternalError("fail to set error buffer"); |
304 | 0 | } |
305 | | // forbid signals |
306 | 12.2k | code = curl_easy_setopt(_curl, CURLOPT_NOSIGNAL, 1L); |
307 | 12.2k | if (code != CURLE_OK) { |
308 | 0 | LOG(WARNING) << "fail to set CURLOPT_NOSIGNAL, msg=" << _to_errmsg(code); |
309 | 0 | return Status::InternalError("fail to set CURLOPT_NOSIGNAL"); |
310 | 0 | } |
311 | | // set fail on error |
312 | | // When this option is set to `1L` (enabled), libcurl will return an error directly |
313 | | // when encountering HTTP error codes (>= 400), without reading the body of the error response. |
314 | 12.2k | if (set_fail_on_error) { |
315 | 11.7k | code = curl_easy_setopt(_curl, CURLOPT_FAILONERROR, 1L); |
316 | 11.7k | if (code != CURLE_OK) { |
317 | 0 | LOG(WARNING) << "fail to set CURLOPT_FAILONERROR, msg=" << _to_errmsg(code); |
318 | 0 | return Status::InternalError("fail to set CURLOPT_FAILONERROR"); |
319 | 0 | } |
320 | 11.7k | } |
321 | | // set redirect |
322 | 12.2k | code = curl_easy_setopt(_curl, CURLOPT_FOLLOWLOCATION, 1L); |
323 | 12.2k | if (code != CURLE_OK) { |
324 | 0 | LOG(WARNING) << "fail to set CURLOPT_FOLLOWLOCATION, msg=" << _to_errmsg(code); |
325 | 0 | return Status::InternalError("fail to set CURLOPT_FOLLOWLOCATION"); |
326 | 0 | } |
327 | 12.2k | code = curl_easy_setopt(_curl, CURLOPT_MAXREDIRS, 20); |
328 | 12.2k | if (code != CURLE_OK) { |
329 | 0 | LOG(WARNING) << "fail to set CURLOPT_MAXREDIRS, msg=" << _to_errmsg(code); |
330 | 0 | return Status::InternalError("fail to set CURLOPT_MAXREDIRS"); |
331 | 0 | } |
332 | | |
333 | 14.4k | curl_write_callback callback = [](char* buffer, size_t size, size_t nmemb, void* param) { |
334 | 14.4k | auto* client = (HttpClient*)param; |
335 | 14.4k | return client->on_response_data(buffer, size * nmemb); |
336 | 14.4k | }; |
337 | | |
338 | | // set callback function |
339 | 12.2k | code = curl_easy_setopt(_curl, CURLOPT_WRITEFUNCTION, callback); |
340 | 12.2k | if (code != CURLE_OK) { |
341 | 0 | LOG(WARNING) << "fail to set CURLOPT_WRITEFUNCTION, msg=" << _to_errmsg(code); |
342 | 0 | return Status::InternalError("fail to set CURLOPT_WRITEFUNCTION"); |
343 | 0 | } |
344 | 12.2k | code = curl_easy_setopt(_curl, CURLOPT_WRITEDATA, (void*)this); |
345 | 12.2k | if (code != CURLE_OK) { |
346 | 0 | LOG(WARNING) << "fail to set CURLOPT_WRITEDATA, msg=" << _to_errmsg(code); |
347 | 0 | return Status::InternalError("fail to set CURLOPT_WRITEDATA"); |
348 | 0 | } |
349 | | |
350 | 12.2k | std::string escaped_url; |
351 | 12.2k | RETURN_IF_ERROR(_escape_url(url, &escaped_url)); |
352 | | // set url |
353 | 12.2k | code = curl_easy_setopt(_curl, CURLOPT_URL, escaped_url.c_str()); |
354 | 12.2k | if (code != CURLE_OK) { |
355 | 0 | LOG(WARNING) << "failed to set CURLOPT_URL, errmsg=" << _to_errmsg(code); |
356 | 0 | return Status::InternalError("fail to set CURLOPT_URL"); |
357 | 0 | } |
358 | | |
359 | 12.2k | #ifndef BE_TEST |
360 | 12.2k | set_auth_token(ExecEnv::GetInstance()->cluster_info()->curr_auth_token); |
361 | 12.2k | #endif |
362 | 12.2k | return Status::OK(); |
363 | 12.2k | } |
364 | | |
365 | 5.92k | void HttpClient::set_method(HttpMethod method) { |
366 | 5.92k | _method = method; |
367 | 5.92k | switch (method) { |
368 | 49 | case GET: |
369 | 49 | curl_easy_setopt(_curl, CURLOPT_HTTPGET, 1L); |
370 | 49 | return; |
371 | 0 | case PUT: |
372 | 0 | curl_easy_setopt(_curl, CURLOPT_UPLOAD, 1L); |
373 | 0 | return; |
374 | 5.86k | case POST: |
375 | 5.86k | curl_easy_setopt(_curl, CURLOPT_POST, 1L); |
376 | 5.86k | return; |
377 | 0 | case DELETE: |
378 | 0 | curl_easy_setopt(_curl, CURLOPT_CUSTOMREQUEST, "DELETE"); |
379 | 0 | return; |
380 | 7 | case HEAD: |
381 | 7 | curl_easy_setopt(_curl, CURLOPT_NOBODY, 1L); |
382 | 7 | return; |
383 | 0 | case OPTIONS: |
384 | 0 | curl_easy_setopt(_curl, CURLOPT_CUSTOMREQUEST, "OPTIONS"); |
385 | 0 | return; |
386 | 0 | default: |
387 | 0 | return; |
388 | 5.92k | } |
389 | 5.92k | } |
390 | | |
391 | 3 | void HttpClient::set_speed_limit() { |
392 | 3 | curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_LIMIT, config::download_low_speed_limit_kbps * 1024); |
393 | 3 | curl_easy_setopt(_curl, CURLOPT_LOW_SPEED_TIME, config::download_low_speed_time); |
394 | 3 | curl_easy_setopt(_curl, CURLOPT_MAX_RECV_SPEED_LARGE, config::max_download_speed_kbps * 1024); |
395 | 3 | } |
396 | | |
397 | 14.4k | size_t HttpClient::on_response_data(const void* data, size_t length) { |
398 | 14.4k | if (*_callback != nullptr) { |
399 | 14.4k | bool is_continue = (*_callback)(data, length); |
400 | 14.4k | if (!is_continue) { |
401 | 0 | return -1; |
402 | 0 | } |
403 | 14.4k | } |
404 | 14.4k | return length; |
405 | 14.4k | } |
406 | | |
407 | 16 | Status HttpClient::execute_post_request(const std::string& payload, std::string* response) { |
408 | 16 | set_method(POST); |
409 | 16 | set_payload(payload); |
410 | 16 | return execute(response); |
411 | 16 | } |
412 | | |
413 | 42 | Status HttpClient::execute_put_request(const std::string& payload, std::string* response) { |
414 | 42 | set_payload(payload); |
415 | 42 | curl_easy_setopt(_curl, CURLOPT_CUSTOMREQUEST, "PUT"); |
416 | 42 | return execute(response); |
417 | 42 | } |
418 | | |
419 | 0 | Status HttpClient::execute_delete_request(const std::string& payload, std::string* response) { |
420 | 0 | set_method(DELETE); |
421 | 0 | set_payload(payload); |
422 | 0 | return execute(response); |
423 | 0 | } |
424 | | |
425 | 12.2k | Status HttpClient::execute(const std::function<bool(const void* data, size_t length)>& callback) { |
426 | 12.2k | if (VLOG_DEBUG_IS_ON) { |
427 | 0 | VLOG_DEBUG << "execute http " << to_method_desc(_method) << " request, url " << _get_url(); |
428 | 0 | } |
429 | 12.2k | _callback = &callback; |
430 | 12.2k | auto code = curl_easy_perform(_curl); |
431 | 12.2k | if (code != CURLE_OK) { |
432 | 67 | std::string url = mask_token(_get_url()); |
433 | 67 | LOG(WARNING) << "fail to execute HTTP client, errmsg=" << _to_errmsg(code) |
434 | 67 | << ", trace=" << get_stack_trace() << ", url=" << url; |
435 | 67 | std::string errmsg = fmt::format("{}, url={}", _to_errmsg(code), url); |
436 | 67 | return Status::HttpError(std::move(errmsg)); |
437 | 67 | } |
438 | 12.1k | if (VLOG_DEBUG_IS_ON) { |
439 | 0 | VLOG_DEBUG << "execute http " << to_method_desc(_method) << " request, url " << _get_url() |
440 | 0 | << " done"; |
441 | 0 | } |
442 | 12.1k | return Status::OK(); |
443 | 12.2k | } |
444 | | |
445 | 3 | Status HttpClient::get_content_md5(std::string* md5) const { |
446 | 3 | struct curl_header* header_ptr; |
447 | 3 | auto code = curl_easy_header(_curl, HttpHeaders::CONTENT_MD5, 0, CURLH_HEADER, 0, &header_ptr); |
448 | 3 | if (code == CURLHE_MISSING || code == CURLHE_NOHEADERS) { |
449 | | // no such headers exists |
450 | 1 | md5->clear(); |
451 | 1 | return Status::OK(); |
452 | 2 | } else if (code != CURLHE_OK) { |
453 | 0 | auto msg = fmt::format("failed to get http header {}: {} ({})", HttpHeaders::CONTENT_MD5, |
454 | 0 | header_error_msg(code), code); |
455 | 0 | LOG(WARNING) << msg << ", trace=" << get_stack_trace(); |
456 | 0 | return Status::HttpError(std::move(msg)); |
457 | 0 | } |
458 | | |
459 | 2 | *md5 = header_ptr->value; |
460 | 2 | return Status::OK(); |
461 | 3 | } |
462 | | |
463 | 2 | Status HttpClient::download(const std::string& local_path) { |
464 | 2 | set_method(GET); |
465 | 2 | set_speed_limit(); |
466 | | |
467 | | // remove the file if it exists, to avoid change the linked files unexpectedly |
468 | 2 | bool exist = false; |
469 | 2 | RETURN_IF_ERROR(io::global_local_filesystem()->exists(local_path, &exist)); |
470 | 2 | if (exist) { |
471 | 0 | remove(local_path.c_str()); |
472 | 0 | } |
473 | | |
474 | 2 | auto fp_closer = [](FILE* fp) { fclose(fp); }; |
475 | 2 | std::unique_ptr<FILE, decltype(fp_closer)> fp(fopen(local_path.c_str(), "w"), fp_closer); |
476 | 2 | if (fp == nullptr) { |
477 | 0 | LOG(WARNING) << "open file failed, file=" << local_path; |
478 | 0 | return Status::InternalError("open file failed"); |
479 | 0 | } |
480 | 2 | Status status; |
481 | 2 | auto callback = [&status, &fp, &local_path](const void* data, size_t length) { |
482 | 2 | auto res = fwrite(data, length, 1, fp.get()); |
483 | 2 | if (res != 1) { |
484 | 0 | LOG(WARNING) << "fail to write data to file, file=" << local_path |
485 | 0 | << ", error=" << ferror(fp.get()); |
486 | 0 | status = Status::InternalError("fail to write data when download"); |
487 | 0 | return false; |
488 | 0 | } |
489 | 2 | return true; |
490 | 2 | }; |
491 | | |
492 | 2 | if (auto s = execute(callback); !s.ok()) { |
493 | 0 | status = s; |
494 | 0 | } |
495 | 2 | if (!status.ok()) { |
496 | 0 | remove(local_path.c_str()); |
497 | 0 | } |
498 | 2 | return status; |
499 | 2 | } |
500 | | |
501 | | Status HttpClient::download_multi_files(const std::string& local_dir, |
502 | 1 | const std::unordered_set<std::string>& expected_files) { |
503 | 1 | set_speed_limit(); |
504 | | |
505 | 1 | MultiFileSplitter splitter(local_dir, expected_files); |
506 | 661 | auto callback = [&](const void* data, size_t length) { |
507 | 661 | return splitter.append(reinterpret_cast<const char*>(data), length); |
508 | 661 | }; |
509 | 1 | if (auto s = execute(callback); !s.ok()) { |
510 | 0 | return s; |
511 | 0 | } |
512 | 1 | return splitter.finish(); |
513 | 1 | } |
514 | | |
515 | 11.2k | Status HttpClient::execute(std::string* response) { |
516 | 11.2k | auto callback = [response](const void* data, size_t length) { |
517 | 11.2k | response->append((char*)data, length); |
518 | 11.2k | return true; |
519 | 11.2k | }; |
520 | 11.2k | return execute(callback); |
521 | 11.2k | } |
522 | | |
523 | 136 | const char* HttpClient::_to_errmsg(CURLcode code) const { |
524 | 136 | if (_error_buf[0] == 0) { |
525 | 0 | return curl_easy_strerror(code); |
526 | 0 | } |
527 | 136 | return _error_buf; |
528 | 136 | } |
529 | | |
530 | 67 | const char* HttpClient::_get_url() const { |
531 | 67 | const char* url = nullptr; |
532 | 67 | curl_easy_getinfo(_curl, CURLINFO_EFFECTIVE_URL, &url); |
533 | 67 | if (!url) { |
534 | 0 | url = "<unknown>"; |
535 | 0 | } |
536 | 67 | return url; |
537 | 67 | } |
538 | | |
539 | | // execute remote call action with retry |
540 | | Status HttpClient::execute(int retry_times, int sleep_time, |
541 | 0 | const std::function<Status(HttpClient*)>& callback) { |
542 | 0 | Status status; |
543 | 0 | for (int i = 0; i < retry_times; ++i) { |
544 | 0 | status = callback(this); |
545 | 0 | if (status.ok()) { |
546 | 0 | auto http_status = get_http_status(); |
547 | 0 | if (http_status == 200) { |
548 | 0 | return status; |
549 | 0 | } else { |
550 | 0 | std::string url = mask_token(_get_url()); |
551 | 0 | auto error_msg = fmt::format("http status code is not 200, code={}, url={}", |
552 | 0 | http_status, url); |
553 | 0 | LOG(WARNING) << error_msg; |
554 | 0 | return Status::HttpError(error_msg); |
555 | 0 | } |
556 | 0 | } |
557 | 0 | sleep(sleep_time); |
558 | 0 | } |
559 | 0 | return status; |
560 | 0 | } |
561 | | |
562 | | Status HttpClient::execute_with_retry(int retry_times, int sleep_time, |
563 | 11.1k | const std::function<Status(HttpClient*)>& callback) { |
564 | 11.1k | Status status; |
565 | 11.2k | for (int i = 0; i < retry_times; ++i) { |
566 | 11.1k | HttpClient client; |
567 | 11.1k | status = callback(&client); |
568 | 11.1k | if (status.ok()) { |
569 | 11.1k | auto http_status = client.get_http_status(); |
570 | 11.1k | if (http_status == 200) { |
571 | 11.1k | return status; |
572 | 11.1k | } else { |
573 | 0 | std::string url = mask_token(client._get_url()); |
574 | 0 | auto error_msg = fmt::format("http status code is not 200, code={}, url={}", |
575 | 0 | http_status, url); |
576 | 0 | LOG(WARNING) << error_msg; |
577 | 0 | return Status::HttpError(error_msg); |
578 | 0 | } |
579 | 11.1k | } |
580 | 43 | sleep(sleep_time); |
581 | 43 | } |
582 | 15 | return status; |
583 | 11.1k | } |
584 | | |
585 | | // http://example.com/page?param1=value1¶m2=value+with+spaces#section |
586 | 12.2k | Status HttpClient::_escape_url(const std::string& url, std::string* escaped_url) { |
587 | 12.2k | size_t query_pos = url.find('?'); |
588 | 12.2k | if (query_pos == std::string::npos) { |
589 | 12.2k | *escaped_url = url; |
590 | 12.2k | return Status::OK(); |
591 | 12.2k | } |
592 | 36 | size_t fragment_pos = url.find('#'); |
593 | 36 | std::string query; |
594 | 36 | std::string fragment; |
595 | | |
596 | 36 | if (fragment_pos == std::string::npos) { |
597 | 35 | query = url.substr(query_pos + 1, url.length() - query_pos - 1); |
598 | 35 | } else { |
599 | 1 | query = url.substr(query_pos + 1, fragment_pos - query_pos - 1); |
600 | 1 | fragment = url.substr(fragment_pos, url.length() - fragment_pos); |
601 | 1 | } |
602 | | |
603 | 36 | std::string encoded_query; |
604 | 36 | size_t ampersand_pos = query.find('&'); |
605 | 36 | size_t equal_pos; |
606 | | |
607 | 36 | if (ampersand_pos == std::string::npos) { |
608 | 18 | ampersand_pos = query.length(); |
609 | 18 | } |
610 | | |
611 | 64 | while (true) { |
612 | 64 | equal_pos = query.find('='); |
613 | 64 | if (equal_pos != std::string::npos) { |
614 | 61 | std::string key = query.substr(0, equal_pos); |
615 | 61 | std::string value = query.substr(equal_pos + 1, ampersand_pos - equal_pos - 1); |
616 | | |
617 | 61 | auto encoded_value = std::unique_ptr<char, decltype(&curl_free)>( |
618 | 61 | curl_easy_escape(_curl, value.c_str(), cast_set<int>(value.length())), |
619 | 61 | &curl_free); |
620 | 61 | if (encoded_value) { |
621 | 61 | encoded_query += key + "=" + std::string(encoded_value.get()); |
622 | 61 | } else { |
623 | 0 | return Status::InternalError("escape url failed, url={}", url); |
624 | 0 | } |
625 | 61 | } else { |
626 | 3 | encoded_query += query.substr(0, ampersand_pos); |
627 | 3 | } |
628 | | |
629 | 64 | if (ampersand_pos == query.length() || ampersand_pos == std::string::npos) { |
630 | 36 | break; |
631 | 36 | } |
632 | | |
633 | 28 | encoded_query += "&"; |
634 | 28 | query = query.substr(ampersand_pos + 1); |
635 | 28 | ampersand_pos = query.find('&'); |
636 | 28 | } |
637 | 36 | *escaped_url = url.substr(0, query_pos + 1) + encoded_query + fragment; |
638 | 36 | return Status::OK(); |
639 | 36 | } |
640 | | } // namespace doris |