using FrostFS.Container; using FrostFS.Netmap; using FrostFS.Object; using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.Cryptography; using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2.Netmap; using FrostFS.Session; using Grpc.Core; using Grpc.Core.Interceptors; using Grpc.Net.Client; using Microsoft.Extensions.Options; using System; using System.Collections.Generic; using System.Net.Http; using System.Security.Cryptography; using System.Threading.Tasks; using Version = FrostFS.SDK.ModelsV2.Version; namespace FrostFS.SDK.ClientV2; public class Client : IFrostFSClient { private bool isDisposed; internal ContainerService.ContainerServiceClient? ContainerServiceClient { get; set; } internal NetmapService.NetmapServiceClient? NetmapServiceClient { get; set; } internal SessionService.SessionServiceClient? SessionServiceClient { get; set; } internal ObjectService.ObjectServiceClient? ObjectServiceClient { get; set; } internal ClientEnvironment ClientCtx { get; set; } public static IFrostFSClient GetInstance(IOptions clientOptions, GrpcChannelOptions? channelOptions = null) { return new Client(clientOptions, channelOptions); } /// /// For test only. Provide custom implementation or mock object to inject required logic instead of internal gRPC client. /// /// Global setting for client /// Setting for gRPC channel /// ContainerService.ContainerServiceClient implementation /// Netmap.NetmapService.NetmapServiceClient implementation /// Session.SessionService.SessionServiceClient implementation /// Object.ObjectService.ObjectServiceClient implementation /// public static IFrostFSClient GetTestInstance( IOptions clientOptions, GrpcChannelOptions? channelOptions, NetmapService.NetmapServiceClient netmapService, SessionService.SessionServiceClient sessionService, ContainerService.ContainerServiceClient containerService, ObjectService.ObjectServiceClient objectService) { return new Client(clientOptions, channelOptions, containerService, netmapService, sessionService, objectService); } private Client( IOptions settings, GrpcChannelOptions? channelOptions, ContainerService.ContainerServiceClient containerService, NetmapService.NetmapServiceClient netmapService, SessionService.SessionServiceClient sessionService, ObjectService.ObjectServiceClient objectService) { var ecdsaKey = settings.Value.Key.LoadWif(); OwnerId.FromKey(ecdsaKey); ClientCtx = new ClientEnvironment( this, key: ecdsaKey, owner: OwnerId.FromKey(ecdsaKey), channel: InitGrpcChannel(settings.Value.Host, channelOptions), version: new Version(2, 13)); ContainerServiceClient = containerService; NetmapServiceClient = netmapService; SessionServiceClient = sessionService; ObjectServiceClient = objectService; } private Client(IOptions options, GrpcChannelOptions? channelOptions) { var clientSettings = (options?.Value) ?? throw new ArgumentException("Options must be initialized"); clientSettings.Validate(); var ecdsaKey = clientSettings.Key.LoadWif(); var channel = InitGrpcChannel(clientSettings.Host, channelOptions); ClientCtx = new ClientEnvironment( this, key: ecdsaKey, owner: OwnerId.FromKey(ecdsaKey), channel: channel, version: new Version(2, 13)); CheckFrostFsVersionSupport(); } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing && !isDisposed) { ClientCtx.Dispose(); isDisposed = true; } } #region ContainerImplementation public Task GetContainerAsync(ContainerId containerId, Context? ctx = null) { var service = GetContainerService(ref ctx); return service.GetContainerAsync(containerId, ctx!); } public IAsyncEnumerable ListContainersAsync(Context? ctx = default) { var service = GetContainerService(ref ctx); return service.ListContainersAsync(ctx!); } public Task CreateContainerAsync(ModelsV2.Container container, Context? ctx = null) { var service = GetContainerService(ref ctx); return service.CreateContainerAsync(container, ctx!); } public Task DeleteContainerAsync(ContainerId containerId, Context? ctx = default) { var service = GetContainerService(ref ctx); return service.DeleteContainerAsync(containerId, ctx!); } #endregion #region NetworkImplementation public Task GetNetmapSnapshotAsync(Context? ctx = default) { var service = GetNetmapService(ref ctx); return service.GetNetmapSnapshotAsync(ctx!); } public Task GetNodeInfoAsync(Context? ctx = default) { var service = GetNetmapService(ref ctx); return service.GetLocalNodeInfoAsync(ctx!); } public Task GetNetworkSettingsAsync(Context? ctx = default) { var service = GetNetmapService(ref ctx); return service.GetNetworkSettingsAsync(ctx!); } #endregion #region ObjectImplementation public Task GetObjectHeadAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) { var service = GetObjectService(ref ctx); return service.GetObjectHeadAsync(containerId, objectId, ctx!); } public Task GetObjectAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) { var service = GetObjectService(ref ctx); return service.GetObjectAsync(containerId, objectId, ctx!); } public Task PutObjectAsync(PutObjectParameters putObjectParameters, Context? ctx = default) { var service = GetObjectService(ref ctx); return service.PutObjectAsync(putObjectParameters, ctx!); } public Task PutSingleObjectAsync(ModelsV2.Object obj, Context? ctx = default) { var service = GetObjectService(ref ctx); return service.PutSingleObjectAsync(obj, ctx!); } public Task DeleteObjectAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) { var service = GetObjectService(ref ctx); return service.DeleteObjectAsync(containerId, objectId, ctx!); } public IAsyncEnumerable SearchObjectsAsync( ContainerId containerId, IEnumerable filters, Context? ctx = default) { var service = GetObjectService(ref ctx); return service.SearchObjectsAsync(containerId, filters, ctx!); } #endregion #region SessionImplementation public async Task CreateSessionAsync(ulong expiration, Context? ctx = null) { var session = await CreateSessionInternalAsync(expiration, ctx); var token = session.Serialize(); return new ModelsV2.SessionToken([], token); } public Task CreateSessionInternalAsync(ulong expiration, Context? ctx = null) { var service = GetSessionService(ref ctx); return service.CreateSessionAsync(expiration, ctx!); } #endregion #region ToolsImplementation public ObjectId CalculateObjectId(ObjectHeader header) { return new ObjectTools(ClientCtx).CalculateObjectId(header); } #endregion private async void CheckFrostFsVersionSupport(Context? ctx = default) { var service = GetNetmapService(ref ctx); var localNodeInfo = await service.GetLocalNodeInfoAsync(ctx!); if (!localNodeInfo.Version.IsSupported(ClientCtx.Version)) { var msg = $"FrostFS {localNodeInfo.Version} is not supported."; Console.WriteLine(msg); throw new ApplicationException(msg); } } private CallInvoker? SetupEnvironment(ref Context? ctx) { if (isDisposed) throw new Exception("Client is disposed."); ctx ??= new Context(); CallInvoker? callInvoker = null; if (ctx.Interceptors != null && ctx.Interceptors.Count > 0) { foreach (var interceptor in ctx.Interceptors) { callInvoker = AddInvoker(callInvoker, interceptor); } } if (ctx.Callback != null) callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Callback)); return callInvoker; CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor) { if (callInvoker == null) callInvoker = ClientCtx.Channel.Intercept(interceptor); else callInvoker.Intercept(interceptor); return callInvoker; } } private NetmapServiceProvider GetNetmapService(ref Context? ctx) { var callInvoker = SetupEnvironment(ref ctx); var client = NetmapServiceClient ?? (callInvoker != null ? new NetmapService.NetmapServiceClient(callInvoker) : new NetmapService.NetmapServiceClient(ClientCtx.Channel)); return new NetmapServiceProvider(client, ClientCtx); } private SessionServiceProvider GetSessionService(ref Context? ctx) { var callInvoker = SetupEnvironment(ref ctx); var client = SessionServiceClient ?? (callInvoker != null ? new SessionService.SessionServiceClient(callInvoker) : new SessionService.SessionServiceClient(ClientCtx.Channel)); return new SessionServiceProvider(client, ClientCtx); } private ContainerServiceProvider GetContainerService(ref Context? ctx) { var callInvoker = SetupEnvironment(ref ctx); var client = ContainerServiceClient ?? (callInvoker != null ? new ContainerService.ContainerServiceClient(callInvoker) : new ContainerService.ContainerServiceClient(ClientCtx.Channel)); return new ContainerServiceProvider(client, ClientCtx); } private ObjectServiceProvider GetObjectService(ref Context? ctx) { var callInvoker = SetupEnvironment(ref ctx); var client = ObjectServiceClient ?? (callInvoker != null ? new ObjectService.ObjectServiceClient(callInvoker) : new ObjectService.ObjectServiceClient(ClientCtx.Channel)); return new ObjectServiceProvider(client, ClientCtx); } private static GrpcChannel InitGrpcChannel(string host, GrpcChannelOptions? channelOptions) { Uri uri; try { uri = new Uri(host); } catch (UriFormatException e) { var msg = $"Host '{host}' has invalid format. Error: {e.Message}"; throw new ArgumentException(msg); } if (channelOptions != null) { return GrpcChannel.ForAddress(uri, channelOptions); } return GrpcChannel.ForAddress(uri, new GrpcChannelOptions { HttpHandler = new HttpClientHandler() }); } }