diff --git a/bindings/dotnet/OpenDAL.Tests/Behavior/ListBehaviorTest.cs b/bindings/dotnet/OpenDAL.Tests/Behavior/ListBehaviorTest.cs index 4e1a441e9210..f901090ea188 100644 --- a/bindings/dotnet/OpenDAL.Tests/Behavior/ListBehaviorTest.cs +++ b/bindings/dotnet/OpenDAL.Tests/Behavior/ListBehaviorTest.cs @@ -31,6 +31,28 @@ public ListBehaviorTest(BehaviorOperatorFixture fixture) { } + [Fact] + public void ListBehavior_Check_Succeeds() + { + if (!Supports(c => c.Read && c.Write && c.List)) + { + return; + } + + Op.Check(); + } + + [Fact] + public async Task ListBehavior_Check_SucceedsAsync() + { + if (!Supports(c => c.Read && c.Write && c.List)) + { + return; + } + + await Op.CheckAsync(CT); + } + [Fact] public void ListBehavior_ListsEntriesUnderPrefix() { diff --git a/bindings/dotnet/OpenDAL.Tests/Behavior/StatBehaviorTest.cs b/bindings/dotnet/OpenDAL.Tests/Behavior/StatBehaviorTest.cs index 45661062f1f7..791a1a8037c0 100644 --- a/bindings/dotnet/OpenDAL.Tests/Behavior/StatBehaviorTest.cs +++ b/bindings/dotnet/OpenDAL.Tests/Behavior/StatBehaviorTest.cs @@ -91,6 +91,40 @@ public async Task StatBehavior_MissingPath_ReturnsNotFoundAsync() Assert.True(IsMissingError(ex)); } + [Fact] + public void StatBehavior_Exists_ReportsPresence() + { + if (!Supports(c => c.Stat && c.Write)) + { + return; + } + + var path = NewPath("exists"); + + Assert.False(Op.Exists(path)); + + Op.Write(path, RandomBytes(16)); + + Assert.True(Op.Exists(path)); + } + + [Fact] + public async Task StatBehavior_Exists_ReportsPresenceAsync() + { + if (!Supports(c => c.Stat && c.Write)) + { + return; + } + + var path = NewPath("exists-async"); + + Assert.False(await Op.ExistsAsync(path, CT)); + + await Op.WriteAsync(path, RandomBytes(16), cancellationToken: CT); + + Assert.True(await Op.ExistsAsync(path, CT)); + } + [Fact] public void StatBehavior_WithIfModifiedSince_AppliesCondition() { diff --git a/bindings/dotnet/OpenDAL/Interop/Result/OpenDALBoolResult.cs b/bindings/dotnet/OpenDAL/Interop/Result/OpenDALBoolResult.cs new file mode 100644 index 000000000000..114b2fc2b897 --- /dev/null +++ b/bindings/dotnet/OpenDAL/Interop/Result/OpenDALBoolResult.cs @@ -0,0 +1,46 @@ +/* + * 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. + */ + +using System.Runtime.InteropServices; +using OpenDAL.Interop.Result.Abstractions; + +namespace OpenDAL.Interop.Result; + +[StructLayout(LayoutKind.Sequential)] +internal struct OpenDALBoolResult : INativeValueResult +{ + public byte Value; + + public OpenDALError Error; + + public readonly void Release() + { + NativeMethods.opendal_error_release(Error); + } + + public readonly OpenDALError GetError() + { + return Error; + } + + public readonly bool ToValue() + { + return Value != 0; + } +} diff --git a/bindings/dotnet/OpenDAL/NativeMethods.cs b/bindings/dotnet/OpenDAL/NativeMethods.cs index cdadba2c6f96..212619200309 100644 --- a/bindings/dotnet/OpenDAL/NativeMethods.cs +++ b/bindings/dotnet/OpenDAL/NativeMethods.cs @@ -332,6 +332,42 @@ long context #endregion + #region Exists + + [LibraryImport(__DllName, EntryPoint = "operator_exists", StringMarshalling = StringMarshalling.Utf8)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial OpenDALBoolResult operator_exists( + Operator op, + string path + ); + + [LibraryImport(__DllName, EntryPoint = "operator_exists_async", StringMarshalling = StringMarshalling.Utf8)] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe partial OpenDALResult operator_exists_async( + Operator op, + string path, + delegate* unmanaged[Cdecl] callback, + long context + ); + + #endregion + + #region Check + + [LibraryImport(__DllName, EntryPoint = "operator_check")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static partial OpenDALResult operator_check(Operator op); + + [LibraryImport(__DllName, EntryPoint = "operator_check_async")] + [UnmanagedCallConv(CallConvs = [typeof(CallConvCdecl)])] + internal static unsafe partial OpenDALResult operator_check_async( + Operator op, + delegate* unmanaged[Cdecl] callback, + long context + ); + + #endregion + #region List [LibraryImport(__DllName, EntryPoint = "operator_list_with_options", StringMarshalling = StringMarshalling.Utf8)] diff --git a/bindings/dotnet/OpenDAL/Operator.cs b/bindings/dotnet/OpenDAL/Operator.cs index 1421c2542f95..0dc8278c58a4 100644 --- a/bindings/dotnet/OpenDAL/Operator.cs +++ b/bindings/dotnet/OpenDAL/Operator.cs @@ -656,6 +656,94 @@ OpenDALResult SubmitStatAsync(long context, IntPtr optionsHandle) } } + /// + /// Checks whether the specified path exists. + /// + /// + /// A NotFound error from the backend yields ; + /// any other error is thrown. + /// + /// Target path in the configured backend. + /// when the path exists. + public bool Exists(string path) + { + ObjectDisposedException.ThrowIf(IsInvalid, this); + var result = NativeMethods.operator_exists(this, path); + return ToValueOrThrowAndRelease(result); + } + + /// + /// Checks whether the specified path exists asynchronously. + /// + /// + /// A NotFound error from the backend yields ; + /// any other error is thrown. + /// + /// Target path in the configured backend. + /// Cancellation token for the managed task. + /// A task that resolves with when the path exists. + public Task ExistsAsync(string path, CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(IsInvalid, this); + + return SubmitAsyncOperation(SubmitExistsAsync, cancellationToken); + + OpenDALResult SubmitExistsAsync(long context) + { + unsafe + { + return NativeMethods.operator_exists_async( + this, + path, + &OnExistsCompleted, + context + ); + } + } + } + + /// + /// Checks whether the operator can reach its service. + /// + /// + /// Lists the root with a limit of one entry. A NotFound error is + /// treated as success; any other error is thrown. + /// + public void Check() + { + ObjectDisposedException.ThrowIf(IsInvalid, this); + var result = NativeMethods.operator_check(this); + ThrowIfErrorAndRelease(result); + } + + /// + /// Checks whether the operator can reach its service asynchronously. + /// + /// + /// Lists the root with a limit of one entry. A NotFound error is + /// treated as success; any other error is thrown. + /// + /// Cancellation token for the managed task. + /// A task that completes when the native callback reports completion. + public Task CheckAsync(CancellationToken cancellationToken = default) + { + ObjectDisposedException.ThrowIf(IsInvalid, this); + + return SubmitAsyncOperation(SubmitCheckAsync, cancellationToken); + + OpenDALResult SubmitCheckAsync(long context) + { + unsafe + { + return NativeMethods.operator_check_async( + this, + &OnCheckCompleted, + context + ); + } + } + } + /// /// Lists entries under the specified path. /// @@ -1470,6 +1558,28 @@ private static void OnStatCompleted(long context, OpenDALMetadataResult result) CompleteAsyncCallback(context, result); } + /// + /// Native callback invoked when an asynchronous exists operation finishes. + /// + /// Opaque async state context previously registered by . + /// Exists completion result returned by the native layer. + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void OnExistsCompleted(long context, OpenDALBoolResult result) + { + CompleteAsyncCallback(context, result); + } + + /// + /// Native callback invoked when an asynchronous check operation finishes. + /// + /// Opaque async state context previously registered by . + /// Check completion result returned by the native layer. + [UnmanagedCallersOnly(CallConvs = [typeof(CallConvCdecl)])] + private static void OnCheckCompleted(long context, OpenDALResult result) + { + CompleteAsyncCallback(context, result); + } + /// /// Native callback invoked when an asynchronous list operation finishes. /// diff --git a/bindings/dotnet/src/operator.rs b/bindings/dotnet/src/operator.rs index a143ae7412bb..001052512fae 100644 --- a/bindings/dotnet/src/operator.rs +++ b/bindings/dotnet/src/operator.rs @@ -28,9 +28,9 @@ use crate::{ }, presign::into_presigned_request_ptr, result::{ - OpendalEntryListResult, OpendalMetadataResult, OpendalOperatorInfoResult, - OpendalOperatorResult, OpendalOptionsResult, OpendalPresignedRequestResult, - OpendalReadResult, OpendalResult, + OpendalBoolResult, OpendalEntryListResult, OpendalMetadataResult, + OpendalOperatorInfoResult, OpendalOperatorResult, OpendalOptionsResult, + OpendalPresignedRequestResult, OpendalReadResult, OpendalResult, }, utils::{collect_options, require_callback, require_cstr, require_data_ptr, require_op_handle}, validators::prelude::{ @@ -94,6 +94,7 @@ impl std::ops::Deref for OperatorHandle { /// invoked by Rust. `VoidCallback` reports success or failure only, while /// `MetadataCallback` carries the metadata that write, copy, and stat return. type VoidCallback = extern "C" fn(context: i64, result: OpendalResult); +type BoolCallback = extern "C" fn(context: i64, result: OpendalBoolResult); type ReadCallback = extern "C" fn(context: i64, result: OpendalReadResult); type MetadataCallback = extern "C" fn(context: i64, result: OpendalMetadataResult); type ListCallback = extern "C" fn(context: i64, result: OpendalEntryListResult); @@ -2230,6 +2231,151 @@ fn operator_stat_with_options_async_inner( Ok(()) } +/// Check whether `path` exists synchronously. +/// # Safety +/// +/// - `op_handle` must be a valid operator pointer from `operator_construct`. +/// - `path` must be a valid null-terminated UTF-8 string. +#[unsafe(no_mangle)] +pub extern "C" fn operator_exists( + op_handle: *const OperatorHandle, + path: *const c_char, +) -> OpendalBoolResult { + match operator_exists_inner(op_handle, path) { + Ok(value) => OpendalBoolResult::ok(value as u8), + Err(error) => OpendalBoolResult::from_error(error), + } +} + +fn operator_exists_inner( + op_handle: *const OperatorHandle, + path: *const c_char, +) -> Result { + let handle = require_op_handle(op_handle)?; + let executor = handle.executor.clone(); + let path = require_cstr(path, "path")?; + + executor + .block_on(handle.exists(path)) + .map_err(OpenDALError::from_opendal_error) +} + +/// Check whether `path` exists asynchronously. +/// +/// The callback is invoked exactly once with the final result. +/// # Safety +/// +/// - `op_handle` must be a valid operator pointer from `operator_construct`. +/// - `path` must be a valid null-terminated UTF-8 string. +/// - `callback` must be a valid function pointer and remain callable until invoked. +#[unsafe(no_mangle)] +pub extern "C" fn operator_exists_async( + op_handle: *const OperatorHandle, + path: *const c_char, + callback: Option, + context: i64, +) -> OpendalResult { + match operator_exists_async_inner(op_handle, path, callback, context) { + Ok(()) => OpendalResult::ok(), + Err(error) => OpendalResult::from_error(error), + } +} + +fn operator_exists_async_inner( + op_handle: *const OperatorHandle, + path: *const c_char, + callback: Option, + context: i64, +) -> Result<(), OpenDALError> { + let handle = require_op_handle(op_handle)?; + let executor = handle.executor.clone(); + let path = require_cstr(path, "path")?.to_string(); + let callback = require_callback(callback)?; + + let op = handle.operator(); + executor.spawn(async move { + let result = op + .exists(&path) + .await + .map_err(OpenDALError::from_opendal_error); + + callback( + context, + match result { + Ok(value) => OpendalBoolResult::ok(value as u8), + Err(error) => OpendalBoolResult::from_error(error), + }, + ); + }); + + Ok(()) +} + +/// Check whether the operator can reach its service. +/// # Safety +/// +/// - `op_handle` must be a valid operator pointer from `operator_construct`. +#[unsafe(no_mangle)] +pub extern "C" fn operator_check(op_handle: *const OperatorHandle) -> OpendalResult { + match operator_check_inner(op_handle) { + Ok(()) => OpendalResult::ok(), + Err(error) => OpendalResult::from_error(error), + } +} + +fn operator_check_inner(op_handle: *const OperatorHandle) -> Result<(), OpenDALError> { + let handle = require_op_handle(op_handle)?; + let executor = handle.executor.clone(); + + executor + .block_on(handle.check()) + .map_err(OpenDALError::from_opendal_error) +} + +/// Check whether the operator can reach its service asynchronously. +/// +/// The callback is invoked exactly once with the final result. +/// # Safety +/// +/// - `op_handle` must be a valid operator pointer from `operator_construct`. +/// - `callback` must be a valid function pointer and remain callable until invoked. +#[unsafe(no_mangle)] +pub extern "C" fn operator_check_async( + op_handle: *const OperatorHandle, + callback: Option, + context: i64, +) -> OpendalResult { + match operator_check_async_inner(op_handle, callback, context) { + Ok(()) => OpendalResult::ok(), + Err(error) => OpendalResult::from_error(error), + } +} + +fn operator_check_async_inner( + op_handle: *const OperatorHandle, + callback: Option, + context: i64, +) -> Result<(), OpenDALError> { + let handle = require_op_handle(op_handle)?; + let executor = handle.executor.clone(); + let callback = require_callback(callback)?; + + let op = handle.operator(); + executor.spawn(async move { + let result = op.check().await.map_err(OpenDALError::from_opendal_error); + + callback( + context, + match result { + Ok(()) => OpendalResult::ok(), + Err(error) => OpendalResult::from_error(error), + }, + ); + }); + + Ok(()) +} + /// List entries from `path` synchronously with options. /// /// On success, returned payload must be released with `opendal_entry_list_result_release`. diff --git a/bindings/dotnet/src/result.rs b/bindings/dotnet/src/result.rs index 617fea7ab9a7..519eeb140e3a 100644 --- a/bindings/dotnet/src/result.rs +++ b/bindings/dotnet/src/result.rs @@ -48,6 +48,14 @@ pub struct OpendalResult { pub error: OpenDALError, } +#[repr(C)] +/// Result for operations returning a boolean value. +pub struct OpendalBoolResult { + /// `1` for true, `0` for false. + pub value: u8, + pub error: OpenDALError, +} + #[repr(C)] /// Result for operations returning an operator handle pointer. pub struct OpendalOperatorResult { @@ -155,6 +163,12 @@ macro_rules! define_result { define_result!(OpendalResult); +define_result!( + OpendalBoolResult, + field = value: u8, + error_value = 0 +); + define_result!( OpendalOperatorResult, field = ptr: *mut c_void, diff --git a/website/docs/20-bindings/dotnet/03-connecting.md b/website/docs/20-bindings/dotnet/03-connecting.md index 5f4d06c96e8b..ec96f9b69a83 100644 --- a/website/docs/20-bindings/dotnet/03-connecting.md +++ b/website/docs/20-bindings/dotnet/03-connecting.md @@ -94,6 +94,16 @@ page under [Services](/services) for the exact keys and credential behavior. Avoid hard-coding secrets in source. Read them from the environment or a secret manager and pass them in when you build the operator. +## Verify the connection + +`Check` lists the service root and throws `OpenDALException` when the service +is unreachable or rejects the request. Call it at startup so wrong credentials +or endpoints fail there instead of on the first real operation: + +```csharp +await op.CheckAsync(); +``` + ## One operator per service and root An operator maps to one service, one root path, and one executor. To work with diff --git a/website/docs/20-bindings/dotnet/04-tasks.md b/website/docs/20-bindings/dotnet/04-tasks.md index 451c3d31e09b..a52a380b3124 100644 --- a/website/docs/20-bindings/dotnet/04-tasks.md +++ b/website/docs/20-bindings/dotnet/04-tasks.md @@ -128,6 +128,17 @@ buffered before flushing to the backend. ## Check existence and metadata +`Exists` returns `false` instead of throwing when the path is missing: + +```csharp +if (op.Exists("path/to/file")) +{ + Console.WriteLine("found"); +} +``` + +`Stat` returns the metadata: + ```csharp var meta = op.Stat("path/to/file"); Console.WriteLine($"{meta.ContentLength} bytes, dir = {meta.IsDir}");