This is an automated email from the ASF dual-hosted git repository.
tison pushed a commit to branch main
in repository https://gitbox.apache.org/repos/asf/opendal.git
The following commit(s) were added to refs/heads/main by this push:
new 6b4f1e075 feat(bindings/go): support presign (#7147)
6b4f1e075 is described below
commit 6b4f1e0751abd97173c9147f126ef9a2e4eac410
Author: Hanchin Hsieh <[email protected]>
AuthorDate: Thu Jan 15 16:57:10 2026 +0800
feat(bindings/go): support presign (#7147)
---
.github/workflows/ci_bindings_c.yml | 6 +-
.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 | 264 ++++++++
bindings/c/tests/Makefile | 4 +-
bindings/c/tests/test_framework.cpp | 4 +
bindings/c/tests/test_framework.h | 19 +-
bindings/c/tests/test_runner.cpp | 2 +
bindings/c/tests/test_suites_presign.cpp | 735 +++++++++++++++++++++++
bindings/go/README.md | 5 +
bindings/go/operator_info.go | 4 +
bindings/go/presign.go | 267 ++++++++
bindings/go/tests/behavior_tests/opendal_test.go | 1 +
bindings/go/tests/behavior_tests/presign_test.go | 134 +++++
bindings/go/types.go | 25 +-
bindings/go/{write.go => writer.go} | 0
core/core/src/blocking/operator.rs | 31 +
19 files changed, 1597 insertions(+), 9 deletions(-)
diff --git a/.github/workflows/ci_bindings_c.yml
b/.github/workflows/ci_bindings_c.yml
index 418253440..68fcc0f8d 100644
--- a/.github/workflows/ci_bindings_c.yml
+++ b/.github/workflows/ci_bindings_c.yml
@@ -51,11 +51,11 @@ jobs:
- name: Install C 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: Clippy Check
working-directory: "bindings/c"
-
+
run: |
cargo clippy -- -D warnings
@@ -64,7 +64,7 @@ jobs:
working-directory: "bindings/c"
run: |
mkdir build && cd build
- cmake .. -DTEST_ENABLE_ASAN=ON -DFEATURES=opendal/services-memory
+ cmake .. -DTEST_ENABLE_ASAN=ON -DFEATURES=opendal/services-memory
make -j$(nproc)
- name: Check diff
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..705f3334a
--- /dev/null
+++ b/bindings/c/src/presign.rs
@@ -0,0 +1,264 @@
+// 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.
+#[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
+ #[allow(dead_code)]
+ header_keys: Vec<CString>,
+ #[allow(dead_code)]
+ 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 header_keys: Vec<CString> = req
+ .header()
+ .iter()
+ .map(|(k, _)| CString::new(k.as_str()).unwrap())
+ .collect();
+ let header_values: Vec<CString> = req
+ .header()
+ .iter()
+ .map(|(_, v)| CString::new(v.to_str().unwrap()).unwrap())
+ .collect();
+
+ let headers: Vec<opendal_http_header_pair> = header_keys
+ .iter()
+ .zip(header_values.iter())
+ .map(|(k, v)| opendal_http_header_pair {
+ key: k.as_ptr(),
+ value: v.as_ptr(),
+ })
+ .collect();
+
+ 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 {
+ assert!(!path.is_null());
+
+ let op = op.deref();
+ let path = CStr::from_ptr(path).to_str().expect("malformed path");
+ 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 {
+ assert!(!path.is_null());
+
+ let op = op.deref();
+ let path = CStr::from_ptr(path).to_str().expect("malformed path");
+ 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 {
+ assert!(!path.is_null());
+
+ let op = op.deref();
+ let path = CStr::from_ptr(path).to_str().expect("malformed path");
+ 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 {
+ assert!(!path.is_null());
+
+ let op = op.deref();
+ let path = CStr::from_ptr(path).to_str().expect("malformed path");
+ 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/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..06f60c27c 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,18 @@ 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.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 +243,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..4ade2815f
--- /dev/null
+++ b/bindings/c/tests/test_suites_presign.cpp
@@ -0,0 +1,735 @@
+/*
+ * 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;
+}
+
+typedef enum presign_prepare_result {
+ PRESIGN_PREPARE_OK = 0,
+ PRESIGN_PREPARE_BUILD_HEADERS_FAIL,
+ PRESIGN_PREPARE_STANDARD_OPTIONS_FAIL,
+ PRESIGN_PREPARE_SET_HEADERS_FAIL,
+} presign_prepare_result;
+
+static presign_prepare_result presign_prepare_curl(CURL* curl,
+ const char* url,
+ const char* method,
+ const opendal_http_header_pair* headers,
+ uintptr_t headers_len,
+ size_t override_content_len,
+ struct curl_slist** out_chunk,
+ CURLcode* out_error)
+{
+ if (out_chunk == NULL) {
+ if (out_error != NULL) {
+ *out_error = CURLE_FAILED_INIT;
+ }
+ return PRESIGN_PREPARE_STANDARD_OPTIONS_FAIL;
+ }
+
+ *out_chunk = NULL;
+
+ if (!presign_build_header_list(headers, headers_len, override_content_len,
out_chunk)) {
+ if (*out_chunk != NULL) {
+ curl_slist_free_all(*out_chunk);
+ *out_chunk = NULL;
+ }
+ if (out_error != NULL) {
+ *out_error = CURLE_OK;
+ }
+ return PRESIGN_PREPARE_BUILD_HEADERS_FAIL;
+ }
+
+ CURLcode opt_res = presign_set_standard_options(curl, url, method);
+ if (opt_res != CURLE_OK) {
+ if (*out_chunk != NULL) {
+ curl_slist_free_all(*out_chunk);
+ *out_chunk = NULL;
+ }
+ if (out_error != NULL) {
+ *out_error = opt_res;
+ }
+ return PRESIGN_PREPARE_STANDARD_OPTIONS_FAIL;
+ }
+
+ if (*out_chunk != NULL) {
+ opt_res = curl_easy_setopt(curl, CURLOPT_HTTPHEADER, *out_chunk);
+ if (opt_res != CURLE_OK) {
+ curl_slist_free_all(*out_chunk);
+ *out_chunk = NULL;
+ if (out_error != NULL) {
+ *out_error = opt_res;
+ }
+ return PRESIGN_PREPARE_SET_HEADERS_FAIL;
+ }
+ }
+
+ if (out_error != NULL) {
+ *out_error = CURLE_OK;
+ }
+ return PRESIGN_PREPARE_OK;
+}
+
+#define PRESIGN_ASSERT_PREPARE_OK(curl_handle, chunk_handle, prepare_result,
prepare_error) \
+ do { \
+ if ((prepare_result) != PRESIGN_PREPARE_OK) { \
+ presign_cleanup_curl((curl_handle), (chunk_handle)); \
+ if ((prepare_result) == PRESIGN_PREPARE_BUILD_HEADERS_FAIL) { \
+ OPENDAL_ASSERT(0, "Building CURL headers should succeed"); \
+ } else if ((prepare_result) ==
PRESIGN_PREPARE_STANDARD_OPTIONS_FAIL) { \
+ OPENDAL_ASSERT_EQ(CURLE_OK, (prepare_error), "Setting standard
CURL options should succeed"); \
+ } else { \
+ OPENDAL_ASSERT_EQ(CURLE_OK, (prepare_error), "Setting CURL
headers should succeed"); \
+ } \
+ } \
+ } while (0)
+
+// 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;
+ CURLcode setup_error = CURLE_OK;
+ presign_prepare_result prepare_res = presign_prepare_curl(curl, url,
method,
+ headers, headers_len, PRESIGN_NO_OVERRIDE, &chunk, &setup_error);
+ PRESIGN_ASSERT_PREPARE_OK(curl, chunk, prepare_res, setup_error);
+
+ CURLcode opt_res = CURLE_OK;
+
+ 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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "Setting CURL write data should
succeed");
+ }
+
+ CURLcode res = curl_easy_perform(curl);
+ if (res != CURLE_OK) {
+ presign_cleanup_curl(curl, chunk);
+ OPENDAL_ASSERT_EQ(CURLE_OK, res, "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;
+ CURLcode setup_error = CURLE_OK;
+ presign_prepare_result prepare_res = presign_prepare_curl(curl, url,
method,
+ headers, headers_len, content_len, &chunk, &setup_error);
+ PRESIGN_ASSERT_PREPARE_OK(curl, chunk, prepare_res, setup_error);
+
+ CURLcode opt_res = CURLE_OK;
+
+ 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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "Setting CURL sink callback
should succeed");
+ }
+
+ CURLcode res = curl_easy_perform(curl);
+ if (res != CURLE_OK) {
+ presign_cleanup_curl(curl, chunk);
+ OPENDAL_ASSERT_EQ(CURLE_OK, res, "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(content_len,
(size_t)opendal_metadata_content_length(stat_res.meta),
+ "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;
+ CURLcode setup_error = CURLE_OK;
+ presign_prepare_result prepare_res = presign_prepare_curl(curl, url,
method,
+ headers, headers_len, PRESIGN_NO_OVERRIDE, &chunk, &setup_error);
+ PRESIGN_ASSERT_PREPARE_OK(curl, chunk, prepare_res, setup_error);
+
+ CURLcode opt_res = CURLE_OK;
+
+ 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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "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(CURLE_OK, opt_res, "Setting CURL sink callback
should succeed");
+ }
+
+ CURLcode res = curl_easy_perform(curl);
+ if (res != CURLE_OK) {
+ presign_cleanup_curl(curl, chunk);
+ OPENDAL_ASSERT_EQ(CURLE_OK, res, "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(CURLE_OK, info_res, "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)
+{
+ opendal_operator_info* info =
opendal_operator_info_new(ctx->config->operator_instance);
+ OPENDAL_ASSERT_NOT_NULL(info, "Operator info should not be null");
+ opendal_capability cap = opendal_operator_info_get_full_capability(info);
+ opendal_operator_info_free(info);
+ if (!cap.presign_delete) {
+ return;
+ }
+ 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;
+ CURLcode setup_error = CURLE_OK;
+ presign_prepare_result prepare_res = presign_prepare_curl(curl, url,
method,
+ headers, headers_len, PRESIGN_NO_OVERRIDE, &chunk, &setup_error);
+ PRESIGN_ASSERT_PREPARE_OK(curl, chunk, prepare_res, setup_error);
+
+ CURLcode opt_res = curl_easy_setopt(curl, CURLOPT_WRITEFUNCTION,
presign_sink_callback);
+ if (opt_res != CURLE_OK) {
+ presign_cleanup_curl(curl, chunk);
+ OPENDAL_ASSERT_EQ(CURLE_OK, opt_res, "Setting CURL sink callback
should succeed");
+ }
+
+ CURLcode res = curl_easy_perform(curl);
+ if (res != CURLE_OK) {
+ presign_cleanup_curl(curl, chunk);
+ OPENDAL_ASSERT_EQ(CURLE_OK, res, "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/bindings/go/README.md b/bindings/go/README.md
index 3d41d1bd4..39e111339 100644
--- a/bindings/go/README.md
+++ b/bindings/go/README.md
@@ -306,6 +306,11 @@ geomean 343.6 12.40 -96.39%
- [ ] Metadata -- Need support from the C binding
- [x] Copy
- [x] Rename
+- [x] Presign
+ - [x] PresignRead
+ - [x] PresignWrite
+ - [x] PresignStat
+ - [x] PresignDelete
## Development
diff --git a/bindings/go/operator_info.go b/bindings/go/operator_info.go
index ca02ed2dd..2438ec890 100644
--- a/bindings/go/operator_info.go
+++ b/bindings/go/operator_info.go
@@ -219,6 +219,10 @@ func (c *Capability) PresignWrite() bool {
return c.inner.presignWrite == 1
}
+func (c *Capability) PresignDelete() bool {
+ return c.inner.presignDelete == 1
+}
+
func (c *Capability) Shared() bool {
return c.inner.shared == 1
}
diff --git a/bindings/go/presign.go b/bindings/go/presign.go
new file mode 100644
index 000000000..4c506829a
--- /dev/null
+++ b/bindings/go/presign.go
@@ -0,0 +1,267 @@
+/*
+ * 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.
+ */
+
+package opendal
+
+import (
+ "context"
+ "fmt"
+ "net/http"
+ "time"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+)
+
+type presignFunc func(op *opendalOperator, path string, expire uint64)
(*opendalPresignedRequest, error)
+
+// PresignRead returns a presigned HTTP request that can be used to read the
object at the given path.
+func (op *Operator) PresignRead(path string, expire time.Duration)
(*http.Request, error) {
+ return op.presign(path, expire, ffiOperatorPresignRead.symbol(op.ctx))
+}
+
+// PresignWrite returns a presigned HTTP request that can be used to write the
object at the given path.
+func (op *Operator) PresignWrite(path string, expire time.Duration)
(*http.Request, error) {
+ return op.presign(path, expire, ffiOperatorPresignWrite.symbol(op.ctx))
+}
+
+// PresignDelete returns a presigned HTTP request that can be used to delete
the object at the given path.
+func (op *Operator) PresignDelete(path string, expire time.Duration)
(*http.Request, error) {
+ return op.presign(path, expire, ffiOperatorPresignDelete.symbol(op.ctx))
+}
+
+// PresignStat returns a presigned HTTP request that can be used to stat the
object at the given path.
+func (op *Operator) PresignStat(path string, expire time.Duration)
(*http.Request, error) {
+ return op.presign(path, expire, ffiOperatorPresignStat.symbol(op.ctx))
+}
+
+func (op *Operator) presign(path string, expire time.Duration, call
presignFunc) (*http.Request, error) {
+ secs := uint64(expire / time.Second)
+
+ req, err := call(op.inner, path, secs)
+ if err != nil {
+ return nil, err
+ }
+ if req == nil {
+ return nil, fmt.Errorf("presigned request should not be nil")
+ }
+ defer ffiPresignedRequestFree.symbol(op.ctx)(req)
+
+ return buildHTTPPresignedRequest(op.ctx, req)
+}
+
+func buildHTTPPresignedRequest(ctx context.Context, ptr
*opendalPresignedRequest) (req *http.Request, err error) {
+ mptr := ffiPresignedRequestMethod.symbol(ctx)(ptr)
+ uptr := ffiPresignedRequestUri.symbol(ctx)(ptr)
+
+ method := BytePtrToString(mptr)
+ uri := BytePtrToString(uptr)
+
+ req, err = http.NewRequest(method, uri, nil)
+ if err != nil {
+ return
+ }
+
+ hptr := ffiPresignedRequestHeaders.symbol(ctx)(ptr)
+ hl := ffiPresignedRequestHeadersLen.symbol(ctx)(ptr)
+ if hptr == nil || hl == 0 {
+ return
+ }
+
+ pairs := unsafe.Slice(hptr, int(hl))
+ for i := range pairs {
+ key := BytePtrToString(pairs[i].key)
+ value := BytePtrToString(pairs[i].value)
+ if key == "" {
+ continue
+ }
+ req.Header.Add(key, value)
+ }
+
+ return
+}
+
+var ffiOperatorPresignRead = newFFI(ffiOpts{
+ sym: "opendal_operator_presign_read",
+ rType: &typeResultPresign,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypeUint64},
+}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, path
string, expire uint64) (*opendalPresignedRequest, error) {
+ return func(op *opendalOperator, path string, expire uint64)
(*opendalPresignedRequest, error) {
+ bytePath, err := BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultPresign
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ unsafe.Pointer(&expire),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.req, nil
+ }
+})
+
+var ffiOperatorPresignWrite = newFFI(ffiOpts{
+ sym: "opendal_operator_presign_write",
+ rType: &typeResultPresign,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypeUint64},
+}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, path
string, expire uint64) (*opendalPresignedRequest, error) {
+ return func(op *opendalOperator, path string, expire uint64)
(*opendalPresignedRequest, error) {
+ bytePath, err := BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultPresign
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ unsafe.Pointer(&expire),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.req, nil
+ }
+})
+
+var ffiOperatorPresignDelete = newFFI(ffiOpts{
+ sym: "opendal_operator_presign_delete",
+ rType: &typeResultPresign,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypeUint64},
+}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, path
string, expire uint64) (*opendalPresignedRequest, error) {
+ return func(op *opendalOperator, path string, expire uint64)
(*opendalPresignedRequest, error) {
+ bytePath, err := BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultPresign
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ unsafe.Pointer(&expire),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.req, nil
+ }
+})
+
+var ffiOperatorPresignStat = newFFI(ffiOpts{
+ sym: "opendal_operator_presign_stat",
+ rType: &typeResultPresign,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypeUint64},
+}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, path
string, expire uint64) (*opendalPresignedRequest, error) {
+ return func(op *opendalOperator, path string, expire uint64)
(*opendalPresignedRequest, error) {
+ bytePath, err := BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultPresign
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ unsafe.Pointer(&expire),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.req, nil
+ }
+})
+
+var ffiPresignedRequestMethod = newFFI(ffiOpts{
+ sym: "opendal_presigned_request_method",
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(req *opendalPresignedRequest)
*byte {
+ return func(req *opendalPresignedRequest) *byte {
+ var result *byte
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&req),
+ )
+ return result
+ }
+})
+
+var ffiPresignedRequestUri = newFFI(ffiOpts{
+ sym: "opendal_presigned_request_uri",
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(req *opendalPresignedRequest)
*byte {
+ return func(req *opendalPresignedRequest) *byte {
+ var result *byte
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&req),
+ )
+ return result
+ }
+})
+
+var ffiPresignedRequestHeaders = newFFI(ffiOpts{
+ sym: "opendal_presigned_request_headers",
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(req *opendalPresignedRequest)
*opendalHttpHeaderPair {
+ return func(req *opendalPresignedRequest) *opendalHttpHeaderPair {
+ var result *opendalHttpHeaderPair
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&req),
+ )
+ return result
+ }
+})
+
+var ffiPresignedRequestHeadersLen = newFFI(ffiOpts{
+ sym: "opendal_presigned_request_headers_len",
+ rType: &ffi.TypeUint64,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(req *opendalPresignedRequest)
uintptr {
+ return func(req *opendalPresignedRequest) uintptr {
+ var length uint64
+ ffiCall(
+ unsafe.Pointer(&length),
+ unsafe.Pointer(&req),
+ )
+ return uintptr(length)
+ }
+})
+
+var ffiPresignedRequestFree = newFFI(ffiOpts{
+ sym: "opendal_presigned_request_free",
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall ffiCall) func(req *opendalPresignedRequest)
{
+ return func(req *opendalPresignedRequest) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&req),
+ )
+ }
+})
diff --git a/bindings/go/tests/behavior_tests/opendal_test.go
b/bindings/go/tests/behavior_tests/opendal_test.go
index d05a162eb..a0931ca1b 100644
--- a/bindings/go/tests/behavior_tests/opendal_test.go
+++ b/bindings/go/tests/behavior_tests/opendal_test.go
@@ -65,6 +65,7 @@ func TestBehavior(t *testing.T) {
tests = append(tests, testsDelete(cap)...)
tests = append(tests, testsList(cap)...)
tests = append(tests, testsRead(cap)...)
+ tests = append(tests, testsPresign(cap)...)
tests = append(tests, testsRename(cap)...)
tests = append(tests, testsStat(cap)...)
tests = append(tests, testsWrite(cap)...)
diff --git a/bindings/go/tests/behavior_tests/presign_test.go
b/bindings/go/tests/behavior_tests/presign_test.go
new file mode 100644
index 000000000..a8f8fd13c
--- /dev/null
+++ b/bindings/go/tests/behavior_tests/presign_test.go
@@ -0,0 +1,134 @@
+/*
+ * 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.
+ */
+
+package opendal_test
+
+import (
+ "bytes"
+ "io"
+ "net/http"
+ "strconv"
+ "time"
+
+ opendal "github.com/apache/opendal/bindings/go"
+ "github.com/stretchr/testify/require"
+)
+
+func testsPresign(cap *opendal.Capability) []behaviorTest {
+ if !cap.Presign() {
+ return nil
+ }
+
+ tests := make([]behaviorTest, 0, 4)
+ if cap.PresignWrite() && cap.Stat() {
+ tests = append(tests, testPresignWrite)
+ }
+ if cap.PresignRead() && cap.Write() {
+ tests = append(tests, testPresignRead)
+ }
+ if cap.PresignStat() && cap.Write() {
+ tests = append(tests, testPresignStat)
+ }
+ if cap.PresignDelete() {
+ tests = append(tests, testPresignDelete)
+ }
+ return tests
+}
+
+func testPresignWrite(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path, content, size := fixture.NewFile()
+
+ req, err := op.PresignWrite(path, time.Hour)
+ assert.Nil(err)
+
+ req.ContentLength = int64(len(content))
+ req.Body = io.NopCloser(bytes.NewReader(content))
+
+ resp, err := http.DefaultClient.Do(req)
+ assert.Nil(err)
+ defer resp.Body.Close()
+ _, err = io.Copy(io.Discard, resp.Body)
+ assert.Nil(err)
+ assert.GreaterOrEqual(resp.StatusCode, 200)
+ assert.Less(resp.StatusCode, 300)
+
+ meta, err := op.Stat(path)
+ assert.Nil(err)
+ assert.EqualValues(size, meta.ContentLength())
+}
+
+func testPresignRead(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content))
+
+ req, err := op.PresignRead(path, time.Hour)
+ assert.Nil(err)
+
+ resp, err := http.DefaultClient.Do(req)
+ assert.Nil(err)
+ defer resp.Body.Close()
+
+ bs, err := io.ReadAll(resp.Body)
+ assert.Nil(err)
+ assert.Equal(int(size), len(bs))
+
+ assert.Equal(content, bs)
+}
+
+func testPresignStat(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content))
+
+ req, err := op.PresignStat(path, time.Hour)
+ assert.Nil(err)
+
+ resp, err := http.DefaultClient.Do(req)
+ assert.Nil(err)
+ defer resp.Body.Close()
+ assert.Equal(http.StatusOK, resp.StatusCode)
+
+ lengthHeader := resp.Header.Get("Content-Length")
+ assert.NotEmpty(lengthHeader)
+ length, err := strconv.ParseUint(lengthHeader, 10, 64)
+ assert.Nil(err)
+ assert.EqualValues(size, length)
+}
+
+func testPresignDelete(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path, content, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content))
+
+ req, err := op.PresignDelete(path, time.Hour)
+ assert.Nil(err)
+
+ resp, err := http.DefaultClient.Do(req)
+ assert.Nil(err)
+ defer resp.Body.Close()
+ _, err = io.Copy(io.Discard, resp.Body)
+ assert.Nil(err)
+ assert.GreaterOrEqual(resp.StatusCode, 200)
+ assert.Less(resp.StatusCode, 300)
+
+ exists, err := op.IsExist(path)
+ assert.Nil(err)
+ assert.False(exists)
+}
diff --git a/bindings/go/types.go b/bindings/go/types.go
index d566b06b4..8f8aa5cf2 100644
--- a/bindings/go/types.go
+++ b/bindings/go/types.go
@@ -135,6 +135,15 @@ var (
}[0],
}
+ typeResultPresign = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
typeCapability = ffi.Type{
Type: ffi.Struct,
Elements: &[]*ffi.Type{
@@ -169,6 +178,7 @@ var (
&ffi.TypeUint8, // presign_read
&ffi.TypeUint8, // presign_stat
&ffi.TypeUint8, // presign_write
+ &ffi.TypeUint8, // presign_delete
&ffi.TypeUint8, // shared
&ffi.TypeUint8, // blocking
nil,
@@ -208,6 +218,7 @@ type opendalCapability struct {
presignRead uint8
presignStat uint8
presignWrite uint8
+ presignDelete uint8
shared uint8
blocking uint8
}
@@ -249,7 +260,7 @@ type resultReaderRead struct {
}
type resultReaderSeek struct {
- pos uint64
+ pos uint64
error *opendalError
}
@@ -263,6 +274,18 @@ type resultStat struct {
error *opendalError
}
+type resultPresign struct {
+ req *opendalPresignedRequest
+ error *opendalError
+}
+
+type opendalPresignedRequest struct{}
+
+type opendalHttpHeaderPair struct {
+ key *byte
+ value *byte
+}
+
type opendalMetadata struct{}
type opendalBytes struct {
diff --git a/bindings/go/write.go b/bindings/go/writer.go
similarity index 100%
rename from bindings/go/write.go
rename to bindings/go/writer.go
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