| // Licensed to the Apache Software Foundation (ASF) under one |
| // or more contributor license agreements. See the NOTICE file |
| // distributed with this work for additional information |
| // regarding copyright ownership. The ASF licenses this file |
| // to you under the Apache License, Version 2.0 (the |
| // "License"); you may not use this file except in compliance |
| // with the License. You may obtain a copy of the License at |
| // |
| // http://www.apache.org/licenses/LICENSE-2.0 |
| // |
| // Unless required by applicable law or agreed to in writing, |
| // software distributed under the License is distributed on an |
| // "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY |
| // KIND, either express or implied. See the License for the |
| // specific language governing permissions and limitations |
| // under the License. |
| |
| #include "azure_obj_storage_client.h" |
| |
| #include <butil/guid.h> |
| |
| #include <cctype> |
| #include <cstdint> |
| #include <string_view> |
| #include <vector> |
| |
| #include "cpp/obj_retry_strategy.h" |
| |
| using namespace Azure::Storage::Blobs; |
| |
| namespace { |
| std::string wrap_object_storage_path_msg(const doris::ObjStoragePath& opts) { |
| return fmt::format("bucket {}, key {}, prefix {}, path {}", opts.bucket, opts.key, opts.prefix, |
| opts.path.native()); |
| } |
| |
| doris::ObjStoragePath with_object_key(const doris::ObjStoragePath& opts, std::string_view key) { |
| auto path = opts; |
| path.key = key; |
| return path; |
| } |
| |
| std::string to_lower_ascii(std::string_view input) { |
| std::string lowered(input); |
| std::transform(lowered.begin(), lowered.end(), lowered.begin(), |
| [](unsigned char ch) { return static_cast<char>(std::tolower(ch)); }); |
| return lowered; |
| } |
| |
| std::string encode_azure_block_id(std::string_view upload_id, int part_num) { |
| // Azure has no multipart upload namespace. Include the writer UUID in every block ID so |
| // concurrent writers for the same key cannot stage interchangeable blocks. |
| std::vector<unsigned char> raw_id(upload_id.begin(), upload_id.end()); |
| auto part = static_cast<uint32_t>(part_num); |
| for (size_t i = 0; i < sizeof(part); ++i) { |
| raw_id.push_back(static_cast<unsigned char>(part >> (i * 8))); |
| } |
| Aws::Utils::ByteBuffer bytes(raw_id.data(), raw_id.size()); |
| return Aws::Utils::HashingUtils::Base64Encode(bytes); |
| } |
| |
| constexpr char SAS_TOKEN_URL_TEMPLATE[] = "{}/{}/{}{}"; |
| constexpr char BlobNotFound[] = "BlobNotFound"; |
| } // namespace |
| |
| namespace doris { |
| |
| std::string azure_multipart_block_id(std::string_view upload_id, int part_num) { |
| return encode_azure_block_id(upload_id, part_num); |
| } |
| |
| std::string build_azure_batch_delete_failure_message(const ObjStoragePath& opts, |
| std::string_view key) { |
| return fmt::format("Azure batch delete failed, path msg {}", |
| wrap_object_storage_path_msg(with_object_key(opts, key))); |
| } |
| |
| // As Azure's doc said, the batch size is 256 |
| // You can find out the num in https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id |
| // > Each batch request supports a maximum of 256 subrequests. |
| constexpr size_t BlobBatchMaxOperations = 256; |
| |
| bool is_azure_tls_ca_error_message(std::string_view message) { |
| std::string lower = to_lower_ascii(message); |
| return lower.find("ssl ca cert") != std::string::npos || |
| lower.find("peer failed verification") != std::string::npos || |
| lower.find("unable to get local issuer certificate") != std::string::npos || |
| lower.find("problem with the ssl ca cert") != std::string::npos; |
| } |
| |
| std::string build_azure_tls_debug_suffix(std::string_view error_message, |
| std::string_view tls_debug_context) { |
| if (tls_debug_context.empty() || !is_azure_tls_ca_error_message(error_message)) { |
| return ""; |
| } |
| return fmt::format(", {}", tls_debug_context); |
| } |
| |
| static ObjStorageResponse make_azure_std_exception_response(const std::exception& e, |
| const ObjStoragePath& opts, |
| std::string_view tls_debug_context) { |
| auto msg = fmt::format("Azure request failed because {}, path msg {}{}", e.what(), |
| wrap_object_storage_path_msg(opts), |
| build_azure_tls_debug_suffix(e.what(), tls_debug_context)); |
| LOG(WARNING) << msg; |
| return {.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, std::move(msg)}, |
| .http_code = 0, |
| .request_id = ""}; |
| } |
| |
| template <typename Func> |
| ObjStorageResponse do_azure_client_call(Func f, const ObjStoragePath& opts, |
| std::string_view tls_debug_context) { |
| try { |
| f(); |
| } catch (Azure::Core::RequestFailedException& e) { |
| doris::record_object_request_failed(static_cast<int>(e.StatusCode)); |
| auto msg = fmt::format( |
| "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
| e.what(), e.Message, static_cast<int>(e.StatusCode), |
| wrap_object_storage_path_msg(opts), |
| build_azure_tls_debug_suffix(fmt::format("{} {}", e.what(), e.Message), |
| tls_debug_context)); |
| LOG(WARNING) << msg; |
| return {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
| std::move(msg)), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId)}; |
| } catch (const std::exception& e) { |
| return make_azure_std_exception_response(e, opts, tls_debug_context); |
| } |
| return ObjStorageResponse::OK(); |
| } |
| |
| struct AzureBatchDeleter { |
| AzureBatchDeleter(BlobContainerClient* client, const ObjStoragePath& opts, |
| std::string_view representative_key, std::string_view tls_debug_context) |
| : _client(client), |
| _batch(client->CreateBatch()), |
| _opts(with_object_key(opts, representative_key)), |
| _tls_debug_context(tls_debug_context) {} |
| // Submit one blob to be deleted in `AzureBatchDeleter::execute` |
| void delete_blob(const std::string& blob_name) { |
| deferred_resps.emplace_back(DeferredDeleteResponse { |
| .key = blob_name, .response = _batch.DeleteBlob(blob_name)}); |
| } |
| ObjStorageResponse execute() { |
| if (deferred_resps.empty()) { |
| return ObjStorageResponse::OK(); |
| } |
| auto resp = do_azure_client_call( |
| [&]() { |
| client_bvar::ScopedLatency scoped_latency( |
| client_bvar::s3_delete_objects_latency); |
| _client->SubmitBatch(_batch); |
| }, |
| _opts, _tls_debug_context); |
| if (resp.status.code != ObjStorageStatus::OK) { |
| return resp; |
| } |
| |
| for (auto&& deferred : deferred_resps) { |
| try { |
| auto r = deferred.response.GetResponse(); |
| if (!r.Value.Deleted) { |
| auto msg = build_azure_batch_delete_failure_message(_opts, deferred.key); |
| LOG(WARNING) << msg; |
| return {.status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, |
| std::move(msg)}, |
| .http_code = 0, |
| .request_id = ""}; |
| } |
| } catch (Azure::Core::RequestFailedException& e) { |
| if (Azure::Core::Http::HttpStatusCode::NotFound == e.StatusCode && |
| 0 == strcmp(e.ErrorCode.c_str(), BlobNotFound)) { |
| continue; |
| } |
| doris::record_object_request_failed(static_cast<int>(e.StatusCode)); |
| auto msg = fmt::format( |
| "Azure request failed because {}, error msg {}, http code {}, path msg " |
| "{}{}", |
| e.what(), e.Message, static_cast<int>(e.StatusCode), |
| wrap_object_storage_path_msg(with_object_key(_opts, deferred.key)), |
| build_azure_tls_debug_suffix(fmt::format("{} {}", e.what(), e.Message), |
| _tls_debug_context)); |
| LOG(WARNING) << msg; |
| return {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
| std::move(msg)), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId)}; |
| } |
| } |
| |
| return ObjStorageResponse::OK(); |
| } |
| |
| private: |
| struct DeferredDeleteResponse { |
| std::string key; |
| Azure::Storage::DeferredResponse<Models::DeleteBlobResult> response; |
| }; |
| |
| BlobContainerClient* _client; |
| BlobContainerBatch _batch; |
| ObjStoragePath _opts; |
| std::string_view _tls_debug_context; |
| std::vector<DeferredDeleteResponse> deferred_resps; |
| }; |
| |
| ObjStorageUploadResult AzureObjStorageClient::create_multipart_upload(const ObjStoragePath&) { |
| // Azure has no provider-side multipart session. This local UUID namespaces the writer's |
| // staged block IDs and is carried through the same interface as an S3 upload ID. |
| auto upload_id = butil::GenerateGUID(); |
| if (upload_id.empty()) { |
| return {.resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, |
| "failed to generate Azure multipart upload ID"}}}; |
| } |
| return ObjStorageUploadResult { |
| .resp = ObjStorageResponse::OK(), |
| .upload_id = std::move(upload_id), |
| }; |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::put_object(const ObjStoragePath& opts, |
| std::string_view stream) { |
| auto client = _client->GetBlockBlobClient(opts.key); |
| return do_azure_client_call( |
| [&]() { |
| client_bvar::ScopedLatency scoped_latency(client_bvar::s3_put_latency); |
| client.UploadFrom(reinterpret_cast<const uint8_t*>(stream.data()), stream.size()); |
| }, |
| opts, _config.tls_debug_context); |
| } |
| |
| ObjStorageUploadResult AzureObjStorageClient::upload_part(const ObjStoragePath& opts, |
| const std::string& upload_id, |
| std::string_view stream, int part_num) { |
| DCHECK(!upload_id.empty()); |
| auto client = _client->GetBlockBlobClient(opts.key); |
| std::string block_id = azure_multipart_block_id(upload_id, part_num); |
| try { |
| Azure::Core::IO::MemoryBodyStream memory_body( |
| reinterpret_cast<const uint8_t*>(stream.data()), stream.size()); |
| // The blockId must be base64 encoded |
| client_bvar::ScopedLatency scoped_latency(client_bvar::s3_multi_part_upload_latency); |
| client.StageBlock(block_id, memory_body); |
| } catch (Azure::Core::RequestFailedException& e) { |
| record_object_request_failed(static_cast<int>(e.StatusCode)); |
| auto tls_debug_suffix = build_azure_tls_debug_suffix( |
| fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
| auto msg = fmt::format( |
| "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
| e.what(), e.Message, static_cast<int>(e.StatusCode), |
| wrap_object_storage_path_msg(opts), tls_debug_suffix); |
| LOG(WARNING) << msg; |
| // clang-format off |
| return { |
| .resp = { |
| .status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
| std::move(msg)), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId), |
| }, |
| }; |
| // clang-format on |
| } catch (const std::exception& e) { |
| return {.resp = make_azure_std_exception_response(e, opts, _config.tls_debug_context)}; |
| } |
| return ObjStorageUploadResult {.resp = ObjStorageResponse::OK(), .etag = std::move(block_id)}; |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::complete_multipart_upload( |
| const ObjStoragePath& opts, const std::string& upload_id, |
| const std::vector<ObjStorageCompletedPart>& completed_parts) { |
| DCHECK(!upload_id.empty()); |
| auto client = _client->GetBlockBlobClient(opts.key); |
| std::vector<std::string> string_block_ids; |
| std::ranges::transform(completed_parts, std::back_inserter(string_block_ids), |
| [&upload_id](const ObjStorageCompletedPart& i) { |
| return azure_multipart_block_id(upload_id, i.part_num); |
| }); |
| return do_azure_client_call( |
| [&]() { |
| client_bvar::ScopedLatency scoped_latency( |
| client_bvar::s3_multi_part_upload_latency); |
| client.CommitBlockList(string_block_ids); |
| }, |
| opts, _config.tls_debug_context); |
| } |
| |
| ObjStorageHeadResult AzureObjStorageClient::head_object(const ObjStoragePath& opts) { |
| try { |
| Models::BlobProperties properties = [&]() { |
| client_bvar::ScopedLatency scoped_latency(client_bvar::s3_head_latency); |
| return _client->GetBlockBlobClient(opts.key).GetProperties().Value; |
| }(); |
| return {.resp = ObjStorageResponse::OK(), .file_size = properties.BlobSize}; |
| } catch (Azure::Core::RequestFailedException& e) { |
| if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound) { |
| return ObjStorageHeadResult { |
| .resp = {.status = obj_storage_status_from_http_code( |
| static_cast<int>(e.StatusCode), ""), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId)}, |
| }; |
| } |
| record_object_request_failed(static_cast<int>(e.StatusCode)); |
| auto tls_debug_suffix = build_azure_tls_debug_suffix( |
| fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
| auto msg = fmt::format( |
| "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
| e.what(), e.Message, static_cast<int>(e.StatusCode), |
| wrap_object_storage_path_msg(opts), tls_debug_suffix); |
| LOG(WARNING) << msg << ", request_id=" << e.RequestId; |
| return ObjStorageHeadResult { |
| .resp = {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
| std::move(msg)), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId)}, |
| }; |
| } catch (const std::exception& e) { |
| return {.resp = make_azure_std_exception_response(e, opts, _config.tls_debug_context)}; |
| } |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::get_object(const ObjStoragePath& opts, void* buffer, |
| size_t offset, size_t bytes_read, |
| size_t* size_return) { |
| auto client = _client->GetBlockBlobClient(opts.key); |
| auto response = do_azure_client_call( |
| [&]() { |
| DownloadBlobToOptions download_opts; |
| Azure::Core::Http::HttpRange range {.Offset = static_cast<int64_t>(offset), |
| .Length = bytes_read}; |
| download_opts.Range = range; |
| client_bvar::ScopedLatency scoped_latency(client_bvar::s3_get_latency); |
| auto resp = client.DownloadTo(reinterpret_cast<uint8_t*>(buffer), bytes_read, |
| download_opts); |
| *size_return = resp.Value.ContentRange.Length.Value(); |
| }, |
| opts, _config.tls_debug_context); |
| if (!response.ok() || *size_return == bytes_read) { |
| return response; |
| } |
| return { |
| .status = {ObjStorageStatus::INTERNAL_ERROR, |
| fmt::format("incomplete read from {}, expect {}, got {}", |
| wrap_object_storage_path_msg(opts), bytes_read, *size_return)}, |
| .http_code = response.http_code, |
| .request_id = std::move(response.request_id), |
| }; |
| } |
| |
| ObjStorageListPageResult AzureObjStorageClient::list_objects_page( |
| const ObjStoragePath& opts, std::string_view continuation_token) { |
| const auto& prefix = opts.prefix.empty() ? opts.key : opts.prefix; |
| ListBlobsOptions request; |
| request.Prefix = prefix; |
| request.PageSizeHint = static_cast<int32_t>(capabilities().max_list_page); |
| if (!continuation_token.empty()) { |
| request.ContinuationToken = std::string(continuation_token); |
| } |
| TEST_SYNC_POINT_CALLBACK("AzureObjStorageClient::list_objects", &request); |
| |
| try { |
| auto response = [&]() { |
| client_bvar::ScopedLatency scoped_latency(client_bvar::s3_list_latency); |
| return _client->ListBlobs(request); |
| }(); |
| const bool has_more = response.NextPageToken.HasValue(); |
| auto next_token = has_more ? response.NextPageToken.Value() : std::string {}; |
| if (has_more && next_token.empty()) { |
| return { |
| .resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, |
| "Azure list response has an empty continuation token"}, |
| .http_code = 0}, |
| }; |
| } |
| ObjStorageListPageResult page {.resp = ObjStorageResponse::OK(), |
| .continuation_token = std::move(next_token), |
| .has_more = has_more}; |
| page.objects.reserve(response.Blobs.size()); |
| for (auto&& item : response.Blobs) { |
| DCHECK(item.Name.starts_with(*request.Prefix)) << item.Name << ' ' << *request.Prefix; |
| page.objects.emplace_back(ObjectMeta { |
| .key = std::move(item.Name), |
| .size = item.BlobSize, |
| // `Azure::DateTime` adds the offset of `SystemClockEpoch` to the given Unix timestamp, |
| // so here we need to subtract this offset to obtain the Unix timestamp of the mtime. |
| // https://github.com/Azure/azure-sdk-for-cpp/blob/azure-core_1.12.0/sdk/core/azure-core/inc/azure/core/datetime.hpp#L129 |
| .mtime_s = duration_cast<std::chrono::seconds>(item.Details.LastModified - |
| SystemClockEpoch) |
| .count()}); |
| } |
| return page; |
| } catch (Azure::Core::RequestFailedException& e) { |
| record_object_request_failed(static_cast<int>(e.StatusCode)); |
| auto tls_debug_suffix = build_azure_tls_debug_suffix( |
| fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
| LOG(WARNING) << fmt::format("Azure request failed because {}, url: {}, prefix: {}{}", |
| e.what(), _client->GetUrl(), request.Prefix.Value(), |
| tls_debug_suffix); |
| return { |
| .resp = {.status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
| e.Message + tls_debug_suffix), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId)}, |
| }; |
| } catch (std::exception& e) { |
| LOG(WARNING) << fmt::format("Azure request failed because {}, url: {}, prefix: {}", |
| e.what(), _client->GetUrl(), request.Prefix.Value()); |
| return { |
| .resp = {.status = {ObjStorageStatus::INTERNAL_ERROR, e.what()}, |
| .http_code = 0, |
| .request_id = ""}, |
| }; |
| } |
| } |
| |
| // As Azure's doc said, the batch size is 256 |
| // You can find out the num in https://learn.microsoft.com/en-us/rest/api/storageservices/blob-batch?tabs=microsoft-entra-id |
| // > Each batch request supports a maximum of 256 subrequests. |
| ObjStorageResponse AzureObjStorageClient::delete_objects(const ObjStoragePath& opts, |
| std::vector<std::string> objs) { |
| // TODO(ByteYue) : use range to adate this code when compiler is ready |
| // auto chunkedView = objs | std::views::chunk(BlobBatchMaxOperations); |
| auto begin = std::begin(objs); |
| auto end = std::end(objs); |
| |
| while (begin != end) { |
| auto chunk_end = begin; |
| size_t batch_size = BlobBatchMaxOperations; |
| TEST_SYNC_POINT_CALLBACK("AzureObjClient::delete_objects", &batch_size); |
| TEST_SYNC_POINT_CALLBACK("AzureObjStorageClient::delete_objects", &batch_size); |
| batch_size = std::max<size_t>(1, batch_size); |
| std::advance(chunk_end, |
| std::min(batch_size, static_cast<size_t>(std::distance(begin, end)))); |
| |
| auto deleter = AzureBatchDeleter(_client.get(), opts, *begin, _config.tls_debug_context); |
| std::ranges::for_each(std::ranges::subrange(begin, chunk_end), |
| [&](const std::string& obj) { deleter.delete_blob(obj); }); |
| begin = chunk_end; |
| if (auto resp = deleter.execute(); resp.status.code != ObjStorageStatus::OK) { |
| return resp; |
| } |
| } |
| return ObjStorageResponse::OK(); |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::delete_object(const ObjStoragePath& opts) { |
| try { |
| auto resp = [&]() { |
| client_bvar::ScopedLatency scoped_latency(client_bvar::s3_delete_object_latency); |
| return _client->DeleteBlob(opts.key); |
| }(); |
| if (!resp.Value.Deleted) { |
| return { |
| .status = ObjStorageStatus {ObjStorageStatus::IO_ERROR, |
| "Delete azure blob failed"}, |
| .http_code = 0, |
| .request_id = "", |
| }; |
| } |
| return ObjStorageResponse::OK(); |
| } catch (Azure::Core::RequestFailedException& e) { |
| if (e.StatusCode == Azure::Core::Http::HttpStatusCode::NotFound && |
| e.ErrorCode == BlobNotFound) { |
| return ObjStorageResponse::OK(); |
| } |
| record_object_request_failed(static_cast<int>(e.StatusCode)); |
| auto tls_debug_suffix = build_azure_tls_debug_suffix( |
| fmt::format("{} {}", e.what(), e.Message), _config.tls_debug_context); |
| auto msg = fmt::format( |
| "Azure request failed because {}, error msg {}, http code {}, path msg {}{}", |
| e.what(), e.Message, static_cast<int>(e.StatusCode), |
| wrap_object_storage_path_msg(opts), tls_debug_suffix); |
| LOG(WARNING) << msg; |
| return { |
| .status = obj_storage_status_from_http_code(static_cast<int>(e.StatusCode), |
| std::move(msg)), |
| .http_code = static_cast<int>(e.StatusCode), |
| .request_id = std::move(e.RequestId), |
| }; |
| } catch (std::exception& e) { |
| auto msg = fmt::format("Azure request failed because {}, path msg {}{}", e.what(), |
| wrap_object_storage_path_msg(opts), |
| build_azure_tls_debug_suffix(e.what(), _config.tls_debug_context)); |
| LOG(WARNING) << msg; |
| return { |
| .status = ObjStorageStatus {ObjStorageStatus::INTERNAL_ERROR, std::move(msg)}, |
| .http_code = 0, |
| .request_id = "", |
| }; |
| } |
| } |
| |
| std::string AzureObjStorageClient::generate_presigned_url(const ObjStoragePath& opts, |
| int64_t expiration_secs) { |
| Azure::Storage::Sas::BlobSasBuilder sas_builder; |
| sas_builder.ExpiresOn = |
| std::chrono::system_clock::now() + std::chrono::seconds(expiration_secs); |
| sas_builder.BlobContainerName = opts.bucket; |
| sas_builder.BlobName = opts.key; |
| sas_builder.Resource = Azure::Storage::Sas::BlobSasResource::Blob; |
| sas_builder.Protocol = Azure::Storage::Sas::SasProtocol::HttpsOnly; |
| sas_builder.SetPermissions(Azure::Storage::Sas::BlobSasPermissions::Read); |
| |
| auto credential = _credential; |
| if (credential == nullptr) { |
| credential = std::make_shared<Azure::Storage::StorageSharedKeyCredential>(_config.ak, |
| _config.sk); |
| } |
| std::string sasToken = sas_builder.GenerateSasToken(*credential); |
| |
| auto sasURL = |
| fmt::format(SAS_TOKEN_URL_TEMPLATE, _config.endpoint, opts.bucket, opts.key, sasToken); |
| if (sasURL.find("://") == std::string::npos) { |
| sasURL = "https://" + sasURL; |
| } |
| return sasURL; |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::get_lifecycle(const std::string& /*bucket*/, |
| int64_t* expiration_days) { |
| // TODO(plat1ko) |
| *expiration_days = INT64_MAX; |
| return ObjStorageResponse::OK(); |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::check_versioning(const std::string& /*bucket*/) { |
| // TODO(plat1ko) |
| return ObjStorageResponse::OK(); |
| } |
| |
| ObjStorageResponse AzureObjStorageClient::abort_multipart_upload(const ObjStoragePath& opts, |
| const std::string& upload_id) { |
| // delete uncommitted blobs |
| // https://learn.microsoft.com/en-us/rest/api/storageservices/delete-blob?tabs=microsoft-entra-id#remarks |
| return delete_object(opts); |
| } |
| } // namespace doris |