diff --git a/bindings/c/include/opendal.h b/bindings/c/include/opendal.h index d08668d141b3..643cc8da2161 100644 --- a/bindings/c/include/opendal.h +++ b/bindings/c/include/opendal.h @@ -734,6 +734,47 @@ typedef struct opendal_list_options { bool deleted; } opendal_list_options; +/** + * \brief The options for copy operations. + * + * Use `opendal_copy_options_new()` to construct and + * `opendal_copy_options_free()` to free. + */ +typedef struct opendal_copy_options { + /** + * Only copy if target does not exist; default false. + */ + bool if_not_exists; + /** + * If-Match condition; NULL means unset. + */ + const char *if_match; + /** + * Source version; NULL means unset. + */ + const char *source_version; + /** + * Whether `source_content_length_hint` has been set. + */ + bool has_source_content_length_hint; + /** + * Known content length of the source object. + */ + uint64_t source_content_length_hint; + /** + * Concurrent copy operations. `0` means sequential copy. + */ + uintptr_t concurrent; + /** + * Whether `chunk` has been set. + */ + bool has_chunk; + /** + * Chunk size for segmented copy operations. + */ + uintptr_t chunk; +} opendal_copy_options; + /** * \brief Metadata for **operator**, users can use this metadata to get information * of operator. @@ -915,6 +956,34 @@ typedef struct opendal_capability { * If operator supports copy. */ bool copy; + /** + * If operator supports copy with if not exists. + */ + bool copy_with_if_not_exists; + /** + * If operator supports copy with if match. + */ + bool copy_with_if_match; + /** + * If operator supports copy with source version. + */ + bool copy_with_source_version; + /** + * If operator supports copy can be split into multiple server-side tasks. + */ + bool copy_can_multi; + /** + * copy_multi_max_size is the max size supported for segmented copy tasks. + * + * If it is not set, this will be zero + */ + uintptr_t copy_multi_max_size; + /** + * copy_multi_min_size is the min size required for segmented copy tasks. + * + * If it is not set, this will be zero + */ + uintptr_t copy_multi_min_size; /** * If operator supports rename. */ @@ -1964,6 +2033,64 @@ struct opendal_error *opendal_operator_copy(const struct opendal_operator *op, const char *src, const char *dest); +/** + * \brief Blocking copy the object in `path` with options. + * + * Copy the object from `src` to `dest` blocking by `op`, using the provided + * `opendal_copy_options` to control the behavior, e.g. `if_not_exists` or + * `if_match` conditions. + * + * @param op The opendal_operator created previously + * @param src The designated source path you want to copy + * @param dest The designated destination path you want to copy + * @param opts The options for the copy operation; pass NULL to use defaults + * @see opendal_operator + * @see opendal_copy_options + * @see opendal_error + * @return NULL if succeeds, otherwise it contains the error code and error message. + * + * # Example + * + * Following is an example + * ```C + * //...prepare your opendal_operator, named op for example + * + * // prepare your data + * char* data = "Hello, World!"; + * opendal_bytes bytes = opendal_bytes { .data = (uint8_t*)data, .len = 13 }; + * opendal_error *error = opendal_operator_write(op, "/testpath", bytes); + * + * assert(error == NULL); + * + * // prepare options + * opendal_copy_options *opts = opendal_copy_options_new(); + * opendal_copy_options_set_if_not_exists(opts, true); + * + * // now you can copy with options! + * opendal_error *error = opendal_operator_copy_with(op, "/testpath", "/testpath2", opts); + * + * // Assert that this succeeds + * assert(error == NULL); + * + * // remember to free the options + * opendal_copy_options_free(opts); + * ``` + * + * # Safety + * + * It is **safe** under the cases below + * * The memory pointed to by `src` and `dest` must contain a valid nul terminator at the end of + * the string. + * + * # Panic + * + * * If the `src` or `dest` points to NULL, this function panics, i.e. exits with information + */ +struct opendal_error *opendal_operator_copy_with(const struct opendal_operator *op, + const char *src, + const char *dest, + const struct opendal_copy_options *opts); + struct opendal_error *opendal_operator_check(const struct opendal_operator *op); /** @@ -2386,6 +2513,48 @@ void opendal_read_options_set_override_cache_control(struct opendal_read_options void opendal_read_options_set_override_content_disposition(struct opendal_read_options *opts, const char *override_content_disposition); +/** + * \brief Construct a heap-allocated opendal_copy_options with default values. + */ +struct opendal_copy_options *opendal_copy_options_new(void); + +/** + * \brief Free the heap memory used by opendal_copy_options. + */ +void opendal_copy_options_free(struct opendal_copy_options *opts); + +/** + * \brief Set if_not_exists. + */ +void opendal_copy_options_set_if_not_exists(struct opendal_copy_options *opts, bool if_not_exists); + +/** + * \brief Set If-Match. + */ +void opendal_copy_options_set_if_match(struct opendal_copy_options *opts, const char *if_match); + +/** + * \brief Set source version. + */ +void opendal_copy_options_set_source_version(struct opendal_copy_options *opts, + const char *source_version); + +/** + * \brief Set source_content_length_hint. + */ +void opendal_copy_options_set_source_content_length_hint(struct opendal_copy_options *opts, + uint64_t source_content_length_hint); + +/** + * \brief Set concurrent. + */ +void opendal_copy_options_set_concurrent(struct opendal_copy_options *opts, uintptr_t concurrent); + +/** + * \brief Set chunk. + */ +void opendal_copy_options_set_chunk(struct opendal_copy_options *opts, uintptr_t chunk); + /** * \brief Construct a heap-allocated opendal_operator_options * diff --git a/bindings/c/src/lib.rs b/bindings/c/src/lib.rs index 21d5d821bf86..1ef4148a2d83 100644 --- a/bindings/c/src/lib.rs +++ b/bindings/c/src/lib.rs @@ -72,6 +72,7 @@ pub use result::opendal_result_writer_write; mod types; pub use types::opendal_bytes; +pub use types::opendal_copy_options; pub use types::opendal_delete_options; pub use types::opendal_list_options; pub use types::opendal_operator_options; diff --git a/bindings/c/src/operator.rs b/bindings/c/src/operator.rs index 00ae40968e76..cb610640242f 100644 --- a/bindings/c/src/operator.rs +++ b/bindings/c/src/operator.rs @@ -1221,6 +1221,84 @@ pub unsafe extern "C" fn opendal_operator_copy( } } +/// \brief Blocking copy the object in `path` with options. +/// +/// Copy the object from `src` to `dest` blocking by `op`, using the provided +/// `opendal_copy_options` to control the behavior, e.g. `if_not_exists` or +/// `if_match` conditions. +/// +/// @param op The opendal_operator created previously +/// @param src The designated source path you want to copy +/// @param dest The designated destination path you want to copy +/// @param opts The options for the copy operation; pass NULL to use defaults +/// @see opendal_operator +/// @see opendal_copy_options +/// @see opendal_error +/// @return NULL if succeeds, otherwise it contains the error code and error message. +/// +/// # Example +/// +/// Following is an example +/// ```C +/// //...prepare your opendal_operator, named op for example +/// +/// // prepare your data +/// char* data = "Hello, World!"; +/// opendal_bytes bytes = opendal_bytes { .data = (uint8_t*)data, .len = 13 }; +/// opendal_error *error = opendal_operator_write(op, "/testpath", bytes); +/// +/// assert(error == NULL); +/// +/// // prepare options +/// opendal_copy_options *opts = opendal_copy_options_new(); +/// opendal_copy_options_set_if_not_exists(opts, true); +/// +/// // now you can copy with options! +/// opendal_error *error = opendal_operator_copy_with(op, "/testpath", "/testpath2", opts); +/// +/// // Assert that this succeeds +/// assert(error == NULL); +/// +/// // remember to free the options +/// opendal_copy_options_free(opts); +/// ``` +/// +/// # Safety +/// +/// It is **safe** under the cases below +/// * The memory pointed to by `src` and `dest` must contain a valid nul terminator at the end of +/// the string. +/// +/// # Panic +/// +/// * If the `src` or `dest` points to NULL, this function panics, i.e. exits with information +#[no_mangle] +pub unsafe extern "C" fn opendal_operator_copy_with( + op: &opendal_operator, + src: *const c_char, + dest: *const c_char, + opts: *const opendal_copy_options, +) -> *mut opendal_error { + assert!(!src.is_null()); + assert!(!dest.is_null()); + let src = std::ffi::CStr::from_ptr(src) + .to_str() + .expect("malformed src"); + let dest = std::ffi::CStr::from_ptr(dest) + .to_str() + .expect("malformed dest"); + let copy_opts = if opts.is_null() { + core::options::CopyOptions::default() + } else { + core::options::CopyOptions::from(&*opts) + }; + if let Err(err) = op.deref().copy_options(src, dest, copy_opts) { + opendal_error::new(err) + } else { + std::ptr::null_mut() + } +} + #[no_mangle] pub unsafe extern "C" fn opendal_operator_check(op: &opendal_operator) -> *mut opendal_error { if let Err(err) = op.deref().check() { diff --git a/bindings/c/src/operator_info.rs b/bindings/c/src/operator_info.rs index 4e0e79be8094..9b9818a74bfa 100644 --- a/bindings/c/src/operator_info.rs +++ b/bindings/c/src/operator_info.rs @@ -136,6 +136,22 @@ pub struct opendal_capability { /// If operator supports copy. pub copy: bool, + /// If operator supports copy with if not exists. + pub copy_with_if_not_exists: bool, + /// If operator supports copy with if match. + pub copy_with_if_match: bool, + /// If operator supports copy with source version. + pub copy_with_source_version: bool, + /// If operator supports copy can be split into multiple server-side tasks. + pub copy_can_multi: bool, + /// copy_multi_max_size is the max size supported for segmented copy tasks. + /// + /// If it is not set, this will be zero + pub copy_multi_max_size: usize, + /// copy_multi_min_size is the min size required for segmented copy tasks. + /// + /// If it is not set, this will be zero + pub copy_multi_min_size: usize, /// If operator supports rename. pub rename: bool, @@ -298,6 +314,12 @@ impl From for opendal_capability { delete_with_version: value.delete_with_version, delete_with_recursive: value.delete_with_recursive, copy: value.copy, + copy_with_if_not_exists: value.copy_with_if_not_exists, + copy_with_if_match: value.copy_with_if_match, + copy_with_source_version: value.copy_with_source_version, + copy_can_multi: value.copy_can_multi, + copy_multi_max_size: value.copy_multi_max_size.unwrap_or(0), + copy_multi_min_size: value.copy_multi_min_size.unwrap_or(0), rename: value.rename, list: value.list, list_with_limit: value.list_with_limit, diff --git a/bindings/c/src/types.rs b/bindings/c/src/types.rs index 80f508723e9e..8ee219aaeab3 100644 --- a/bindings/c/src/types.rs +++ b/bindings/c/src/types.rs @@ -1018,6 +1018,153 @@ impl From<&opendal_read_options> for options::ReadOptions { } } +/// \brief The options for copy operations. +/// +/// Use `opendal_copy_options_new()` to construct and +/// `opendal_copy_options_free()` to free. +#[repr(C)] +pub struct opendal_copy_options { + /// Only copy if target does not exist; default false. + pub if_not_exists: bool, + /// If-Match condition; NULL means unset. + pub if_match: *const c_char, + /// Source version; NULL means unset. + pub source_version: *const c_char, + /// Whether `source_content_length_hint` has been set. + pub has_source_content_length_hint: bool, + /// Known content length of the source object. + pub source_content_length_hint: u64, + /// Concurrent copy operations. `0` means sequential copy. + pub concurrent: usize, + /// Whether `chunk` has been set. + pub has_chunk: bool, + /// Chunk size for segmented copy operations. + pub chunk: usize, +} + +impl opendal_copy_options { + /// \brief Construct a heap-allocated opendal_copy_options with default values. + #[no_mangle] + pub extern "C" fn opendal_copy_options_new() -> *mut Self { + Box::into_raw(Box::new(Self::default())) + } + + /// \brief Free the heap memory used by opendal_copy_options. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_free(opts: *mut opendal_copy_options) { + if !opts.is_null() { + drop(Box::from_raw(opts)); + } + } + + /// \brief Set if_not_exists. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_set_if_not_exists( + opts: *mut opendal_copy_options, + if_not_exists: bool, + ) { + if !opts.is_null() { + (*opts).if_not_exists = if_not_exists; + } + } + + /// \brief Set If-Match. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_set_if_match( + opts: *mut opendal_copy_options, + if_match: *const c_char, + ) { + if !opts.is_null() { + (*opts).if_match = if_match; + } + } + + /// \brief Set source version. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_set_source_version( + opts: *mut opendal_copy_options, + source_version: *const c_char, + ) { + if !opts.is_null() { + (*opts).source_version = source_version; + } + } + + /// \brief Set source_content_length_hint. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_set_source_content_length_hint( + opts: *mut opendal_copy_options, + source_content_length_hint: u64, + ) { + if !opts.is_null() { + (*opts).has_source_content_length_hint = true; + (*opts).source_content_length_hint = source_content_length_hint; + } + } + + /// \brief Set concurrent. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_set_concurrent( + opts: *mut opendal_copy_options, + concurrent: usize, + ) { + if !opts.is_null() { + (*opts).concurrent = concurrent; + } + } + + /// \brief Set chunk. + #[no_mangle] + pub unsafe extern "C" fn opendal_copy_options_set_chunk( + opts: *mut opendal_copy_options, + chunk: usize, + ) { + if !opts.is_null() { + (*opts).has_chunk = true; + (*opts).chunk = chunk; + } + } +} + +impl Default for opendal_copy_options { + fn default() -> Self { + Self { + if_not_exists: false, + if_match: std::ptr::null(), + source_version: std::ptr::null(), + has_source_content_length_hint: false, + source_content_length_hint: 0, + concurrent: 0, + has_chunk: false, + chunk: 0, + } + } +} + +impl From<&opendal_copy_options> for options::CopyOptions { + fn from(value: &opendal_copy_options) -> Self { + Self { + if_not_exists: value.if_not_exists, + if_match: unsafe { optional_cstr(value.if_match) }, + source_version: unsafe { optional_cstr(value.source_version) }, + source_content_length_hint: value + .has_source_content_length_hint + .then_some(value.source_content_length_hint), + concurrent: value.concurrent, + chunk: value.has_chunk.then_some(value.chunk), + } + } +} + +impl Drop for opendal_bytes { + fn drop(&mut self) { + unsafe { + // Safety: the pointer is always valid + Self::opendal_bytes_free(self); + } + } +} + impl From<&opendal_bytes> for Buffer { fn from(v: &opendal_bytes) -> Self { let slice = unsafe { std::slice::from_raw_parts(v.data, v.len) }; diff --git a/bindings/go/copy.go b/bindings/go/copy.go new file mode 100644 index 000000000000..39cb4be0bcc1 --- /dev/null +++ b/bindings/go/copy.go @@ -0,0 +1,297 @@ +/* + * 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" + "runtime" + "unsafe" + + "github.com/jupiterrider/ffi" +) + +// WithCopyFn is a functional option for copy operations. +type WithCopyFn func(*copyOptions) + +// CopyWithIfNotExists sets whether the copy operation should only succeed +// if the target does not exist. +func CopyWithIfNotExists(ifNotExists bool) WithCopyFn { + return func(o *copyOptions) { + o.ifNotExists = ifNotExists + } +} + +// CopyWithIfMatch sets the If-Match condition for the copy operation. +// +// The copy will only succeed when the destination object's ETag matches +// the given value. +func CopyWithIfMatch(ifMatch string) WithCopyFn { + return func(o *copyOptions) { + o.ifMatch = ifMatch + } +} + +// CopyWithSourceVersion sets the source version for the copy operation. +// +// The copy will read from the specified source version instead of the +// current source object. +func CopyWithSourceVersion(sourceVersion string) WithCopyFn { + return func(o *copyOptions) { + o.sourceVersion = sourceVersion + } +} + +// CopyWithSourceContentLengthHint provides a hint of the source object's +// content length, avoiding extra metadata requests. +func CopyWithSourceContentLengthHint(length uint64) WithCopyFn { + return func(o *copyOptions) { + o.sourceContentLengthHint = length + } +} + +// CopyWithConcurrent sets the maximum number of concurrent copy operations. +func CopyWithConcurrent(concurrent uint) WithCopyFn { + return func(o *copyOptions) { + o.concurrent = concurrent + } +} + +// CopyWithChunk sets the chunk size for segmented copy operations. +func CopyWithChunk(chunk uint) WithCopyFn { + return func(o *copyOptions) { + o.chunk = chunk + } +} + +type copyOptions struct { + ifNotExists bool + ifMatch string + sourceVersion string + sourceContentLengthHint uint64 + concurrent uint + chunk uint +} + +// Copy duplicates a file from the source path to the destination path. +// +// Copy is a wrapper around the C-binding function `opendal_operator_copy`. +// When options are provided, it uses `opendal_operator_copy_with`. +// +// # Parameters +// +// - src: The source file path. +// - dest: The destination file path. +// - opts: Optional copy options. +// +// # Returns +// +// - error: An error if the copy operation fails, or nil if successful. +// +// # Behavior +// +// - Both src and dest must be file paths, not directories. +// - If dest already exists, it will be overwritten. +// - If src and dest 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 *opendal.Operator) { +// err := op.Copy("path/from/file", "path/to/file") +// if err != nil { +// log.Fatal(err) +// } +// } +// +// Note: This example assumes proper error handling and import statements. +func (op *Operator) Copy(src, dest string, opts ...WithCopyFn) error { + if len(opts) == 0 { + return ffiOperatorCopy.symbol(op.ctx)(op.inner, src, dest) + } + + o := ©Options{} + for _, opt := range opts { + opt(o) + } + cOpts := ffiCopyOptionsNew.symbol(op.ctx)() + free := ffiCopyOptionsFree.symbol(op.ctx) + + ffiCopyOptionsSetIfNotExists.symbol(op.ctx)(cOpts, o.ifNotExists) + var ifMatchData []byte + if o.ifMatch != "" { + data, err := ffiCopyOptionsSetIfMatch.symbol(op.ctx)(cOpts, o.ifMatch) + if err != nil { + free(cOpts) + return err + } + ifMatchData = data + } + var sourceVersionData []byte + if o.sourceVersion != "" { + data, err := ffiCopyOptionsSetSourceVersion.symbol(op.ctx)(cOpts, o.sourceVersion) + if err != nil { + free(cOpts) + return err + } + sourceVersionData = data + } + if o.sourceContentLengthHint != 0 { + ffiCopyOptionsSetSourceContentLengthHint.symbol(op.ctx)(cOpts, o.sourceContentLengthHint) + } + if o.concurrent != 0 { + ffiCopyOptionsSetConcurrent.symbol(op.ctx)(cOpts, o.concurrent) + } + if o.chunk != 0 { + ffiCopyOptionsSetChunk.symbol(op.ctx)(cOpts, o.chunk) + } + err := ffiOperatorCopyWith.symbol(op.ctx)(op.inner, src, dest, cOpts) + free(cOpts) + runtime.KeepAlive(ifMatchData) + runtime.KeepAlive(sourceVersionData) + return err +} + +var ffiCopyOptionsNew = newFFI(ffiOpts{ + sym: "opendal_copy_options_new", + rType: &ffi.TypePointer, +}, func(_ context.Context, ffiCall ffiCall) func() *opendalCopyOptions { + return func() *opendalCopyOptions { + var opts *opendalCopyOptions + ffiCall(unsafe.Pointer(&opts)) + return opts + } +}) + +var ffiCopyOptionsFree = newFFI(ffiOpts{ + sym: "opendal_copy_options_free", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions) { + return func(opts *opendalCopyOptions) { + ffiCall( + nil, + unsafe.Pointer(&opts), + ) + } +}) + +var ffiCopyOptionsSetIfNotExists = newFFI(ffiOpts{ + sym: "opendal_copy_options_set_if_not_exists", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypeUint8}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions, ifNotExists bool) { + return func(opts *opendalCopyOptions, ifNotExists bool) { + var v uint8 + if ifNotExists { + v = 1 + } + ffiCall(nil, unsafe.Pointer(&opts), unsafe.Pointer(&v)) + } +}) + +var ffiCopyOptionsSetIfMatch = newFFI(ffiOpts{ + sym: "opendal_copy_options_set_if_match", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions, ifMatch string) ([]byte, error) { + return func(opts *opendalCopyOptions, ifMatch string) ([]byte, error) { + data, err := byteSliceFromString(ifMatch) + if err != nil { + return nil, err + } + byteValue := &data[0] + ffiCall(nil, unsafe.Pointer(&opts), unsafe.Pointer(&byteValue)) + return data, nil + } +}) + +var ffiCopyOptionsSetSourceVersion = newFFI(ffiOpts{ + sym: "opendal_copy_options_set_source_version", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions, sourceVersion string) ([]byte, error) { + return func(opts *opendalCopyOptions, sourceVersion string) ([]byte, error) { + data, err := byteSliceFromString(sourceVersion) + if err != nil { + return nil, err + } + byteValue := &data[0] + ffiCall(nil, unsafe.Pointer(&opts), unsafe.Pointer(&byteValue)) + return data, nil + } +}) + +var ffiCopyOptionsSetSourceContentLengthHint = newFFI(ffiOpts{ + sym: "opendal_copy_options_set_source_content_length_hint", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypeUint64}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions, sourceContentLengthHint uint64) { + return func(opts *opendalCopyOptions, sourceContentLengthHint uint64) { + ffiCall(nil, unsafe.Pointer(&opts), unsafe.Pointer(&sourceContentLengthHint)) + } +}) + +var ffiCopyOptionsSetConcurrent = newFFI(ffiOpts{ + sym: "opendal_copy_options_set_concurrent", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions, concurrent uint) { + return func(opts *opendalCopyOptions, concurrent uint) { + c := uintptr(concurrent) + ffiCall(nil, unsafe.Pointer(&opts), unsafe.Pointer(&c)) + } +}) + +var ffiCopyOptionsSetChunk = newFFI(ffiOpts{ + sym: "opendal_copy_options_set_chunk", + rType: &ffi.TypeVoid, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer}, +}, func(_ context.Context, ffiCall ffiCall) func(opts *opendalCopyOptions, chunk uint) { + return func(opts *opendalCopyOptions, chunk uint) { + c := uintptr(chunk) + ffiCall(nil, unsafe.Pointer(&opts), unsafe.Pointer(&c)) + } +}) + +var ffiOperatorCopyWith = newFFI(ffiOpts{ + sym: "opendal_operator_copy_with", + rType: &ffi.TypePointer, + aTypes: []*ffi.Type{&ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer, &ffi.TypePointer}, +}, func(ctx context.Context, ffiCall ffiCall) func(op *opendalOperator, src, dest string, opts *opendalCopyOptions) error { + return func(op *opendalOperator, src, dest string, opts *opendalCopyOptions) error { + byteSrc, err := BytePtrFromString(src) + if err != nil { + return err + } + byteDest, err := BytePtrFromString(dest) + if err != nil { + return err + } + var e *opendalError + ffiCall( + unsafe.Pointer(&e), + unsafe.Pointer(&op), + unsafe.Pointer(&byteSrc), + unsafe.Pointer(&byteDest), + unsafe.Pointer(&opts), + ) + return parseError(ctx, e) + } +}) diff --git a/bindings/go/operator.go b/bindings/go/operator.go index 792b0d953abf..ac0094494221 100644 --- a/bindings/go/operator.go +++ b/bindings/go/operator.go @@ -27,42 +27,6 @@ import ( "github.com/jupiterrider/ffi" ) -// 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 { - return ffiOperatorCopy.symbol(op.ctx)(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. diff --git a/bindings/go/operator_info.go b/bindings/go/operator_info.go index c8d842ebcd61..664579ee14c8 100644 --- a/bindings/go/operator_info.go +++ b/bindings/go/operator_info.go @@ -251,6 +251,30 @@ func (c *Capability) Copy() bool { return c.inner.copy == 1 } +func (c *Capability) CopyWithIfNotExists() bool { + return c.inner.copyWithIfNotExists == 1 +} + +func (c *Capability) CopyWithIfMatch() bool { + return c.inner.copyWithIfMatch == 1 +} + +func (c *Capability) CopyWithSourceVersion() bool { + return c.inner.copyWithSourceVersion == 1 +} + +func (c *Capability) CopyCanMulti() bool { + return c.inner.copyCanMulti == 1 +} + +func (c *Capability) CopyMultiMaxSize() uint { + return c.inner.copyMultiMaxSize +} + +func (c *Capability) CopyMultiMinSize() uint { + return c.inner.copyMultiMinSize +} + func (c *Capability) Rename() bool { return c.inner.rename == 1 } diff --git a/bindings/go/tests/behavior_tests/copy_test.go b/bindings/go/tests/behavior_tests/copy_test.go index 1e9f33fa157a..eaf812694f3b 100644 --- a/bindings/go/tests/behavior_tests/copy_test.go +++ b/bindings/go/tests/behavior_tests/copy_test.go @@ -22,7 +22,7 @@ package opendal_test import ( "fmt" - "github.com/apache/opendal/bindings/go" + opendal "github.com/apache/opendal/bindings/go" "github.com/google/uuid" "github.com/stretchr/testify/require" ) @@ -31,16 +31,30 @@ func testsCopy(cap *opendal.Capability) []behaviorTest { if !cap.Read() || !cap.Write() || !cap.Copy() { return nil } - return []behaviorTest{ + tests := []behaviorTest{ testCopyFileWithASCIIName, testCopyFileWithNonASCIIName, testCopyNonExistingSource, - testCopySourceDir, - testCopyTargetDir, testCopySelf, testCopyNested, testCopyOverwrite, } + if cap.CreateDir() { + tests = append(tests, testCopySourceDir, testCopyTargetDir) + } + if isCapEnabled(cap.CopyWithIfNotExists, "copy_with_if_not_exists") { + tests = append(tests, testCopyWithIfNotExistsToNewFile, testCopyWithIfNotExistsToExistingFile) + } + if isCapEnabled(cap.CopyWithIfMatch, "copy_with_if_match") { + tests = append(tests, testCopyWithIfMatchMatch, testCopyWithIfMatchMismatch) + } + if isCapEnabled(cap.CopyWithSourceVersion, "copy_with_source_version") { + tests = append(tests, testCopyWithSourceVersionToNewFile, testCopyWithSourceVersionToSameFile) + } + if isCapEnabled(cap.CopyCanMulti, "copy_can_multi") { + tests = append(tests, testCopyWithChunk, testCopyWithChunkAndConcurrent) + } + return tests } func testCopyFileWithASCIIName(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { @@ -79,10 +93,6 @@ func testCopyNonExistingSource(assert *require.Assertions, op *opendal.Operator, } func testCopySourceDir(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { - if !op.Info().GetFullCapability().CreateDir() { - return - } - sourcePath := fixture.NewDirPath() targetPath := uuid.NewString() @@ -94,10 +104,6 @@ func testCopySourceDir(assert *require.Assertions, op *opendal.Operator, fixture } 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)) @@ -156,3 +162,158 @@ func testCopyOverwrite(assert *require.Assertions, op *opendal.Operator, fixture assert.Nil(err, "read must succeed") assert.Equal(sourceContent, targetContent) } + +func testCopyWithIfNotExistsToNewFile(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, opendal.CopyWithIfNotExists(true))) + + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(sourceContent, bs) +} + +func testCopyWithIfNotExistsToExistingFile(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + sourcePath, sourceContent, _ := fixture.NewFile() + assert.Nil(op.Write(sourcePath, sourceContent)) + + targetPath, targetContent, _ := fixture.NewFile() + assert.Nil(op.Write(targetPath, targetContent)) + + err := op.Copy(sourcePath, targetPath, opendal.CopyWithIfNotExists(true)) + assert.NotNil(err) + assert.Equal(opendal.CodeConditionNotMatch, assertErrorCode(err)) + + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(targetContent, bs, "target must not be overwritten") +} + +func testCopyWithIfMatchMatch(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)) + + meta, err := op.Stat(targetPath) + assert.Nil(err, "stat must succeed") + etag, ok := meta.ETag() + assert.True(ok, "etag must exist") + + assert.Nil(op.Copy(sourcePath, targetPath, opendal.CopyWithIfMatch(etag))) + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(sourceContent, bs) +} + +func testCopyWithIfMatchMismatch(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)) + + err := op.Copy(sourcePath, targetPath, opendal.CopyWithIfMatch("wrong-etag")) + assert.NotNil(err) + assert.Equal(opendal.CodeConditionNotMatch, assertErrorCode(err)) + + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(targetContent, bs, "target must not be overwritten") +} + +func testCopyWithSourceVersionToNewFile(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + sourcePath, sourceContent, _ := fixture.NewFile() + assert.Nil(op.Write(sourcePath, sourceContent)) + + meta, err := op.Stat(sourcePath) + assert.Nil(err, "stat must succeed") + version, ok := meta.Version() + if !ok { + return + } + + newContent := genFixedBytes(uint(len(sourceContent)) + 1) + assert.Nil(op.Write(sourcePath, newContent), "overwrite must succeed") + + targetPath := fixture.NewFilePath() + assert.Nil(op.Copy(sourcePath, targetPath, opendal.CopyWithSourceVersion(version))) + + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(sourceContent, bs) +} + +func testCopyWithSourceVersionToSameFile(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + sourcePath, sourceContent, _ := fixture.NewFile() + assert.Nil(op.Write(sourcePath, sourceContent)) + + meta, err := op.Stat(sourcePath) + assert.Nil(err, "stat must succeed") + version, ok := meta.Version() + if !ok { + return + } + + newContent := genFixedBytes(uint(len(sourceContent)) + 1) + assert.Nil(op.Write(sourcePath, newContent), "overwrite must succeed") + + assert.Nil(op.Copy(sourcePath, sourcePath, opendal.CopyWithSourceVersion(version))) + + bs, err := op.Read(sourcePath) + assert.Nil(err, "read must succeed") + assert.Equal(sourceContent, bs) +} + +func copyMultiChunkSize(cap *opendal.Capability) (uint, uint) { + chunk := cap.CopyMultiMinSize() + if chunk == 0 { + return 0, 0 + } + maxChunk := cap.CopyMultiMaxSize() + if maxChunk != 0 && chunk > maxChunk { + return 0, 0 + } + return chunk, chunk + 1 +} + +func testCopyWithChunk(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + chunk, sourceSize := copyMultiChunkSize(op.Info().GetFullCapability()) + if sourceSize == 0 { + return + } + + sourcePath := fixture.NewFilePath() + sourceContent := genFixedBytes(sourceSize) + assert.Nil(op.Write(sourcePath, sourceContent)) + + targetPath := fixture.NewFilePath() + assert.Nil(op.Copy(sourcePath, targetPath, opendal.CopyWithChunk(uint(chunk)))) + + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(sourceContent, bs) +} + +func testCopyWithChunkAndConcurrent(assert *require.Assertions, op *opendal.Operator, fixture *fixture) { + chunk, sourceSize := copyMultiChunkSize(op.Info().GetFullCapability()) + if sourceSize == 0 { + return + } + + sourcePath := fixture.NewFilePath() + sourceContent := genFixedBytes(sourceSize) + assert.Nil(op.Write(sourcePath, sourceContent)) + + targetPath := fixture.NewFilePath() + assert.Nil(op.Copy(sourcePath, targetPath, opendal.CopyWithChunk(uint(chunk)), opendal.CopyWithConcurrent(4))) + + bs, err := op.Read(targetPath) + assert.Nil(err, "read must succeed") + assert.Equal(sourceContent, bs) +} diff --git a/bindings/go/types.go b/bindings/go/types.go index b74908a66549..bd39c4177cee 100644 --- a/bindings/go/types.go +++ b/bindings/go/types.go @@ -185,6 +185,12 @@ var ( &ffi.TypeUint8, // delete_with_version &ffi.TypeUint8, // delete_with_recursive &ffi.TypeUint8, // copy + &ffi.TypeUint8, // copy_with_if_not_exists + &ffi.TypeUint8, // copy_with_if_match + &ffi.TypeUint8, // copy_with_source_version + &ffi.TypeUint8, // copy_can_multi + &ffi.TypePointer, // copy_multi_max_size + &ffi.TypePointer, // copy_multi_min_size &ffi.TypeUint8, // rename &ffi.TypeUint8, // list &ffi.TypeUint8, // list_with_limit @@ -242,6 +248,12 @@ type opendalCapability struct { deleteWithVersion uint8 deleteWithRecursive uint8 copy uint8 + copyWithIfNotExists uint8 + copyWithIfMatch uint8 + copyWithSourceVersion uint8 + copyCanMulti uint8 + copyMultiMaxSize uint + copyMultiMinSize uint rename uint8 list uint8 listWithLimit uint8 @@ -355,6 +367,8 @@ type opendalDeleteOptions struct{} type opendalReadOptions struct{} +type opendalCopyOptions struct{} + type opendalWriteOptions struct{} type opendalStatOptions struct{}