This is an automated email from the ASF dual-hosted git repository. yuchanns pushed a commit to branch feat/go-binding/presign in repository https://gitbox.apache.org/repos/asf/opendal.git
commit 7c7a3e006aac5e6e163855892a311a3539fc6e9a Author: Hanchin Hsieh <[email protected]> AuthorDate: Tue Jan 13 13:37:16 2026 +0800 feat(bindings/c): support presign --- .github/workflows/test_behavior_binding_c.yml | 2 +- bindings/c/include/opendal.h | 95 ++++ bindings/c/src/lib.rs | 5 + bindings/c/src/operator_info.rs | 3 + bindings/c/src/presign.rs | 289 +++++++++++ bindings/c/tests/Makefile | 4 +- bindings/c/tests/opendal_test_runner | Bin 0 -> 416312 bytes bindings/c/tests/test_framework.cpp | 4 + bindings/c/tests/test_framework.h | 20 +- bindings/c/tests/test_runner.cpp | 2 + bindings/c/tests/test_suites_presign.cpp | 699 ++++++++++++++++++++++++++ core/core/src/blocking/operator.rs | 31 ++ 12 files changed, 1149 insertions(+), 5 deletions(-) diff --git a/.github/workflows/test_behavior_binding_c.yml b/.github/workflows/test_behavior_binding_c.yml index 29dc8fa36..5b113aad5 100644 --- a/.github/workflows/test_behavior_binding_c.yml +++ b/.github/workflows/test_behavior_binding_c.yml @@ -54,7 +54,7 @@ jobs: - name: Install build tools run: | sudo apt-get update - sudo apt-get install -y build-essential pkg-config uuid-dev + sudo apt-get install -y build-essential pkg-config uuid-dev libcurl4-gnutls-dev - name: Build OpenDAL C library working-directory: "bindings/c" diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 270de2e91..4b8d95ba5 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -87,6 +87,8 @@ typedef enum opendal_code { OPENDAL_RANGE_NOT_SATISFIED, } opendal_code; +typedef struct opendal_presigned_request_inner opendal_presigned_request_inner; + /** * \brief opendal_bytes carries raw-bytes with its length * @@ -577,12 +579,52 @@ typedef struct opendal_capability { * If operator supports presign write. */ bool presign_write; + /** + * If operator supports presign delete. + */ + bool presign_delete; /** * If operator supports shared. */ bool shared; } opendal_capability; +/** + * \brief The underlying presigned request, which contains the HTTP method, URI, and headers. + * This is an opaque struct, please use the accessor functions to get the fields. + */ +typedef struct opendal_presigned_request { + struct opendal_presigned_request_inner *inner; +} opendal_presigned_request; + +/** + * @brief The result of a presign operation. + */ +typedef struct opendal_result_presign { + /** + * The presigned request. + */ + struct opendal_presigned_request *req; + /** + * The error. + */ + struct opendal_error *error; +} opendal_result_presign; + +/** + * \brief The key-value pair for the headers of the presigned request. + */ +typedef struct opendal_http_header_pair { + /** + * The key of the header. + */ + const char *key; + /** + * The value of the header. + */ + const char *value; +} opendal_http_header_pair; + /** * \brief The is the result type returned by opendal_reader_read(). * The result type contains a size field, which is the size of the data read, @@ -1376,6 +1418,59 @@ struct opendal_capability opendal_operator_info_get_full_capability(const struct */ struct opendal_capability opendal_operator_info_get_native_capability(const struct opendal_operator_info *self); +/** + * \brief Presign a read operation. + */ +struct opendal_result_presign opendal_operator_presign_read(const struct opendal_operator *op, + const char *path, + uint64_t expire_secs); + +/** + * \brief Presign a write operation. + */ +struct opendal_result_presign opendal_operator_presign_write(const struct opendal_operator *op, + const char *path, + uint64_t expire_secs); + +/** + * \brief Presign a delete operation. + */ +struct opendal_result_presign opendal_operator_presign_delete(const struct opendal_operator *op, + const char *path, + uint64_t expire_secs); + +/** + * \brief Presign a stat operation. + */ +struct opendal_result_presign opendal_operator_presign_stat(const struct opendal_operator *op, + const char *path, + uint64_t expire_secs); + +/** + * Get the method of the presigned request. + */ +const char *opendal_presigned_request_method(const struct opendal_presigned_request *req); + +/** + * Get the URI of the presigned request. + */ +const char *opendal_presigned_request_uri(const struct opendal_presigned_request *req); + +/** + * Get the headers of the presigned request. + */ +const struct opendal_http_header_pair *opendal_presigned_request_headers(const struct opendal_presigned_request *req); + +/** + * Get the length of the headers of the presigned request. + */ +uintptr_t opendal_presigned_request_headers_len(const struct opendal_presigned_request *req); + +/** + * \brief Free the presigned request. + */ +void opendal_presigned_request_free(struct opendal_presigned_request *req); + /** * \brief Frees the heap memory used by the opendal_bytes */ diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 1156428cb..754df7b35 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -47,6 +47,11 @@ pub use operator::opendal_operator; mod operator_info; +mod presign; +pub use presign::opendal_http_header_pair; +pub use presign::opendal_presigned_request; +pub use presign::opendal_result_presign; + mod result; pub use result::opendal_result_exists; pub use result::opendal_result_is_exist; diff --git a/bindings/c/src/operator_info.rs b/bindings/c/src/operator_info.rs index 61bc1c38b..f78fcde7b 100644 --- a/bindings/c/src/operator_info.rs +++ b/bindings/c/src/operator_info.rs @@ -125,6 +125,8 @@ pub struct opendal_capability { pub presign_stat: bool, /// If operator supports presign write. pub presign_write: bool, + /// If operator supports presign delete. + pub presign_delete: bool, /// If operator supports shared. pub shared: bool, @@ -253,6 +255,7 @@ impl From<core::Capability> for opendal_capability { presign_read: value.presign_read, presign_stat: value.presign_stat, presign_write: value.presign_write, + presign_delete: value.presign_delete, shared: value.shared, } } diff --git a/bindings/c/src/presign.rs b/bindings/c/src/presign.rs new file mode 100644 index 000000000..687487d95 --- /dev/null +++ b/bindings/c/src/presign.rs @@ -0,0 +1,289 @@ +// 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. + +use std::ffi::{c_char, CStr, CString}; +use std::time::Duration; + +use opendal::raw::PresignedRequest as ocorePresignedRequest; + +use crate::error::opendal_error; +use crate::operator::opendal_operator; + +/// \brief The key-value pair for the headers of the presigned request. +#[repr(C)] +#[derive(Debug)] +pub struct opendal_http_header_pair { + /// The key of the header. + pub key: *const c_char, + /// The value of the header. + pub value: *const c_char, +} + +// The internal Rust-only struct that holds the owned data. +#[allow(dead_code)] +#[derive(Debug)] +struct opendal_presigned_request_inner { + method: CString, + uri: CString, + headers: Vec<opendal_http_header_pair>, + // These vecs own the CString data for the headers + header_keys: Vec<CString>, + header_values: Vec<CString>, +} + +impl opendal_presigned_request_inner { + fn new(req: ocorePresignedRequest) -> Self { + let method = CString::new(req.method().as_str()).unwrap(); + let uri = CString::new(req.uri().to_string()).unwrap(); + + let mut header_keys = Vec::with_capacity(req.header().len()); + let mut header_values = Vec::with_capacity(req.header().len()); + for (k, v) in req.header().iter() { + header_keys.push(CString::new(k.as_str()).unwrap()); + header_values.push(CString::new(v.to_str().unwrap()).unwrap()); + } + + let mut headers: Vec<opendal_http_header_pair> = Vec::with_capacity(header_keys.len()); + for i in 0..header_keys.len() { + headers.push(opendal_http_header_pair { + key: header_keys[i].as_ptr(), + value: header_values[i].as_ptr(), + }); + } + + Self { + method, + uri, + headers, + header_keys, + header_values, + } + } +} + +/// \brief The underlying presigned request, which contains the HTTP method, URI, and headers. +/// This is an opaque struct, please use the accessor functions to get the fields. +#[repr(C)] +pub struct opendal_presigned_request { + inner: *mut opendal_presigned_request_inner, +} + +/// @brief The result of a presign operation. +#[repr(C)] +pub struct opendal_result_presign { + /// The presigned request. + pub req: *mut opendal_presigned_request, + /// The error. + pub error: *mut opendal_error, +} + +/// \brief Presign a read operation. +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_presign_read( + op: &opendal_operator, + path: *const c_char, + expire_secs: u64, +) -> opendal_result_presign { + if path.is_null() { + return opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(opendal::Error::new( + opendal::ErrorKind::Unexpected, + "path is null", + )), + }; + } + + let op = op.deref(); + let path = CStr::from_ptr(path).to_str().unwrap(); + let duration = Duration::from_secs(expire_secs); + + match op.presign_read(path, duration) { + Ok(req) => { + let inner = Box::new(opendal_presigned_request_inner::new(req)); + let presigned_req = Box::new(opendal_presigned_request { + inner: Box::into_raw(inner), + }); + opendal_result_presign { + req: Box::into_raw(presigned_req), + error: std::ptr::null_mut(), + } + } + Err(e) => opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(e), + }, + } +} + +/// \brief Presign a write operation. +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_presign_write( + op: &opendal_operator, + path: *const c_char, + expire_secs: u64, +) -> opendal_result_presign { + if path.is_null() { + return opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(opendal::Error::new( + opendal::ErrorKind::Unexpected, + "path is null", + )), + }; + } + + let op = op.deref(); + let path = CStr::from_ptr(path).to_str().unwrap(); + let duration = Duration::from_secs(expire_secs); + + match op.presign_write(path, duration) { + Ok(req) => { + let inner = Box::new(opendal_presigned_request_inner::new(req)); + let presigned_req = Box::new(opendal_presigned_request { + inner: Box::into_raw(inner), + }); + opendal_result_presign { + req: Box::into_raw(presigned_req), + error: std::ptr::null_mut(), + } + } + Err(e) => opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(e), + }, + } +} + +/// \brief Presign a delete operation. +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_presign_delete( + op: &opendal_operator, + path: *const c_char, + expire_secs: u64, +) -> opendal_result_presign { + if path.is_null() { + return opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(opendal::Error::new( + opendal::ErrorKind::Unexpected, + "path is null", + )), + }; + } + let op = op.deref(); + let path = CStr::from_ptr(path).to_str().unwrap(); + let duration = Duration::from_secs(expire_secs); + match op.presign_delete(path, duration) { + Ok(req) => { + let inner = Box::new(opendal_presigned_request_inner::new(req)); + let presigned_req = Box::new(opendal_presigned_request { + inner: Box::into_raw(inner), + }); + opendal_result_presign { + req: Box::into_raw(presigned_req), + error: std::ptr::null_mut(), + } + } + Err(e) => opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(e), + }, + } +} + +/// \brief Presign a stat operation. +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_presign_stat( + op: &opendal_operator, + path: *const c_char, + expire_secs: u64, +) -> opendal_result_presign { + if path.is_null() { + return opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(opendal::Error::new( + opendal::ErrorKind::Unexpected, + "path is null", + )), + }; + } + + let op = op.deref(); + let path = CStr::from_ptr(path).to_str().unwrap(); + let duration = Duration::from_secs(expire_secs); + + match op.presign_stat(path, duration) { + Ok(req) => { + let inner = Box::new(opendal_presigned_request_inner::new(req)); + let presigned_req = Box::new(opendal_presigned_request { + inner: Box::into_raw(inner), + }); + opendal_result_presign { + req: Box::into_raw(presigned_req), + error: std::ptr::null_mut(), + } + } + Err(e) => opendal_result_presign { + req: std::ptr::null_mut(), + error: opendal_error::new(e), + }, + } +} + +/// Get the method of the presigned request. +#[no_mangle] +pub unsafe extern "C" fn opendal_presigned_request_method( + req: *const opendal_presigned_request, +) -> *const c_char { + (*(*req).inner).method.as_ptr() +} + +/// Get the URI of the presigned request. +#[no_mangle] +pub unsafe extern "C" fn opendal_presigned_request_uri( + req: *const opendal_presigned_request, +) -> *const c_char { + (*(*req).inner).uri.as_ptr() +} + +/// Get the headers of the presigned request. +#[no_mangle] +pub unsafe extern "C" fn opendal_presigned_request_headers( + req: *const opendal_presigned_request, +) -> *const opendal_http_header_pair { + (*(*req).inner).headers.as_ptr() +} + +/// Get the length of the headers of the presigned request. +#[no_mangle] +pub unsafe extern "C" fn opendal_presigned_request_headers_len( + req: *const opendal_presigned_request, +) -> usize { + (*(*req).inner).headers.len() +} + +/// \brief Free the presigned request. +#[no_mangle] +pub unsafe extern "C" fn opendal_presigned_request_free(req: *mut opendal_presigned_request) { + if !req.is_null() { + // Drop the inner struct + drop(Box::from_raw((*req).inner)); + // Drop the outer struct + drop(Box::from_raw(req)); + } +} diff --git a/bindings/c/tests/Makefile b/bindings/c/tests/Makefile index 11344eba8..630070022 100644 --- a/bindings/c/tests/Makefile +++ b/bindings/c/tests/Makefile @@ -19,14 +19,14 @@ CXX = g++ CXXFLAGS = -std=c++17 -Wall -Wextra -O2 -g INCLUDES = -I../include -I. -LIBS = -lopendal_c +LIBS = -lopendal_c -lcurl ifneq ($(shell uname), Darwin) LIBS += -luuid endif # Source files FRAMEWORK_SOURCES = test_framework.cpp -SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp +SUITE_SOURCES = test_suites_basic.cpp test_suites_list.cpp test_suites_reader_writer.cpp test_suites_presign.cpp RUNNER_SOURCES = test_runner.cpp ALL_SOURCES = $(FRAMEWORK_SOURCES) $(SUITE_SOURCES) $(RUNNER_SOURCES) diff --git a/bindings/c/tests/opendal_test_runner b/bindings/c/tests/opendal_test_runner new file mode 100755 index 000000000..1ac1d1a2d Binary files /dev/null and b/bindings/c/tests/opendal_test_runner differ diff --git a/bindings/c/tests/test_framework.cpp b/bindings/c/tests/test_framework.cpp index ea714386c..94434acf9 100644 --- a/bindings/c/tests/test_framework.cpp +++ b/bindings/c/tests/test_framework.cpp @@ -193,6 +193,10 @@ bool opendal_check_capability(const opendal_operator* op, result = false; if (required.presign_write && !cap.presign_write) result = false; + if (required.presign_stat && !cap.presign_stat) + result = false; + if (required.presign_delete && !cap.presign_delete) + result = false; opendal_operator_info_free(info); return result; diff --git a/bindings/c/tests/test_framework.h b/bindings/c/tests/test_framework.h index fc525bdf5..a30ec4855 100644 --- a/bindings/c/tests/test_framework.h +++ b/bindings/c/tests/test_framework.h @@ -27,6 +27,7 @@ #include <time.h> #include <unistd.h> #include <ctype.h> +#include <curl/curl.h> #ifdef __cplusplus extern "C" { @@ -73,6 +74,8 @@ typedef struct opendal_required_capability { bool presign; bool presign_read; bool presign_write; + bool presign_stat; + bool presign_delete; } opendal_required_capability; // Test function pointer type @@ -158,7 +161,7 @@ typedef struct opendal_test_suite { } while(0) // Utility macros -#define NO_CAPABILITY { false, false, false, false, false, false, false, false, false, false, false, false, false } +#define NO_CAPABILITY { false, false, false, false, false, false, false, false, false, false, false, false, false, false, false } // Helper functions for capability creation (C++ compatible) inline opendal_required_capability make_capability_read_write() { @@ -209,6 +212,19 @@ inline opendal_required_capability make_capability_create_dir_list() { return cap; } +inline opendal_required_capability make_capability_presign() { + opendal_required_capability cap = NO_CAPABILITY; + cap.read = true; + cap.write = true; + cap.delete_ = true; + cap.stat = true; + cap.presign = true; + cap.presign_read = true; + cap.presign_write = true; + cap.presign_stat = true; + return cap; +} + // Function declarations opendal_test_config* opendal_test_config_new(); void opendal_test_config_free(opendal_test_config* config); @@ -228,4 +244,4 @@ typedef struct opendal_test_data { opendal_test_data* opendal_test_data_new(const char* path, const char* content); void opendal_test_data_free(opendal_test_data* data); -#endif // _OPENDAL_TEST_FRAMEWORK_H \ No newline at end of file +#endif // _OPENDAL_TEST_FRAMEWORK_H diff --git a/bindings/c/tests/test_runner.cpp b/bindings/c/tests/test_runner.cpp index 62e013549..cf1bf2397 100644 --- a/bindings/c/tests/test_runner.cpp +++ b/bindings/c/tests/test_runner.cpp @@ -24,12 +24,14 @@ // External test suite declarations extern opendal_test_suite basic_suite; extern opendal_test_suite list_suite; +extern opendal_test_suite presign_suite; extern opendal_test_suite reader_writer_suite; // List of all test suites static opendal_test_suite* all_suites[] = { &basic_suite, &list_suite, + &presign_suite, &reader_writer_suite, }; diff --git a/bindings/c/tests/test_suites_presign.cpp b/bindings/c/tests/test_suites_presign.cpp new file mode 100644 index 000000000..a7ca1e740 --- /dev/null +++ b/bindings/c/tests/test_suites_presign.cpp @@ -0,0 +1,699 @@ +/* + * 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 "test_framework.h" + +typedef struct presign_header_context { + size_t content_length; + int content_length_found; +} presign_header_context; + +typedef struct presign_body_context { + const char* expected; + size_t expected_len; + size_t offset; + int mismatch; +} presign_body_context; + +typedef struct presign_upload_context { + const char* data; + size_t len; + size_t offset; +} presign_upload_context; + +static const size_t PRESIGN_NO_OVERRIDE = (size_t)(-1); + +static int presign_seek_callback(void* userdata, curl_off_t offset, int origin) +{ + presign_upload_context* ctx = (presign_upload_context*)userdata; + + if (ctx == NULL) { + return CURL_SEEKFUNC_CANTSEEK; + } + + if (origin != SEEK_SET) { + return CURL_SEEKFUNC_CANTSEEK; + } + + if (offset < 0 || (size_t)offset > ctx->len) { + return CURL_SEEKFUNC_FAIL; + } + + ctx->offset = (size_t)offset; + return CURL_SEEKFUNC_OK; +} + +static size_t presign_header_callback(char* buffer, size_t size, size_t nmemb, void* userdata) +{ + size_t total = size * nmemb; + presign_header_context* ctx = (presign_header_context*)userdata; + const char header_name[] = "content-length:"; + size_t header_len = sizeof(header_name) - 1; + + if (ctx == NULL || buffer == NULL) { + return total; + } + + if (total >= header_len) { + size_t i = 0; + for (; i < header_len && i < total; i++) { + char c = buffer[i]; + if (c >= 'A' && c <= 'Z') { + c = (char)(c - 'A' + 'a'); + } + if (c != header_name[i]) { + break; + } + } + + if (i == header_len) { + size_t pos = header_len; + while (pos < total && (buffer[pos] == ' ' || buffer[pos] == '\t')) { + pos++; + } + size_t end = pos; + while (end < total && buffer[end] != '\r' && buffer[end] != '\n') { + end++; + } + if (end > pos) { + size_t value_len = end - pos; + char value_buf[64]; + if (value_len >= sizeof(value_buf)) { + value_len = sizeof(value_buf) - 1; + } + memcpy(value_buf, buffer + pos, value_len); + value_buf[value_len] = '\0'; + ctx->content_length = (size_t)strtoull(value_buf, NULL, 10); + ctx->content_length_found = 1; + } + } + } + + return total; +} + +static size_t presign_write_callback(char* buffer, size_t size, size_t nmemb, void* userdata) +{ + size_t total = size * nmemb; + presign_body_context* ctx = (presign_body_context*)userdata; + + if (ctx == NULL || buffer == NULL) { + return total; + } + + if (ctx->offset + total > ctx->expected_len) { + ctx->mismatch = 1; + ctx->offset += total; + return total; + } + + if (memcmp(ctx->expected + ctx->offset, buffer, total) != 0) { + ctx->mismatch = 1; + } + + ctx->offset += total; + return total; +} + +static size_t presign_upload_callback(char* buffer, size_t size, size_t nmemb, void* userdata) +{ + presign_upload_context* ctx = (presign_upload_context*)userdata; + size_t max_write = size * nmemb; + size_t remaining = 0; + + if (ctx == NULL || buffer == NULL) { + return 0; + } + + if (ctx->offset >= ctx->len) { + return 0; + } + + remaining = ctx->len - ctx->offset; + if (remaining > max_write) { + remaining = max_write; + } + + memcpy(buffer, ctx->data + ctx->offset, remaining); + ctx->offset += remaining; + return remaining; +} + +static size_t presign_sink_callback(char* buffer, size_t size, size_t nmemb, void* userdata) +{ + (void)buffer; + (void)userdata; + return size * nmemb; +} + +static int presign_str_ieq(const char* a, const char* b) +{ + if (a == NULL || b == NULL) { + return 0; + } + + while (*a != '\0' && *b != '\0') { + char ca = *a; + char cb = *b; + if (ca >= 'A' && ca <= 'Z') { + ca = (char)(ca - 'A' + 'a'); + } + if (cb >= 'A' && cb <= 'Z') { + cb = (char)(cb - 'A' + 'a'); + } + if (ca != cb) { + return 0; + } + a++; + b++; + } + + return *a == '\0' && *b == '\0'; +} + +static int presign_build_header_list(const opendal_http_header_pair* headers, + uintptr_t headers_len, + size_t override_content_len, + struct curl_slist** out) +{ + struct curl_slist* chunk = NULL; + + if (headers == NULL || headers_len == 0) { + *out = NULL; + return 1; + } + + for (uintptr_t i = 0; i < headers_len; i++) { + const char* key = headers[i].key; + const char* value = headers[i].value; + if (key == NULL || value == NULL) { + if (chunk != NULL) { + curl_slist_free_all(chunk); + } + *out = NULL; + return 0; + } + size_t key_len = strlen(key); + size_t value_len = strlen(value); + const char* value_to_use = value; + char length_override[32]; + if (override_content_len != PRESIGN_NO_OVERRIDE && presign_str_ieq(key, "content-length")) { + int written = snprintf(length_override, sizeof(length_override), "%zu", override_content_len); + if (written <= 0 || (size_t)written >= sizeof(length_override)) { + if (chunk != NULL) { + curl_slist_free_all(chunk); + } + *out = NULL; + return 0; + } + value_to_use = length_override; + value_len = (size_t)written; + } + size_t header_len = key_len + 2 + value_len; + char* header_line = (char*)malloc(header_len + 1); + if (header_line == NULL) { + if (chunk != NULL) { + curl_slist_free_all(chunk); + } + *out = NULL; + return 0; + } + memcpy(header_line, key, key_len); + header_line[key_len] = ':'; + header_line[key_len + 1] = ' '; + memcpy(header_line + key_len + 2, value_to_use, value_len); + header_line[header_len] = '\0'; + struct curl_slist* new_chunk = curl_slist_append(chunk, header_line); + free(header_line); + if (new_chunk == NULL) { + if (chunk != NULL) { + curl_slist_free_all(chunk); + } + *out = NULL; + return 0; + } + chunk = new_chunk; + } + + *out = chunk; + return 1; +} + +static void presign_cleanup_curl(CURL* curl, struct curl_slist* chunk) +{ + if (chunk != NULL) { + curl_slist_free_all(chunk); + } + if (curl != NULL) { + curl_easy_cleanup(curl); + } +} + +static CURLcode presign_set_standard_options(CURL* curl, const char* url, const char* method) +{ + CURLcode opt_res = curl_easy_setopt(curl, CURLOPT_URL, url); + if (opt_res != CURLE_OK) { + return opt_res; + } + + opt_res = curl_easy_setopt(curl, CURLOPT_FOLLOWLOCATION, 1L); + if (opt_res != CURLE_OK) { + return opt_res; + } + + if (method != NULL) { + if (presign_str_ieq(method, "GET")) { + opt_res = curl_easy_setopt(curl, CURLOPT_HTTPGET, 1L); + } else if (presign_str_ieq(method, "HEAD")) { + opt_res = curl_easy_setopt(curl, CURLOPT_NOBODY, 1L); + if (opt_res != CURLE_OK) { + return opt_res; + } + opt_res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); + } else if (presign_str_ieq(method, "POST")) { + opt_res = curl_easy_setopt(curl, CURLOPT_POST, 1L); + if (opt_res != CURLE_OK) { + return opt_res; + } + opt_res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); + } else { + opt_res = curl_easy_setopt(curl, CURLOPT_CUSTOMREQUEST, method); + } + } + + return opt_res; +} + +// Test: Presign read operation +void test_presign_read(opendal_test_context* ctx) +{ + const char* path = "test_presign.txt"; + const char* content = "Presign test content"; + + opendal_bytes data; + data.data = (uint8_t*)content; + data.len = strlen(content); + data.capacity = strlen(content); + + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); + OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); + + opendal_result_presign presign_result = opendal_operator_presign_read(ctx->config->operator_instance, path, 3600); + OPENDAL_ASSERT_NO_ERROR(presign_result.error, "Presign read should succeed"); + OPENDAL_ASSERT_NOT_NULL(presign_result.req, "Presigned request should not be null"); + + const char* method = opendal_presigned_request_method(presign_result.req); + const char* url = opendal_presigned_request_uri(presign_result.req); + const opendal_http_header_pair* headers = opendal_presigned_request_headers(presign_result.req); + uintptr_t headers_len = opendal_presigned_request_headers_len(presign_result.req); + OPENDAL_ASSERT_NOT_NULL(method, "Presigned method should not be null"); + OPENDAL_ASSERT_STR_EQ("GET", method, "Presigned method should be GET"); + OPENDAL_ASSERT_NOT_NULL(url, "Presigned URL should not be null"); + OPENDAL_ASSERT(headers_len == 0 || headers != NULL, "Headers pointer must be valid when headers exist"); + + CURL* curl = curl_easy_init(); + OPENDAL_ASSERT_NOT_NULL(curl, "CURL initialization should succeed"); + + size_t expected_len = strlen(content); + struct curl_slist* chunk = NULL; + int build_ok = presign_build_header_list(headers, headers_len, PRESIGN_NO_OVERRIDE, &chunk); + if (!build_ok) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT(build_ok, "Building CURL headers should succeed"); + } + + CURLcode opt_res = presign_set_standard_options(curl, url, method); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting standard CURL options should succeed"); + } + + if (chunk != NULL) { + opt_res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL headers should succeed"); + } + } + + presign_header_context header_ctx; + header_ctx.content_length = 0; + header_ctx.content_length_found = 0; + opt_res = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, presign_header_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL header callback should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_HEADERDATA, &header_ctx); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL header data should succeed"); + } + + presign_body_context body_ctx; + body_ctx.expected = content; + body_ctx.expected_len = expected_len; + body_ctx.offset = 0; + body_ctx.mismatch = 0; + opt_res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, presign_write_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL write callback should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_WRITEDATA, &body_ctx); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL write data should succeed"); + } + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(res, CURLE_OK, "CURL perform should succeed"); + } + + presign_cleanup_curl(curl, chunk); + + int header_found = header_ctx.content_length_found; + size_t header_length = header_ctx.content_length; + size_t received_len = body_ctx.offset; + int mismatch = body_ctx.mismatch; + + opendal_presigned_request_free(presign_result.req); + + opendal_error* delete_error = opendal_operator_delete(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(delete_error, "Cleanup delete should succeed"); + + OPENDAL_ASSERT(header_found, "Content-Length header should be present"); + OPENDAL_ASSERT_EQ(expected_len, header_length, + "Content-Length header should match object size"); + OPENDAL_ASSERT_EQ(expected_len, received_len, + "Downloaded data length should match object size"); + OPENDAL_ASSERT(mismatch == 0, "Downloaded content should match stored content"); +} + +// Test: Presign write operation +void test_presign_write(opendal_test_context* ctx) +{ + const char* path = "test_presign_write.txt"; + const char* content = "Presign write content"; + size_t content_len = strlen(content); + + opendal_result_presign presign_result = opendal_operator_presign_write(ctx->config->operator_instance, path, 3600); + OPENDAL_ASSERT_NO_ERROR(presign_result.error, "Presign write should succeed"); + OPENDAL_ASSERT_NOT_NULL(presign_result.req, "Presigned request should not be null"); + + const char* method = opendal_presigned_request_method(presign_result.req); + const char* url = opendal_presigned_request_uri(presign_result.req); + const opendal_http_header_pair* headers = opendal_presigned_request_headers(presign_result.req); + uintptr_t headers_len = opendal_presigned_request_headers_len(presign_result.req); + OPENDAL_ASSERT_NOT_NULL(method, "Presigned method should not be null"); + OPENDAL_ASSERT_NOT_NULL(url, "Presigned URL should not be null"); + OPENDAL_ASSERT(headers_len == 0 || headers != NULL, "Headers pointer must be valid when headers exist"); + + CURL* curl = curl_easy_init(); + OPENDAL_ASSERT_NOT_NULL(curl, "CURL initialization should succeed"); + + struct curl_slist* chunk = NULL; + int build_ok = presign_build_header_list(headers, headers_len, content_len, &chunk); + if (!build_ok) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT(build_ok, "Building CURL headers should succeed"); + } + CURLcode opt_res = presign_set_standard_options(curl, url, method); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting standard CURL options should succeed"); + } + + if (chunk != NULL) { + opt_res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL headers should succeed"); + } + } + + presign_upload_context upload_ctx; + upload_ctx.data = content; + upload_ctx.len = content_len; + upload_ctx.offset = 0; + + opt_res = curl_easy_setopt(curl, CURLOPT_UPLOAD, 1L); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Enabling CURL upload should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_READFUNCTION, presign_upload_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL read callback should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_READDATA, &upload_ctx); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL read data should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_SEEKFUNCTION, presign_seek_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL seek callback should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_SEEKDATA, &upload_ctx); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL seek data should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_INFILESIZE_LARGE, (curl_off_t)content_len); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL upload size should succeed"); + } + + opt_res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, presign_sink_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL sink callback should succeed"); + } + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(res, CURLE_OK, "CURL perform should succeed"); + } + + presign_cleanup_curl(curl, chunk); + opendal_presigned_request_free(presign_result.req); + + opendal_result_stat stat_res = opendal_operator_stat(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(stat_res.error, "Stat after presign write should succeed"); + OPENDAL_ASSERT_NOT_NULL(stat_res.meta, "Stat metadata should not be null"); + OPENDAL_ASSERT_EQ((size_t)opendal_metadata_content_length(stat_res.meta), content_len, + "Stat size should match uploaded content length"); + + opendal_metadata_free(stat_res.meta); + + opendal_result_read read_res = opendal_operator_read(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(read_res.error, "Read after presign write should succeed"); + OPENDAL_ASSERT_EQ(content_len, read_res.data.len, "Read length should match uploaded content length"); + OPENDAL_ASSERT(memcmp(content, read_res.data.data, read_res.data.len) == 0, + "Read content should match uploaded content"); + opendal_bytes_free(&read_res.data); + + opendal_error* delete_error = opendal_operator_delete(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(delete_error, "Cleanup delete should succeed"); +} + +// Test: Presign stat operation +void test_presign_stat(opendal_test_context* ctx) +{ + const char* path = "test_presign_stat.txt"; + const char* content = "Presign stat content"; + size_t content_len = strlen(content); + + opendal_bytes data; + data.data = (uint8_t*)content; + data.len = content_len; + data.capacity = content_len; + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); + OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); + + opendal_result_presign presign_result = opendal_operator_presign_stat(ctx->config->operator_instance, path, 3600); + OPENDAL_ASSERT_NO_ERROR(presign_result.error, "Presign stat should succeed"); + OPENDAL_ASSERT_NOT_NULL(presign_result.req, "Presigned request should not be null"); + + const char* method = opendal_presigned_request_method(presign_result.req); + const char* url = opendal_presigned_request_uri(presign_result.req); + const opendal_http_header_pair* headers = opendal_presigned_request_headers(presign_result.req); + uintptr_t headers_len = opendal_presigned_request_headers_len(presign_result.req); + OPENDAL_ASSERT_NOT_NULL(method, "Presigned method should not be null"); + OPENDAL_ASSERT_NOT_NULL(url, "Presigned URL should not be null"); + + CURL* curl = curl_easy_init(); + OPENDAL_ASSERT_NOT_NULL(curl, "CURL initialization should succeed"); + + struct curl_slist* chunk = NULL; + int build_ok = presign_build_header_list(headers, headers_len, PRESIGN_NO_OVERRIDE, &chunk); + if (!build_ok) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT(build_ok, "Building CURL headers should succeed"); + } + CURLcode opt_res = presign_set_standard_options(curl, url, method); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting standard CURL options should succeed"); + } + + if (chunk != NULL) { + opt_res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL headers should succeed"); + } + } + + presign_header_context header_ctx; + header_ctx.content_length = 0; + header_ctx.content_length_found = 0; + opt_res = curl_easy_setopt(curl, CURLOPT_HEADERFUNCTION, presign_header_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL header callback should succeed"); + } + opt_res = curl_easy_setopt(curl, CURLOPT_HEADERDATA, &header_ctx); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL header data should succeed"); + } + + opt_res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, presign_sink_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL sink callback should succeed"); + } + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(res, CURLE_OK, "CURL perform should succeed"); + } + + long response_code = 0; + CURLcode info_res = curl_easy_getinfo(curl, CURLINFO_RESPONSE_CODE, &response_code); + if (info_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(info_res, CURLE_OK, "Retrieving response code should succeed"); + } + + presign_cleanup_curl(curl, chunk); + opendal_presigned_request_free(presign_result.req); + + OPENDAL_ASSERT_EQ(200, response_code, "Presign stat should return 200 status"); + OPENDAL_ASSERT(header_ctx.content_length_found, "Stat response should include Content-Length"); + OPENDAL_ASSERT_EQ(content_len, header_ctx.content_length, + "Stat Content-Length should match written data"); + + opendal_error* delete_error = opendal_operator_delete(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(delete_error, "Cleanup delete should succeed"); +} + +// Test: Presign delete operation +void test_presign_delete(opendal_test_context* ctx) +{ + const char* path = "test_presign_delete.txt"; + const char* content = "Presign delete content"; + size_t content_len = strlen(content); + + opendal_bytes data; + data.data = (uint8_t*)content; + data.len = content_len; + data.capacity = content_len; + opendal_error* error = opendal_operator_write(ctx->config->operator_instance, path, &data); + OPENDAL_ASSERT_NO_ERROR(error, "Write operation should succeed"); + + opendal_result_presign presign_result = opendal_operator_presign_delete(ctx->config->operator_instance, path, 3600); + OPENDAL_ASSERT_NO_ERROR(presign_result.error, "Presign delete should succeed"); + OPENDAL_ASSERT_NOT_NULL(presign_result.req, "Presigned request should not be null"); + + const char* method = opendal_presigned_request_method(presign_result.req); + const char* url = opendal_presigned_request_uri(presign_result.req); + const opendal_http_header_pair* headers = opendal_presigned_request_headers(presign_result.req); + uintptr_t headers_len = opendal_presigned_request_headers_len(presign_result.req); + OPENDAL_ASSERT_NOT_NULL(method, "Presigned method should not be null"); + OPENDAL_ASSERT_NOT_NULL(url, "Presigned URL should not be null"); + + CURL* curl = curl_easy_init(); + OPENDAL_ASSERT_NOT_NULL(curl, "CURL initialization should succeed"); + + struct curl_slist* chunk = NULL; + int build_ok = presign_build_header_list(headers, headers_len, PRESIGN_NO_OVERRIDE, &chunk); + if (!build_ok) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT(build_ok, "Building CURL headers should succeed"); + } + CURLcode opt_res = presign_set_standard_options(curl, url, method); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting standard CURL options should succeed"); + } + + if (chunk != NULL) { + opt_res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, chunk); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL headers should succeed"); + } + } + + opt_res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION, presign_sink_callback); + if (opt_res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(opt_res, CURLE_OK, "Setting CURL sink callback should succeed"); + } + + CURLcode res = curl_easy_perform(curl); + if (res != CURLE_OK) { + presign_cleanup_curl(curl, chunk); + OPENDAL_ASSERT_EQ(res, CURLE_OK, "CURL perform should succeed"); + } + + presign_cleanup_curl(curl, chunk); + opendal_presigned_request_free(presign_result.req); + + opendal_result_exists exists_res = opendal_operator_exists(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(exists_res.error, "Exists after presign delete should succeed"); + OPENDAL_ASSERT(!exists_res.exists, "Object should not exist after presign delete"); + + // Ensure cleanup is idempotent + opendal_error* delete_error = opendal_operator_delete(ctx->config->operator_instance, path); + OPENDAL_ASSERT_NO_ERROR(delete_error, "Delete after presign delete should be idempotent"); +} + +opendal_test_case presign_tests[] = { + { "presign_read", test_presign_read, make_capability_presign() }, + { "presign_write", test_presign_write, make_capability_presign() }, + { "presign_stat", test_presign_stat, make_capability_presign() }, + // { "presign_delete", test_presign_delete, make_capability_presign() }, +}; + +opendal_test_suite presign_suite = { + "Presign Operations", + presign_tests, + sizeof(presign_tests) / sizeof(presign_tests[0]), +}; diff --git a/core/core/src/blocking/operator.rs b/core/core/src/blocking/operator.rs index 740fec949..ce479cb6f 100644 --- a/core/core/src/blocking/operator.rs +++ b/core/core/src/blocking/operator.rs @@ -15,9 +15,12 @@ // specific language governing permissions and limitations // under the License. +use std::time::Duration; + use tokio::runtime::Handle; use crate::Operator as AsyncOperator; +use crate::raw::PresignedRequest; use crate::types::IntoOperatorUri; use crate::*; @@ -164,6 +167,34 @@ impl Operator { /// # Operator blocking API. impl Operator { + /// Create a presigned request for stat. + /// + /// See [`Operator::presign_stat`] for more details. + pub fn presign_stat(&self, path: &str, expire: Duration) -> Result<PresignedRequest> { + self.handle.block_on(self.op.presign_stat(path, expire)) + } + + /// Create a presigned request for read. + /// + /// See [`Operator::presign_read`] for more details. + pub fn presign_read(&self, path: &str, expire: Duration) -> Result<PresignedRequest> { + self.handle.block_on(self.op.presign_read(path, expire)) + } + + /// Create a presigned request for write. + /// + /// See [`Operator::presign_write`] for more details. + pub fn presign_write(&self, path: &str, expire: Duration) -> Result<PresignedRequest> { + self.handle.block_on(self.op.presign_write(path, expire)) + } + + /// Create a presigned request for delete. + /// + /// See [`Operator::presign_delete`] for more details. + pub fn presign_delete(&self, path: &str, expire: Duration) -> Result<PresignedRequest> { + self.handle.block_on(self.op.presign_delete(path, expire)) + } + /// Get given path's metadata. /// /// # Behavior
