using System; using System.Collections.Generic; using System.Net.Http; using System.Threading.Tasks; using Frostfs.V2.Ape; using Frostfs.V2.Apemanager; using FrostFS.Accounting; using FrostFS.Container; using FrostFS.Netmap; using FrostFS.Object; using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Services; using FrostFS.SDK.Cryptography; using FrostFS.Session; using Grpc.Core; using Grpc.Core.Interceptors; using Grpc.Net.Client; using Microsoft.Extensions.Options; namespace FrostFS.SDK.ClientV2; public class FrostFSClient : IFrostFSClient { private bool isDisposed; internal ContainerService.ContainerServiceClient? ContainerServiceClient { get; set; } internal NetmapService.NetmapServiceClient? NetmapServiceClient { get; set; } internal APEManagerService.APEManagerServiceClient? ApeManagerServiceClient { get; set; } internal SessionService.SessionServiceClient? SessionServiceClient { get; set; } internal ObjectService.ObjectServiceClient? ObjectServiceClient { get; set; } internal AccountingService.AccountingServiceClient? AccountingServiceClient { get; set; } internal ClientContext ClientCtx { get; set; } public static IFrostFSClient GetInstance(IOptions clientOptions, GrpcChannelOptions? channelOptions = null) { return new FrostFSClient(clientOptions, channelOptions); } public static IFrostFSClient GetSingleOwnerInstance(IOptions clientOptions, GrpcChannelOptions? channelOptions = null) { return new FrostFSClient(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) { if (clientOptions is null) { throw new ArgumentNullException(nameof(clientOptions)); } return new FrostFSClient(clientOptions, channelOptions, containerService, netmapService, sessionService, objectService); } private FrostFSClient( IOptions settings, GrpcChannelOptions? channelOptions, ContainerService.ContainerServiceClient containerService, NetmapService.NetmapServiceClient netmapService, SessionService.SessionServiceClient sessionService, ObjectService.ObjectServiceClient objectService) { if (settings is null) { throw new ArgumentNullException(nameof(settings)); } var ecdsaKey = settings.Value.Key.LoadWif(); FrostFsOwner.FromKey(ecdsaKey); ClientCtx = new ClientContext( client: this, key: ecdsaKey, owner: FrostFsOwner.FromKey(ecdsaKey), channel: InitGrpcChannel(settings.Value.Host, channelOptions), version: new FrostFsVersion(2, 13)); ContainerServiceClient = containerService ?? throw new ArgumentNullException(nameof(containerService)); NetmapServiceClient = netmapService ?? throw new ArgumentNullException(nameof(netmapService)); SessionServiceClient = sessionService ?? throw new ArgumentNullException(nameof(sessionService)); ObjectServiceClient = objectService ?? throw new ArgumentNullException(nameof(objectService)); } private FrostFSClient(IOptions options, GrpcChannelOptions? channelOptions) { var clientSettings = (options?.Value) ?? throw new ArgumentNullException(nameof(options), "Options value must be initialized"); clientSettings.Validate(); var channel = InitGrpcChannel(clientSettings.Host, channelOptions); ClientCtx = new ClientContext( this, key: null, owner: null, channel: channel, version: new FrostFsVersion(2, 13)); // TODO: define timeout logic // CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) }); } private FrostFSClient(IOptions options, GrpcChannelOptions? channelOptions) { var clientSettings = (options?.Value) ?? throw new ArgumentNullException(nameof(options), "Options value must be initialized"); clientSettings.Validate(); var ecdsaKey = clientSettings.Key.LoadWif(); var channel = InitGrpcChannel(clientSettings.Host, channelOptions); ClientCtx = new ClientContext( this, key: ecdsaKey, owner: FrostFsOwner.FromKey(ecdsaKey), channel: channel, version: new FrostFsVersion(2, 13)); // TODO: define timeout logic // CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20) }); } internal FrostFSClient(WrapperPrm prm, SessionCache cache) { ClientCtx = new ClientContext( client: this, key: prm.Key, owner: FrostFsOwner.FromKey(prm.Key!), channel: InitGrpcChannel(prm.Address, null), //prm.GrpcChannelOptions), version: new FrostFsVersion(2, 13)) { SessionCache = cache }; } public void Dispose() { Dispose(true); GC.SuppressFinalize(this); } protected virtual void Dispose(bool disposing) { if (disposing && !isDisposed) { ClientCtx?.Dispose(); isDisposed = true; } } #region ApeManagerImplementation public Task AddChainAsync(PrmApeChainAdd args) { if (args is null) { throw new ArgumentNullException(nameof(args)); } var service = GetApeManagerService(args); return service.AddChainAsync(args); } public Task RemoveChainAsync(PrmApeChainRemove args) { if (args is null) { throw new ArgumentNullException(nameof(args)); } var service = GetApeManagerService(args); return service.RemoveChainAsync(args); } public Task ListChainAsync(PrmApeChainList args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetApeManagerService(args); return service.ListChainAsync(args); } #endregion #region ContainerImplementation public Task GetContainerAsync(PrmContainerGet args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetContainerService(args); return service.GetContainerAsync(args); } public IAsyncEnumerable ListContainersAsync(PrmContainerGetAll? args = null) { args ??= new PrmContainerGetAll(); var service = GetContainerService(args); return service.ListContainersAsync(args); } public Task CreateContainerAsync(PrmContainerCreate args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetContainerService(args); return service.CreateContainerAsync(args); } public Task DeleteContainerAsync(PrmContainerDelete args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetContainerService(args); return service.DeleteContainerAsync(args); } #endregion #region NetworkImplementation public Task GetNetmapSnapshotAsync(PrmNetmapSnapshot? args) { args ??= new PrmNetmapSnapshot(); var service = GetNetmapService(args); return service.GetNetmapSnapshotAsync(args); } public Task GetNodeInfoAsync(PrmNodeInfo? args) { args ??= new PrmNodeInfo(); var service = GetNetmapService(args); return service.GetLocalNodeInfoAsync(args); } public Task GetNetworkSettingsAsync(PrmNetworkSettings? args) { args ??= new PrmNetworkSettings(); var service = GetNetmapService(args); return service.GetNetworkSettingsAsync(args.Context!); } #endregion #region ObjectImplementation public Task GetObjectHeadAsync(PrmObjectHeadGet args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.GetObjectHeadAsync(args); } public Task GetObjectAsync(PrmObjectGet args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.GetObjectAsync(args); } public Task GetRangeAsync(PrmRangeGet args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.GetRangeAsync(args); } public Task>> GetRangeHashAsync(PrmRangeHashGet args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.GetRangeHashAsync(args); } public Task PutObjectAsync(PrmObjectPut args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.PutObjectAsync(args); } public Task PutSingleObjectAsync(PrmSingleObjectPut args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.PutSingleObjectAsync(args); } public Task PatchObjectAsync(PrmObjectPatch args) { if (args is null) { throw new ArgumentNullException(nameof(args)); } var service = GetObjectService(args); return service.PatchObjectAsync(args); } public Task DeleteObjectAsync(PrmObjectDelete args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.DeleteObjectAsync(args); } public IAsyncEnumerable SearchObjectsAsync(PrmObjectSearch args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetObjectService(args); return service.SearchObjectsAsync(args); } #endregion #region Session Implementation public async Task CreateSessionAsync(PrmSessionCreate args) { if (args is null) throw new ArgumentNullException(nameof(args)); var session = await CreateSessionInternalAsync(args).ConfigureAwait(false); var token = session.Serialize(); return new FrostFsSessionToken(token, session.Body.Id.ToUuid()); } internal Task CreateSessionInternalAsync(PrmSessionCreate args) { if (args is null) throw new ArgumentNullException(nameof(args)); var service = GetSessionService(args); return service.CreateSessionAsync(args); } #endregion #region Accounting Implementation public async Task GetBalanceAsync(PrmBalance? args) { args ??= new PrmBalance(); var service = GetAccouningService(args); return await service.GetBallance(args).ConfigureAwait(false); } #endregion #region ToolsImplementation public FrostFsObjectId CalculateObjectId(FrostFsObjectHeader header, CallContext ctx) { if (header == null) throw new ArgumentNullException(nameof(header)); return ObjectTools.CalculateObjectId(header, ctx); } #endregion private async void CheckFrostFsVersionSupport(CallContext? ctx = default) { var args = new PrmNodeInfo(ctx); if (ctx?.Version == null) throw new ArgumentNullException(nameof(ctx), "Version must be initialized"); var service = GetNetmapService(args); var localNodeInfo = await service.GetLocalNodeInfoAsync(args).ConfigureAwait(false); if (!localNodeInfo.Version.IsSupported(ctx.Version)) { var msg = $"FrostFS {localNodeInfo.Version} is not supported."; throw new FrostFsException(msg); } } private CallInvoker? SetupClientContext(IContext ctx) { if (isDisposed) throw new FrostFsInvalidObjectException("Client is disposed."); if (ctx.Context!.Key == null) { if (ClientCtx.Key == null) { throw new ArgumentNullException(nameof(ctx), "Key is not initialized."); } ctx.Context.Key = ClientCtx.Key.ECDsaKey; } if (ctx.Context.OwnerId == null) { ctx.Context.OwnerId = ClientCtx.Owner ?? FrostFsOwner.FromKey(ctx.Context.Key); } if (ctx.Context.Version == null) { if (ClientCtx.Version == null) { throw new ArgumentNullException(nameof(ctx), "Version is not initialized."); } ctx.Context.Version = ClientCtx.Version; } CallInvoker? callInvoker = null; foreach (var interceptor in ctx.Context.Interceptors) callInvoker = AddInvoker(callInvoker, interceptor); if (ctx.Context.Callback != null) callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback)); if (ctx.Context.PoolErrorHandler != null) callInvoker = AddInvoker(callInvoker, new ErrorInterceptor(ctx.Context.PoolErrorHandler)); return callInvoker; CallInvoker AddInvoker(CallInvoker? callInvoker, Interceptor interceptor) { if (callInvoker == null) callInvoker = ClientCtx.Channel.Intercept(interceptor); else callInvoker = callInvoker.Intercept(interceptor); return callInvoker; } } private NetmapServiceProvider GetNetmapService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = NetmapServiceClient ?? (callInvoker != null ? new NetmapService.NetmapServiceClient(callInvoker) : new NetmapService.NetmapServiceClient(ClientCtx.Channel)); return new NetmapServiceProvider(client, ClientCtx); } private SessionServiceProvider GetSessionService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = SessionServiceClient ?? (callInvoker != null ? new SessionService.SessionServiceClient(callInvoker) : new SessionService.SessionServiceClient(ClientCtx.Channel)); return new SessionServiceProvider(client, ClientCtx); } private ApeManagerServiceProvider GetApeManagerService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = ApeManagerServiceClient ?? (callInvoker != null ? new APEManagerService.APEManagerServiceClient(callInvoker) : new APEManagerService.APEManagerServiceClient(ClientCtx.Channel)); return new ApeManagerServiceProvider(client, ClientCtx); } private AccountingServiceProvider GetAccouningService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = AccountingServiceClient ?? (callInvoker != null ? new AccountingService.AccountingServiceClient(callInvoker) : new AccountingService.AccountingServiceClient(ClientCtx.Channel)); return new AccountingServiceProvider(client, ClientCtx); } private ContainerServiceProvider GetContainerService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = ContainerServiceClient ?? (callInvoker != null ? new ContainerService.ContainerServiceClient(callInvoker) : new ContainerService.ContainerServiceClient(ClientCtx.Channel)); return new ContainerServiceProvider(client, ClientCtx); } private ObjectServiceProvider GetObjectService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = ObjectServiceClient ?? (callInvoker != null ? new ObjectService.ObjectServiceClient(callInvoker) : new ObjectService.ObjectServiceClient(ClientCtx.Channel)); return new ObjectServiceProvider(client, ClientCtx); } private AccountingServiceProvider GetAccountService(IContext ctx) { var callInvoker = SetupClientContext(ctx); var client = AccountingServiceClient ?? (callInvoker != null ? new AccountingService.AccountingServiceClient(callInvoker) : new AccountingService.AccountingServiceClient(ClientCtx.Channel)); return new AccountingServiceProvider(client, ClientCtx); } private static GrpcChannel InitGrpcChannel(string host, GrpcChannelOptions? channelOptions) { try { var uri = new Uri(host); if (channelOptions != null) return GrpcChannel.ForAddress(uri, channelOptions); return GrpcChannel.ForAddress(uri, new GrpcChannelOptions { HttpHandler = new HttpClientHandler() }); } catch (UriFormatException e) { throw new ArgumentException($"Host '{host}' has invalid format. Error: {e.Message}"); } } public async Task Dial(CallContext ctx) { var prm = new PrmBalance(ctx); var service = GetAccouningService(prm); _ = await service.GetBallance(prm).ConfigureAwait(false); return null; } public bool RestartIfUnhealthy(CallContext ctx) { throw new NotImplementedException(); } public void Close() { Dispose(); } }