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 0fecd25104999d2c1cb1551caaa92678753770ae
Author: Hanchin Hsieh <[email protected]>
AuthorDate: Tue Jan 13 13:37:16 2026 +0800

    feat(bindings/c): support presign
---
 bindings/c/include/opendal.h             |  91 +++++
 bindings/c/src/lib.rs                    |   5 +
 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        |  21 +-
 bindings/c/tests/test_runner.cpp         |   2 +
 bindings/c/tests/test_suites_presign.cpp | 678 +++++++++++++++++++++++++++++++
 core/core/src/blocking/operator.rs       |  31 ++
 10 files changed, 1121 insertions(+), 4 deletions(-)

diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h
index 270de2e91..71bbb3992 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
  *
@@ -583,6 +585,42 @@ typedef struct opendal_capability {
   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 +1414,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/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..8d3708e47 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 }
 
 // Helper functions for capability creation (C++ compatible)
 inline opendal_required_capability make_capability_read_write() {
@@ -209,6 +212,20 @@ 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;
+    cap.presign_delete = true;
+    return cap;
+}
+
 // Function declarations
 opendal_test_config* opendal_test_config_new();
 void opendal_test_config_free(opendal_test_config* config);
@@ -228,4 +245,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..17509f00a
--- /dev/null
+++ b/bindings/c/tests/test_suites_presign.cpp
@@ -0,0 +1,678 @@
+/*
+ * 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 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_headers_has(const opendal_http_header_pair* headers, 
uintptr_t headers_len, const char* key)
+{
+    if (headers == NULL || key == NULL) {
+        return 0;
+    }
+
+    for (uintptr_t i = 0; i < headers_len; i++) {
+        const char* header_key = headers[i].key;
+        if (presign_str_ieq(header_key, key)) {
+            return 1;
+        }
+    }
+
+    return 0;
+}
+
+static int presign_build_header_list(const opendal_http_header_pair* headers,
+    uintptr_t headers_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);
+        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, 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, &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, &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 (!presign_headers_has(headers, headers_len, "content-length")) {
+        char length_header[64];
+        int written = snprintf(length_header, sizeof(length_header), 
"Content-Length: %zu", content_len);
+        OPENDAL_ASSERT(written > 0 && (size_t)written < sizeof(length_header),
+            "Formatting Content-Length header should succeed");
+        struct curl_slist* new_chunk = curl_slist_append(chunk, length_header);
+        OPENDAL_ASSERT_NOT_NULL(new_chunk, "Appending Content-Length header 
should succeed");
+        chunk = new_chunk;
+    }
+
+    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_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, &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, &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


Reply via email to