diff --git a/.github/scripts/test_go_binding/matrix.yaml b/.github/scripts/test_go_binding/matrix.yaml index 27ba73a8501a..e477aea073ef 100644 --- a/.github/scripts/test_go_binding/matrix.yaml +++ b/.github/scripts/test_go_binding/matrix.yaml @@ -22,5 +22,5 @@ build: goos: "linux" goarch: "amd64" service: - - "memory" + - "fs" diff --git a/.github/workflows/ci_bindings_go.yml b/.github/workflows/ci_bindings_go.yml index bde9e0cb2d84..5ffd5dca56c1 100644 --- a/.github/workflows/ci_bindings_go.yml +++ b/.github/workflows/ci_bindings_go.yml @@ -127,5 +127,6 @@ jobs: - name: Run tests env: OPENDAL_TEST: ${{ matrix.service }} + OPENDAL_FS_ROOT: "/tmp/opendal/" working-directory: bindings/go/tests/behavior_tests run: CGO_ENABLE=0 go test -v -run TestBehavior diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index 585d0be0231f..2337b84d0aea 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -146,6 +146,12 @@ typedef struct BlockingLister BlockingLister; */ typedef struct BlockingOperator BlockingOperator; +/** + * BlockingWriter is designed to write data into given path in an blocking + * manner. + */ +typedef struct BlockingWriter BlockingWriter; + /** * Entry returned by [`Lister`] or [`BlockingLister`] to represent a path and it's relative metadata. * @@ -430,6 +436,31 @@ typedef struct opendal_result_operator_reader { struct opendal_error *error; } opendal_result_operator_reader; +/** + * \brief The result type returned by opendal's writer operation. + * \note The opendal_writer actually owns a pointer to + * a opendal::BlockingWriter, which is inside the Rust core code. + */ +typedef struct opendal_writer { + struct BlockingWriter *inner; +} opendal_writer; + +/** + * \brief The result type returned by opendal_operator_writer(). + * The result type for opendal_operator_writer(), the field `writer` contains the writer + * of the path, which is an iterator of the objects under the path. the field `code` represents + */ +typedef struct opendal_result_operator_writer { + /** + * The pointer for opendal_writer + */ + struct opendal_writer *writer; + /** + * The error, if ok, it is null + */ + struct opendal_error *error; +} opendal_result_operator_writer; + /** * \brief The result type returned by opendal_operator_is_exist(). * @@ -680,6 +711,22 @@ typedef struct opendal_result_reader_read { struct opendal_error *error; } opendal_result_reader_read; +/** + * \brief The result type returned by opendal_writer_write(). + * The result type contains a size field, which is the size of the data written, + * which is zero on error. The error field is the error code and error message. + */ +typedef struct opendal_result_writer_write { + /** + * The write size if succeed. + */ + uintptr_t size; + /** + * The error, if ok, it is null + */ + struct opendal_error *error; +} opendal_result_writer_write; + #ifdef __cplusplus extern "C" { #endif // __cplusplus @@ -970,6 +1017,47 @@ struct opendal_result_read opendal_operator_read(const struct opendal_operator * struct opendal_result_operator_reader opendal_operator_reader(const struct opendal_operator *op, const char *path); +/** + * \brief Blockingly create a writer for the specified path. + * + * This function prepares a writer that can be used to write data to the specified path + * using the provided operator. If successful, it returns a valid writer; otherwise, it + * returns an error. + * + * @param op The opendal_operator created previously + * @param path The designated path where the writer will be used + * @see opendal_operator + * @see opendal_result_operator_writer + * @see opendal_error + * @return Returns opendal_result_operator_writer, containing a writer and an opendal_error. + * If the operation succeeds, the `writer` field holds a valid writer and the `error` field + * is null. Otherwise, the `writer` will be null and the `error` will be set correspondingly. + * + * # Example + * + * Following is an example + * ```C + * //...prepare your opendal_operator, named op for example + * + * opendal_result_operator_writer result = opendal_operator_writer(op, "/testpath"); + * assert(result.error == NULL); + * opendal_writer *writer = result.writer; + * // Use the writer to write data... + * ``` + * + * # Safety + * + * It is **safe** under the cases below + * * The memory pointed to by `path` must contain a valid nul terminator at the end of + * the string. + * + * # Panic + * + * * If the `path` points to NULL, this function panics, i.e. exits with information + */ +struct opendal_result_operator_writer opendal_operator_writer(const struct opendal_operator *op, + const char *path); + /** * \brief Blockingly delete the object in `path`. * @@ -1419,6 +1507,18 @@ struct opendal_result_reader_read opendal_reader_read(const struct opendal_reade */ void opendal_reader_free(struct opendal_reader *ptr); +/** + * \brief Write data to the writer. + */ +struct opendal_result_writer_write opendal_writer_write(const struct opendal_writer *writer, + struct opendal_bytes bytes); + +/** + * \brief Frees the heap memory used by the opendal_writer. + * \note This function make sure all data have been stored. + */ +void opendal_writer_free(struct opendal_writer *ptr); + #ifdef __cplusplus } // extern "C" #endif // __cplusplus diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index cfd46f92eddf..ac125b0d4244 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -51,9 +51,11 @@ pub use result::opendal_result_list; pub use result::opendal_result_lister_next; pub use result::opendal_result_operator_new; pub use result::opendal_result_operator_reader; +pub use result::opendal_result_operator_writer; pub use result::opendal_result_read; pub use result::opendal_result_reader_read; pub use result::opendal_result_stat; +pub use result::opendal_result_writer_write; mod types; pub use types::opendal_bytes; @@ -64,3 +66,6 @@ pub use entry::opendal_entry; mod reader; pub use reader::opendal_reader; + +mod writer; +pub use writer::opendal_writer; diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs index 71a1e52e68cd..ebfca45fdccd 100644 --- a/bindings/c/src/operator.rs +++ b/bindings/c/src/operator.rs @@ -391,6 +391,69 @@ pub unsafe extern "C" fn opendal_operator_reader( } } +/// \brief Blockingly create a writer for the specified path. +/// +/// This function prepares a writer that can be used to write data to the specified path +/// using the provided operator. If successful, it returns a valid writer; otherwise, it +/// returns an error. +/// +/// @param op The opendal_operator created previously +/// @param path The designated path where the writer will be used +/// @see opendal_operator +/// @see opendal_result_operator_writer +/// @see opendal_error +/// @return Returns opendal_result_operator_writer, containing a writer and an opendal_error. +/// If the operation succeeds, the `writer` field holds a valid writer and the `error` field +/// is null. Otherwise, the `writer` will be null and the `error` will be set correspondingly. +/// +/// # Example +/// +/// Following is an example +/// ```C +/// //...prepare your opendal_operator, named op for example +/// +/// opendal_result_operator_writer result = opendal_operator_writer(op, "/testpath"); +/// assert(result.error == NULL); +/// opendal_writer *writer = result.writer; +/// // Use the writer to write data... +/// ``` +/// +/// # Safety +/// +/// It is **safe** under the cases below +/// * The memory pointed to by `path` must contain a valid nul terminator at the end of +/// the string. +/// +/// # Panic +/// +/// * If the `path` points to NULL, this function panics, i.e. exits with information +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_writer( + op: *const opendal_operator, + path: *const c_char, +) -> opendal_result_operator_writer { + if path.is_null() { + panic!("The path given is pointing at NULL"); + } + let op = (*op).as_ref(); + + let path = unsafe { std::ffi::CStr::from_ptr(path).to_str().unwrap() }; + let writer = match op.writer(path) { + Ok(writer) => writer, + Err(err) => { + return opendal_result_operator_writer { + writer: std::ptr::null_mut(), + error: opendal_error::new(err), + } + } + }; + + opendal_result_operator_writer { + writer: Box::into_raw(Box::new(opendal_writer::new(writer))), + error: std::ptr::null_mut(), + } +} + /// \brief Blockingly delete the object in `path`. /// /// Delete the object in `path` blockingly by `op_ptr`. diff --git a/bindings/c/src/result.rs b/bindings/c/src/result.rs index 64b27892288f..ba7e1c64a028 100644 --- a/bindings/c/src/result.rs +++ b/bindings/c/src/result.rs @@ -131,3 +131,25 @@ pub struct opendal_result_reader_read { /// The error, if ok, it is null pub error: *mut opendal_error, } + +/// \brief The result type returned by opendal_operator_writer(). +/// The result type for opendal_operator_writer(), the field `writer` contains the writer +/// of the path, which is an iterator of the objects under the path. the field `code` represents +#[repr(C)] +pub struct opendal_result_operator_writer { + /// The pointer for opendal_writer + pub writer: *mut opendal_writer, + /// The error, if ok, it is null + pub error: *mut opendal_error, +} + +/// \brief The result type returned by opendal_writer_write(). +/// The result type contains a size field, which is the size of the data written, +/// which is zero on error. The error field is the error code and error message. +#[repr(C)] +pub struct opendal_result_writer_write { + /// The write size if succeed. + pub size: usize, + /// The error, if ok, it is null + pub error: *mut opendal_error, +} diff --git a/bindings/c/src/writer.rs b/bindings/c/src/writer.rs new file mode 100644 index 000000000000..5113e55c7533 --- /dev/null +++ b/bindings/c/src/writer.rs @@ -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. + +use ::opendal as core; + +use super::*; + +/// \brief The result type returned by opendal's writer operation. +/// \note The opendal_writer actually owns a pointer to +/// a opendal::BlockingWriter, which is inside the Rust core code. +#[repr(C)] +pub struct opendal_writer { + inner: *mut core::BlockingWriter, +} + +impl opendal_writer { + pub(crate) fn new(writer: core::BlockingWriter) -> Self { + Self { + inner: Box::into_raw(Box::new(writer)), + } + } + + /// \brief Write data to the writer. + #[no_mangle] + pub unsafe extern "C" fn opendal_writer_write( + writer: *const Self, + bytes: opendal_bytes, + ) -> opendal_result_writer_write { + let inner = unsafe { &mut *(*writer).inner }; + let size = bytes.len; + match inner.write(bytes) { + Ok(()) => opendal_result_writer_write { + size, + error: std::ptr::null_mut(), + }, + Err(e) => opendal_result_writer_write { + size: 0, + error: opendal_error::new( + core::Error::new(core::ErrorKind::Unexpected, "write failed from writer") + .set_source(e), + ), + }, + } + } + + /// \brief Frees the heap memory used by the opendal_writer. + /// \note This function make sure all data have been stored. + #[no_mangle] + pub unsafe extern "C" fn opendal_writer_free(ptr: *mut opendal_writer) { + if !ptr.is_null() { + let mut w = unsafe { Box::from_raw((*ptr).inner) }; + let _ = w.close(); + let _ = unsafe { Box::from_raw(ptr) }; + } + } +} diff --git a/bindings/c/tests/bdd.cpp b/bindings/c/tests/bdd.cpp index 370c17fd5b3f..39856e81e5f0 100644 --- a/bindings/c/tests/bdd.cpp +++ b/bindings/c/tests/bdd.cpp @@ -90,6 +90,20 @@ TEST_F(OpendalBddTest, FeatureTest) EXPECT_EQ(this->content[i], (char)(r.data->data[i])); } + // The blocking file should be deleted + error = opendal_operator_delete(this->p, this->path.c_str()); + EXPECT_EQ(error, nullptr); + e = opendal_operator_is_exist(this->p, this->path.c_str()); + EXPECT_EQ(e.error, nullptr); + EXPECT_FALSE(e.is_exist); + + opendal_result_operator_writer writer = opendal_operator_writer(this->p, this->path.c_str()); + EXPECT_EQ(writer.error, nullptr); + opendal_result_writer_write w = opendal_writer_write(writer.writer, data); + EXPECT_EQ(w.error, nullptr); + EXPECT_EQ(w.size, this->content.length()); + opendal_writer_free(writer.writer); + // The blocking file "test" must have content "Hello, World!" and read into buffer int length = this->content.length(); unsigned char buffer[this->content.length()]; @@ -102,13 +116,6 @@ TEST_F(OpendalBddTest, FeatureTest) } opendal_reader_free(reader.reader); - // The blocking file should be deleted - error = opendal_operator_delete(this->p, this->path.c_str()); - EXPECT_EQ(error, nullptr); - e = opendal_operator_is_exist(this->p, this->path.c_str()); - EXPECT_EQ(e.error, nullptr); - EXPECT_FALSE(e.is_exist); - // The deletion operation should be idempotent error = opendal_operator_delete(this->p, this->path.c_str()); EXPECT_EQ(error, nullptr); diff --git a/bindings/go/ffi.go b/bindings/go/ffi.go index 8d5f2d58443f..931c345f2a07 100644 --- a/bindings/go/ffi.go +++ b/bindings/go/ffi.go @@ -140,4 +140,8 @@ var withFFIs = []contextWithFFI{ withOperatorReader, withReaderRead, withReaderFree, + + withOperatorWriter, + withWriterWrite, + withWriterFree, } diff --git a/bindings/go/tests/behavior_tests/go.mod b/bindings/go/tests/behavior_tests/go.mod index b6c1ff39f0da..dae1477ff169 100644 --- a/bindings/go/tests/behavior_tests/go.mod +++ b/bindings/go/tests/behavior_tests/go.mod @@ -20,7 +20,8 @@ module opendal_test go 1.22.5 require ( - github.com/apache/opendal-go-services/memory v0.0.0-20240719030108-74ff217cfef9 + github.com/apache/opendal-go-services/fs v0.1.3 + github.com/apache/opendal-go-services/memory v0.1.3 github.com/apache/opendal/bindings/go v0.0.0-20240719044908-d9d4279b3a24 github.com/google/uuid v1.6.0 github.com/stretchr/testify v1.9.0 @@ -30,7 +31,7 @@ require ( github.com/davecgh/go-spew v1.1.1 // indirect github.com/ebitengine/purego v0.7.1 // indirect github.com/jupiterrider/ffi v0.1.0-beta.9 // indirect - github.com/klauspost/compress v1.17.9 // indirect + github.com/klauspost/compress v1.17.10 // indirect github.com/pmezard/go-difflib v1.0.0 // indirect golang.org/x/sys v0.22.0 // indirect gopkg.in/yaml.v3 v3.0.1 // indirect diff --git a/bindings/go/tests/behavior_tests/go.sum b/bindings/go/tests/behavior_tests/go.sum index 3594204ba238..d9f6957f138e 100644 --- a/bindings/go/tests/behavior_tests/go.sum +++ b/bindings/go/tests/behavior_tests/go.sum @@ -1,5 +1,7 @@ -github.com/apache/opendal-go-services/memory v0.0.0-20240719030108-74ff217cfef9 h1:tV4Xmoa5Zq2RyrygaXreayLz10C6JqTpyvMYAtdshXQ= -github.com/apache/opendal-go-services/memory v0.0.0-20240719030108-74ff217cfef9/go.mod h1:vldOQuikErKA1wfGnqvjAYB9MON/PWTuFIulMCKIQqM= +github.com/apache/opendal-go-services/fs v0.1.3 h1:k5pA73gKbQ3MHH2envsKhr1cec2spLm2tl/bCyU53j8= +github.com/apache/opendal-go-services/fs v0.1.3/go.mod h1:7EnuyeXRuQh+L47rZ7y2OrhYJLlUYvgvFPItM98XJ5s= +github.com/apache/opendal-go-services/memory v0.1.3 h1:lUe4n4Y9AmwS6a1KV/ZTLyWLtWpRSSuNZHchcW2s+LQ= +github.com/apache/opendal-go-services/memory v0.1.3/go.mod h1:vldOQuikErKA1wfGnqvjAYB9MON/PWTuFIulMCKIQqM= github.com/apache/opendal/bindings/go v0.0.0-20240719044908-d9d4279b3a24 h1:2fAl+WS/lZMTtP6onlrmDbb3pltf+5xNTc0Aeu9nYWE= github.com/apache/opendal/bindings/go v0.0.0-20240719044908-d9d4279b3a24/go.mod h1:jyMN6M6h0jMDZitnjvB3KPobM+oZiESrFb3XUplLxhI= github.com/davecgh/go-spew v1.1.1 h1:vj9j/u1bqnvCEfJOwUhtlOARqs3+rkHYY13jYWTU97c= @@ -10,8 +12,8 @@ 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/klauspost/compress v1.17.10 h1:oXAz+Vh0PMUvJczoi+flxpnBEPxoER1IaAnU/NMPtT0= +github.com/klauspost/compress v1.17.10/go.mod h1:pMDklpSncoRMuLFrf1W9Ss9KT+0rH90U12bZKk7uwG0= 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.9.0 h1:HtqpIVDClZ4nwg75+f6Lvsy/wHu+3BoSGCbBAcpTsTg= diff --git a/bindings/go/tests/behavior_tests/list_test.go b/bindings/go/tests/behavior_tests/list_test.go index f4f26acf4c35..1f6a4a36aabd 100644 --- a/bindings/go/tests/behavior_tests/list_test.go +++ b/bindings/go/tests/behavior_tests/list_test.go @@ -201,13 +201,21 @@ func testListNestedDir(assert *require.Assertions, op *opendal.Operator, fixture assert.Nil(err) defer obs.Close() var paths []string + var foundParent bool + var foundDir bool for obs.Next() { entry := obs.Entry() paths = append(paths, entry.Path()) - assert.Equal(dir, entry.Path()) + if entry.Path() == parent { + foundParent = true + } else if entry.Path() == dir { + foundDir = true + } } assert.Nil(obs.Error()) - assert.Equal(1, len(paths), "parent should only got 1 entry") + assert.Equal(2, len(paths), "parent should only got 2 entry") + assert.Equal(foundParent, true, "parent should be found in list") + assert.Equal(foundDir, true, "dir should be found in list") obs, err = op.List(dir) assert.Nil(err) @@ -215,7 +223,7 @@ func testListNestedDir(assert *require.Assertions, op *opendal.Operator, fixture paths = nil var foundFile bool var foundDirPath bool - var foundDir bool + foundDir = false for obs.Next() { entry := obs.Entry() paths = append(paths, entry.Path()) diff --git a/bindings/go/tests/behavior_tests/opendal_test.go b/bindings/go/tests/behavior_tests/opendal_test.go index 551087c5f3ec..8991f971a3ec 100644 --- a/bindings/go/tests/behavior_tests/opendal_test.go +++ b/bindings/go/tests/behavior_tests/opendal_test.go @@ -30,6 +30,7 @@ import ( "sync" "testing" + "github.com/apache/opendal-go-services/fs" "github.com/apache/opendal-go-services/memory" opendal "github.com/apache/opendal/bindings/go" "github.com/google/uuid" @@ -39,6 +40,7 @@ import ( // Add more schemes for behavior tests here. var schemes = []opendal.Scheme{ memory.Scheme, + fs.Scheme, } var op *opendal.Operator @@ -223,8 +225,8 @@ func (f *fixture) Cleanup(assert *require.Assertions) { f.lock.Lock() defer f.lock.Unlock() - for _, path := range f.paths { - assert.Nil(f.op.Delete(path), "delete must succeed: %s", path) + for i := len(f.paths) - 1; i >= 0; i-- { + assert.Nil(f.op.Delete(f.paths[i]), "delete must succeed: %s", f.paths[i]) } } diff --git a/bindings/go/tests/behavior_tests/write_test.go b/bindings/go/tests/behavior_tests/write_test.go index 69cece7d4361..05d626972d5f 100644 --- a/bindings/go/tests/behavior_tests/write_test.go +++ b/bindings/go/tests/behavior_tests/write_test.go @@ -35,6 +35,7 @@ func testsWrite(cap *opendal.Capability) []behaviorTest { testWriteWithDirPath, testWriteWithSpecialChars, testWriteOverwrite, + testWriterWrite, } } @@ -99,3 +100,32 @@ func testWriteOverwrite(assert *require.Assertions, op *opendal.Operator, fixtur assert.NotEqual(contentOne, bs, "content_one must be overwrote") assert.Equal(contentTwo, bs, "read content_two") } + +func testWriterWrite(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + if !op.Info().GetFullCapability().WriteCanMulti() { + return + } + + path := fixture.NewFilePath() + size := uint(5 * 1024 * 1024) + contentA := genFixedBytes(size) + contentB := genFixedBytes(size) + + w, err := op.Writer(path) + assert.Nil(err) + _, err = w.Write(contentA) + assert.Nil(err) + _, err = w.Write(contentB) + assert.Nil(err) + assert.Nil(w.Close()) + + meta, err := op.Stat(path) + assert.Nil(err, "stat must succeed") + assert.Equal(uint64(size*2), meta.ContentLength()) + + bs, err := op.Read(path) + assert.Nil(err, "read must succeed") + assert.Equal(uint64(size*2), uint64(len(bs)), "read size") + assert.Equal(contentA, bs[:size], "read contentA") + assert.Equal(contentB, bs[size:], "read contentB") +} diff --git a/bindings/go/types.go b/bindings/go/types.go index 49c0daf5d3f8..59583a94afd5 100644 --- a/bindings/go/types.go +++ b/bindings/go/types.go @@ -89,6 +89,24 @@ var ( }[0], } + typeResultOperatorWriter = ffi.Type{ + Type: ffi.Struct, + Elements: &[]*ffi.Type{ + &ffi.TypePointer, + &ffi.TypePointer, + nil, + }[0], + } + + typeResultWriterWrite = ffi.Type{ + Type: ffi.Struct, + Elements: &[]*ffi.Type{ + &ffi.TypePointer, + &ffi.TypePointer, + nil, + }[0], + } + typeResultReaderRead = ffi.Type{ Type: ffi.Struct, Elements: &[]*ffi.Type{ @@ -209,6 +227,18 @@ type resultOperatorReader struct { error *opendalError } +type opendalWriter struct{} + +type resultOperatorWriter struct { + writer *opendalWriter + error *opendalError +} + +type resultWriterWrite struct { + size uint + error *opendalError +} + type resultReaderRead struct { size uint error *opendalError @@ -257,6 +287,9 @@ func toOpendalBytes(data []byte) opendalBytes { l := len(data) if l > 0 { ptr = &data[0] + } else { + var b byte + ptr = &b } return opendalBytes{ data: ptr, diff --git a/bindings/go/write.go b/bindings/go/write.go index e1d18a132234..d287524416c5 100644 --- a/bindings/go/write.go +++ b/bindings/go/write.go @@ -21,6 +21,7 @@ package opendal import ( "context" + "io" "unsafe" "github.com/jupiterrider/ffi" @@ -98,6 +99,96 @@ func (op *Operator) CreateDir(path string) error { return createDir(op.inner, path) } +// Writer returns a new Writer for the specified path. +// +// Writer is a wrapper around the C-binding function `opendal_operator_writer`. +// It provides a way to obtain a writer for writing data to the storage system. +// +// # Parameters +// +// - path: The destination path where data will be written. +// +// # Returns +// +// - *Writer: A pointer to a Writer instance, or an error if the operation fails. +// +// # Example +// +// func exampleWriter(op *opendal.Operator) { +// writer, err := op.Writer("test/") +// if err != nil { +// log.Fatal(err) +// } +// defer writer.Close() +// _, err = writer.Write([]byte("Hello opendal writer!")) +// if err != nil { +// log.Fatal(err) +// } +// } +// +// Note: This example assumes proper error handling and import statements. +func (op *Operator) Writer(path string) (*Writer, error) { + getWriter := getFFI[operatorWriter](op.ctx, + symOperatorWriter) + inner, err := getWriter(op.inner, path) + if err != nil { + return nil, err + } + writer := &Writer{ + inner: inner, + ctx: op.ctx, + } + return writer, nil +} + +type Writer struct { + inner *opendalWriter + ctx context.Context +} + +// 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. Write can be called multiple times to write +// additional data to the same path. +// +// The maximum size of the data that can be written in a single call is 256KB. +// +// # 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 (w *Writer) Write(p []byte) (n int, err error) { + write := getFFI[writerWrite](w.ctx, symWriterWrite) + return write(w.inner, p) +} + +// Close finishes the write and releases the resources associated with the Writer. +// It is important to call Close after writing all the data to ensure that the data is +// properly flushed and written to the storage. Otherwise, the data may be lost. +func (w *Writer) Close() error { + free := getFFI[writerFree](w.ctx, symWriterFree) + free(w.inner) + return nil +} + +var _ io.WriteCloser = (*Writer)(nil) + const symOperatorWrite = "opendal_operator_write" type operatorWrite func(op *opendalOperator, path string, data []byte) error @@ -113,9 +204,6 @@ var withOperatorWrite = withFFI(ffiOpts{ return err } bytes := toOpendalBytes(data) - if len(data) > 0 { - bytes.data = &data[0] - } var e *opendalError ffiCall( unsafe.Pointer(&e), @@ -150,3 +238,71 @@ var withOperatorCreateDir = withFFI(ffiOpts{ return parseError(ctx, e) } }) + +const symOperatorWriter = "opendal_operator_writer" + +type operatorWriter func(op *opendalOperator, path string) (*opendalWriter, error) + +var withOperatorWriter = withFFI(ffiOpts{ + sym: symOperatorWriter, + rType: &typeResultOperatorWriter, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, +}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues ...unsafe.Pointer)) operatorWriter { + return func(op *opendalOperator, path string) (*opendalWriter, error) { + bytePath, err := unix.BytePtrFromString(path) + if err != nil { + return nil, err + } + var result resultOperatorWriter + ffiCall( + unsafe.Pointer(&result), + unsafe.Pointer(&op), + unsafe.Pointer(&bytePath), + ) + if result.error != nil { + return nil, parseError(ctx, result.error) + } + return result.writer, nil + } +}) + +const symWriterFree = "opendal_writer_free" + +type writerFree func(w *opendalWriter) + +var withWriterFree = withFFI(ffiOpts{ + sym: symWriterFree, + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer}, +}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues ...unsafe.Pointer)) writerFree { + return func(r *opendalWriter) { + ffiCall( + nil, + unsafe.Pointer(&r), + ) + } +}) + +const symWriterWrite = "opendal_writer_write" + +type writerWrite func(r *opendalWriter, buf []byte) (size int, err error) + +var withWriterWrite = withFFI(ffiOpts{ + sym: symWriterWrite, + rType: &typeResultWriterWrite, + aTypes: []*ffi.Type{&ffi.TypePointer, &typeBytes}, +}, func(ctx context.Context, ffiCall func(rValue unsafe.Pointer, aValues ...unsafe.Pointer)) writerWrite { + return func(r *opendalWriter, data []byte) (size int, err error) { + bytes := toOpendalBytes(data) + var result resultWriterWrite + ffiCall( + unsafe.Pointer(&result), + unsafe.Pointer(&r), + unsafe.Pointer(&bytes), + ) + if result.error != nil { + return 0, parseError(ctx, result.error) + } + return int(result.size), nil + } +})