This is an automated email from the ASF dual-hosted git repository.
xuanwo 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 bf15cecd5e feat(bindings/go): Add full native support from C to Go.
(#4886)
bf15cecd5e is described below
commit bf15cecd5e3be6ecaa7056b5594589c9f4d85673
Author: Hanchin Hsieh <[email protected]>
AuthorDate: Sat Jul 13 13:33:13 2024 +0800
feat(bindings/go): Add full native support from C to Go. (#4886)
Signed-off-by: Hanchin Hsieh <[email protected]>
---
.github/workflows/ci_bindings_go.yml | 29 +-
bindings/go/DEPENDENCIES.md | 2 +-
bindings/go/README.md | 131 +++++--
bindings/go/copy_test.go | 158 ++++++++
.../go/{build_dynamic.go => create_dir_test.go} | 44 ++-
bindings/go/delete.go | 70 ++++
bindings/go/delete_test.go | 78 ++++
bindings/go/error.go | 116 ++++++
bindings/go/ffi.go | 136 +++++++
bindings/go/go.mod | 16 +-
bindings/go/go.sum | 17 +-
bindings/go/list_test.go | 254 ++++++++++++
bindings/go/lister.go | 424 +++++++++++++++++++++
bindings/go/metadata.go | 179 +++++++++
bindings/go/opendal.go | 234 +++++++++---
bindings/go/opendal_test.go | 223 +++++++++--
bindings/go/operator.go | 298 +++++++++++++++
bindings/go/operator_info.go | 381 ++++++++++++++++++
bindings/go/read_test.go | 98 +++++
bindings/go/reader.go | 299 +++++++++++++++
bindings/go/rename_test.go | 156 ++++++++
bindings/go/stat.go | 157 ++++++++
bindings/go/stat_test.go | 143 +++++++
bindings/go/types.go | 286 ++++++++++++++
bindings/go/write.go | 152 ++++++++
bindings/go/write_test.go | 101 +++++
26 files changed, 4020 insertions(+), 162 deletions(-)
diff --git a/.github/workflows/ci_bindings_go.yml
b/.github/workflows/ci_bindings_go.yml
index 5ec7c3cc43..edb8539218 100644
--- a/.github/workflows/ci_bindings_go.yml
+++ b/.github/workflows/ci_bindings_go.yml
@@ -46,32 +46,9 @@ jobs:
steps:
- uses: actions/checkout@v4
- uses: actions/setup-go@v5
- with:
- go-version: '1.20'
-
- - name: Setup Rust toolchain
- uses: ./.github/actions/setup
-
- - name: Build c binding
- working-directory: bindings/c
- run: make build
-
- - name: Check diff
- run: git diff --exit-code
-
- - name: Generate pkg-config file
- run: |
- echo "libdir=$(pwd)/bindings/c/target/debug/" >> opendal_c.pc
- echo "includedir=$(pwd)/bindings/c/include/" >> opendal_c.pc
- echo "Name: opendal_c" >> opendal_c.pc
- echo "Description: opendal c binding" >> opendal_c.pc
- echo "Version: 0.0.1" >> opendal_c.pc
- echo "Libs: -L\${libdir} -lopendal_c" >> opendal_c.pc
- echo "Cflags: -I\${includedir}" >> opendal_c.pc
-
- echo "PKG_CONFIG_PATH=$(pwd)" >> $GITHUB_ENV
- echo "LD_LIBRARY_PATH=$(pwd)/bindings/c/target/debug" >> $GITHUB_ENV
- name: Run tests
+ env:
+ OPENDAL_TEST: "memory"
working-directory: bindings/go
- run: go test -tags dynamic .
+ run: CGO_ENABLE=0 go test -v -run TestBehavior
diff --git a/bindings/go/DEPENDENCIES.md b/bindings/go/DEPENDENCIES.md
index 46c744cc28..9e3222eeb3 100644
--- a/bindings/go/DEPENDENCIES.md
+++ b/bindings/go/DEPENDENCIES.md
@@ -1,4 +1,4 @@
# Dependencies
OpenDAL Go Binding is based on the C Binding.
-There are no extra runtime dependencies except those conveyed from C Binding.
+Installation of libffi is required.
diff --git a/bindings/go/README.md b/bindings/go/README.md
index a51567ea5d..12871e8aac 100644
--- a/bindings/go/README.md
+++ b/bindings/go/README.md
@@ -2,60 +2,123 @@

-opendal-go requires opendal-c to be installed.
+opendal-go is a **Native** support Go binding without CGO enabled and is built
on top of opendal-c.
-```shell
-cd bindings/c
-make build
+```bash
+go get github.com/apache/opendal/bindings/go@latest
```
-You will find `libopendal_c.so` under `{root}/target`.
+opendal-go requires **libffi** to be installed.
-Then, we need to add a `opendal_c.pc` files
+## Basic Usage
-```pc
-libdir=/path/to/opendal/target/debug/
-includedir=/path/to/opendal/bindings/c/include/
+```go
+package main
-Name: opendal_c
-Description: opendal c binding
-Version:
+import (
+ "fmt"
+ "os"
-Libs: -L${libdir} -lopendal_c
-Cflags: -I${includedir}
-```
+ "github.com/yuchanns/opendal-go-services/memory"
+ "github.com/apache/opendal/bindings/go"
+)
-And set the `PKG_CONFIG_PATH` environment variable to the directory where
`opendal_c.pc` is located.
+func main() {
+ // Initialize a new in-memory operator
+ op, err := opendal.NewOperator(memory.Scheme, opendal.OperatorOptions{})
+ if err != nil {
+ panic(err)
+ }
+ defer op.Close()
-```shell
-export PKG_CONFIG_PATH=/dir/of/opendal_c.pc
-```
+ // Write data to a file named "test"
+ err = op.Write("test", []byte("Hello opendal go binding!"))
+ if err != nil {
+ panic(err)
+ }
-Then, we can build the go binding.
+ // Read data from the file "test"
+ data, err := op.Read("test")
+ if err != nil {
+ panic(err)
+ }
+ fmt.Printf("Read content: %s\n", data)
-```shell
-cd bindings/go
-go build -tags dynamic .
-```
+ // List all entries under the root directory "/"
+ lister, err := op.List("/")
+ if err != nil {
+ panic(err)
+ }
+ defer lister.Close()
-To running the go binding tests, we need to tell the linker where to find the
`libopendal_c.so` file.
+ // Iterate through all entries
+ for lister.Next() {
+ entry := lister.Entry()
-```shell
-expose LD_LIBRARY_PATH=/path/to/opendal/bindings/c/target/debug/
-```
+ // Get entry name (not used in this example)
+ _ = entry.Name()
+
+ // Get metadata for the current entry
+ meta, _ := op.Stat(entry.Path())
+
+ // Print file size
+ fmt.Printf("Size: %d bytes\n", meta.ContentLength())
-Then, we can run the tests.
+ // Print last modified time
+ fmt.Printf("Last modified: %s\n", meta.LastModified())
-```shell
-go test -tags dynamic .
+ // Check if the entry is a directory or a file
+ fmt.Printf("Is directory: %v, Is file: %v\n", meta.IsDir(),
meta.IsFile())
+ }
+
+ // Check for any errors that occurred during iteration
+ if err := lister.Error(); err != nil {
+ panic(err)
+ }
+
+ // Copy a file
+ op.Copy("test", "test_copy")
+
+ // Rename a file
+ op.Rename("test", "test_rename")
+
+ // Delete a file
+ op.Delete("test_rename")
+}
```
-For benchmark
+## Run Tests
-```shell
-go test -bench=. -tags dynamic .
+```bash
+# Run all tests
+CGO_ENABLE=0 go test -v -run TestBehavior
+# Run specific test
+CGO_ENABLE=0 go test -v -run TestBehavior/Write
+# Run synchronously
+CGO_ENABLE=0 GOMAXPROCS=1 go test -v -run TestBehavior
```
+## Capabilities
+
+- [x] OperatorInfo
+- [x] Stat
+ - [x] Metadata
+- [x] IsExist
+- [x] Read
+ - [x] Read
+ - [x] Reader -- implement as `io.ReadCloser`
+- [ ] Write
+ - [x] Write
+ - [ ] Writer -- Need support from the C binding
+- [x] Delete
+- [x] CreateDir
+- [ ] Lister
+ - [x] Entry
+ - [ ] Metadata -- Need support from the C binding
+- [x] Copy
+- [x] Rename
+
+
## License and Trademarks
Licensed under the Apache License, Version 2.0:
http://www.apache.org/licenses/LICENSE-2.0
diff --git a/bindings/go/copy_test.go b/bindings/go/copy_test.go
new file mode 100644
index 0000000000..1e9f33fa15
--- /dev/null
+++ b/bindings/go/copy_test.go
@@ -0,0 +1,158 @@
+/*
+ * 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 (
+ "fmt"
+
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsCopy(cap *opendal.Capability) []behaviorTest {
+ if !cap.Read() || !cap.Write() || !cap.Copy() {
+ return nil
+ }
+ return []behaviorTest{
+ testCopyFileWithASCIIName,
+ testCopyFileWithNonASCIIName,
+ testCopyNonExistingSource,
+ testCopySourceDir,
+ testCopyTargetDir,
+ testCopySelf,
+ testCopyNested,
+ testCopyOverwrite,
+ }
+}
+
+func testCopyFileWithASCIIName(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent))
+
+ targetPath := fixture.NewFilePath()
+
+ assert.Nil(op.Copy(sourcePath, targetPath))
+
+ targetContent, err := op.Read(targetPath)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(sourceContent, targetContent)
+}
+
+func testCopyFileWithNonASCIIName(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFileWithPath("🐂🍺中文.docx")
+ targetPath := fixture.PushPath("😈🐅Français.docx")
+
+ assert.Nil(op.Write(sourcePath, sourceContent))
+ assert.Nil(op.Copy(sourcePath, targetPath))
+
+ targetContent, err := op.Read(targetPath)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(sourceContent, targetContent)
+}
+
+func testCopyNonExistingSource(assert *require.Assertions, op
*opendal.Operator, _ *fixture) {
+ sourcePath := uuid.NewString()
+ targetPath := uuid.NewString()
+
+ err := op.Copy(sourcePath, targetPath)
+ assert.NotNil(err, "copy must fail")
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+}
+
+func testCopySourceDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ sourcePath := fixture.NewDirPath()
+ targetPath := uuid.NewString()
+
+ assert.Nil(op.CreateDir(sourcePath))
+
+ err := op.Copy(sourcePath, targetPath)
+ assert.NotNil(err, "copy must fail")
+ assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
+}
+
+func testCopyTargetDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent))
+
+ targetPath := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(targetPath))
+
+ err := op.Copy(sourcePath, targetPath)
+ assert.NotNil(err, "copy must fail")
+ assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
+}
+
+func testCopySelf(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent))
+
+ err := op.Copy(sourcePath, sourcePath)
+ assert.NotNil(err, "copy must fail")
+ assert.Equal(opendal.CodeIsSameFile, assertErrorCode(err))
+}
+
+func testCopyNested(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent))
+
+ targetPath := fixture.PushPath(fmt.Sprintf(
+ "%s/%s/%s",
+ uuid.NewString(),
+ uuid.NewString(),
+ uuid.NewString(),
+ ))
+
+ assert.Nil(op.Copy(sourcePath, targetPath))
+
+ targetContent, err := op.Read(targetPath)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(sourceContent, targetContent)
+}
+
+func testCopyOverwrite(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent))
+
+ targetPath, targetContent, _ := fixture.NewFile()
+ assert.NotEqual(sourceContent, targetContent)
+
+ assert.Nil(op.Write(targetPath, targetContent))
+
+ assert.Nil(op.Copy(sourcePath, targetPath))
+
+ targetContent, err := op.Read(targetPath)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(sourceContent, targetContent)
+}
diff --git a/bindings/go/build_dynamic.go b/bindings/go/create_dir_test.go
similarity index 50%
rename from bindings/go/build_dynamic.go
rename to bindings/go/create_dir_test.go
index feb13dded3..8b0937b612 100644
--- a/bindings/go/build_dynamic.go
+++ b/bindings/go/create_dir_test.go
@@ -1,6 +1,3 @@
-//go:build dynamic
-// +build dynamic
-
/*
* Licensed to the Apache Software Foundation (ASF) under one
* or more contributor license agreements. See the NOTICE file
@@ -20,11 +17,40 @@
* under the License.
*/
-package opendal
+package opendal_test
-/*
-#cgo pkg-config: opendal_c
-*/
-import "C"
+import (
+ "github.com/apache/opendal/bindings/go"
+ "github.com/stretchr/testify/require"
+)
+
+func testsCreateDir(cap *opendal.Capability) []behaviorTest {
+ if !cap.CreateDir() || !cap.Stat() {
+ return nil
+ }
+ return []behaviorTest{
+ testCreateDir,
+ testCreateDirExisting,
+ }
+}
+
+func testCreateDir(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(path))
+
+ meta, err := op.Stat(path)
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+}
+
+func testCreateDirExisting(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(path))
+ assert.Nil(op.CreateDir(path))
-const LibopendalLinkInfo = "dynamically linked to libopendal_c"
+ meta, err := op.Stat(path)
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+}
diff --git a/bindings/go/delete.go b/bindings/go/delete.go
new file mode 100644
index 0000000000..f92df487ad
--- /dev/null
+++ b/bindings/go/delete.go
@@ -0,0 +1,70 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Delete removes the file or directory at the specified path.
+//
+// # Parameters
+//
+// - path: The path of the file or directory to delete.
+//
+// # Returns
+//
+// - error: An error if the deletion fails, or nil if successful.
+//
+// # Note
+//
+// Use with caution as this operation is irreversible.
+func (op *Operator) Delete(path string) error {
+ delete := getFFI[operatorDelete](op.ctx, symOperatorDelete)
+ return delete(op.inner, path)
+}
+
+type operatorDelete func(op *opendalOperator, path string) error
+
+const symOperatorDelete = "opendal_operator_delete"
+
+var withOperatorDelete = withFFI(ffiOpts{
+ sym: symOperatorDelete,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorDelete {
+ return func(op *opendalOperator, path string) error {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ var e *opendalError
+ ffiCall(
+ unsafe.Pointer(&e),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ return parseError(ctx, e)
+ }
+})
diff --git a/bindings/go/delete_test.go b/bindings/go/delete_test.go
new file mode 100644
index 0000000000..8f38ca73d0
--- /dev/null
+++ b/bindings/go/delete_test.go
@@ -0,0 +1,78 @@
+/*
+ * 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 (
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsDelete(cap *opendal.Capability) []behaviorTest {
+ if !cap.Stat() || !cap.Delete() || !cap.Write() {
+ return nil
+ }
+ tests := []behaviorTest{
+ testDeleteFile,
+ testDeleteEmptyDir,
+ testDeleteWithSpecialChars,
+ testDeleteNotExisting,
+ }
+ return tests
+}
+
+func testDeleteFile(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ assert.Nil(op.Delete(path))
+
+ assert.False(op.IsExist(path))
+}
+
+func testDeleteEmptyDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ path := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(path), "create must succeed")
+
+ assert.Nil(op.Delete(path))
+}
+
+func testDeleteWithSpecialChars(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ path := uuid.NewString() + " !@#$%^&()_+-=;',.txt"
+ path, content, _ := fixture.NewFileWithPath(path)
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ assert.Nil(op.Delete(path))
+
+ assert.False(op.IsExist(path))
+}
+
+func testDeleteNotExisting(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path := uuid.NewString()
+
+ assert.Nil(op.Delete(path))
+}
diff --git a/bindings/go/error.go b/bindings/go/error.go
new file mode 100644
index 0000000000..33ed9b8c28
--- /dev/null
+++ b/bindings/go/error.go
@@ -0,0 +1,116 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+)
+
+// ErrorCode is all kinds of ErrorCode of opendal
+type ErrorCode int32
+
+const (
+ // OpenDAL don't know what happened here, and no actions other than just
+ // returning it back. For example, s3 returns an internal service error.
+ CodeUnexpected ErrorCode = iota
+ // Underlying service doesn't support this operation.
+ CodeUnsupported
+ // The config for backend is invalid.
+ CodeConfigInvalid
+ // The given path is not found.
+ CodeNotFound
+ // The given path doesn't have enough permission for this operation
+ CodePermissioDenied
+ // The given path is a directory.
+ CodeIsADirectory
+ // The given path is not a directory.
+ CodeNotADirectory
+ // The given path already exists thus we failed to the specified
operation on it.
+ CodeAlreadyExists
+ // Requests that sent to this path is over the limit, please slow down.
+ CodeRateLimited
+ // The given file paths are same.
+ CodeIsSameFile
+ // The condition of this operation is not match.
+ //
+ // The `condition` itself is context based.
+ //
+ // For example, in S3, the `condition` can be:
+ // 1. writing a file with If-Match header but the file's ETag is not
match (will get a 412 Precondition Failed).
+ // 2. reading a file with If-None-Match header but the file's ETag is
match (will get a 304 Not Modified).
+ //
+ // As OpenDAL cannot handle the `condition not match` error, it will
always return this error to users.
+ // So users could to handle this error by themselves.
+ CodeConditionNotMatch
+ // The range of the content is not satisfied.
+ //
+ // OpenDAL returns this error to indicate that the range of the read
request is not satisfied.
+ CodeRangeNotSatisfied
+)
+
+func parseError(ctx context.Context, err *opendalError) error {
+ if err == nil {
+ return nil
+ }
+ free := getFFI[errorFree](ctx, symErrorFree)
+ defer free(err)
+ return &Error{
+ code: ErrorCode(err.code),
+ message: string(parseBytes(&err.message)),
+ }
+}
+
+type Error struct {
+ code ErrorCode
+ message string
+}
+
+func (e *Error) Error() string {
+ return fmt.Sprintf("%d %s", e.code, e.message)
+}
+
+func (e *Error) Code() ErrorCode {
+ return e.code
+}
+
+func (e *Error) Message() string {
+ return e.message
+}
+
+type errorFree func(e *opendalError)
+
+const symErrorFree = "opendal_error_free"
+
+var withErrorFree = withFFI(ffiOpts{
+ sym: symErrorFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) errorFree {
+ return func(e *opendalError) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&e),
+ )
+ }
+})
diff --git a/bindings/go/ffi.go b/bindings/go/ffi.go
new file mode 100644
index 0000000000..802ae48582
--- /dev/null
+++ b/bindings/go/ffi.go
@@ -0,0 +1,136 @@
+/*
+ * 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"
+ "errors"
+ "unsafe"
+
+ "github.com/ebitengine/purego"
+ "github.com/jupiterrider/ffi"
+)
+
+func contextWithFFIs(path string) (ctx context.Context, cancel
context.CancelFunc, err error) {
+ libopendal, err := purego.Dlopen(path,
purego.RTLD_LAZY|purego.RTLD_GLOBAL)
+ if err != nil {
+ return
+ }
+ ctx = context.Background()
+ for _, withFFI := range withFFIs {
+ ctx, err = withFFI(ctx, libopendal)
+ if err != nil {
+ return
+ }
+ }
+ cancel = func() {
+ purego.Dlclose(libopendal)
+ }
+ return
+}
+
+type contextWithFFI func(ctx context.Context, libopendal uintptr)
(context.Context, error)
+
+func getFFI[T any](ctx context.Context, key string) T {
+ return ctx.Value(key).(T)
+}
+
+type ffiOpts struct {
+ sym string
+ rType *ffi.Type
+ aTypes []*ffi.Type
+}
+
+func withFFI[T any](
+ opts ffiOpts,
+ withFunc func(
+ ctx context.Context,
+ ffiCall func(rValue unsafe.Pointer, aValues ...unsafe.Pointer),
+ ) T,
+) func(ctx context.Context, libopendal uintptr) (context.Context, error) {
+ return func(ctx context.Context, libopendal uintptr) (context.Context,
error) {
+ var cif ffi.Cif
+ if status := ffi.PrepCif(
+ &cif,
+ ffi.DefaultAbi,
+ uint32(len(opts.aTypes)),
+ opts.rType,
+ opts.aTypes...,
+ ); status != ffi.OK {
+ return nil, errors.New(status.String())
+ }
+ fn, err := purego.Dlsym(libopendal, opts.sym)
+ if err != nil {
+ return nil, err
+ }
+ return context.WithValue(ctx, opts.sym,
+ withFunc(ctx, func(rValue unsafe.Pointer, aValues
...unsafe.Pointer) {
+ ffi.Call(&cif, fn, rValue, aValues...)
+ }),
+ ), nil
+ }
+}
+
+var withFFIs = []contextWithFFI{
+ // free must be on top
+ withBytesFree,
+ withErrorFree,
+
+ withOperatorOptionsNew,
+ withOperatorOptionsSet,
+ withOperatorOptionsFree,
+
+ withOperatorNew,
+ withOperatorFree,
+
+ withOperatorInfoNew,
+ withOperatorInfoGetFullCapability,
+ withOperatorInfoGetNativeCapability,
+ withOperatorInfoGetScheme,
+ withOperatorInfoGetRoot,
+ withOperatorInfoGetName,
+ withOperatorInfoFree,
+
+ withOperatorCreateDir,
+ withOperatorRead,
+ withOperatorWrite,
+ withOperatorDelete,
+ withOperatorStat,
+ withOperatorIsExists,
+ withOperatorCopy,
+ withOperatorRename,
+
+ withMetaContentLength,
+ withMetaIsFile,
+ withMetaIsDir,
+ withMetaLastModified,
+ withMetaFree,
+
+ withOperatorList,
+ withListerNext,
+ withListerFree,
+ withEntryName,
+ withEntryPath,
+ withEntryFree,
+
+ withOperatorReader,
+ withReaderRead,
+ withReaderFree,
+}
diff --git a/bindings/go/go.mod b/bindings/go/go.mod
index 11bf83647b..21d89c79a1 100644
--- a/bindings/go/go.mod
+++ b/bindings/go/go.mod
@@ -15,14 +15,24 @@
// specific language governing permissions and limitations
// under the License.
-module opendal.apache.org/go
+module github.com/apache/opendal/bindings/go
-go 1.20
+go 1.22.4
-require github.com/stretchr/testify v1.8.4
+toolchain go1.22.5
+
+require (
+ github.com/ebitengine/purego v0.7.1
+ github.com/google/uuid v1.6.0
+ github.com/jupiterrider/ffi v0.1.0-beta.9
+ github.com/stretchr/testify v1.9.0
+ github.com/yuchanns/opendal-go-services v0.0.1
+ golang.org/x/sys v0.22.0
+)
require (
github.com/davecgh/go-spew v1.1.1 // indirect
+ github.com/klauspost/compress v1.17.9 // indirect
github.com/pmezard/go-difflib v1.0.0 // indirect
gopkg.in/yaml.v3 v3.0.1 // indirect
)
diff --git a/bindings/go/go.sum b/bindings/go/go.sum
index 8cf66553bc..729e933025 100644
--- a/bindings/go/go.sum
+++ b/bindings/go/go.sum
@@ -1,9 +1,22 @@
github.com/davecgh/go-spew v1.1.1
h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c=
github.com/davecgh/go-spew v1.1.1/go.mod
h1:J7Y8YcW2NihsgmVo/mv3lAwl/skON4iLHjSsI+c5H38=
+github.com/ebitengine/purego v0.7.1
h1:6/55d26lG3o9VCZX8lping+bZcmShseiqlh2bnUDiPA=
+github.com/ebitengine/purego v0.7.1/go.mod
h1:ah1In8AOtksoNK6yk5z1HTJeUkC1Ez4Wk2idgGslMwQ=
+github.com/google/uuid v1.6.0 h1:NIvaJDMOsjHA8n1jAhLSgzrAzy1Hgr+hNrb57e+94F0=
+github.com/google/uuid v1.6.0/go.mod
h1:TIyPZe4MgqvfeYDBFedMoGGpEw/LqOeaOT+nhxU+yHo=
+github.com/jupiterrider/ffi v0.1.0-beta.9
h1:HCeAPTsTFgwvcfavyJwy1L2ANz0c85W+ZE7LfzjZi3A=
+github.com/jupiterrider/ffi v0.1.0-beta.9/go.mod
h1:sOp6VJGFaYyr4APi8gwy6g20QNHv5F8Iq1CVbtC900s=
+github.com/klauspost/compress v1.17.9
h1:6KIumPrER1LHsvBVuDa0r5xaG0Es51mhhB9BQB2qeMA=
+github.com/klauspost/compress v1.17.9/go.mod
h1:Di0epgTjJY877eYKx5yC51cX2A2Vl2ibi7bDH9ttBbw=
github.com/pmezard/go-difflib v1.0.0
h1:4DBwDE0NGyQoBHbLQYPwSUPoCMWR5BEzIk/f1lZbAQM=
github.com/pmezard/go-difflib v1.0.0/go.mod
h1:iKH77koFhYxTK1pcRnkKkqfTogsbg7gZNVY4sRDYZ/4=
-github.com/stretchr/testify v1.8.4
h1:CcVxjf3Q8PM0mHUKJCdn+eZZtm5yQwehR5yeSVQQcUk=
-github.com/stretchr/testify v1.8.4/go.mod
h1:sz/lmYIOXD/1dqDmKjjqLyZ2RngseejIcXlSw2iwfAo=
+github.com/stretchr/testify v1.9.0
h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg=
+github.com/stretchr/testify v1.9.0/go.mod
h1:r2ic/lqez/lEtzL7wO/rwa5dbSLXVDPFyf8C91i36aY=
+github.com/yuchanns/opendal-go-services v0.0.1
h1:qeKv0mOhypQNm97g+u94DnijJK5bdEAp5pdjBGf8N7w=
+github.com/yuchanns/opendal-go-services v0.0.1/go.mod
h1:tw8QXHu3hzsLpiUCQ+pOIhGw7FJFumnh++rKF/BK96I=
+golang.org/x/sys v0.22.0 h1:RI27ohtqKCnwULzJLqkv897zojh5/DwS/ENaMzUOaWI=
+golang.org/x/sys v0.22.0/go.mod h1:/VUhepiaJMQUp4+oa/7Zr1D23ma6VTLIYjOOTFZPUcA=
+gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405
h1:yhCVgyC4o1eVCa2tZl7eS0r+SDo693bJlVdllGtEeKM=
gopkg.in/check.v1 v0.0.0-20161208181325-20d25e280405/go.mod
h1:Co6ibVJAznAaIkqp8huTwlJQCZ016jof/cbN4VW5Yz0=
gopkg.in/yaml.v3 v3.0.1 h1:fxVm/GzAzEWqLHuvctI91KS9hhNmmWOoWu0XTYJS7CA=
gopkg.in/yaml.v3 v3.0.1/go.mod h1:K4uyk7z7BCEPqu6E+C64Yfv1cQ7kz7rIZviUmN+EgEM=
diff --git a/bindings/go/list_test.go b/bindings/go/list_test.go
new file mode 100644
index 0000000000..e31f8b212e
--- /dev/null
+++ b/bindings/go/list_test.go
@@ -0,0 +1,254 @@
+/*
+ * 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 (
+ "fmt"
+ "slices"
+ "strings"
+
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsList(cap *opendal.Capability) []behaviorTest {
+ if !cap.Read() || !cap.Write() || !cap.List() {
+ return nil
+ }
+ return []behaviorTest{
+ testListCheck,
+ testListDir,
+ testListPrefix,
+ testListRichDir,
+ testListEmptyDir,
+ testListNonExistDir,
+ testListSubDir,
+ testListNestedDir,
+ testListDirWithFilePath,
+ }
+}
+
+func testListCheck(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ assert.Nil(op.Check(), "operator check must succeed")
+}
+
+func testListDir(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ parent := fixture.NewDirPath()
+ path, content, size := fixture.NewFileWithPath(fmt.Sprintf("%s%s",
parent, uuid.NewString()))
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ obs, err := op.List(parent)
+ assert.Nil(err)
+ defer obs.Close()
+
+ var found bool
+ for obs.Next() {
+ entry := obs.Entry()
+
+ if entry.Path() != path {
+ continue
+ }
+
+ meta, err := op.Stat(entry.Path())
+ assert.Nil(err)
+ assert.True(meta.IsFile())
+ assert.Equal(uint64(size), meta.ContentLength())
+ found = true
+ break
+ }
+ assert.Nil(obs.Error())
+ assert.True(found, "file must be found in list")
+}
+
+func testListPrefix(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ obs, err := op.List(path[:len(path)-1])
+ assert.Nil(err)
+ defer obs.Close()
+ assert.True(obs.Next())
+ assert.Nil(obs.Error())
+
+ entry := obs.Entry()
+ assert.Equal(path, entry.Path())
+}
+
+func testListRichDir(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ parent := fixture.NewDirPath()
+ assert.Nil(op.CreateDir(parent))
+
+ var expected []string
+ for range 10 {
+ path, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s",
parent, uuid.NewString()))
+ expected = append(expected, path)
+ assert.Nil(op.Write(path, content))
+ }
+
+ obs, err := op.List(parent)
+ assert.Nil(err)
+ defer obs.Close()
+ var actual []string
+ for obs.Next() {
+ entry := obs.Entry()
+ actual = append(actual, entry.Path())
+ }
+ assert.Nil(obs.Error())
+
+ slices.Sort(expected)
+ slices.Sort(actual)
+
+ assert.Equal(expected, actual)
+}
+
+func testListEmptyDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ dir := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(dir), "create must succeed")
+
+ obs, err := op.List(dir)
+ assert.Nil(err)
+ defer obs.Close()
+ var paths []string
+ for obs.Next() {
+ entry := obs.Entry()
+ paths = append(paths, entry.Path())
+ }
+ assert.Nil(obs.Error())
+ assert.Equal(0, len(paths), "dir should only return empty")
+
+ obs, err = op.List(strings.TrimSuffix(dir, "/"))
+ assert.Nil(err)
+ defer obs.Close()
+ for obs.Next() {
+ entry := obs.Entry()
+ path := entry.Path()
+ paths = append(paths, path)
+ meta, err := op.Stat(path)
+ assert.Nil(err, "given dir should exist")
+ assert.True(meta.IsDir(), "given dir must be dir, but found:
%v", path)
+ }
+ assert.Nil(obs.Error())
+ assert.Equal(1, len(paths), "only return the dir itself, but found:
%v", paths)
+}
+
+func testListNonExistDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ dir := fixture.NewDirPath()
+
+ obs, err := op.List(dir)
+ assert.Nil(err)
+ defer obs.Close()
+ assert.False(obs.Next(), "dir should only return empty")
+}
+
+func testListSubDir(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(path), "create must succeed")
+
+ obs, err := op.List("/")
+ assert.Nil(err)
+ defer obs.Close()
+
+ var found bool
+ for obs.Next() {
+ entry := obs.Entry()
+ if path != entry.Path() {
+ continue
+ }
+ found = true
+ break
+ }
+ assert.Nil(obs.Error())
+ assert.True(found, "dir should be found in list")
+}
+
+func testListNestedDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ parent := fixture.NewDirPath()
+ dir := fixture.PushPath(fmt.Sprintf("%s%s/", parent, uuid.NewString()))
+
+ filePath := fixture.PushPath(fmt.Sprintf("%s%s", dir, uuid.NewString()))
+ dirPath := fixture.PushPath(fmt.Sprintf("%s%s/", dir, uuid.NewString()))
+
+ assert.Nil(op.CreateDir(dir), "create must succeed")
+ assert.Nil(op.Write(filePath, []byte("test_list_nested_dir")), "write
must succeed")
+ assert.Nil(op.CreateDir(dirPath), "create must succeed")
+
+ obs, err := op.List(parent)
+ assert.Nil(err)
+ defer obs.Close()
+ var paths []string
+ for obs.Next() {
+ entry := obs.Entry()
+ paths = append(paths, entry.Path())
+ assert.Equal(dir, entry.Path())
+ }
+ assert.Nil(obs.Error())
+ assert.Equal(1, len(paths), "parent should only got 1 entry")
+
+ obs, err = op.List(dir)
+ assert.Nil(err)
+ defer obs.Close()
+ paths = nil
+ var foundFile bool
+ var foundDir bool
+ for obs.Next() {
+ entry := obs.Entry()
+ paths = append(paths, entry.Path())
+ if entry.Path() == filePath {
+ foundFile = true
+ } else if entry.Path() == dirPath {
+ foundDir = true
+ }
+ }
+ assert.Nil(obs.Error())
+ assert.Equal(2, len(paths), "parent should only got 2 entries")
+
+ assert.True(foundFile, "file should be found in list")
+ meta, err := op.Stat(filePath)
+ assert.Nil(err)
+ assert.True(meta.IsFile())
+ assert.Equal(uint64(20), meta.ContentLength())
+
+ assert.True(foundDir, "dir should be found in list")
+ meta, err = op.Stat(dirPath)
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+}
+
+func testListDirWithFilePath(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ parent := fixture.NewDirPath()
+ path, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent,
uuid.NewString()))
+
+ assert.Nil(op.Write(path, content))
+
+ obs, err := op.List(strings.TrimSuffix(parent, "/"))
+ assert.Nil(err)
+ defer obs.Close()
+
+ for obs.Next() {
+ entry := obs.Entry()
+ assert.Equal(parent, entry.Path())
+ }
+ assert.Nil(obs.Error())
+}
diff --git a/bindings/go/lister.go b/bindings/go/lister.go
new file mode 100644
index 0000000000..bbd58f1f11
--- /dev/null
+++ b/bindings/go/lister.go
@@ -0,0 +1,424 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Check verifies if the operator is functioning correctly.
+//
+// This function performs a health check on the operator by sending a `list`
request
+// to the root path. It returns any errors encountered during this process.
+//
+// # Returns
+//
+// - error: An error if the check fails, or nil if the operator is working
correctly.
+//
+// # Details
+//
+// The check is performed by attempting to list the contents of the root
directory.
+// This operation tests the basic functionality of the operator, including
+// connectivity and permissions.
+//
+// # Example
+//
+// func exampleCheck(op *opendal.Operator) {
+// err = op.Check()
+// if err != nil {
+// log.Printf("Operator check failed: %v", err)
+// } else {
+// log.Println("Operator is functioning correctly")
+// }
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Check() (err error) {
+ ds, err := op.List("/")
+ if err != nil {
+ return
+ }
+ defer ds.Close()
+ ds.Next()
+ err = ds.Error()
+ if err, ok := err.(*Error); ok && err.Code() == CodeNotFound {
+ return nil
+ }
+ return
+}
+
+// List returns a Lister to iterate over entries that start with the given
path in the parent directory.
+//
+// This function creates a new Lister to enumerate entries in the specified
path.
+//
+// # Parameters
+//
+// - path: The starting path for listing entries.
+//
+// # Returns
+//
+// - *Lister: A new Lister instance for iterating over entries.
+// - error: An error if the listing operation fails, or nil if successful.
+//
+// # Notes
+//
+// 1. List is a wrapper around the C-binding function
`opendal_operator_list`. Recursive listing is not currently supported.
+// 2. Returned entries do not include metadata information. Use op.Stat to
fetch metadata for individual entries.
+//
+// # Example
+//
+// func exampleList(op *opendal.Operator) {
+// lister, err := op.List("test")
+// if err != nil {
+// log.Fatal(err)
+// }
+//
+// for lister.Next() {
+// entry := lister.Entry()
+//
+// meta, err := op.Stat(entry.Path())
+// if err != nil {
+// log.Printf("Error fetching metadata for %s:
%v", entry.Path(), err)
+// continue
+// }
+//
+// fmt.Printf("Name: %s\n", entry.Name())
+// fmt.Printf("Length: %d\n", meta.ContentLength())
+// fmt.Printf("Last Modified: %s\n", meta.LastModified())
+// fmt.Printf("Is Directory: %v, Is File: %v\n",
meta.IsDir(), meta.IsFile())
+// fmt.Println("---")
+// }
+// if err := lister.Err(); err != nil {
+// log.Printf("Error during listing: %v", err)
+// }
+// }
+//
+// Note: Always check lister.Err() after the loop to catch any errors that
+// occurred during iteration.
+func (op *Operator) List(path string) (*Lister, error) {
+ list := getFFI[operatorList](op.ctx, symOperatorList)
+ inner, err := list(op.inner, path)
+ if err != nil {
+ return nil, err
+ }
+ lister := &Lister{
+ inner: inner,
+ ctx: op.ctx,
+ }
+ return lister, nil
+}
+
+// Lister provides an mechanism for listing entries at a specified path.
+//
+// Lister is a wrapper around the C-binding function `opendal_operator_list`.
It allows
+// for efficient iteration over entries in a storage system.
+//
+// # Limitations
+//
+// - The current implementation does not support the `list_with`
functionality.
+//
+// # Usage
+//
+// Lister should be used in conjunction with its Next() and Entry() methods to
+// iterate through entries. The iteration ends when there are no more entries
+// or when an error occurs.
+//
+// # Behavior
+//
+// - Next() returns false when there are no more entries or if an error has
occurred.
+// - Entry() returns nil if there are no more entries or if an error has
been encountered.
+//
+// # Example
+//
+// lister, err := op.List("path/to/list")
+// if err != nil {
+// log.Fatal(err)
+// }
+//
+// for lister.Next() {
+// entry := lister.Entry()
+// // Process the entry
+// fmt.Println(entry.Name())
+// }
+type Lister struct {
+ inner *opendalLister
+ ctx context.Context
+ entry *Entry
+ err error
+}
+
+// This method implements the io.Closer interface. It should be called when
+// the Lister is no longer needed to ensure proper resource cleanup.
+func (l *Lister) Close() error {
+ free := getFFI[listerFree](l.ctx, symListerFree)
+ free(l.inner)
+
+ return nil
+}
+
+func (l *Lister) Error() error {
+ return l.err
+}
+
+// Next advances the Lister to the next entry in the list.
+//
+// This method must be called before accessing the current entry. It prepares
+// the next entry for reading and indicates whether there are more entries
+// to process.
+//
+// # Returns
+//
+// - bool: true if there is another entry to process, false if the end of
the list
+// has been reached or an error occurred.
+//
+// # Usage
+//
+// Next should be used in a loop condition to iterate through all entries:
+//
+// for lister.Next() {
+// entry := lister.Entry()
+// // Process the entry
+// }
+//
+// # Error Handling
+//
+// If an error occurs during iteration, Next will return false. The error
+// can then be retrieved by calling the Err method on the Lister.
+//
+// # Example
+//
+// lister, err := op.List("path/to/list")
+// if err != nil {
+// log.Fatal(err)
+// }
+//
+// for lister.Next() {
+// entry := lister.Entry()
+// fmt.Println(entry.Name())
+// }
+func (l *Lister) Next() bool {
+ next := getFFI[listerNext](l.ctx, symListerNext)
+ inner, err := next(l.inner)
+ if inner == nil || err != nil {
+ l.err = err
+ l.entry = nil
+ return false
+ }
+
+ entry := newEntry(l.ctx, inner)
+
+ l.entry = entry
+ return true
+}
+
+// Entry returns the current Entry in the list.
+// Returns nil if there are no more entries
+func (l *Lister) Entry() *Entry {
+ return l.entry
+}
+
+// Entry represents a path and its associated metadata as returned by Lister.
+//
+// An Entry provides basic information about a file or directory encountered
+// during a list operation. It contains the path of the item and minimal
metadata.
+//
+// # Limitations
+//
+// The Entry itself does not contain comprehensive metadata. For detailed
+// metadata information, use the op.Stat() method with the Entry's path.
+//
+// # Usage
+//
+// Entries are typically obtained through iteration of a Lister:
+//
+// for lister.Next() {
+// entry := lister.Entry()
+// // Process the entry
+// fmt.Println(entry.Name())
+// }
+//
+// # Fetching Detailed Metadata
+//
+// To obtain comprehensive metadata for an Entry, use op.Stat():
+//
+// meta, err := op.Stat(entry.Path())
+// if err != nil {
+// log.Printf("Error fetching metadata: %v", err)
+// return
+// }
+// fmt.Printf("Size: %d, Last Modified: %s\n", meta.ContentLength(),
meta.LastModified())
+//
+// # Methods
+//
+// Entry provides methods to access basic information:
+// - Path(): Returns the full path of the entry.
+// - Name(): Returns the name of the entry (last component of the path).
+type Entry struct {
+ name string
+ path string
+}
+
+func newEntry(ctx context.Context, inner *opendalEntry) *Entry {
+ name := getFFI[entryName](ctx, symEntryName)
+ path := getFFI[entryPath](ctx, symEntryPath)
+ free := getFFI[entryFree](ctx, symEntryFree)
+
+ defer free(inner)
+
+ return &Entry{
+ name: name(inner),
+ path: path(inner),
+ }
+}
+
+// Name returns the last component of the entry's path.
+func (e *Entry) Name() string {
+ return e.name
+}
+
+// Path returns the full path of the entry.
+func (e *Entry) Path() string {
+ return e.path
+}
+
+const symOperatorList = "opendal_operator_list"
+
+type operatorList func(op *opendalOperator, path string) (*opendalLister,
error)
+
+var withOperatorList = withFFI(ffiOpts{
+ sym: symOperatorList,
+ rType: &typeResultList,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorList {
+ return func(op *opendalOperator, path string) (*opendalLister, error) {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result opendalResultList
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ if result.err != nil {
+ return nil, parseError(ctx, result.err)
+ }
+ return result.lister, nil
+ }
+})
+
+const symListerFree = "opendal_lister_free"
+
+type listerFree func(l *opendalLister)
+
+var withListerFree = withFFI(ffiOpts{
+ sym: symListerFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) listerFree {
+ return func(l *opendalLister) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&l),
+ )
+ }
+})
+
+const symListerNext = "opendal_lister_next"
+
+type listerNext func(l *opendalLister) (*opendalEntry, error)
+
+var withListerNext = withFFI(ffiOpts{
+ sym: symListerNext,
+ rType: &typeResultListerNext,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) listerNext {
+ return func(l *opendalLister) (*opendalEntry, error) {
+ var result opendalResultListerNext
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&l),
+ )
+ if result.err != nil {
+ return nil, parseError(ctx, result.err)
+ }
+ return result.entry, nil
+ }
+})
+
+const symEntryFree = "opendal_entry_free"
+
+type entryFree func(e *opendalEntry)
+
+var withEntryFree = withFFI(ffiOpts{
+ sym: symEntryFree,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) entryFree {
+ return func(e *opendalEntry) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&e),
+ )
+ }
+})
+
+const symEntryName = "opendal_entry_name"
+
+type entryName func(e *opendalEntry) string
+
+var withEntryName = withFFI(ffiOpts{
+ sym: symEntryName,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) entryName {
+ return func(e *opendalEntry) string {
+ var bytePtr *byte
+ ffiCall(
+ unsafe.Pointer(&bytePtr),
+ unsafe.Pointer(&e),
+ )
+ return unix.BytePtrToString(bytePtr)
+ }
+})
+
+const symEntryPath = "opendal_entry_path"
+
+type entryPath func(e *opendalEntry) string
+
+var withEntryPath = withFFI(ffiOpts{
+ sym: symEntryPath,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) entryPath {
+ return func(e *opendalEntry) string {
+ var bytePtr *byte
+ ffiCall(
+ unsafe.Pointer(&bytePtr),
+ unsafe.Pointer(&e),
+ )
+ return unix.BytePtrToString(bytePtr)
+ }
+})
diff --git a/bindings/go/metadata.go b/bindings/go/metadata.go
new file mode 100644
index 0000000000..440653e65f
--- /dev/null
+++ b/bindings/go/metadata.go
@@ -0,0 +1,179 @@
+/*
+ * 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"
+ "time"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+)
+
+// Metadata represents essential information about a file or directory.
+//
+// This struct contains basic attributes commonly used in file systems
+// and object storage systems.
+type Metadata struct {
+ contentLength uint64
+ isFile bool
+ isDir bool
+ lastModified time.Time
+}
+
+func newMetadata(ctx context.Context, inner *opendalMetadata) *Metadata {
+ getLength := getFFI[metaContentLength](ctx, symMetadataContentLength)
+ isFile := getFFI[metaIsFile](ctx, symMetadataIsFile)
+ isDir := getFFI[metaIsDir](ctx, symMetadataIsDir)
+ getLastModified := getFFI[metaLastModified](ctx,
symMetadataLastModified)
+
+ var lastModified time.Time
+ ms := getLastModified(inner)
+ if ms != -1 {
+ lastModified = time.UnixMilli(ms)
+ }
+
+ free := getFFI[metaFree](ctx, symMetadataFree)
+ defer free(inner)
+
+ return &Metadata{
+ contentLength: getLength(inner),
+ isFile: isFile(inner),
+ isDir: isDir(inner),
+ lastModified: lastModified,
+ }
+}
+
+// ContentLength returns the size of the file in bytes.
+//
+// For directories, this value may not be meaningful and could be zero.
+func (m *Metadata) ContentLength() uint64 {
+ return m.contentLength
+}
+
+// IsFile returns true if the metadata represents a file, false otherwise.
+func (m *Metadata) IsFile() bool {
+ return m.isFile
+}
+
+// IsDir returns true if the metadata represents a directory, false otherwise.
+func (m *Metadata) IsDir() bool {
+ return m.isDir
+}
+
+// LastModified returns the time when the file or directory was last modified.
+//
+// The returned time is in UTC.
+func (m *Metadata) LastModified() time.Time {
+ return m.lastModified
+}
+
+type metaContentLength func(m *opendalMetadata) uint64
+
+const symMetadataContentLength = "opendal_metadata_content_length"
+
+var withMetaContentLength = withFFI(ffiOpts{
+ sym: symMetadataContentLength,
+ rType: &ffi.TypeUint64,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) metaContentLength {
+ return func(m *opendalMetadata) uint64 {
+ var length uint64
+ ffiCall(
+ unsafe.Pointer(&length),
+ unsafe.Pointer(&m),
+ )
+ return length
+ }
+})
+
+type metaIsFile func(m *opendalMetadata) bool
+
+const symMetadataIsFile = "opendal_metadata_is_file"
+
+var withMetaIsFile = withFFI(ffiOpts{
+ sym: symMetadataIsFile,
+ rType: &ffi.TypeUint8,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) metaIsFile {
+ return func(m *opendalMetadata) bool {
+ var result uint8
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&m),
+ )
+ return result == 1
+ }
+})
+
+type metaIsDir func(m *opendalMetadata) bool
+
+const symMetadataIsDir = "opendal_metadata_is_dir"
+
+var withMetaIsDir = withFFI(ffiOpts{
+ sym: symMetadataIsDir,
+ rType: &ffi.TypeUint8,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) metaIsDir {
+ return func(m *opendalMetadata) bool {
+ var result uint8
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&m),
+ )
+ return result == 1
+ }
+})
+
+type metaLastModified func(m *opendalMetadata) int64
+
+const symMetadataLastModified = "opendal_metadata_last_modified_ms"
+
+var withMetaLastModified = withFFI(ffiOpts{
+ sym: symMetadataLastModified,
+ rType: &ffi.TypeSint64,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) metaLastModified {
+ return func(m *opendalMetadata) int64 {
+ var result int64
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&m),
+ )
+ return result
+ }
+})
+
+type metaFree func(m *opendalMetadata)
+
+const symMetadataFree = "opendal_metadata_free"
+
+var withMetaFree = withFFI(ffiOpts{
+ sym: symMetadataFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) metaFree {
+ return func(m *opendalMetadata) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&m),
+ )
+ }
+})
diff --git a/bindings/go/opendal.go b/bindings/go/opendal.go
index 027d87ad24..9640e4ddcf 100644
--- a/bindings/go/opendal.go
+++ b/bindings/go/opendal.go
@@ -17,85 +17,197 @@
* under the License.
*/
+// Package opendal provides a Go binding for Apache OpenDAL (Open Data Access
Layer).
+//
+// OpenDAL is a data access layer that allows users to easily interact with
various
+// storage services using a unified API. This Go binding enables Go developers
to
+// leverage OpenDAL's capabilities without the need for CGO.
+//
+// Key features:
+// - Unified interface for multiple storage backends (e.g., S3, Azure Blob,
local filesystem)
+// - Native Go implementation using purego and libffi
+// - No CGO dependency, ensuring better portability and easier
cross-compilation
+// - Supports common operations like read, write, delete, list, and metadata
retrieval
+//
+// Basic usage:
+//
+// import (
+// "github.com/apache/opendal/bindings/go
+// "github.com/yuchanns/opendal-go-services/memory
+// )
+//
+// func main() {
+// op, err := opendal.NewOperator(memory.Scheme,
opendal.OperatorOptions{
+// "root": "/path/to/root",
+// })
+// if err != nil {
+// log.Fatal(err)
+// }
+// defer op.Close()
+//
+// // Perform operations using the operator
+// err = op.Write("example.txt", []byte("Hello, OpenDAL!"))
+// if err != nil {
+// log.Fatal(err)
+// }
+// }
+//
+// This package aims to provide a seamless experience for Go developers
working with
+// various storage systems, combining the flexibility of OpenDAL with the
performance
+// and simplicity of native Go code.
package opendal
-/*
-#include "opendal.h"
-*/
-import "C"
import (
- "errors"
- "fmt"
- "unsafe"
+ "context"
)
-var (
- errInvalidScheme = errors.New("invalid scheme")
- errValueEmpty = errors.New("value is empty")
-)
+// Scheme defines the interface for storage scheme implementations.
+//
+// A Scheme represents a specific storage backend (e.g., S3, filesystem,
memory)
+// and provides methods to identify and initialize the scheme.
+//
+// Implementations of this interface should be thread-safe, especially the
LoadOnce method.
+type Scheme interface {
+ // Name returns the unique identifier of the scheme.
+ Name() string
+
+ // Path returns the filesystem path where the scheme's shared library
(.so) is located.
+ Path() string
+
+ // LoadOnce initializes the scheme. It ensures that initialization
occurs only once,
+ // even if called multiple times. Subsequent calls after the first
should be no-ops.
+ //
+ // Returns an error if initialization fails.
+ LoadOnce() error
+}
-type Options map[string]string
+// OperatorOptions contains configuration parameters for creating an Operator.
+//
+// This struct allows users to specify various settings and credentials
+// required for connecting to and interacting with different storage backends.
+//
+// Fields in this struct vary depending on the storage scheme being used.
+// Refer to the documentation of specific storage backends for details on
+// required and optional fields.
+//
+// Example usage:
+//
+// options := opendal.OperatorOptions{
+// "root": "/path/to/root",
+// "endpoint": "https://example.com",
+// "access_key_id": "your_access_key",
+// "secret_access_key": "your_secret_key",
+// }
+type OperatorOptions map[string]string
+// Operator is the entry point for all public APIs in OpenDAL.
+//
+// Operator provides a unified interface for interacting with various storage
services.
+// It encapsulates the underlying storage operations and presents a consistent
API
+// regardless of the storage backend.
+//
+// # Usage
+//
+// Create an Operator using NewOperator, perform operations, and always
remember
+// to Close the operator when finished to release resources.
+//
+// # Example
+//
+// func main() {
+// // Create a new operator for the memory backend
+// op, err := opendal.NewOperator(memory.Scheme,
opendal.OperatorOptions{})
+// if err != nil {
+// log.Fatal(err)
+// }
+// defer op.Close() // Ensure the operator is closed when done
+//
+// // Perform operations using the operator
+// err = op.Write("example.txt", []byte("Hello, OpenDAL!"))
+// if err != nil {
+// log.Fatal(err)
+// }
+//
+// data, err := op.Read("example.txt")
+// if err != nil {
+// log.Fatal(err)
+// }
+// fmt.Println(string(data))
+// }
+//
+// Note: Always use defer op.Close() to ensure proper resource cleanup.
+//
+// # Available Operations
+//
+// Operator provides methods for common storage operations including:
+// - Read: Read data from a path
+// - Write: Write data to a path
+// - Stat: Get metadata for a path
+// - Delete: Remove a file or directory
+// - List: Enumerate entries in a directory
+// - and more...
+//
+// Refer to the individual method documentation for detailed usage information.
type Operator struct {
- inner *C.opendal_operator
+ ctx context.Context
+ cancel context.CancelFunc
+
+ inner *opendalOperator
}
-func NewOperator(scheme string, opt Options) (*Operator, error) {
- if len(scheme) == 0 {
- return nil, errInvalidScheme
- }
- opts := C.opendal_operator_options_new()
- defer C.opendal_operator_options_free(opts)
- for k, v := range opt {
- C.opendal_operator_options_set(opts, C.CString(k), C.CString(v))
- }
- ret := C.opendal_operator_new(C.CString(scheme), opts)
- if ret.error != nil {
- defer C.opendal_error_free(ret.error)
- code, message := parseError(ret.error)
- return nil, errors.New(fmt.Sprintf("create operator failed,
error code: %d, error message: %s", code, message))
+// NewOperator creates and initializes a new Operator for the specified
storage scheme.
+//
+// Parameters:
+// - scheme: The storage scheme (e.g., "memory", "s3", "fs").
+// - options: Configuration options for the operator.
+//
+// Returns:
+// - *Operator: A new Operator instance.
+// - error: An error if initialization fails, or nil if successful.
+//
+// Note: Remember to call Close() on the returned Operator when it's no longer
needed.
+func NewOperator(scheme Scheme, opts OperatorOptions) (op *Operator, err
error) {
+ err = scheme.LoadOnce()
+ if err != nil {
+ return
}
- return &Operator{
- inner: ret.op,
- }, nil
-}
-func (o *Operator) Write(key string, value []byte) error {
- if len(value) == 0 {
- return errValueEmpty
+ ctx, cancel, err := contextWithFFIs(scheme.Path())
+ if err != nil {
+ return
}
- bytes := C.opendal_bytes{data: (*C.uchar)(unsafe.Pointer(&value[0])),
len: C.ulong(len(value))}
- ret := C.opendal_operator_write(o.inner, C.CString(key), bytes)
- if ret != nil {
- defer C.opendal_error_free(ret)
- code, message := parseError(ret)
- return errors.New(fmt.Sprintf("write failed, error code: %d,
error message: %s", code, message))
+
+ options := getFFI[operatorOptionsNew](ctx, symOperatorOptionsNew)()
+ setOptions := getFFI[operatorOptionsSet](ctx, symOperatorOptionSet)
+ optionsFree := getFFI[operatorOptionsFree](ctx, symOperatorOptionsFree)
+
+ for key, value := range opts {
+ setOptions(options, key, value)
}
- return nil
-}
+ defer optionsFree(options)
-func (o *Operator) Read(key string) ([]byte, error) {
- ret := C.opendal_operator_read(o.inner, C.CString(key))
- if ret.error != nil {
- defer C.opendal_error_free(ret.error)
- code, message := parseError(ret.error)
- return nil, errors.New(fmt.Sprintf("read failed, error code:
%d, error message: %s", code, message))
+ inner, err := getFFI[operatorNew](ctx, symOperatorNew)(scheme, options)
+ if err != nil {
+ cancel()
+ return
}
- return C.GoBytes(unsafe.Pointer(ret.data.data), C.int(ret.data.len)),
nil
-}
-func (o *Operator) Close() error {
- C.opendal_operator_free(o.inner)
- return nil
-}
+ op = &Operator{
+ inner: inner,
+ ctx: ctx,
+ cancel: cancel,
+ }
-func decodeBytes(bs C.opendal_bytes) []byte {
- return C.GoBytes(unsafe.Pointer(bs.data), C.int(bs.len))
+ return
}
-func parseError(err *C.opendal_error) (int, string) {
- code := int(err.code)
- message := string(decodeBytes(err.message))
-
- return code, message
+// Close releases all resources associated with the Operator.
+//
+// It's important to call this method when the Operator is no longer needed
+// to ensure proper cleanup of underlying resources.
+//
+// Note: It's recommended to use defer op.Close() immediately after creating
an Operator.
+func (op *Operator) Close() {
+ free := getFFI[operatorFree]
+ free(op.ctx, symOperatorFree)(op.inner)
+ op.cancel()
}
diff --git a/bindings/go/opendal_test.go b/bindings/go/opendal_test.go
index 9ac76c2735..6ffcb67438 100644
--- a/bindings/go/opendal_test.go
+++ b/bindings/go/opendal_test.go
@@ -17,42 +17,213 @@
* under the License.
*/
-package opendal
+package opendal_test
import (
- "bytes"
+ "crypto/rand"
+ "fmt"
+ "math/big"
+ "os"
+ "reflect"
+ "runtime"
+ "strings"
+ "sync"
"testing"
- "github.com/stretchr/testify/assert"
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+ "github.com/yuchanns/opendal-go-services/aliyun_drive"
+ "github.com/yuchanns/opendal-go-services/memory"
)
-func TestOperations(t *testing.T) {
- opts := make(Options)
- opts["root"] = "/myroot"
- operator, err := NewOperator("memory", opts)
- assert.NoError(t, err)
- defer operator.Close()
- err = operator.Write("test", []byte("Hello World from OpenDAL CGO!"))
- assert.NoError(t, err)
- value, err := operator.Read("test")
- assert.NoError(t, err)
- assert.Equal(t, "Hello World from OpenDAL CGO!", string(value))
+// Add more schemes for behavior tests here.
+var schemes = []opendal.Scheme{
+ aliyun_drive.Scheme,
+ memory.Scheme,
+}
+
+type behaviorTest = func(assert *require.Assertions, op *opendal.Operator,
fixture *fixture)
+
+func TestBehavior(t *testing.T) {
+ assert := require.New(t)
+
+ op, closeFunc, err := newOperator()
+ assert.Nil(err)
+
+ cap := op.Info().GetFullCapability()
+
+ var tests []behaviorTest
+
+ tests = append(tests, testsCopy(cap)...)
+ tests = append(tests, testsCreateDir(cap)...)
+ tests = append(tests, testsDelete(cap)...)
+ tests = append(tests, testsList(cap)...)
+ tests = append(tests, testsRead(cap)...)
+ tests = append(tests, testsRename(cap)...)
+ tests = append(tests, testsStat(cap)...)
+ tests = append(tests, testsWrite(cap)...)
+
+ fixture := newFixture(op)
- println(string(value))
+ t.Cleanup(func() {
+ fixture.Cleanup(assert)
+ op.Close()
+
+ if closeFunc != nil {
+ closeFunc()
+ }
+ })
+
+ for i := range tests {
+ test := tests[i]
+
+ fullName :=
runtime.FuncForPC(reflect.ValueOf(test).Pointer()).Name()
+ parts := strings.Split(fullName, ".")
+ testName := strings.TrimPrefix(parts[len((parts))-1], "test")
+
+ t.Run(testName, func(t *testing.T) {
+ // Run all tests in parallel by default.
+ // To run synchronously for specific services, set
GOMAXPROCS=1.
+ t.Parallel()
+ assert := require.New(t)
+
+ test(assert, op, fixture)
+ })
+ }
}
-func BenchmarkOperator_Write(b *testing.B) {
- opts := make(Options)
- opts["root"] = "/myroot"
- op, _ := NewOperator("memory", opts)
- defer op.Close()
+func newOperator() (op *opendal.Operator, closeFunc func(), err error) {
+ test := os.Getenv("OPENDAL_TEST")
+ var scheme opendal.Scheme
+ for _, s := range schemes {
+ if s.Name() != test {
+ continue
+ }
+ err = s.LoadOnce()
+ if err != nil {
+ return
+ }
+ closeFunc = func() {
+ os.Remove(s.Path())
+ }
+ scheme = s
+ break
+ }
+ if scheme == nil {
+ err = fmt.Errorf("unsupported scheme: %s", test)
+ return
+ }
+
+ prefix := fmt.Sprintf("OPENDAL_%s_", strings.ToUpper(scheme.Name()))
- bs := bytes.Repeat([]byte("a"), 1024*1024)
- op.Write("test", bs)
+ opts := opendal.OperatorOptions{}
+ for _, env := range os.Environ() {
+ pair := strings.SplitN(env, "=", 2)
+ if len(pair) != 2 {
+ continue
+ }
+ key := pair[0]
+ value := pair[1]
+ if !strings.HasPrefix(key, prefix) {
+ continue
+ }
+ opts[strings.ToLower(strings.TrimPrefix(key, prefix))] = value
+ }
- b.SetBytes(1024 * 1024)
- b.ResetTimer()
- for i := 0; i < b.N; i++ {
- op.Read("test")
+ op, err = opendal.NewOperator(scheme, opts)
+ if err != nil {
+ err = fmt.Errorf("create operator must succeed: %s", err)
}
+
+ return
+}
+
+func assertErrorCode(err error) opendal.ErrorCode {
+ return err.(*opendal.Error).Code()
+}
+
+func genBytesWithRange(min, max uint) ([]byte, uint) {
+ diff := max - min
+ n, _ := rand.Int(rand.Reader, big.NewInt(int64(diff+1)))
+ size := uint(n.Int64()) + min
+
+ content := make([]byte, size)
+
+ _, _ = rand.Read(content)
+
+ return content, size
+}
+
+func genFixedBytes(size uint) []byte {
+ content, _ := genBytesWithRange(size, size)
+ return content
+}
+
+type fixture struct {
+ op *opendal.Operator
+ lock *sync.Mutex
+
+ paths []string
+}
+
+func newFixture(op *opendal.Operator) *fixture {
+ return &fixture{
+ op: op,
+ lock: &sync.Mutex{},
+ }
+}
+
+func (f *fixture) NewDirPath() string {
+ path := fmt.Sprintf("%s/", uuid.NewString())
+ f.PushPath(path)
+
+ return path
+}
+
+func (f *fixture) NewFilePath() string {
+ path := uuid.NewString()
+ f.PushPath(path)
+
+ return path
+}
+
+func (f *fixture) NewFile() (string, []byte, uint) {
+ return f.NewFileWithPath(uuid.NewString())
+}
+
+func (f *fixture) NewFileWithPath(path string) (string, []byte, uint) {
+ maxSize := f.op.Info().GetFullCapability().WriteTotalMaxSize()
+ if maxSize == 0 {
+ maxSize = 4 * 1024 * 1024
+ }
+ return f.NewFileWithRange(path, 1, maxSize)
+}
+
+func (f *fixture) NewFileWithRange(path string, min, max uint) (string,
[]byte, uint) {
+ f.PushPath(path)
+
+ content, size := genBytesWithRange(min, max)
+ return path, content, size
+}
+
+func (f *fixture) Cleanup(assert *require.Assertions) {
+ if !f.op.Info().GetFullCapability().Delete() {
+ return
+ }
+ f.lock.Lock()
+ defer f.lock.Unlock()
+
+ for _, path := range f.paths {
+ assert.Nil(f.op.Delete(path), "delete must succeed: %s", path)
+ }
+}
+
+func (f *fixture) PushPath(path string) string {
+ f.lock.Lock()
+ defer f.lock.Unlock()
+
+ f.paths = append(f.paths, path)
+
+ return path
}
diff --git a/bindings/go/operator.go b/bindings/go/operator.go
new file mode 100644
index 0000000000..feb841e3ec
--- /dev/null
+++ b/bindings/go/operator.go
@@ -0,0 +1,298 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Copy duplicates a file from the source path to the destination path.
+//
+// This function copies the contents of the file at 'from' to a new or
existing file at 'to'.
+//
+// # Parameters
+//
+// - from: The source file path.
+// - to: The destination file path.
+//
+// # Returns
+//
+// - error: An error if the copy operation fails, or nil if successful.
+//
+// # Behavior
+//
+// - Both 'from' and 'to' must be file paths, not directories.
+// - If 'to' already exists, it will be overwritten.
+// - If 'from' and 'to' are identical, an 'IsSameFile' error will be
returned.
+// - The copy operation is idempotent; repeated calls with the same
parameters will yield the same result.
+//
+// # Example
+//
+// func exampleCopy(op *operatorCopy) {
+// err = op.Copy("path/from/file", "path/to/file")
+// if err != nil {
+// log.Printf("Copy operation failed: %v", err)
+// } else {
+// log.Println("File copied successfully")
+// }
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Copy(src, dest string) error {
+ cp := getFFI[operatorCopy](op.ctx, symOperatorCopy)
+ return cp(op.inner, src, dest)
+}
+
+// Rename changes the name or location of a file from the source path to the
destination path.
+//
+// This function moves a file from 'from' to 'to', effectively renaming or
relocating it.
+//
+// # Parameters
+//
+// - from: The current file path.
+// - to: The new file path.
+//
+// # Returns
+//
+// - error: An error if the rename operation fails, or nil if successful.
+//
+// # Behavior
+//
+// - Both 'from' and 'to' must be file paths, not directories.
+// - If 'to' already exists, it will be overwritten.
+// - If 'from' and 'to' are identical, an 'IsSameFile' error will be
returned.
+//
+// # Example
+//
+// func exampleRename(op *opendal.Operator) {
+// err = op.Rename("path/from/file", "path/to/file")
+// if err != nil {
+// log.Printf("Rename operation failed: %v", err)
+// } else {
+// log.Println("File renamed successfully")
+// }
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Rename(src, dest string) error {
+ rename := getFFI[operatorRename](op.ctx, symOperatorRename)
+ return rename(op.inner, src, dest)
+}
+
+const symOperatorNew = "opendal_operator_new"
+
+type operatorNew func(scheme Scheme, opts *operatorOptions) (op
*opendalOperator, err error)
+
+var withOperatorNew = withFFI(ffiOpts{
+ sym: symOperatorNew,
+ rType: &typeResultOperatorNew,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorNew {
+ return func(scheme Scheme, opts *operatorOptions) (op *opendalOperator,
err error) {
+ var byteName *byte
+ byteName, err = unix.BytePtrFromString(scheme.Name())
+ if err != nil {
+ return
+ }
+ var result resultOperatorNew
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&byteName),
+ unsafe.Pointer(&opts),
+ )
+ if result.error != nil {
+ err = parseError(ctx, result.error)
+ return
+ }
+ op = result.op
+ return
+ }
+})
+
+const symOperatorFree = "opendal_operator_free"
+
+type operatorFree func(op *opendalOperator)
+
+var withOperatorFree = withFFI(ffiOpts{
+ sym: symOperatorFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorFree {
+ return func(op *opendalOperator) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&op),
+ )
+ }
+})
+
+type operatorOptions struct {
+ inner uintptr
+}
+
+const symOperatorOptionsNew = "opendal_operator_options_new"
+
+type operatorOptionsNew func() (opts *operatorOptions)
+
+var withOperatorOptionsNew = withFFI(ffiOpts{
+ sym: symOperatorOptionsNew,
+ rType: &ffi.TypePointer,
+}, func(_ context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorOptionsNew {
+ return func() (opts *operatorOptions) {
+ ffiCall(unsafe.Pointer(&opts))
+ return
+ }
+})
+
+const symOperatorOptionSet = "opendal_operator_options_set"
+
+type operatorOptionsSet func(opts *operatorOptions, key, value string) error
+
+var withOperatorOptionsSet = withFFI(ffiOpts{
+ sym: symOperatorOptionSet,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypePointer},
+}, func(_ context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorOptionsSet {
+ return func(opts *operatorOptions, key, value string) (err error) {
+ var (
+ byteKey *byte
+ byteValue *byte
+ )
+ byteKey, err = unix.BytePtrFromString(key)
+ if err != nil {
+ return err
+ }
+ byteValue, err = unix.BytePtrFromString(value)
+ if err != nil {
+ return err
+ }
+ ffiCall(
+ nil,
+ unsafe.Pointer(&opts),
+ unsafe.Pointer(&byteKey),
+ unsafe.Pointer(&byteValue),
+ )
+ return nil
+ }
+})
+
+const symOperatorOptionsFree = "opendal_operator_options_free"
+
+type operatorOptionsFree func(opts *operatorOptions)
+
+var withOperatorOptionsFree = withFFI(ffiOpts{
+ sym: symOperatorOptionsFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorOptionsFree {
+ return func(opts *operatorOptions) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&opts),
+ )
+ }
+})
+
+const symOperatorCopy = "opendal_operator_copy"
+
+type operatorCopy func(op *opendalOperator, src, dest string) (err error)
+
+var withOperatorCopy = withFFI(ffiOpts{
+ sym: symOperatorCopy,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorCopy {
+ return func(op *opendalOperator, src, dest string) (err error) {
+ var (
+ byteSrc *byte
+ byteDest *byte
+ )
+ byteSrc, err = unix.BytePtrFromString(src)
+ if err != nil {
+ return err
+ }
+ byteDest, err = unix.BytePtrFromString(dest)
+ if err != nil {
+ return err
+ }
+ var e *opendalError
+ ffiCall(
+ unsafe.Pointer(&e),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&byteSrc),
+ unsafe.Pointer(&byteDest),
+ )
+ return parseError(ctx, e)
+ }
+})
+
+const symOperatorRename = "opendal_operator_rename"
+
+type operatorRename func(op *opendalOperator, src, dest string) (err error)
+
+var withOperatorRename = withFFI(ffiOpts{
+ sym: symOperatorRename,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorRename {
+ return func(op *opendalOperator, src, dest string) (err error) {
+ var (
+ byteSrc *byte
+ byteDest *byte
+ )
+ byteSrc, err = unix.BytePtrFromString(src)
+ if err != nil {
+ return err
+ }
+ byteDest, err = unix.BytePtrFromString(dest)
+ if err != nil {
+ return err
+ }
+ var e *opendalError
+ ffiCall(
+ unsafe.Pointer(&e),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&byteSrc),
+ unsafe.Pointer(&byteDest),
+ )
+ return parseError(ctx, e)
+ }
+})
+
+const symBytesFree = "opendal_bytes_free"
+
+type bytesFree func(b *opendalBytes)
+
+var withBytesFree = withFFI(ffiOpts{
+ sym: symBytesFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(_ context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) bytesFree {
+ return func(b *opendalBytes) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&b),
+ )
+ }
+})
diff --git a/bindings/go/operator_info.go b/bindings/go/operator_info.go
new file mode 100644
index 0000000000..4a3c32afe3
--- /dev/null
+++ b/bindings/go/operator_info.go
@@ -0,0 +1,381 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Info returns metadata about the Operator.
+//
+// This method provides access to essential information about the Operator,
+// including its storage scheme, root path, name, and capabilities.
+//
+// Returns:
+// - *OperatorInfo: A pointer to an OperatorInfo struct containing the
Operator's metadata.
+func (op *Operator) Info() *OperatorInfo {
+ newInfo := getFFI[operatorInfoNew](op.ctx, symOperatorInfoNew)
+ inner := newInfo(op.inner)
+ getFullCap := getFFI[operatorInfoGetFullCapability](op.ctx,
symOperatorInfoGetFullCapability)
+ getNativeCap := getFFI[operatorInfoGetNativeCapability](op.ctx,
symOperatorInfoGetNativeCapability)
+ getScheme := getFFI[operatorInfoGetScheme](op.ctx,
symOperatorInfoGetScheme)
+ getRoot := getFFI[operatorInfoGetRoot](op.ctx, symOperatorInfoGetRoot)
+ getName := getFFI[operatorInfoGetName](op.ctx, symOperatorInfoGetName)
+
+ free := getFFI[operatorInfoFree](op.ctx, symOperatorInfoFree)
+ defer free(inner)
+
+ return &OperatorInfo{
+ scheme: getScheme(inner),
+ root: getRoot(inner),
+ name: getName(inner),
+ fullCap: &Capability{inner: getFullCap(inner)},
+ nativeCap: &Capability{inner: getNativeCap(inner)},
+ }
+}
+
+// OperatorInfo provides metadata about an Operator instance.
+//
+// This struct contains essential information about the storage backend
+// and its capabilities, allowing users to query details about the
+// Operator they are working with.
+type OperatorInfo struct {
+ scheme string
+ root string
+ name string
+ fullCap *Capability
+ nativeCap *Capability
+}
+
+func (i *OperatorInfo) GetFullCapability() *Capability {
+ return i.fullCap
+}
+
+func (i *OperatorInfo) GetNativeCapability() *Capability {
+ return i.nativeCap
+}
+
+func (i *OperatorInfo) GetScheme() string {
+ return i.scheme
+}
+
+func (i *OperatorInfo) GetRoot() string {
+ return i.root
+}
+
+func (i *OperatorInfo) GetName() string {
+ return i.name
+}
+
+// Capability represents the set of operations and features supported by an
Operator.
+//
+// Each field indicates the support level for a specific capability:
+// - bool fields: false indicates no support, true indicates support.
+// - uint fields: Represent size limits or thresholds for certain operations.
+//
+// This struct covers a wide range of capabilities including:
+// - Basic operations: stat, read, write, delete, copy, rename, list
+// - Advanced features: multipart uploads, presigned URLs, batch operations
+// - Operation modifiers: cache control, content type, if-match conditions
+//
+// The capability information helps in understanding the functionalities
+// available for a specific storage backend or Operator configuration.
+type Capability struct {
+ inner *opendalCapability
+}
+
+func (c *Capability) Stat() bool {
+ return c.inner.stat == 1
+}
+
+func (c *Capability) StatWithIfmatch() bool {
+ return c.inner.statWithIfmatch == 1
+}
+
+func (c *Capability) StatWithIfNoneMatch() bool {
+ return c.inner.statWithIfNoneMatch == 1
+}
+
+func (c *Capability) Read() bool {
+ return c.inner.read == 1
+}
+
+func (c *Capability) ReadWithIfmatch() bool {
+ return c.inner.readWithIfmatch == 1
+}
+
+func (c *Capability) ReadWithIfMatchNone() bool {
+ return c.inner.readWithIfMatchNone == 1
+}
+
+func (c *Capability) ReadWithOverrideCacheControl() bool {
+ return c.inner.readWithOverrideCacheControl == 1
+}
+
+func (c *Capability) ReadWithOverrideContentDisposition() bool {
+ return c.inner.readWithOverrideContentDisposition == 1
+}
+
+func (c *Capability) ReadWithOverrideContentType() bool {
+ return c.inner.readWithOverrideContentType == 1
+}
+
+func (c *Capability) Write() bool {
+ return c.inner.write == 1
+}
+
+func (c *Capability) WriteCanMulti() bool {
+ return c.inner.writeCanMulti == 1
+}
+
+func (c *Capability) WriteCanEmpty() bool {
+ return c.inner.writeCanEmpty == 1
+}
+
+func (c *Capability) WriteCanAppend() bool {
+ return c.inner.writeCanAppend == 1
+}
+
+func (c *Capability) WriteWithContentType() bool {
+ return c.inner.writeWithContentType == 1
+}
+
+func (c *Capability) WriteWithContentDisposition() bool {
+ return c.inner.writeWithContentDisposition == 1
+}
+
+func (c *Capability) WriteWithCacheControl() bool {
+ return c.inner.writeWithCacheControl == 1
+}
+
+func (c *Capability) WriteMultiMaxSize() uint {
+ return c.inner.writeMultiMaxSize
+}
+
+func (c *Capability) WriteMultiMinSize() uint {
+ return c.inner.writeMultiMinSize
+}
+
+func (c *Capability) WriteMultiAlignSize() uint {
+ return c.inner.writeMultiAlignSize
+}
+
+func (c *Capability) WriteTotalMaxSize() uint {
+ return c.inner.writeTotalMaxSize
+}
+
+func (c *Capability) CreateDir() bool {
+ return c.inner.createDir == 1
+}
+
+func (c *Capability) Delete() bool {
+ return c.inner.delete == 1
+}
+
+func (c *Capability) Copy() bool {
+ return c.inner.copy == 1
+}
+
+func (c *Capability) Rename() bool {
+ return c.inner.rename == 1
+}
+
+func (c *Capability) List() bool {
+ return c.inner.list == 1
+}
+
+func (c *Capability) ListWithLimit() bool {
+ return c.inner.listWithLimit == 1
+}
+
+func (c *Capability) ListWithStartAfter() bool {
+ return c.inner.listWithStartAfter == 1
+}
+
+func (c *Capability) ListWithRecursive() bool {
+ return c.inner.listWithRecursive == 1
+}
+
+func (c *Capability) Presign() bool {
+ return c.inner.presign == 1
+}
+
+func (c *Capability) PresignRead() bool {
+ return c.inner.presignRead == 1
+}
+
+func (c *Capability) PresignStat() bool {
+ return c.inner.presignStat == 1
+}
+
+func (c *Capability) PresignWrite() bool {
+ return c.inner.presignWrite == 1
+}
+
+func (c *Capability) Batch() bool {
+ return c.inner.batch == 1
+}
+
+func (c *Capability) BatchDelete() bool {
+ return c.inner.batchDelete == 1
+}
+
+func (c *Capability) BatchMaxOperations() uint {
+ return c.inner.batchMaxOperations
+}
+
+func (c *Capability) Blocking() bool {
+ return c.inner.blocking == 1
+}
+
+const symOperatorInfoNew = "opendal_operator_info_new"
+
+type operatorInfoNew func(op *opendalOperator) *opendalOperatorInfo
+
+var withOperatorInfoNew = withFFI(ffiOpts{
+ sym: symOperatorInfoNew,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoNew {
+ return func(op *opendalOperator) *opendalOperatorInfo {
+ var result *opendalOperatorInfo
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ )
+ return result
+ }
+})
+
+const symOperatorInfoFree = "opendal_operator_info_free"
+
+type operatorInfoFree func(info *opendalOperatorInfo)
+
+var withOperatorInfoFree = withFFI(ffiOpts{
+ sym: symOperatorInfoFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoFree {
+ return func(info *opendalOperatorInfo) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&info),
+ )
+ }
+})
+
+const symOperatorInfoGetFullCapability =
"opendal_operator_info_get_full_capability"
+
+type operatorInfoGetFullCapability func(info *opendalOperatorInfo)
*opendalCapability
+
+var withOperatorInfoGetFullCapability = withFFI(ffiOpts{
+ sym: symOperatorInfoGetFullCapability,
+ rType: &typeCapability,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoGetFullCapability {
+ return func(info *opendalOperatorInfo) *opendalCapability {
+ var cap opendalCapability
+ ffiCall(
+ unsafe.Pointer(&cap),
+ unsafe.Pointer(&info),
+ )
+ return &cap
+ }
+})
+
+const symOperatorInfoGetNativeCapability =
"opendal_operator_info_get_native_capability"
+
+type operatorInfoGetNativeCapability func(info *opendalOperatorInfo)
*opendalCapability
+
+var withOperatorInfoGetNativeCapability = withFFI(ffiOpts{
+ sym: symOperatorInfoGetNativeCapability,
+ rType: &typeCapability,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoGetNativeCapability {
+ return func(info *opendalOperatorInfo) *opendalCapability {
+ var cap opendalCapability
+ ffiCall(
+ unsafe.Pointer(&cap),
+ unsafe.Pointer(&info),
+ )
+ return &cap
+ }
+})
+
+const symOperatorInfoGetScheme = "opendal_operator_info_get_scheme"
+
+type operatorInfoGetScheme func(info *opendalOperatorInfo) string
+
+var withOperatorInfoGetScheme = withFFI(ffiOpts{
+ sym: symOperatorInfoGetScheme,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoGetScheme {
+ return func(info *opendalOperatorInfo) string {
+ var bytePtr *byte
+ ffiCall(
+ unsafe.Pointer(&bytePtr),
+ unsafe.Pointer(&info),
+ )
+ return unix.BytePtrToString(bytePtr)
+ }
+})
+
+const symOperatorInfoGetRoot = "opendal_operator_info_get_root"
+
+type operatorInfoGetRoot func(info *opendalOperatorInfo) string
+
+var withOperatorInfoGetRoot = withFFI(ffiOpts{
+ sym: symOperatorInfoGetRoot,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoGetRoot {
+ return func(info *opendalOperatorInfo) string {
+ var bytePtr *byte
+ ffiCall(
+ unsafe.Pointer(&bytePtr),
+ unsafe.Pointer(&info),
+ )
+ return unix.BytePtrToString(bytePtr)
+ }
+})
+
+const symOperatorInfoGetName = "opendal_operator_info_get_name"
+
+type operatorInfoGetName func(info *opendalOperatorInfo) string
+
+var withOperatorInfoGetName = withFFI(ffiOpts{
+ sym: symOperatorInfoGetName,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorInfoGetName {
+ return func(info *opendalOperatorInfo) string {
+ var bytePtr *byte
+ ffiCall(
+ unsafe.Pointer(&bytePtr),
+ unsafe.Pointer(&info),
+ )
+ return unix.BytePtrToString(bytePtr)
+ }
+})
diff --git a/bindings/go/read_test.go b/bindings/go/read_test.go
new file mode 100644
index 0000000000..ff125cf1d1
--- /dev/null
+++ b/bindings/go/read_test.go
@@ -0,0 +1,98 @@
+/*
+ * 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 (
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsRead(cap *opendal.Capability) []behaviorTest {
+ if !cap.Read() || !cap.Write() {
+ return nil
+ }
+ return []behaviorTest{
+ testReadFull,
+ testReader,
+ testReadNotExist,
+ testReadWithDirPath,
+ testReadWithSpecialChars,
+ }
+}
+
+func testReadFull(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ bs, err := op.Read(path)
+ assert.Nil(err)
+ assert.Equal(size, uint(len(bs)), "read size")
+ assert.Equal(content, bs, "read content")
+}
+
+func testReader(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ r, err := op.Reader(path)
+ assert.Nil(err)
+ defer r.Close()
+ bs := make([]byte, size)
+ n, err := r.Read(bs)
+ assert.Nil(err)
+ assert.Equal(size, uint(n), "read size")
+ assert.Equal(content, bs[:n], "read content")
+}
+
+func testReadNotExist(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path := fixture.NewFilePath()
+
+ _, err := op.Read(path)
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+}
+
+func testReadWithDirPath(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ path := fixture.NewDirPath()
+
+ assert.Nil(op.CreateDir(path), "create must succeed")
+
+ _, err := op.Read(path)
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
+}
+
+func testReadWithSpecialChars(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ path, content, size := fixture.NewFileWithPath(uuid.NewString() + "
!@#$%^&()_+-=;',.txt")
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ bs, err := op.Read(path)
+ assert.Nil(err)
+ assert.Equal(size, uint(len(bs)))
+ assert.Equal(content, bs)
+}
diff --git a/bindings/go/reader.go b/bindings/go/reader.go
new file mode 100644
index 0000000000..c6fbb68238
--- /dev/null
+++ b/bindings/go/reader.go
@@ -0,0 +1,299 @@
+/*
+ * 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"
+ "io"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Read reads the entire contents of the file at the specified path into a
byte slice.
+//
+// This function is a wrapper around the C-binding function
`opendal_operator_read`.
+//
+// # Parameters
+//
+// - path: The path of the file to read.
+//
+// # Returns
+//
+// - []byte: The contents of the file as a byte slice.
+// - error: An error if the read operation fails, or nil if successful.
+//
+// # Notes
+//
+// - This implementation does not support the `read_with` functionality.
+// - Read allocates a new byte slice internally. For more precise memory
control
+// or lazy reading, consider using the Reader() method instead.
+//
+// # Example
+//
+// func exampleRead(op *opendal.Operator) {
+// data, err := op.Read("test")
+// if err != nil {
+// log.Fatal(err)
+// }
+// fmt.Printf("Read: %s\n", data)
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Read(path string) ([]byte, error) {
+ read := getFFI[operatorRead](op.ctx, symOperatorRead)
+ bytes, err := read(op.inner, path)
+ if err != nil {
+ return nil, err
+ }
+
+ data := parseBytes(bytes)
+ if len(data) > 0 {
+ free := getFFI[bytesFree](op.ctx, symBytesFree)
+ free(bytes)
+
+ }
+ return data, nil
+}
+
+// Reader creates a new Reader for reading the contents of a file at the
specified path.
+//
+// This function is a wrapper around the C-binding function
`opendal_operator_reader`.
+//
+// # Parameters
+//
+// - path: The path of the file to read.
+//
+// # Returns
+//
+// - *Reader: A reader for accessing the file's contents. It implements
`io.ReadCloser`.
+// - error: An error if the reader creation fails, or nil if successful.
+//
+// # Notes
+//
+// - This implementation does not support the `reader_with` functionality.
+// - The returned reader allows for more controlled and efficient reading of
large files.
+//
+// # Example
+//
+// func exampleReader(op *opendal.Operator) {
+// r, err := op.Reader("path/to/file")
+// if err != nil {
+// log.Fatal(err)
+// }
+// defer r.Close()
+//
+// size := 1024 // Read 1KB at a time
+// buffer := make([]byte, size)
+//
+// for {
+// n, err := r.Read(buffer)
+// if err != nil {
+// log.Fatal(err)
+// }
+// fmt.Printf("Read %d bytes: %s\n", n, buffer[:n])
+// }
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Reader(path string) (*Reader, error) {
+ getReader := getFFI[operatorReader](op.ctx, symOperatorReader)
+ inner, err := getReader(op.inner, path)
+ if err != nil {
+ return nil, err
+ }
+ reader := &Reader{
+ inner: inner,
+ ctx: op.ctx,
+ }
+ return reader, nil
+}
+
+type Reader struct {
+ inner *opendalReader
+ ctx context.Context
+}
+
+var _ io.ReadCloser = (*Reader)(nil)
+
+// Read reads data from the underlying storage into the provided buffer.
+//
+// This method implements the io.Reader interface for OperatorReader.
+//
+// # Parameters
+//
+// - buf: A pre-allocated byte slice where the read data will be stored.
+// The length of buf determines the maximum number of bytes to read.
+//
+// # Returns
+//
+// - int: The number of bytes read. Returns 0 if no data is available or the
end of the file is reached.
+// - error: An error if the read operation fails, or nil if successful.
+// Note that this method does not return io.EOF; it returns nil at the end
of the file.
+//
+// # Notes
+//
+// - This method only returns OpenDAL-specific errors, not io.EOF.
+// - If no data is read (end of file), it returns (0, nil) instead of (0,
io.EOF).
+// - The caller is responsible for pre-allocating the buffer and determining
its size.
+//
+// # Example
+//
+// reader, err := op.Reader("path/to/file")
+// if err != nil {
+// log.Fatal(err)
+// }
+// defer reader.Close()
+//
+// buf := make([]byte, 1024)
+// for {
+// n, err := reader.Read(buf)
+// if err != nil {
+// log.Fatal(err)
+// }
+// if n == 0 {
+// break // End of file
+// }
+// // Process buf[:n]
+// }
+//
+// Note: Always check the number of bytes read (n) as it may be less than
len(buf).
+func (r *Reader) Read(buf []byte) (int, error) {
+ length := uint(len(buf))
+ read := getFFI[readerRead](r.ctx, symReaderRead)
+ var (
+ totalSize uint
+ size uint
+ err error
+ )
+ for {
+ size, err = read(r.inner, buf[totalSize:])
+ totalSize += size
+ if size == 0 || err != nil || totalSize >= length {
+ break
+ }
+ }
+ return int(totalSize), err
+}
+
+// Close releases resources associated with the OperatorReader.
+func (r *Reader) Close() error {
+ free := getFFI[readerFree](r.ctx, symReaderFree)
+ free(r.inner)
+ return nil
+}
+
+const symOperatorRead = "opendal_operator_read"
+
+type operatorRead func(op *opendalOperator, path string) (*opendalBytes, error)
+
+var withOperatorRead = withFFI(ffiOpts{
+ sym: symOperatorRead,
+ rType: &typeResultRead,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorRead {
+ return func(op *opendalOperator, path string) (*opendalBytes, error) {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultRead
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ return result.data, parseError(ctx, result.error)
+ }
+})
+
+const symOperatorReader = "opendal_operator_reader"
+
+type operatorReader func(op *opendalOperator, path string) (*opendalReader,
error)
+
+var withOperatorReader = withFFI(ffiOpts{
+ sym: symOperatorReader,
+ rType: &typeResultOperatorReader,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorReader {
+ return func(op *opendalOperator, path string) (*opendalReader, error) {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultOperatorReader
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.reader, nil
+ }
+})
+
+const symReaderFree = "opendal_reader_free"
+
+type readerFree func(r *opendalReader)
+
+var withReaderFree = withFFI(ffiOpts{
+ sym: symReaderFree,
+ rType: &ffi.TypeVoid,
+ aTypes: []*ffi.Type{&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) readerFree {
+ return func(r *opendalReader) {
+ ffiCall(
+ nil,
+ unsafe.Pointer(&r),
+ )
+ }
+})
+
+const symReaderRead = "opendal_reader_read"
+
+type readerRead func(r *opendalReader, buf []byte) (size uint, err error)
+
+var withReaderRead = withFFI(ffiOpts{
+ sym: symReaderRead,
+ rType: &typeResultReaderRead,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer,
&ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) readerRead {
+ return func(r *opendalReader, buf []byte) (size uint, err error) {
+ var length = len(buf)
+ if length == 0 {
+ return 0, nil
+ }
+ bytePtr := &buf[0]
+ var result resultReaderRead
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&r),
+ unsafe.Pointer(&bytePtr),
+ unsafe.Pointer(&length),
+ )
+ if result.error != nil {
+ return 0, parseError(ctx, result.error)
+ }
+ return result.size, nil
+ }
+})
diff --git a/bindings/go/rename_test.go b/bindings/go/rename_test.go
new file mode 100644
index 0000000000..3a71e1b578
--- /dev/null
+++ b/bindings/go/rename_test.go
@@ -0,0 +1,156 @@
+/*
+ * 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 (
+ "fmt"
+
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsRename(cap *opendal.Capability) []behaviorTest {
+ if !cap.Read() || !cap.Write() || !cap.Rename() {
+ return nil
+ }
+ return []behaviorTest{
+ testRenameFile,
+ testRenameNonExistingSource,
+ testRenameSourceDir,
+ testRenameTargetDir,
+ testRenameSelf,
+ testRenameNested,
+ testRenameOverwrite,
+ }
+}
+
+func testRenameFile(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent), "write must succeed")
+
+ targetPath := fixture.NewFilePath()
+
+ assert.Nil(op.Rename(sourcePath, targetPath))
+
+ _, err := op.Stat(sourcePath)
+ assert.NotNil(err, "stat must fail")
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+
+ targetContent, err := op.Read(targetPath)
+ assert.Nil(err)
+ assert.Equal(sourceContent, targetContent)
+}
+
+func testRenameNonExistingSource(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ sourcePath := fixture.NewFilePath()
+ targetPath := fixture.NewFilePath()
+
+ err := op.Rename(sourcePath, targetPath)
+ assert.NotNil(err, "rename must fail")
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+}
+
+func testRenameSourceDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ sourcePath := fixture.NewDirPath()
+ targetPth := fixture.NewFilePath()
+
+ assert.Nil(op.CreateDir(sourcePath), "create must succeed")
+
+ err := op.Rename(sourcePath, targetPth)
+ assert.NotNil(err, "rename must fail")
+ assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
+}
+
+func testRenameTargetDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent), "write must succeed")
+
+ targetPath := fixture.NewDirPath()
+ assert.Nil(op.CreateDir(targetPath))
+
+ err := op.Rename(sourcePath, targetPath)
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
+}
+
+func testRenameSelf(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent), "write must succeed")
+
+ err := op.Rename(sourcePath, sourcePath)
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeIsSameFile, assertErrorCode(err))
+}
+
+func testRenameNested(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent), "write must succeed")
+
+ targetPath := fixture.PushPath(fmt.Sprintf(
+ "%s/%s/%s",
+ uuid.NewString(),
+ uuid.NewString(),
+ uuid.NewString(),
+ ))
+
+ assert.Nil(op.Rename(sourcePath, targetPath))
+
+ _, err := op.Stat(sourcePath)
+ assert.NotNil(err, "stat must fail")
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+
+ targetContent, err := op.Read(targetPath)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(sourceContent, targetContent)
+}
+
+func testRenameOverwrite(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ sourcePath, sourceContent, _ := fixture.NewFile()
+
+ assert.Nil(op.Write(sourcePath, sourceContent), "write must succeed")
+
+ targetPath, targetContent, _ := fixture.NewFile()
+ assert.NotEqual(sourceContent, targetContent)
+
+ assert.Nil(op.Write(targetPath, targetContent), "write must succeed")
+
+ assert.Nil(op.Rename(sourcePath, targetPath))
+
+ _, err := op.Stat(sourcePath)
+ assert.NotNil(err, "stat must fail")
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+
+ targetContent, err = op.Read(targetPath)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(sourceContent, targetContent)
+}
diff --git a/bindings/go/stat.go b/bindings/go/stat.go
new file mode 100644
index 0000000000..6b83ad605c
--- /dev/null
+++ b/bindings/go/stat.go
@@ -0,0 +1,157 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Stat retrieves metadata for the specified path.
+//
+// This function is a wrapper around the C-binding function
`opendal_operator_stat`.
+//
+// # Parameters
+//
+// - path: The path of the file or directory to get metadata for.
+//
+// # Returns
+//
+// - *Metadata: Metadata of the specified path.
+// - error: An error if the operation fails, or nil if successful.
+//
+// # Notes
+//
+// - The current implementation does not support `stat_with` functionality.
+// - If the path does not exist, an error with code opendal.CodeNotFound
will be returned.
+//
+// # Example
+//
+// func exampleStat(op *opendal.Operator) {
+// meta, err := op.Stat("/path/to/file")
+// if err != nil {
+// if e, ok := err.(*opendal.Error); ok && e.Code() ==
opendal.CodeNotFound {
+// fmt.Println("File not found")
+// return
+// }
+// log.Fatalf("Stat operation failed: %v", err)
+// }
+// fmt.Printf("File size: %d bytes\n", meta.ContentLength())
+// fmt.Printf("Last modified: %v\n", meta.LastModified())
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Stat(path string) (*Metadata, error) {
+ stat := getFFI[operatorStat](op.ctx, symOperatorStat)
+ meta, err := stat(op.inner, path)
+ if err != nil {
+ return nil, err
+ }
+ return newMetadata(op.ctx, meta), nil
+}
+
+// IsExist checks if a file or directory exists at the specified path.
+//
+// This method provides a convenient way to determine the existence of a
resource
+// without fetching its full metadata.
+//
+// # Parameters
+//
+// - path: The path of the file or directory to check.
+//
+// # Returns
+//
+// - bool: true if the resource exists, false otherwise.
+// - error: An error if the check operation fails, or nil if the check is
successful.
+// Note that a false return value with a nil error indicates that the
resource does not exist.
+//
+// # Example
+//
+// exists, err := op.IsExist("path/to/file")
+// if err != nil {
+// log.Fatalf("Error checking existence: %v", err)
+// }
+// if exists {
+// fmt.Println("The file exists")
+// } else {
+// fmt.Println("The file does not exist")
+// }
+//
+func (op *Operator) IsExist(path string) (bool, error) {
+ isExist := getFFI[operatorIsExist](op.ctx, symOperatorIsExist)
+ return isExist(op.inner, path)
+}
+
+const symOperatorStat = "opendal_operator_stat"
+
+type operatorStat func(op *opendalOperator, path string) (*opendalMetadata,
error)
+
+var withOperatorStat = withFFI(ffiOpts{
+ sym: symOperatorStat,
+ rType: &typeResultStat,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorStat {
+ return func(op *opendalOperator, path string) (*opendalMetadata, error)
{
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return nil, err
+ }
+ var result resultStat
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ if result.error != nil {
+ return nil, parseError(ctx, result.error)
+ }
+ return result.meta, nil
+ }
+})
+
+const symOperatorIsExist = "opendal_operator_is_exist"
+
+type operatorIsExist func(op *opendalOperator, path string) (bool, error)
+
+var withOperatorIsExists = withFFI(ffiOpts{
+ sym: symOperatorIsExist,
+ rType: &typeResultIsExist,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorIsExist {
+ return func(op *opendalOperator, path string) (bool, error) {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return false, err
+ }
+ var result resultIsExist
+ ffiCall(
+ unsafe.Pointer(&result),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ if result.error != nil {
+ return false, parseError(ctx, result.error)
+ }
+ return result.is_exist == 1, nil
+ }
+})
diff --git a/bindings/go/stat_test.go b/bindings/go/stat_test.go
new file mode 100644
index 0000000000..f59b71be12
--- /dev/null
+++ b/bindings/go/stat_test.go
@@ -0,0 +1,143 @@
+/*
+ * 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 (
+ "fmt"
+ "strings"
+
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsStat(cap *opendal.Capability) []behaviorTest {
+ if !cap.Write() || !cap.Stat() {
+ return nil
+ }
+ return []behaviorTest{
+ testStatFile,
+ testStatDir,
+ testStatNestedParentDir,
+ testStatWithSpecialChars,
+ testStatNotCleanedPath,
+ testStatNotExist,
+ testStatRoot,
+ }
+}
+
+func testStatFile(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content))
+
+ meta, err := op.Stat(path)
+ assert.Nil(err)
+ assert.True(meta.IsFile())
+ assert.Equal(meta.ContentLength(), uint64(size))
+
+ if op.Info().GetFullCapability().CreateDir() {
+ _, err := op.Stat(fmt.Sprintf("%s/", path))
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+ }
+}
+
+func testStatDir(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ path := fixture.NewDirPath()
+ assert.Nil(op.CreateDir(path))
+
+ meta, err := op.Stat(path)
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+
+ meta, err = op.Stat(strings.TrimSuffix(path, "/"))
+ if err != nil {
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+ } else {
+ assert.True(meta.IsDir())
+ }
+}
+
+func testStatNestedParentDir(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().CreateDir() {
+ return
+ }
+
+ parent := fixture.NewDirPath()
+ path, content, _ := fixture.NewFileWithPath(fmt.Sprintf("%s%s", parent,
uuid.NewString()))
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ meta, err := op.Stat(parent)
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+}
+
+func testStatWithSpecialChars(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ path, content, size := fixture.NewFileWithPath(uuid.NewString() + "
!@#$%^&()_+-=;',.txt")
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ meta, err := op.Stat(path)
+ assert.Nil(err)
+ assert.True(meta.IsFile())
+ assert.Equal(uint64(size), meta.ContentLength())
+}
+
+func testStatNotCleanedPath(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content), "write must succeed")
+
+ meta, err := op.Stat(fmt.Sprintf("//%s", path))
+ assert.Nil(err)
+ assert.True(meta.IsFile())
+ assert.Equal(uint64(size), meta.ContentLength())
+}
+
+func testStatNotExist(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path := fixture.NewFilePath()
+
+ _, err := op.Stat(path)
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+
+ if op.Info().GetFullCapability().CreateDir() {
+ _, err := op.Stat(fmt.Sprintf("%s/", path))
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeNotFound, assertErrorCode(err))
+ }
+}
+
+func testStatRoot(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ meta, err := op.Stat("")
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+
+ meta, err = op.Stat("/")
+ assert.Nil(err)
+ assert.True(meta.IsDir())
+
+}
diff --git a/bindings/go/types.go b/bindings/go/types.go
new file mode 100644
index 0000000000..9fe2159723
--- /dev/null
+++ b/bindings/go/types.go
@@ -0,0 +1,286 @@
+/*
+ * 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 (
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+)
+
+var (
+ typeResultOperatorNew = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultRead = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeBytes = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultStat = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultList = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultListerNext = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultOperatorReader = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultReaderRead = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypePointer,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeResultIsExist = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypeUint8,
+ &ffi.TypePointer,
+ nil,
+ }[0],
+ }
+
+ typeCapability = ffi.Type{
+ Type: ffi.Struct,
+ Elements: &[]*ffi.Type{
+ &ffi.TypeUint8, // stat
+ &ffi.TypeUint8, // stat_with_if_match
+ &ffi.TypeUint8, // stat_with_if_none_match
+ &ffi.TypeUint8, // read
+ &ffi.TypeUint8, // read_with_if_match
+ &ffi.TypeUint8, // read_with_if_match_none
+ &ffi.TypeUint8, // read_with_override_cache_control
+ &ffi.TypeUint8, //
read_with_override_content_disposition
+ &ffi.TypeUint8, // read_with_override_content_type
+ &ffi.TypeUint8, // write
+ &ffi.TypeUint8, // write_can_multi
+ &ffi.TypeUint8, // write_can_empty
+ &ffi.TypeUint8, // write_can_append
+ &ffi.TypeUint8, // write_with_content_type
+ &ffi.TypeUint8, // write_with_content_disposition
+ &ffi.TypeUint8, // write_with_cache_control
+ &ffi.TypePointer, // write_multi_max_size
+ &ffi.TypePointer, // write_multi_min_size
+ &ffi.TypePointer, // write_multi_align_size
+ &ffi.TypePointer, // write_total_max_size
+ &ffi.TypeUint8, // create_dir
+ &ffi.TypeUint8, // delete
+ &ffi.TypeUint8, // copy
+ &ffi.TypeUint8, // rename
+ &ffi.TypeUint8, // list
+ &ffi.TypeUint8, // list_with_limit
+ &ffi.TypeUint8, // list_with_start_after
+ &ffi.TypeUint8, // list_with_recursive
+ &ffi.TypeUint8, // presign
+ &ffi.TypeUint8, // presign_read
+ &ffi.TypeUint8, // presign_stat
+ &ffi.TypeUint8, // presign_write
+ &ffi.TypeUint8, // batch
+ &ffi.TypeUint8, // batch_delete
+ &ffi.TypePointer, // batch_max_operations
+ &ffi.TypeUint8, // blocking
+ nil,
+ }[0],
+ }
+)
+
+type opendalCapability struct {
+ stat uint8
+ statWithIfmatch uint8
+ statWithIfNoneMatch uint8
+ read uint8
+ readWithIfmatch uint8
+ readWithIfMatchNone uint8
+ readWithOverrideCacheControl uint8
+ readWithOverrideContentDisposition uint8
+ readWithOverrideContentType uint8
+ write uint8
+ writeCanMulti uint8
+ writeCanEmpty uint8
+ writeCanAppend uint8
+ writeWithContentType uint8
+ writeWithContentDisposition uint8
+ writeWithCacheControl uint8
+ writeMultiMaxSize uint
+ writeMultiMinSize uint
+ writeMultiAlignSize uint
+ writeTotalMaxSize uint
+ createDir uint8
+ delete uint8
+ copy uint8
+ rename uint8
+ list uint8
+ listWithLimit uint8
+ listWithStartAfter uint8
+ listWithRecursive uint8
+ presign uint8
+ presignRead uint8
+ presignStat uint8
+ presignWrite uint8
+ batch uint8
+ batchDelete uint8
+ batchMaxOperations uint
+ blocking uint8
+}
+
+type resultOperatorNew struct {
+ op *opendalOperator
+ error *opendalError
+}
+
+type opendalOperator struct {
+ ptr uintptr
+}
+
+type resultRead struct {
+ data *opendalBytes
+ error *opendalError
+}
+
+type opendalReader struct {
+ inner uintptr
+}
+
+type resultOperatorReader struct {
+ reader *opendalReader
+ error *opendalError
+}
+
+type resultReaderRead struct {
+ size uint
+ error *opendalError
+}
+
+type resultIsExist struct {
+ is_exist uint8
+ error *opendalError
+}
+
+type resultStat struct {
+ meta *opendalMetadata
+ error *opendalError
+}
+
+type opendalMetadata struct {
+ inner uintptr
+}
+
+type opendalBytes struct {
+ data *byte
+ len uintptr
+}
+
+type opendalError struct {
+ code int32
+ message opendalBytes
+}
+
+type opendalOperatorInfo struct {
+ inner uintptr
+}
+
+type opendalResultList struct {
+ lister *opendalLister
+ err *opendalError
+}
+
+type opendalLister struct {
+ inner uintptr
+}
+
+type opendalResultListerNext struct {
+ entry *opendalEntry
+ err *opendalError
+}
+
+type opendalEntry struct {
+ inner uintptr
+}
+
+func toOpendalBytes(data []byte) opendalBytes {
+ var ptr *byte
+ l := len(data)
+ if l > 0 {
+ ptr = &data[0]
+ }
+ return opendalBytes{
+ data: ptr,
+ len: uintptr(l),
+ }
+}
+
+func parseBytes(b *opendalBytes) (data []byte) {
+ if b == nil || b.len == 0 {
+ return nil
+ }
+ data = make([]byte, b.len)
+ copy(data, unsafe.Slice(b.data, b.len))
+ return
+}
diff --git a/bindings/go/write.go b/bindings/go/write.go
new file mode 100644
index 0000000000..e1d18a1322
--- /dev/null
+++ b/bindings/go/write.go
@@ -0,0 +1,152 @@
+/*
+ * 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"
+ "unsafe"
+
+ "github.com/jupiterrider/ffi"
+ "golang.org/x/sys/unix"
+)
+
+// Write writes the given bytes to the specified path.
+//
+// Write is a wrapper around the C-binding function `opendal_operator_write`.
It provides a simplified
+// interface for writing data to the storage. Currently, this implementation
does not support the
+// `Operator::write_with` method from the original Rust library, nor does it
support streaming writes
+// or multipart uploads.
+//
+// # Parameters
+//
+// - path: The destination path where the bytes will be written.
+// - data: The byte slice containing the data to be written.
+//
+// # Returns
+//
+// - error: An error if the write operation fails, or nil if successful.
+//
+// # Example
+//
+// func exampleWrite(op *opendal.Operator) {
+// err = op.Write("test", []byte("Hello opendal go binding!"))
+// if err != nil {
+// log.Fatal(err)
+// }
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+func (op *Operator) Write(path string, data []byte) error {
+ write := getFFI[operatorWrite](op.ctx, symOperatorWrite)
+ return write(op.inner, path, data)
+}
+
+// CreateDir creates a directory at the specified path.
+//
+// CreateDir is a wrapper around the C-binding function
`opendal_operator_create_dir`.
+// It provides a way to create directories in the storage system.
+//
+// # Parameters
+//
+// - path: The path where the directory should be created.
+//
+// # Returns
+//
+// - error: An error if the directory creation fails, or nil if successful.
+//
+// # Notes
+//
+// It is mandatory to include a trailing slash (/) in the path to indicate
+// that it is a directory. Failing to do so may result in a `CodeNotADirectory`
+// error being returned by OpenDAL.
+//
+// # Behavior
+//
+// - Creating a directory that already exists will succeed without error.
+// - Directory creation is always recursive, similar to the `mkdir -p`
command.
+//
+// # Example
+//
+// func exampleCreateDir(op *opendal.Operator) {
+// err = op.CreateDir("test/")
+// if err != nil {
+// log.Fatal(err)
+// }
+// }
+//
+// Note: This example assumes proper error handling and import statements.
+// The trailing slash in "test/" is important to indicate it's a directory.
+func (op *Operator) CreateDir(path string) error {
+ createDir := getFFI[operatorCreateDir](op.ctx, symOperatorCreateDir)
+ return createDir(op.inner, path)
+}
+
+const symOperatorWrite = "opendal_operator_write"
+
+type operatorWrite func(op *opendalOperator, path string, data []byte) error
+
+var withOperatorWrite = withFFI(ffiOpts{
+ sym: symOperatorWrite,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer, &typeBytes},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorWrite {
+ return func(op *opendalOperator, path string, data []byte) error {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ bytes := toOpendalBytes(data)
+ if len(data) > 0 {
+ bytes.data = &data[0]
+ }
+ var e *opendalError
+ ffiCall(
+ unsafe.Pointer(&e),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ unsafe.Pointer(&bytes),
+ )
+ return parseError(ctx, e)
+ }
+})
+
+const symOperatorCreateDir = "opendal_operator_create_dir"
+
+type operatorCreateDir func(op *opendalOperator, path string) error
+
+var withOperatorCreateDir = withFFI(ffiOpts{
+ sym: symOperatorCreateDir,
+ rType: &ffi.TypePointer,
+ aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer},
+}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues
...unsafe.Pointer)) operatorCreateDir {
+ return func(op *opendalOperator, path string) error {
+ bytePath, err := unix.BytePtrFromString(path)
+ if err != nil {
+ return err
+ }
+ var e *opendalError
+ ffiCall(
+ unsafe.Pointer(&e),
+ unsafe.Pointer(&op),
+ unsafe.Pointer(&bytePath),
+ )
+ return parseError(ctx, e)
+ }
+})
diff --git a/bindings/go/write_test.go b/bindings/go/write_test.go
new file mode 100644
index 0000000000..69cece7d43
--- /dev/null
+++ b/bindings/go/write_test.go
@@ -0,0 +1,101 @@
+/*
+ * 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 (
+ "github.com/apache/opendal/bindings/go"
+ "github.com/google/uuid"
+ "github.com/stretchr/testify/require"
+)
+
+func testsWrite(cap *opendal.Capability) []behaviorTest {
+ if !cap.Write() || !cap.Stat() || !cap.Read() {
+ return nil
+ }
+ return []behaviorTest{
+ testWriteOnly,
+ testWriteWithEmptyContent,
+ testWriteWithDirPath,
+ testWriteWithSpecialChars,
+ testWriteOverwrite,
+ }
+}
+
+func testWriteOnly(assert *require.Assertions, op *opendal.Operator, fixture
*fixture) {
+ path, content, size := fixture.NewFile()
+
+ assert.Nil(op.Write(path, content))
+
+ meta, err := op.Stat(path)
+ assert.Nil(err, "stat must succeed")
+ assert.Equal(uint64(size), meta.ContentLength())
+}
+
+func testWriteWithEmptyContent(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ if !op.Info().GetFullCapability().WriteCanEmpty() {
+ return
+ }
+
+ path := fixture.NewFilePath()
+ assert.Nil(op.Write(path, []byte{}))
+
+ meta, err := op.Stat(path)
+ assert.Nil(err, "stat must succeed")
+ assert.Equal(uint64(0), meta.ContentLength())
+}
+
+func testWriteWithDirPath(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ path := fixture.NewDirPath()
+
+ err := op.Write(path, []byte("1"))
+ assert.NotNil(err)
+ assert.Equal(opendal.CodeIsADirectory, assertErrorCode(err))
+}
+
+func testWriteWithSpecialChars(assert *require.Assertions, op
*opendal.Operator, fixture *fixture) {
+ path, content, size := fixture.NewFileWithPath(uuid.NewString() + "
!@#$%^&()_+-=;',.txt")
+
+ assert.Nil(op.Write(path, content))
+
+ meta, err := op.Stat(path)
+ assert.Nil(err, "stat must succeed")
+ assert.Equal(uint64(size), meta.ContentLength())
+}
+
+func testWriteOverwrite(assert *require.Assertions, op *opendal.Operator,
fixture *fixture) {
+ if !op.Info().GetFullCapability().WriteCanMulti() {
+ return
+ }
+
+ path := fixture.NewFilePath()
+ size := uint(5 * 1024 * 1024)
+ contentOne, contentTwo := genFixedBytes(size), genFixedBytes(size)
+
+ assert.Nil(op.Write(path, contentOne))
+ bs, err := op.Read(path)
+ assert.Nil(err, "read must succeed")
+ assert.Equal(contentOne, bs, "read content_one")
+
+ assert.Nil(op.Write(path, contentTwo))
+ bs, err = op.Read(path)
+ assert.Nil(err, "read must succeed")
+ assert.NotEqual(contentOne, bs, "content_one must be overwrote")
+ assert.Equal(contentTwo, bs, "read content_two")
+}