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 "io/fs/s3_file_system.h" |
19 | | |
20 | | #include <fmt/format.h> |
21 | | |
22 | | #include <cstddef> |
23 | | |
24 | | #include "common/compiler_util.h" // IWYU pragma: keep |
25 | | // IWYU pragma: no_include <bits/chrono.h> |
26 | | #include <aws/core/utils/threading/Executor.h> |
27 | | #include <aws/s3/S3Client.h> |
28 | | |
29 | | #include <chrono> // IWYU pragma: keep |
30 | | #include <filesystem> |
31 | | #include <fstream> // IWYU pragma: keep |
32 | | #include <future> |
33 | | #include <memory> |
34 | | #include <variant> |
35 | | |
36 | | #include "common/check.h" |
37 | | #include "common/config.h" |
38 | | #include "common/logging.h" |
39 | | #include "common/status.h" |
40 | | #include "cpp/obj-client/obj_storage_client.h" |
41 | | #include "cpp/obj-client/s3_common.h" |
42 | | #include "cpp/sync_point.h" |
43 | | #include "io/fs/err_utils.h" |
44 | | #include "io/fs/file_system.h" |
45 | | #include "io/fs/file_writer.h" |
46 | | #include "io/fs/local_file_system.h" |
47 | | #include "io/fs/remote_file_system.h" |
48 | | #include "io/fs/s3_file_reader.h" |
49 | | #include "io/fs/s3_file_writer.h" |
50 | | #include "io/obj_storage/obj_client_factory.h" |
51 | | #include "io/obj_storage/obj_storage_config_resolver.h" |
52 | | #include "io/obj_storage/obj_storage_uri.h" |
53 | | #include "runtime/exec_env.h" |
54 | | #include "runtime/thread_context.h" |
55 | | #include "util/s3_uri.h" |
56 | | #include "util/s3_util.h" |
57 | | |
58 | | namespace doris::io { |
59 | | namespace { |
60 | | constexpr std::string_view OSS_PRIVATE_ENDPOINT_SUFFIX = "-internal.aliyuncs.com"; |
61 | | constexpr int LEN_OF_OSS_PRIVATE_SUFFIX = 9; // length of "-internal" |
62 | | |
63 | | #ifndef CHECK_S3_CLIENT |
64 | | #define CHECK_S3_CLIENT(client) \ |
65 | 2.08k | if (!client) { \ |
66 | 0 | return Status::InvalidArgument("init s3 client error"); \ |
67 | 0 | } |
68 | | #endif |
69 | | |
70 | | } // namespace |
71 | | |
72 | | ObjClientHolder::ObjClientHolder(S3ClientConf conf) |
73 | 77 | : _state(std::make_shared<ObjClientState>(ObjClientState { |
74 | 77 | .config = ObjStorageConfigResolver::from_client_conf(conf), |
75 | 77 | .legacy_conf = std::move(conf), |
76 | 77 | .client = nullptr, |
77 | 77 | .generation = 0, |
78 | 77 | })) {} |
79 | | |
80 | | ObjClientHolder::ObjClientHolder(ObjClientConfig config) |
81 | 2 | : _state(std::make_shared<ObjClientState>(ObjClientState { |
82 | 2 | .config = std::move(config), |
83 | 2 | .legacy_conf = std::nullopt, |
84 | 2 | .client = nullptr, |
85 | 2 | .generation = 0, |
86 | 2 | })) {} |
87 | | |
88 | 78 | ObjClientHolder::~ObjClientHolder() = default; |
89 | | |
90 | 19 | Status ObjClientHolder::init() { |
91 | 19 | auto current = state(); |
92 | 19 | std::shared_ptr<ObjStorageClient> client; |
93 | 19 | if (current->legacy_conf.has_value()) { |
94 | | // A legacy conf still goes through S3ClientFactory, which applies the legacy |
95 | | // validation rules (e.g. a non-empty region) and the test client hook. |
96 | 17 | client = DORIS_TRY(S3ClientFactory::instance().create(*current->legacy_conf)); |
97 | 17 | } else { |
98 | 2 | client = DORIS_TRY(ObjClientFactory::instance().get_or_create(current->config)); |
99 | 1 | } |
100 | 18 | return _publish(current, current->config, current->legacy_conf, std::move(client)); |
101 | 19 | } |
102 | | |
103 | 1 | S3ClientConf ObjClientHolder::s3_client_conf() const { |
104 | 1 | auto current = state(); |
105 | 1 | DORIS_CHECK(current->legacy_conf.has_value()); |
106 | 1 | return *current->legacy_conf; |
107 | 1 | } |
108 | | |
109 | 5 | Status ObjClientHolder::reset(const S3ClientConf& conf) { |
110 | 5 | auto current = state(); |
111 | 5 | DORIS_CHECK(current->legacy_conf.has_value()); |
112 | 5 | S3ClientConf reset_conf = *current->legacy_conf; |
113 | 5 | reset_conf.ak = conf.ak; |
114 | 5 | reset_conf.sk = conf.sk; |
115 | 5 | reset_conf.token = conf.token; |
116 | 5 | reset_conf.bucket = conf.bucket; |
117 | 5 | reset_conf.connect_timeout_ms = conf.connect_timeout_ms; |
118 | 5 | reset_conf.max_connections = conf.max_connections; |
119 | 5 | reset_conf.request_timeout_ms = conf.request_timeout_ms; |
120 | 5 | reset_conf.use_virtual_addressing = conf.use_virtual_addressing; |
121 | 5 | reset_conf.is_internal_bucket = conf.is_internal_bucket; |
122 | | |
123 | 5 | reset_conf.role_arn = conf.role_arn; |
124 | 5 | reset_conf.external_id = conf.external_id; |
125 | 5 | reset_conf.cred_provider_type = conf.cred_provider_type; |
126 | | |
127 | | // Compare full-field equality of the merged conf, not get_hash(): the hash is |
128 | | // an XOR of crc32s and distinct configurations can collide, which would skip a |
129 | | // required client rebuild (e.g. a credential update). |
130 | 5 | if (reset_conf == *current->legacy_conf) { |
131 | 1 | return Status::OK(); // Same conf |
132 | 1 | } |
133 | | |
134 | 4 | auto client = DORIS_TRY(S3ClientFactory::instance().create(reset_conf)); |
135 | | |
136 | 4 | LOG(WARNING) << "reset s3 client with new conf: " << conf.to_string(); |
137 | | |
138 | 4 | auto config = ObjStorageConfigResolver::from_client_conf(reset_conf); |
139 | 4 | return _publish(current, std::move(config), std::move(reset_conf), std::move(client)); |
140 | 4 | } |
141 | | |
142 | | Status ObjClientHolder::_publish(const std::shared_ptr<const ObjClientState>& expected, |
143 | | ObjClientConfig config, std::optional<S3ClientConf> legacy_conf, |
144 | 22 | std::shared_ptr<ObjStorageClient> client) { |
145 | | // Declared before the lock: after the swap below it holds the replaced state, and |
146 | | // possibly the last reference to its client, which is then released after unlocking. |
147 | 22 | std::shared_ptr<const ObjClientState> next = std::make_shared<ObjClientState>(ObjClientState { |
148 | 22 | .config = std::move(config), |
149 | 22 | .legacy_conf = std::move(legacy_conf), |
150 | 22 | .client = std::move(client), |
151 | 22 | .generation = expected->generation + 1, |
152 | 22 | }); |
153 | 22 | { |
154 | 22 | std::lock_guard lock(_mtx); |
155 | | // Two resets that both start from generation N merge their conf onto N. Publishing |
156 | | // the second one on top of N + 1 would silently drop the first update, so it is |
157 | | // rejected and the caller retries from the new state. |
158 | 22 | if (_state->generation != expected->generation) { |
159 | 1 | return Status::Aborted( |
160 | 1 | "s3 client was updated concurrently, expected generation {}, current {}", |
161 | 1 | expected->generation, _state->generation); |
162 | 1 | } |
163 | 21 | _state.swap(next); |
164 | 21 | } |
165 | 0 | return Status::OK(); |
166 | 22 | } |
167 | | |
168 | | Result<int64_t> ObjClientHolder::object_file_size(const std::string& bucket, |
169 | 5 | const std::string& key) const { |
170 | 5 | auto client = get(); |
171 | 5 | if (!client) { |
172 | 0 | return ResultError(Status::InvalidArgument("init s3 client error")); |
173 | 0 | } |
174 | | |
175 | 5 | auto resp = client->head_object({ |
176 | 5 | .bucket = bucket, |
177 | 5 | .key = key, |
178 | 5 | }); |
179 | | |
180 | 5 | if (resp.resp.status.code != ErrorCode::OK) { |
181 | 3 | return ResultError(std::move(Status(resp.resp.status.code, std::move(resp.resp.status.msg)) |
182 | 3 | .append(fmt::format("failed to head s3 file {}", |
183 | 3 | full_s3_path(bucket, key))))); |
184 | 3 | } |
185 | | |
186 | 2 | return resp.file_size; |
187 | 5 | } |
188 | | |
189 | 3 | std::string ObjClientHolder::full_s3_path(std::string_view bucket, std::string_view key) const { |
190 | 3 | return fmt::format("{}/{}/{}", state()->config.endpoint(), bucket, key); |
191 | 3 | } |
192 | | |
193 | 0 | std::string S3FileSystem::full_s3_path(std::string_view key) const { |
194 | 0 | return _client->full_s3_path(_bucket, key); |
195 | 0 | } |
196 | | |
197 | 12 | Result<std::shared_ptr<S3FileSystem>> S3FileSystem::create(S3Conf s3_conf, std::string id) { |
198 | 12 | std::shared_ptr<S3FileSystem> fs(new S3FileSystem(std::move(s3_conf), std::move(id))); |
199 | 12 | RETURN_IF_ERROR_RESULT(fs->init()); |
200 | 12 | return fs; |
201 | 12 | } |
202 | | |
203 | | Result<std::shared_ptr<S3FileSystem>> S3FileSystem::create(ObjClientConfig config, |
204 | 0 | std::string bucket, std::string id) { |
205 | 0 | std::shared_ptr<S3FileSystem> fs( |
206 | 0 | new S3FileSystem(std::move(config), std::move(bucket), std::move(id))); |
207 | 0 | RETURN_IF_ERROR_RESULT(fs->init()); |
208 | 0 | return fs; |
209 | 0 | } |
210 | | |
211 | | S3FileSystem::S3FileSystem(S3Conf s3_conf, std::string id) |
212 | 12 | : RemoteFileSystem(s3_conf.prefix, std::move(id), FileSystemType::S3), |
213 | 12 | _bucket(std::move(s3_conf.bucket)), |
214 | 12 | _prefix(std::move(s3_conf.prefix)), |
215 | 12 | _client(std::make_shared<ObjClientHolder>(std::move(s3_conf.client_conf))) { |
216 | | // FIXME(plat1ko): Normalize prefix |
217 | | // remove the first and last '/' |
218 | 12 | if (!_prefix.empty()) { |
219 | 12 | size_t start = _prefix.find_first_not_of('/'); |
220 | 12 | if (start == std::string::npos) { |
221 | 0 | _prefix = ""; |
222 | 12 | } else { |
223 | 12 | size_t end = _prefix.find_last_not_of('/'); |
224 | 12 | if (start > 0 || end < _prefix.size() - 1) { |
225 | 0 | _prefix = _prefix.substr(start, end - start + 1); |
226 | 0 | } |
227 | 12 | } |
228 | 12 | } |
229 | 12 | } |
230 | | |
231 | | S3FileSystem::S3FileSystem(ObjClientConfig config, std::string bucket, std::string id) |
232 | 0 | : RemoteFileSystem("", std::move(id), FileSystemType::S3), |
233 | 0 | _bucket(std::move(bucket)), |
234 | 0 | _native_azure_location(std::holds_alternative<AzureClientConfig>(config.backend)), |
235 | 0 | _client(std::make_shared<ObjClientHolder>(std::move(config))) {} |
236 | | |
237 | 12 | Status S3FileSystem::init() { |
238 | 12 | return _client->init(); |
239 | 12 | } |
240 | | |
241 | 2.09k | Result<std::string> S3FileSystem::_get_key(const Path& full_path) const { |
242 | 2.09k | if (_native_azure_location) { |
243 | | // Typed Azure access keeps the location FE got from the table, e.g. |
244 | | // abfss://c1@acct.dfs.core.windows.net/db/t/f.parquet, which S3URI cannot parse. |
245 | 0 | auto uri = ObjStorageUri::parse_azure(full_path.native()); |
246 | 0 | if (!uri.has_value()) { |
247 | 0 | return ResultError(std::move(uri).error()); |
248 | 0 | } |
249 | 0 | return std::move(uri.value().key); |
250 | 0 | } |
251 | | // FIXME(plat1ko): Check bucket in full path and support relative path |
252 | 2.09k | S3URI uri(full_path.native()); |
253 | 2.09k | RETURN_IF_ERROR_RESULT(uri.parse()); |
254 | 2.09k | return uri.get_key(); |
255 | 2.09k | } |
256 | | |
257 | 11 | S3FileSystem::~S3FileSystem() = default; |
258 | | |
259 | | Status S3FileSystem::create_file_impl(const Path& file, FileWriterPtr* writer, |
260 | 2.08k | const FileWriterOptions* opts) { |
261 | 2.08k | auto client = _client->get(); |
262 | 2.08k | CHECK_S3_CLIENT(client); |
263 | 2.08k | auto key = DORIS_TRY(_get_key(file)); |
264 | 2.08k | *writer = std::make_unique<S3FileWriter>(_client, _bucket, std::move(key), opts); |
265 | 2.08k | return Status::OK(); |
266 | 2.08k | } |
267 | | |
268 | | Status S3FileSystem::open_file_internal(const Path& file, FileReaderSPtr* reader, |
269 | 3 | const FileReaderOptions& opts) { |
270 | 3 | TEST_SYNC_POINT_CALLBACK("S3FileSystem::open_file_internal", &file, &opts); |
271 | 3 | auto key = DORIS_TRY(_get_key(file)); |
272 | 3 | *reader = DORIS_TRY(S3FileReader::create(_client, _bucket, key, opts.file_size, nullptr)); |
273 | 0 | return Status::OK(); |
274 | 3 | } |
275 | | |
276 | 0 | Status S3FileSystem::create_directory_impl(const Path& dir, bool failed_if_exists) { |
277 | 0 | return Status::OK(); |
278 | 0 | } |
279 | | |
280 | 0 | Status S3FileSystem::delete_file_impl(const Path& file) { |
281 | 0 | auto client = _client->get(); |
282 | 0 | CHECK_S3_CLIENT(client); |
283 | |
|
284 | 0 | auto key = DORIS_TRY(_get_key(file)); |
285 | |
|
286 | 0 | auto resp = client->delete_object({.bucket = _bucket, .key = key}); |
287 | |
|
288 | 0 | if (resp.status.code == ErrorCode::OK || resp.status.code == ErrorCode::NOT_FOUND) { |
289 | 0 | return Status::OK(); |
290 | 0 | } |
291 | 0 | return std::move(Status(resp.status.code, std::move(resp.status.msg)) |
292 | 0 | .append(fmt::format("failed to delete file {}", full_s3_path(key)))); |
293 | 0 | } |
294 | | |
295 | 0 | Status S3FileSystem::delete_directory_impl(const Path& dir) { |
296 | 0 | auto client = _client->get(); |
297 | 0 | CHECK_S3_CLIENT(client); |
298 | |
|
299 | 0 | auto prefix = DORIS_TRY(_get_key(dir)); |
300 | 0 | if (!prefix.empty() && prefix.back() != '/') { |
301 | 0 | prefix.push_back('/'); |
302 | 0 | } |
303 | |
|
304 | 0 | ObjStoragePath delete_path { |
305 | 0 | .path = full_s3_path(prefix), |
306 | 0 | .bucket = _bucket, |
307 | 0 | .prefix = prefix, |
308 | 0 | }; |
309 | 0 | auto resp = delete_objects_recursively(std::move(client), delete_path); |
310 | 0 | return {resp.status.code, std::move(resp.status.msg)}; |
311 | 0 | } |
312 | | |
313 | 0 | Status S3FileSystem::batch_delete_impl(const std::vector<Path>& remote_files) { |
314 | 0 | auto client = _client->get(); |
315 | 0 | CHECK_S3_CLIENT(client); |
316 | | |
317 | | // `DeleteObjectsRequest` can only contain 1000 keys at most. |
318 | 0 | constexpr size_t max_delete_batch = 1000; |
319 | 0 | auto path_iter = remote_files.begin(); |
320 | |
|
321 | 0 | do { |
322 | 0 | std::vector<std::string> objects; |
323 | 0 | auto path_begin = path_iter; |
324 | 0 | for (; path_iter != remote_files.end() && (path_iter - path_begin < max_delete_batch); |
325 | 0 | ++path_iter) { |
326 | 0 | auto key = DORIS_TRY(_get_key(*path_iter)); |
327 | 0 | objects.emplace_back(std::move(key)); |
328 | 0 | } |
329 | 0 | if (objects.empty()) { |
330 | 0 | return Status::OK(); |
331 | 0 | } |
332 | | // clang-format off |
333 | 0 | if (auto resp = client->delete_objects( {.bucket = _bucket,}, std::move(objects)); resp.status.code != ErrorCode::OK) { |
334 | 0 | return {resp.status.code, std::move(resp.status.msg)}; |
335 | 0 | } |
336 | | // clang-format on |
337 | 0 | } while (path_iter != remote_files.end()); |
338 | | |
339 | 0 | return Status::OK(); |
340 | 0 | } |
341 | | |
342 | 4 | Status S3FileSystem::exists_impl(const Path& path, bool* res) const { |
343 | 4 | auto client = _client->get(); |
344 | 4 | CHECK_S3_CLIENT(client); |
345 | 4 | auto key = DORIS_TRY(_get_key(path)); |
346 | | |
347 | 4 | VLOG_DEBUG << "key:" << key << " path:" << path; |
348 | | |
349 | 4 | auto resp = client->head_object({.bucket = _bucket, .key = key}); |
350 | | |
351 | 4 | if (resp.resp.status.code == ErrorCode::OK) { |
352 | 0 | *res = true; |
353 | 4 | } else if (resp.resp.status.code == ErrorCode::NOT_FOUND) { |
354 | 4 | *res = false; |
355 | 4 | } else { |
356 | 0 | return std::move( |
357 | 0 | Status(resp.resp.status.code, std::move(resp.resp.status.msg)) |
358 | 0 | .append(fmt::format(" failed to check exists {}", full_s3_path(key)))); |
359 | 0 | } |
360 | 4 | return Status::OK(); |
361 | 4 | } |
362 | | |
363 | 2 | Status S3FileSystem::file_size_impl(const Path& file, int64_t* file_size) const { |
364 | 2 | auto key = DORIS_TRY(_get_key(file)); |
365 | 2 | *file_size = DORIS_TRY(_client->object_file_size(_bucket, key)); |
366 | 2 | return Status::OK(); |
367 | 2 | } |
368 | | |
369 | | Status S3FileSystem::list_impl(const Path& dir, bool only_file, std::vector<FileInfo>* files, |
370 | 0 | bool* exists) { |
371 | | // For object storage, this path is always not exist. |
372 | | // So we ignore this property and set exists to true. |
373 | 0 | *exists = true; |
374 | 0 | auto client = _client->get(); |
375 | 0 | CHECK_S3_CLIENT(client); |
376 | 0 | auto prefix = DORIS_TRY(_get_key(dir)); |
377 | 0 | if (!prefix.empty() && prefix.back() != '/') { |
378 | 0 | prefix.push_back('/'); |
379 | 0 | } |
380 | |
|
381 | 0 | std::vector<ObjectMeta> objects; |
382 | 0 | auto resp = client->list_objects({.bucket = _bucket, .prefix = prefix}, &objects); |
383 | 0 | if (!resp.ok()) { |
384 | 0 | files->clear(); |
385 | 0 | return {resp.status.code, std::move(resp.status.msg)}; |
386 | 0 | } |
387 | 0 | files->reserve(files->size() + objects.size()); |
388 | 0 | for (auto& obj : objects) { |
389 | 0 | obj.key.erase(0, prefix.size()); |
390 | 0 | bool is_dir = obj.key.empty() || obj.key.back() == '/'; |
391 | 0 | files->emplace_back(FileInfo { |
392 | 0 | .file_name = std::move(obj.key), .file_size = obj.size, .is_file = !is_dir}); |
393 | 0 | } |
394 | |
|
395 | 0 | return Status::OK(); |
396 | 0 | } |
397 | | |
398 | 0 | Status S3FileSystem::rename_impl(const Path& orig_name, const Path& new_name) { |
399 | 0 | return Status::NotSupported("S3FileSystem::rename_impl"); |
400 | 0 | } |
401 | | |
402 | 0 | Status S3FileSystem::upload_impl(const Path& local_file, const Path& remote_file) { |
403 | 0 | auto client = _client->get(); |
404 | 0 | CHECK_S3_CLIENT(client); |
405 | |
|
406 | 0 | auto key = DORIS_TRY(_get_key(remote_file)); |
407 | 0 | auto start = std::chrono::steady_clock::now(); |
408 | 0 | FileWriterPtr obj_writer; |
409 | 0 | RETURN_IF_ERROR(create_file_impl(key, &obj_writer, nullptr)); |
410 | 0 | FileReaderSPtr local_reader; |
411 | 0 | RETURN_IF_ERROR(io::global_local_filesystem()->open_file(local_file, &local_reader)); |
412 | 0 | size_t local_buffer_size = config::s3_file_system_local_upload_buffer_size; |
413 | 0 | std::unique_ptr<char[]> write_buffer = |
414 | 0 | std::make_unique_for_overwrite<char[]>(local_buffer_size); |
415 | 0 | size_t cur_read = 0; |
416 | 0 | while (cur_read < local_reader->size()) { |
417 | 0 | size_t bytes_read = 0; |
418 | 0 | RETURN_IF_ERROR(local_reader->read_at( |
419 | 0 | cur_read, Slice {write_buffer.get(), local_buffer_size}, &bytes_read)); |
420 | 0 | RETURN_IF_ERROR(obj_writer->append({write_buffer.get(), bytes_read})); |
421 | 0 | cur_read += bytes_read; |
422 | 0 | } |
423 | 0 | RETURN_IF_ERROR(obj_writer->close()); |
424 | 0 | auto duration = std::chrono::duration<float>(std::chrono::steady_clock::now() - start); |
425 | |
|
426 | 0 | auto size = local_reader->size(); |
427 | 0 | LOG(INFO) << "Upload " << local_file.native() << " to " << full_s3_path(key) |
428 | 0 | << ", duration=" << duration.count() << ", bytes=" << size; |
429 | |
|
430 | 0 | return Status::OK(); |
431 | 0 | } |
432 | | |
433 | | Status S3FileSystem::batch_upload_impl(const std::vector<Path>& local_files, |
434 | 0 | const std::vector<Path>& remote_files) { |
435 | 0 | auto client = _client->get(); |
436 | 0 | CHECK_S3_CLIENT(client); |
437 | |
|
438 | 0 | if (local_files.size() != remote_files.size()) { |
439 | 0 | return Status::InvalidArgument("local_files.size({}) != remote_files.size({})", |
440 | 0 | local_files.size(), remote_files.size()); |
441 | 0 | } |
442 | | |
443 | 0 | std::vector<FileWriterPtr> obj_writers(local_files.size()); |
444 | |
|
445 | 0 | auto upload_task = [&, this](size_t idx) { |
446 | 0 | SCOPED_ATTACH_TASK(ExecEnv::GetInstance()->s3_file_buffer_tracker()); |
447 | 0 | const auto& local_file = local_files[idx]; |
448 | 0 | const auto& remote_file = remote_files[idx]; |
449 | 0 | auto& obj_writer = obj_writers[idx]; |
450 | 0 | auto key = DORIS_TRY(_get_key(remote_file)); |
451 | 0 | LOG(INFO) << "Start to upload " << local_file.native() << " to " << full_s3_path(key); |
452 | 0 | RETURN_IF_ERROR(create_file_impl(key, &obj_writer, nullptr)); |
453 | 0 | FileReaderSPtr local_reader; |
454 | 0 | RETURN_IF_ERROR(io::global_local_filesystem()->open_file(local_file, &local_reader)); |
455 | 0 | size_t local_buffer_size = config::s3_file_system_local_upload_buffer_size; |
456 | 0 | std::unique_ptr<char[]> write_buffer = |
457 | 0 | std::make_unique_for_overwrite<char[]>(local_buffer_size); |
458 | 0 | size_t cur_read = 0; |
459 | 0 | while (cur_read < local_reader->size()) { |
460 | 0 | size_t bytes_read = 0; |
461 | 0 | RETURN_IF_ERROR(local_reader->read_at( |
462 | 0 | cur_read, Slice {write_buffer.get(), local_buffer_size}, &bytes_read)); |
463 | 0 | RETURN_IF_ERROR((*obj_writer).append({write_buffer.get(), bytes_read})); |
464 | 0 | cur_read += bytes_read; |
465 | 0 | } |
466 | 0 | RETURN_IF_ERROR((*obj_writer).close()); |
467 | 0 | return Status::OK(); |
468 | 0 | }; |
469 | |
|
470 | 0 | Status s = Status::OK(); |
471 | 0 | std::vector<std::future<Status>> futures; |
472 | 0 | for (int i = 0; i < local_files.size(); ++i) { |
473 | 0 | auto task = std::make_shared<std::packaged_task<Status(size_t idx)>>(upload_task); |
474 | 0 | futures.emplace_back(task->get_future()); |
475 | 0 | auto st = ExecEnv::GetInstance()->s3_file_system_thread_pool()->submit_func( |
476 | 0 | [t = std::move(task), idx = i]() mutable { (*t)(idx); }); |
477 | | // We shouldn't return immediately since the previous submitted tasks might still be running in the thread pool |
478 | 0 | if (!st.ok()) { |
479 | 0 | s = st; |
480 | 0 | break; |
481 | 0 | } |
482 | 0 | } |
483 | 0 | for (auto&& f : futures) { |
484 | 0 | auto cur_s = f.get(); |
485 | 0 | if (!cur_s.ok()) { |
486 | 0 | s = std::move(cur_s); |
487 | 0 | } |
488 | 0 | } |
489 | 0 | return s; |
490 | 0 | } |
491 | | |
492 | 0 | Status S3FileSystem::download_impl(const Path& remote_file, const Path& local_file) { |
493 | 0 | auto client = _client->get(); |
494 | 0 | CHECK_S3_CLIENT(client); |
495 | 0 | auto key = DORIS_TRY(_get_key(remote_file)); |
496 | 0 | int64_t size; |
497 | 0 | RETURN_IF_ERROR(file_size(remote_file, &size)); |
498 | 0 | std::unique_ptr<char[]> buf = std::make_unique_for_overwrite<char[]>(size); |
499 | 0 | size_t bytes_read = 0; |
500 | | // clang-format off |
501 | 0 | auto resp = client->get_object( {.bucket = _bucket, .key = key,}, |
502 | 0 | buf.get(), 0, size, &bytes_read); |
503 | | // clang-format on |
504 | 0 | if (resp.status.code != ErrorCode::OK) { |
505 | 0 | return {resp.status.code, std::move(resp.status.msg)}; |
506 | 0 | } |
507 | 0 | Aws::OFStream local_file_s; |
508 | 0 | local_file_s.open(local_file, std::ios::out | std::ios::binary); |
509 | 0 | if (local_file_s.good()) { |
510 | 0 | local_file_s << StringViewStream(buf.get(), size).rdbuf(); |
511 | 0 | } else { |
512 | 0 | return localfs_error(errno, fmt::format("failed to write file {}", local_file.native())); |
513 | 0 | } |
514 | | |
515 | 0 | return Status::OK(); |
516 | 0 | } |
517 | | |
518 | | // oss has public endpoint and private endpoint, is_public_endpoint determines |
519 | | // whether to return a public endpoint. |
520 | | std::string S3FileSystem::generate_presigned_url(const Path& path, int64_t expiration_secs, |
521 | 0 | bool is_public_endpoint) const { |
522 | 0 | std::string key = fmt::format("{}/{}", _prefix, path.native()); |
523 | 0 | std::shared_ptr<ObjStorageClient> client; |
524 | | // Read the conf and the client from one snapshot so a concurrent reset cannot pair the |
525 | | // endpoint of one generation with the client of another. |
526 | 0 | auto state = _client->state(); |
527 | 0 | if (is_public_endpoint && state->config.endpoint().ends_with(OSS_PRIVATE_ENDPOINT_SUFFIX)) { |
528 | | // Only S3-protocol backends have an OSS private endpoint. Their file systems are |
529 | | // created from an S3Conf (the typed constructor is used for Azure only), so the holder |
530 | | // keeps the legacy conf that the public-endpoint client is derived from. |
531 | 0 | DORIS_CHECK(state->legacy_conf.has_value()); |
532 | 0 | auto new_s3_conf = *state->legacy_conf; |
533 | 0 | new_s3_conf.endpoint.erase(new_s3_conf.endpoint.size() - OSS_PRIVATE_ENDPOINT_SUFFIX.size(), |
534 | 0 | LEN_OF_OSS_PRIVATE_SUFFIX); |
535 | 0 | auto client_result = S3ClientFactory::instance().create(new_s3_conf); |
536 | 0 | if (!client_result) { |
537 | 0 | LOG(WARNING) << "failed to create S3 client for presigned URL: " |
538 | 0 | << client_result.error(); |
539 | 0 | return {}; |
540 | 0 | } |
541 | 0 | client = std::move(client_result).value(); |
542 | 0 | } else { |
543 | 0 | client = state->client; |
544 | 0 | } |
545 | 0 | return client->generate_presigned_url({.bucket = _bucket, .key = key, .prefix = ""}, |
546 | 0 | expiration_secs); |
547 | 0 | } |
548 | | |
549 | | } // namespace doris::io |