diff --git a/src/FrostFS.SDK.ClientV2/Client.cs b/src/FrostFS.SDK.ClientV2/Client.cs index 6e9cbd0..6d09283 100644 --- a/src/FrostFS.SDK.ClientV2/Client.cs +++ b/src/FrostFS.SDK.ClientV2/Client.cs @@ -2,6 +2,7 @@ using FrostFS.Netmap; using FrostFS.Object; using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.ClientV2.Parameters; using FrostFS.SDK.Cryptography; using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2.Netmap; @@ -96,7 +97,8 @@ public class Client : IFrostFSClient channel: channel, version: new Version(2, 13)); - CheckFrostFsVersionSupport(); + // TODO: define timeout logic + CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20)}); } public void Dispose() @@ -115,105 +117,106 @@ public class Client : IFrostFSClient } #region ContainerImplementation - public Task GetContainerAsync(ContainerId containerId, Context? ctx = null) + public Task GetContainerAsync(PrmGetContainer args) { - var service = GetContainerService(ref ctx); - return service.GetContainerAsync(containerId, ctx!); + var service = GetContainerService(args); + return service.GetContainerAsync(args); } - public IAsyncEnumerable ListContainersAsync(Context? ctx = default) + public IAsyncEnumerable ListContainersAsync(PrmListContainer? args = null) { - var service = GetContainerService(ref ctx); - return service.ListContainersAsync(ctx!); + args = args ?? new PrmListContainer(); + var service = GetContainerService(args); + return service.ListContainersAsync(args); } - public Task CreateContainerAsync(ModelsV2.Container container, Context? ctx = null) + public Task CreateContainerAsync(PrmCreateContainer args) { - var service = GetContainerService(ref ctx); - return service.CreateContainerAsync(container, ctx!); + var service = GetContainerService(args); + return service.CreateContainerAsync(args); } - public Task DeleteContainerAsync(ContainerId containerId, Context? ctx = default) + public Task DeleteContainerAsync(PrmDeleteContainer args) { - var service = GetContainerService(ref ctx); - return service.DeleteContainerAsync(containerId, ctx!); + var service = GetContainerService(args); + return service.DeleteContainerAsync(args); } #endregion #region NetworkImplementation - public Task GetNetmapSnapshotAsync(Context? ctx = default) + public Task GetNetmapSnapshotAsync(PrmGetNetmapSnapshot? args) { - var service = GetNetmapService(ref ctx); - return service.GetNetmapSnapshotAsync(ctx!); + args ??= new PrmGetNetmapSnapshot(); + var service = GetNetmapService(args); + return service.GetNetmapSnapshotAsync(args); } - public Task GetNodeInfoAsync(Context? ctx = default) + public Task GetNodeInfoAsync(PrmGetNodeInfo? args) { - var service = GetNetmapService(ref ctx); - return service.GetLocalNodeInfoAsync(ctx!); + args ??= new PrmGetNodeInfo(); + var service = GetNetmapService(args); + return service.GetLocalNodeInfoAsync(args); } - public Task GetNetworkSettingsAsync(Context? ctx = default) - { - var service = GetNetmapService(ref ctx); - return service.GetNetworkSettingsAsync(ctx!); + public Task GetNetworkSettingsAsync(PrmGetNetworkSettings? args) + { + args ??= new PrmGetNetworkSettings(); + var service = GetNetmapService(args); + return service.GetNetworkSettingsAsync(args.Context!); } #endregion #region ObjectImplementation - public Task GetObjectHeadAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) + public Task GetObjectHeadAsync(PrmGetObjectHead args) { - var service = GetObjectService(ref ctx); - return service.GetObjectHeadAsync(containerId, objectId, ctx!); + var service = GetObjectService(args); + return service.GetObjectHeadAsync(args); } - public Task GetObjectAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) + public Task GetObjectAsync(PrmGetObject args) { - var service = GetObjectService(ref ctx); - return service.GetObjectAsync(containerId, objectId, ctx!); + var service = GetObjectService(args); + return service.GetObjectAsync(args); } - public Task PutObjectAsync(PutObjectParameters putObjectParameters, Context? ctx = default) + public Task PutObjectAsync(PrmPutObject args) { - var service = GetObjectService(ref ctx); - return service.PutObjectAsync(putObjectParameters, ctx!); + var service = GetObjectService(args); + return service.PutObjectAsync(args); } - public Task PutSingleObjectAsync(FrostFsObject obj, Context? ctx = default) + public Task PutSingleObjectAsync(PrmPutSingleObject args) { - var service = GetObjectService(ref ctx); - return service.PutSingleObjectAsync(obj, ctx!); + var service = GetObjectService(args); + return service.PutSingleObjectAsync(args); } - public Task DeleteObjectAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) + public Task DeleteObjectAsync(PrmDeleteObject args) { - var service = GetObjectService(ref ctx); - return service.DeleteObjectAsync(containerId, objectId, ctx!); + var service = GetObjectService(args); + return service.DeleteObjectAsync(args); } - public IAsyncEnumerable SearchObjectsAsync( - ContainerId containerId, - IEnumerable filters, - Context? ctx = default) + public IAsyncEnumerable SearchObjectsAsync(PrmSearchObject args) { - var service = GetObjectService(ref ctx); - return service.SearchObjectsAsync(containerId, filters, ctx!); + var service = GetObjectService(args); + return service.SearchObjectsAsync(args); } #endregion #region SessionImplementation - public async Task CreateSessionAsync(ulong expiration, Context? ctx = null) + public async Task CreateSessionAsync(PrmCreateSession args) { - var session = await CreateSessionInternalAsync(expiration, ctx); + var session = await CreateSessionInternalAsync(args); var token = session.Serialize(); - return new ModelsV2.SessionToken([], token); + return new ModelsV2.SessionToken(token); } - public Task CreateSessionInternalAsync(ulong expiration, Context? ctx = null) + internal Task CreateSessionInternalAsync(PrmCreateSession args) { - var service = GetSessionService(ref ctx); - return service.CreateSessionAsync(expiration, ctx!); + var service = GetSessionService(args); + return service.CreateSessionAsync(args); } #endregion @@ -226,8 +229,9 @@ public class Client : IFrostFSClient private async void CheckFrostFsVersionSupport(Context? ctx = default) { - var service = GetNetmapService(ref ctx); - var localNodeInfo = await service.GetLocalNodeInfoAsync(ctx!); + var args = new PrmGetNodeInfo(ctx); + var service = GetNetmapService(args); + var localNodeInfo = await service.GetLocalNodeInfoAsync(args); if (!localNodeInfo.Version.IsSupported(ClientCtx.Version)) { @@ -237,24 +241,24 @@ public class Client : IFrostFSClient } } - private CallInvoker? SetupEnvironment(ref Context? ctx) + private CallInvoker? SetupEnvironment(IContext ctx) { if (isDisposed) throw new Exception("Client is disposed."); - ctx ??= new Context(); + ctx.Context ??= new Context(); CallInvoker? callInvoker = null; - if (ctx.Interceptors != null && ctx.Interceptors.Count > 0) + if (ctx.Context.Interceptors != null && ctx.Context.Interceptors.Count > 0) { - foreach (var interceptor in ctx.Interceptors) + foreach (var interceptor in ctx.Context.Interceptors) { callInvoker = AddInvoker(callInvoker, interceptor); } } - if (ctx.Callback != null) - callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Callback)); + if (ctx.Context.Callback != null) + callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback)); return callInvoker; @@ -269,9 +273,9 @@ public class Client : IFrostFSClient } } - private NetmapServiceProvider GetNetmapService(ref Context? ctx) + private NetmapServiceProvider GetNetmapService(IContext ctx) { - var callInvoker = SetupEnvironment(ref ctx); + var callInvoker = SetupEnvironment(ctx); var client = NetmapServiceClient ?? (callInvoker != null ? new NetmapService.NetmapServiceClient(callInvoker) : new NetmapService.NetmapServiceClient(ClientCtx.Channel)); @@ -279,9 +283,9 @@ public class Client : IFrostFSClient return new NetmapServiceProvider(client, ClientCtx); } - private SessionServiceProvider GetSessionService(ref Context? ctx) + private SessionServiceProvider GetSessionService(IContext ctx) { - var callInvoker = SetupEnvironment(ref ctx); + var callInvoker = SetupEnvironment(ctx); var client = SessionServiceClient ?? (callInvoker != null ? new SessionService.SessionServiceClient(callInvoker) : new SessionService.SessionServiceClient(ClientCtx.Channel)); @@ -289,9 +293,9 @@ public class Client : IFrostFSClient return new SessionServiceProvider(client, ClientCtx); } - private ContainerServiceProvider GetContainerService(ref Context? ctx) + private ContainerServiceProvider GetContainerService(IContext ctx) { - var callInvoker = SetupEnvironment(ref ctx); + var callInvoker = SetupEnvironment(ctx); var client = ContainerServiceClient ?? (callInvoker != null ? new ContainerService.ContainerServiceClient(callInvoker) : new ContainerService.ContainerServiceClient(ClientCtx.Channel)); @@ -299,9 +303,9 @@ public class Client : IFrostFSClient return new ContainerServiceProvider(client, ClientCtx); } - private ObjectServiceProvider GetObjectService(ref Context? ctx) + private ObjectServiceProvider GetObjectService(IContext ctx) { - var callInvoker = SetupEnvironment(ref ctx); + var callInvoker = SetupEnvironment(ctx); var client = ObjectServiceClient ?? (callInvoker != null ? new ObjectService.ObjectServiceClient(callInvoker) : new ObjectService.ObjectServiceClient(ClientCtx.Channel)); diff --git a/src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs b/src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs new file mode 100644 index 0000000..7772e1e --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Exceptions/ResponseException.cs @@ -0,0 +1,12 @@ +using System; +using FrostFS.SDK.ModelsV2; + +public class ResponseException : Exception +{ + public Status Status { get; set; } + + public ResponseException(Status status) : base() + { + Status = status; + } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs index bed4136..a483abc 100644 --- a/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs +++ b/src/FrostFS.SDK.ClientV2/Interfaces/IFrostFSClient.cs @@ -1,52 +1,50 @@ using System; using System.Collections.Generic; using System.Threading.Tasks; - +using FrostFS.SDK.ClientV2.Parameters; using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2.Netmap; - namespace FrostFS.SDK.ClientV2.Interfaces; public interface IFrostFSClient : IDisposable { #region Network - Task GetNetmapSnapshotAsync(Context? context = default); + Task GetNetmapSnapshotAsync(PrmGetNetmapSnapshot? args = null); - Task GetNodeInfoAsync(Context? context = default); + Task GetNodeInfoAsync(PrmGetNodeInfo? args = null); - Task GetNetworkSettingsAsync(Context? context = default); + Task GetNetworkSettingsAsync(PrmGetNetworkSettings? args = null); #endregion #region Session - Task CreateSessionAsync(ulong expiration, Context? context = default); + Task CreateSessionAsync(PrmCreateSession args); #endregion #region Container - Task GetContainerAsync(ContainerId containerId, Context? context = default); + Task GetContainerAsync(PrmGetContainer args); - IAsyncEnumerable ListContainersAsync(Context? context = default); + IAsyncEnumerable ListContainersAsync(PrmListContainer? args = null); - Task CreateContainerAsync(ModelsV2.Container container, Context? context = default); + Task CreateContainerAsync(PrmCreateContainer args); - Task DeleteContainerAsync(ContainerId containerId, Context? context = default); + Task DeleteContainerAsync(PrmDeleteContainer args); #endregion #region Object - Task GetObjectHeadAsync(ContainerId containerId, ObjectId objectId, Context? context = default); + Task GetObjectHeadAsync(PrmGetObjectHead args); - Task GetObjectAsync(ContainerId containerId, ObjectId objectId, Context? context = default); + Task GetObjectAsync(PrmGetObject args); - Task PutObjectAsync(PutObjectParameters putObjectParameters, Context? context = default); + Task PutObjectAsync(PrmPutObject putObjectParameters); - Task PutSingleObjectAsync(FrostFsObject obj, Context? context = default); + Task PutSingleObjectAsync(PrmPutSingleObject args); - Task DeleteObjectAsync(ContainerId containerId, ObjectId objectId, Context? context = default); + Task DeleteObjectAsync(PrmDeleteObject args); - IAsyncEnumerable SearchObjectsAsync(ContainerId cid, IEnumerable filters, Context? context = default); + IAsyncEnumerable SearchObjectsAsync(PrmSearchObject args); #endregion #region Tools ObjectId CalculateObjectId(ObjectHeader header); #endregion } - diff --git a/src/FrostFS.SDK.ClientV2/Mappers/Object/Object.cs b/src/FrostFS.SDK.ClientV2/Mappers/Object/Object.cs index 2804b65..bda31fe 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/Object/Object.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/Object/Object.cs @@ -2,13 +2,13 @@ using FrostFS.SDK.ModelsV2; namespace FrostFS.SDK.ClientV2.Mappers.GRPC; -public static class ObjectMapper +internal static class ObjectMapper { - public static FrostFsObject ToModel(this Object.Object obj) + internal static FrostFsObject ToModel(this Object.Object obj) { - return new FrostFsObject( - ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()), - obj.Header.ToModel(), - obj.Payload.ToByteArray()); + return new FrostFsObject(obj.Header.ToModel()) + { + ObjectId = ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()) + }; } } diff --git a/src/FrostFS.SDK.ClientV2/Mappers/Session/Session.cs b/src/FrostFS.SDK.ClientV2/Mappers/Session/SessionMapper.cs similarity index 62% rename from src/FrostFS.SDK.ClientV2/Mappers/Session/Session.cs rename to src/FrostFS.SDK.ClientV2/Mappers/Session/SessionMapper.cs index e565891..97b1c69 100644 --- a/src/FrostFS.SDK.ClientV2/Mappers/Session/Session.cs +++ b/src/FrostFS.SDK.ClientV2/Mappers/Session/SessionMapper.cs @@ -4,7 +4,7 @@ namespace FrostFS.SDK.ClientV2; public static class SessionMapper { - internal static byte[] Serialize(this Session.SessionToken token) + public static byte[] Serialize(this Session.SessionToken token) { byte[] bytes = new byte[token.CalculateSize()]; CodedOutputStream stream = new(bytes); @@ -13,11 +13,9 @@ public static class SessionMapper return bytes; } - internal static Session.SessionToken DeserializeSessionToken(this byte[] bytes) + public static Session.SessionToken Deserialize(this Session.SessionToken token, byte[] bytes) { - Session.SessionToken token = new(); token.MergeFrom(bytes); - return token; } -} \ No newline at end of file +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs b/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs new file mode 100644 index 0000000..c48dc8a --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/IContext.cs @@ -0,0 +1,11 @@ +namespace FrostFS.SDK.ClientV2.Parameters; + +public interface IContext +{ + /// + /// The method call can be extended with additional behavior like canceling by timeout or user's request, + /// callbacks, interceptors. + /// + /// Additional parameters for calling the method + Context? Context { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/ISessionToken.cs b/src/FrostFS.SDK.ClientV2/Parameters/ISessionToken.cs new file mode 100644 index 0000000..c5ac1da --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/ISessionToken.cs @@ -0,0 +1,14 @@ +using FrostFS.SDK.ModelsV2; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public interface ISessionToken +{ + /// + /// Object represents token of the FrostFS Object session. A session is opened between any two sides of the + /// system, and implements a mechanism for transferring the power of attorney of actions to another network + /// member. The session has a limited validity period, and applies to a strictly defined set of operations. + /// + /// Instance of the session obtained from the server + SessionToken? SessionToken { get; set; } +} \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmCreateContainer.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmCreateContainer.cs new file mode 100644 index 0000000..9209a03 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmCreateContainer.cs @@ -0,0 +1,22 @@ +using System.Collections.Specialized; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmCreateContainer(ModelsV2.Container container) : IContext +{ + public ModelsV2.Container Container { get; set; } = container; + + /// + /// Since the container becomes available with some delay, it needs to poll the container status + /// + /// Rules for polling the result + public PrmWait? WaitParams { get; set; } + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmCreateSession.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmCreateSession.cs new file mode 100644 index 0000000..870a481 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmCreateSession.cs @@ -0,0 +1,16 @@ +using System.Collections.Specialized; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmCreateSession(ulong expiration, Context? context = default) : IContext +{ + public ulong Expiration { get; set; } = expiration; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = context; +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmDeleteContainer.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmDeleteContainer.cs new file mode 100644 index 0000000..4451b00 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmDeleteContainer.cs @@ -0,0 +1,23 @@ +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmDeleteContainer(ContainerId containerId, Context? ctx = null) : IContext +{ + public ContainerId ContainerId { get; set; } = containerId; + + /// + /// Since the container is removed with some delay, it needs to poll the container status + /// + /// Rules for polling the result + public PrmWait? WaitParams { get; set; } + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = ctx; +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmDeleteObject.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmDeleteObject.cs new file mode 100644 index 0000000..8241f64 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmDeleteObject.cs @@ -0,0 +1,21 @@ +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmDeleteObject(ContainerId containerId, ObjectId objectId) : IContext, ISessionToken +{ + public ContainerId ContainerId { get; set; } = containerId; + public ObjectId ObjectId { get; set; } = objectId; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } + + /// + public SessionToken? SessionToken { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmGetContainer.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetContainer.cs new file mode 100644 index 0000000..59350fe --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetContainer.cs @@ -0,0 +1,17 @@ +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmGetContainer(ContainerId containerId, Context? ctx = null) : IContext +{ + public ContainerId ContainerId { get; set; } = containerId; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = ctx; +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNetmapSnapshot.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNetmapSnapshot.cs new file mode 100644 index 0000000..e3e28c6 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNetmapSnapshot.cs @@ -0,0 +1,14 @@ +using System.Collections.Specialized; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmGetNetmapSnapshot(Context? context = default) : IContext +{ + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = context; +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNetworkSettings.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNetworkSettings.cs new file mode 100644 index 0000000..8db583e --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNetworkSettings.cs @@ -0,0 +1,14 @@ +using System.Collections.Specialized; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmGetNetworkSettings(Context? context = default) : IContext +{ + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = context; +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNodeInfo.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNodeInfo.cs new file mode 100644 index 0000000..78a7697 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetNodeInfo.cs @@ -0,0 +1,14 @@ +using System.Collections.Specialized; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmGetNodeInfo(Context? context = default) : IContext +{ + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = context; +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmGetObject.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetObject.cs new file mode 100644 index 0000000..5ee1fd9 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetObject.cs @@ -0,0 +1,21 @@ +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmGetObject(ContainerId containerId, ObjectId objectId) : IContext, ISessionToken +{ + public ContainerId ContainerId { get; set; } = containerId; + public ObjectId ObjectId { get; set; } = objectId; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } + + /// + public SessionToken? SessionToken { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmGetObjectHead.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetObjectHead.cs new file mode 100644 index 0000000..88287a8 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmGetObjectHead.cs @@ -0,0 +1,21 @@ +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmGetObjectHead(ContainerId containerId, ObjectId objectId) : IContext, ISessionToken +{ + public ContainerId ContainerId { get; set; } = containerId; + public ObjectId ObjectId { get; set; } = objectId; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } + + /// + public SessionToken? SessionToken { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmListContainer.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmListContainer.cs new file mode 100644 index 0000000..3d4c1a2 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmListContainer.cs @@ -0,0 +1,16 @@ +using System.Collections.Specialized; + +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmListContainer() : IContext +{ + public string SessionToken { get; set; } = string.Empty; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmPutObject.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmPutObject.cs new file mode 100644 index 0000000..7e4999d --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmPutObject.cs @@ -0,0 +1,44 @@ +using System.Collections.Specialized; +using System.IO; +using FrostFS.SDK.ModelsV2; +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmPutObject : IContext, ISessionToken +{ + /// + /// Need to provide values like ContainerId and ObjectType to create and object. + /// Optional parameters ike Attributes can be provided as well. + /// + /// Header with required parameters to create an object + public ObjectHeader? Header { get; set; } + + /// + /// A stream with source data + /// + public Stream? Payload { get; set; } + + /// + /// Object size is limited. In the data exceeds the limit, the object will be splitted. + /// If the parameter is true, the client side cut is applied. Otherwise, the data is transferred + /// as a stream and will be cut on server side. + /// + /// Is client cut is applied + public bool ClientCut { get; set; } + + /// + /// Overrides default size of the buffer for stream transferring. + /// + /// Size of the buffer + public int BufferMaxSize { get; set; } + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } + + /// + public SessionToken? SessionToken { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmPutSingleObject.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmPutSingleObject.cs new file mode 100644 index 0000000..95e514f --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmPutSingleObject.cs @@ -0,0 +1,19 @@ +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmPutSingleObject(FrostFsObject frostFsObject, Context? context = null) : IContext, ISessionToken +{ + public FrostFsObject FrostFsObject { get; set; } = frostFsObject; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } = context; + + /// + public SessionToken? SessionToken { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmSearchObject.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmSearchObject.cs new file mode 100644 index 0000000..8c0ad8c --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmSearchObject.cs @@ -0,0 +1,26 @@ +using System.Collections.Generic; +using System.Collections.Specialized; +using FrostFS.SDK.ModelsV2; +namespace FrostFS.SDK.ClientV2.Parameters; + +public sealed class PrmSearchObject(ContainerId containerId, params ObjectFilter[] filters) : IContext, ISessionToken +{ + public ContainerId ContainerId { get; set; } = containerId; + + /// + /// Defines the search criteria + /// + /// Collection of filters + public IEnumerable Filters { get; set; } = filters; + + /// + /// FrostFS request X-Headers + /// + public NameValueCollection XHeaders { get; set; } = []; + + /// + public Context? Context { get; set; } + + /// + public SessionToken? SessionToken { get; set; } +} diff --git a/src/FrostFS.SDK.ClientV2/Parameters/PrmWait.cs b/src/FrostFS.SDK.ClientV2/Parameters/PrmWait.cs new file mode 100644 index 0000000..86e32d7 --- /dev/null +++ b/src/FrostFS.SDK.ClientV2/Parameters/PrmWait.cs @@ -0,0 +1,23 @@ +using System; +namespace FrostFS.SDK.ClientV2.Parameters; + +public class PrmWait(TimeSpan timeout, TimeSpan pollInterval) +{ + private static TimeSpan DefaultTimeout = TimeSpan.FromSeconds(120); + private static TimeSpan DefaultPollInterval = TimeSpan.FromSeconds(5); + + public PrmWait(int timeout, int interval) : this(TimeSpan.FromSeconds(timeout), TimeSpan.FromSeconds(interval)) + { + } + + public static PrmWait DefaultParams { get; } = new PrmWait(DefaultTimeout, DefaultPollInterval); + + public TimeSpan Timeout { get; set; } = timeout.Ticks == 0 ? DefaultTimeout : timeout; + + public TimeSpan PollInterval { get; set; } = pollInterval.Ticks == 0 ? DefaultPollInterval : pollInterval; + + public DateTime GetDeadline() + { + return DateTime.UtcNow.AddTicks(Timeout.Ticks); + } +} diff --git a/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs index 6a3036b..dfbd614 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ContainerServiceProvider.cs @@ -6,6 +6,10 @@ using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.Cryptography; using System.Collections.Generic; using FrostFS.SDK.ModelsV2; +using FrostFS.Refs; +using System; +using FrostFS.SDK.ClientV2.Parameters; +using System.Collections.Specialized; namespace FrostFS.SDK.ClientV2; @@ -19,28 +23,21 @@ internal class ContainerServiceProvider : ContextAccessor containerServiceClient = service; } - internal async Task GetContainerAsync(ContainerId cid, Context context) + internal async Task GetContainerAsync(PrmGetContainer args) { - var request = new GetRequest - { - Body = new GetRequest.Types.Body - { - ContainerId = cid.ToGrpcMessage() - }, - }; + GetRequest request = GetContainerRequest(args.ContainerId.ToGrpcMessage(), args.XHeaders); - request.AddMetaHeader(); - request.Sign(Context.Key); - - var response = await containerServiceClient.GetAsync(request, null, context.Deadline, context.CancellationToken); + var response = await containerServiceClient.GetAsync(request, null, args.Context!.Deadline, args.Context.CancellationToken); Verifier.CheckResponse(response); - return response.Body.Container.ToModel(); + return response.Body.Container.ToModel(); } - internal async IAsyncEnumerable ListContainersAsync(Context ctx) + internal async IAsyncEnumerable ListContainersAsync(PrmListContainer args) { + var ctx = args.Context!; + var request = new ListRequest { Body = new ListRequest.Types.Body @@ -49,7 +46,7 @@ internal class ContainerServiceProvider : ContextAccessor } }; - request.AddMetaHeader(); + request.AddMetaHeader(args.XHeaders); request.Sign(Context.Key); var response = await containerServiceClient.ListAsync(request, null, ctx.Deadline, ctx.CancellationToken); @@ -62,9 +59,10 @@ internal class ContainerServiceProvider : ContextAccessor } } - internal async Task CreateContainerAsync(ModelsV2.Container container, Context ctx) + internal async Task CreateContainerAsync(PrmCreateContainer args) { - var grpcContainer = container.ToGrpcMessage(); + var ctx = args.Context!; + var grpcContainer = args.Container.ToGrpcMessage(); grpcContainer.OwnerId = Context.Owner.ToGrpcMessage(); grpcContainer.Version = Context.Version.ToGrpcMessage(); @@ -73,37 +71,113 @@ internal class ContainerServiceProvider : ContextAccessor Body = new PutRequest.Types.Body { Container = grpcContainer, - Signature = Context.Key.SignRFC6979(grpcContainer), + Signature = Context.Key.SignRFC6979(grpcContainer) } }; - request.AddMetaHeader(); + + request.AddMetaHeader(args.XHeaders); request.Sign(Context.Key); var response = await containerServiceClient.PutAsync(request, null, ctx.Deadline, ctx.CancellationToken); Verifier.CheckResponse(response); + + await WaitForContainer(WaitExpects.Exists, response.Body.ContainerId, args.WaitParams, ctx); return new ContainerId(Base58.Encode(response.Body.ContainerId.Value.ToByteArray())); } - internal async Task DeleteContainerAsync(ContainerId cid, Context ctx) + internal async Task DeleteContainerAsync(PrmDeleteContainer args) { + var ctx = args.Context!; var request = new DeleteRequest { Body = new DeleteRequest.Types.Body { - ContainerId = cid.ToGrpcMessage(), - Signature = Context.Key.SignRFC6979(cid.ToGrpcMessage().Value) + ContainerId = args.ContainerId.ToGrpcMessage(), + Signature = Context.Key.SignRFC6979(args.ContainerId.ToGrpcMessage().Value) } }; - request.AddMetaHeader(); + request.AddMetaHeader(args.XHeaders); request.Sign(Context.Key); var response = await containerServiceClient.DeleteAsync(request, null, ctx.Deadline, ctx.CancellationToken); + + await WaitForContainer(WaitExpects.Removed, request.Body.ContainerId, args.WaitParams, ctx); Verifier.CheckResponse(response); } + + private GetRequest GetContainerRequest(ContainerID id, NameValueCollection? xHeaders) + { + var request = new GetRequest + { + Body = new GetRequest.Types.Body + { + ContainerId = id + } + }; + + request.AddMetaHeader(xHeaders); + request.Sign(Context.Key); + + return request; + } + + private enum WaitExpects + { + Exists, + Removed + } + + private async Task WaitForContainer(WaitExpects expect, ContainerID id, PrmWait? waitParams, Context ctx) + { + var request = GetContainerRequest(id, null); + + async Task action() + { + var response = await containerServiceClient.GetAsync(request, null, ctx.Deadline, ctx.CancellationToken); + Verifier.CheckResponse(response); + } + + await WaitFor(action, expect, waitParams); + } + + private static async Task WaitFor( + Func action, + WaitExpects expect, + PrmWait? waitParams) + { + waitParams ??= PrmWait.DefaultParams; + var deadLine = waitParams.GetDeadline(); + + while (true) + { + try + { + await action(); + + if (expect == WaitExpects.Exists) + return; + + await Task.Delay(waitParams.PollInterval); + } + catch (ResponseException ex) + { + if (DateTime.UtcNow >= deadLine) + throw new TimeoutException(); + + if (ex.Status.Code != ModelsV2.Enums.StatusCode.ContainerNotFound) + throw; + + if (expect == WaitExpects.Removed) + return; + + await Task.Delay(waitParams.PollInterval); + } + } + } } diff --git a/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs index aabc133..0d0a6d3 100644 --- a/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/NetmapServiceProvider.cs @@ -7,6 +7,7 @@ using NodeInfo = FrostFS.SDK.ModelsV2.Netmap.NodeInfo; using FrostFS.SDK.ModelsV2.Netmap; using static FrostFS.Netmap.NetworkConfig.Types; +using FrostFS.SDK.ClientV2.Parameters; namespace FrostFS.SDK.ClientV2; @@ -39,14 +40,15 @@ internal class NetmapServiceProvider : ContextAccessor return settings; } - internal async Task GetLocalNodeInfoAsync(Context ctx) + internal async Task GetLocalNodeInfoAsync(PrmGetNodeInfo args) { + var ctx = args.Context!; var request = new LocalNodeInfoRequest { Body = new LocalNodeInfoRequest.Types.Body { } }; - request.AddMetaHeader(); + request.AddMetaHeader(args.XHeaders); request.Sign(Context.Key); var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); @@ -58,12 +60,9 @@ internal class NetmapServiceProvider : ContextAccessor internal async Task GetNetworkInfoAsync(Context ctx) { - var request = new NetworkInfoRequest - { - Body = new NetworkInfoRequest.Types.Body { } - }; + var request = new NetworkInfoRequest(); - request.AddMetaHeader(); + request.AddMetaHeader(null); request.Sign(Context.Key); var response = await netmapServiceClient.NetworkInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); @@ -73,14 +72,13 @@ internal class NetmapServiceProvider : ContextAccessor return response; } - internal async Task GetNetmapSnapshotAsync(Context ctx) + internal async Task GetNetmapSnapshotAsync(PrmGetNetmapSnapshot args) { - var request = new NetmapSnapshotRequest - { - Body = new NetmapSnapshotRequest.Types.Body { } - }; + var ctx = args.Context!; - request.AddMetaHeader(); + var request = new NetmapSnapshotRequest(); + + request.AddMetaHeader(args.XHeaders); request.Sign(Context.Key); var response = await netmapServiceClient.NetmapSnapshotAsync(request, null, ctx.Deadline, ctx.CancellationToken); diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs index bc38d7b..4d4cece 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/ObjectServiceProvider.cs @@ -12,6 +12,8 @@ using FrostFS.SDK.Cryptography; using FrostFS.Session; using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ClientV2.Extensions; +using System.Threading; +using FrostFS.SDK.ClientV2.Parameters; namespace FrostFS.SDK.ClientV2; @@ -19,21 +21,30 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C { readonly ObjectTools tools = new(ctx); - internal async Task GetObjectHeadAsync(ContainerId cid, ObjectId oid, Context ctx) + internal async Task GetObjectHeadAsync(PrmGetObjectHead args) { + var ctx = args.Context!; var request = new HeadRequest { Body = new HeadRequest.Types.Body { Address = new Address { - ContainerId = cid.ToGrpcMessage(), - ObjectId = oid.ToGrpcMessage() + ContainerId = args.ContainerId.ToGrpcMessage(), + ObjectId = args.ObjectId.ToGrpcMessage() } } }; - request.AddMetaHeader(); + var sessionToken = await GetOrCreateSession(args, ctx); + + sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Head, + Context.Key); + + request.AddMetaHeader(args.XHeaders, sessionToken); + request.Sign(Context.Key); var response = await client!.HeadAsync(request, null, ctx.Deadline, ctx.CancellationToken); @@ -43,51 +54,59 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C return response.Body.Header.Header.ToModel(); } - internal async Task GetObjectAsync(ContainerId cid, ObjectId oid, Context ctx) + internal async Task GetObjectAsync(PrmGetObject args) { - var sessionToken = await GetOrCreateSession(ctx); - + var ctx = args.Context!; + var request = new GetRequest { Body = new GetRequest.Types.Body { Address = new Address { - ContainerId = cid.ToGrpcMessage(), - ObjectId = oid.ToGrpcMessage() + ContainerId = args.ContainerId.ToGrpcMessage(), + ObjectId = args.ObjectId.ToGrpcMessage() } } }; - request.AddMetaHeader(); - request.AddObjectSessionToken( - sessionToken, - cid.ToGrpcMessage(), - oid.ToGrpcMessage(), + var sessionToken = await GetOrCreateSession(args, ctx); + + sessionToken.CreateObjectTokenContext( + request.Body.Address, ObjectSessionContext.Types.Verb.Get, - Context.Key - ); + Context.Key); + + request.AddMetaHeader(args.XHeaders, sessionToken); request.Sign(Context.Key); return await GetObject(request, ctx); } - internal async Task DeleteObjectAsync(ContainerId cid, ObjectId oid, Context ctx) + internal async Task DeleteObjectAsync(PrmDeleteObject args) { + var ctx = args.Context!; var request = new DeleteRequest { Body = new DeleteRequest.Types.Body { Address = new Address { - ContainerId = cid.ToGrpcMessage(), - ObjectId = oid.ToGrpcMessage() + ContainerId = args.ContainerId.ToGrpcMessage(), + ObjectId = args.ObjectId.ToGrpcMessage() } } }; - request.AddMetaHeader(); + var sessionToken = await GetOrCreateSession(args, ctx); + + sessionToken.CreateObjectTokenContext( + request.Body.Address, + ObjectSessionContext.Types.Verb.Delete, + Context.Key); + + request.AddMetaHeader(args.XHeaders, sessionToken); request.Sign(Context.Key); var response = await client.DeleteAsync(request, null, ctx.Deadline, ctx.CancellationToken); @@ -95,24 +114,30 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C Verifier.CheckResponse(response); } - internal async IAsyncEnumerable SearchObjectsAsync( - ContainerId cid, - IEnumerable filters, - Context ctx) + internal async IAsyncEnumerable SearchObjectsAsync(PrmSearchObject args) { + var ctx = args.Context!; var request = new SearchRequest { Body = new SearchRequest.Types.Body { - ContainerId = cid.ToGrpcMessage(), + ContainerId = args.ContainerId.ToGrpcMessage(), Filters = { }, Version = 1 // TODO: clarify this param } }; - request.Body.Filters.AddRange(filters.Select(f => f.ToGrpcMessage())); + request.Body.Filters.AddRange(args.Filters.Select(f => f.ToGrpcMessage())); - request.AddMetaHeader(); + var sessionToken = await GetOrCreateSession(args, ctx); + + sessionToken.CreateObjectTokenContext( + new Address { ContainerId = request.Body.ContainerId }, + ObjectSessionContext.Types.Verb.Search, + Context.Key); + + request.AddMetaHeader(args.XHeaders, sessionToken); + request.Sign(Context.Key); var objectsIds = SearchObjects(request, ctx); @@ -123,25 +148,24 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C } } - internal Task PutObjectAsync(PutObjectParameters parameters, Context ctx) + internal Task PutObjectAsync(PrmPutObject args) { - if (parameters.Header == null) - throw new ArgumentException("Value cannot be null", nameof(parameters.Header)); + if (args.Header == null) + throw new ArgumentException("Value cannot be null", nameof(args.Header)); - if (parameters.Payload == null) - throw new ArgumentException("Value cannot be null", nameof(parameters.Payload)); + if (args.Payload == null) + throw new ArgumentException("Value cannot be null", nameof(args.Payload)); - if (parameters.ClientCut) - return PutClientCutObject(parameters, ctx); + if (args.ClientCut) + return PutClientCutObject(args); else - return PutStreamObject(parameters, ctx); + return PutStreamObject(args); } - internal async Task PutSingleObjectAsync(FrostFsObject modelObject, Context ctx) + internal async Task PutSingleObjectAsync(PrmPutSingleObject args) { - var sessionToken = await GetOrCreateSession(ctx); - - var grpcObject = tools.CreateObject(modelObject); + var ctx = args.Context!; + var grpcObject = tools.CreateObject(args.FrostFsObject); var request = new PutSingleRequest { @@ -151,14 +175,14 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C } }; - request.AddMetaHeader(); - request.AddObjectSessionToken( - sessionToken, - grpcObject.Header.ContainerId, - grpcObject.ObjectId, + var sessionToken = await GetOrCreateSession(args, ctx); + + sessionToken.CreateObjectTokenContext( + new Address { ContainerId = grpcObject.Header.ContainerId, ObjectId = grpcObject.ObjectId}, ObjectSessionContext.Types.Verb.Put, - Context.Key - ); + Context.Key); + + request.AddMetaHeader(args.XHeaders, sessionToken); request.Sign(Context.Key); @@ -169,17 +193,23 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C return ObjectId.FromHash(grpcObject.ObjectId.Value.ToByteArray()); } - private async Task PutClientCutObject(PutObjectParameters parameters, Context ctx) + static readonly AsyncLocal asyncLocalSession = new (); + + private async Task PutClientCutObject(PrmPutObject args) { - var payloadStream = parameters.Payload!; - var header = parameters.Header!; + var ctx = args.Context!; + var tokenRaw = await GetOrCreateSession(args, ctx); + var token = new ModelsV2.SessionToken(tokenRaw.Serialize()); + + var payloadStream = args.Payload!; + var header = args.Header!; ObjectId? objectId; List sentObjectIds = []; FrostFsObject? currentObject; - var networkSettings = await Context.Client.GetNetworkSettingsAsync(ctx); + var networkSettings = await Context.Client.GetNetworkSettingsAsync(new PrmGetNetworkSettings(ctx)); var objectSize = (int)networkSettings.MaxObjectSize; @@ -208,27 +238,30 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C split.Previous = sentObjectIds.LastOrDefault(); - largeObject.AppendBlock(buffer, bytesCount); + largeObject.Header.PayloadLength += (ulong)bytesCount; - currentObject = new FrostFsObject(header.ContainerId, bytesCount < objectSize ? buffer[..bytesCount] : buffer) + currentObject = new FrostFsObject(header.ContainerId) + .SetPayload(bytesCount < objectSize ? buffer[..bytesCount] : buffer) .SetSplit(split); if (largeObject.PayloadLength == fullLength) break; - objectId = await PutSingleObjectAsync(currentObject, ctx); - + objectId = await PutSingleObjectAsync(new PrmPutSingleObject(currentObject, ctx) { SessionToken = token }); + sentObjectIds.Add(objectId!); } if (sentObjectIds.Count != 0) { - largeObject.AddAttributes(parameters.Header!.Attributes); - largeObject.CalculateHash(); - + largeObject.AddAttributes(args.Header!.Attributes); + currentObject.SetParent(largeObject); - objectId = await PutSingleObjectAsync(currentObject, ctx); + var putSingleObjectParams = new PrmPutSingleObject(currentObject, ctx) { SessionToken = token }; + + objectId = await PutSingleObjectAsync(putSingleObjectParams); + sentObjectIds.Add(objectId); var linkObject = new LinkObject(header.ContainerId, split.SplitId, largeObject) @@ -236,22 +269,21 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C linkObject.Header.Attributes.Clear(); - _ = await PutSingleObjectAsync(linkObject, ctx); + _ = await PutSingleObjectAsync(new PrmPutSingleObject(linkObject, ctx){ SessionToken = token }); return tools.CalculateObjectId(largeObject.Header); } - currentObject.AddAttributes(parameters.Header!.Attributes); + currentObject.AddAttributes(args.Header!.Attributes); - return await PutSingleObjectAsync(currentObject, ctx); + return await PutSingleObjectAsync(new PrmPutSingleObject(currentObject, ctx)); } - private async Task PutStreamObject(PutObjectParameters parameters, Context ctx) + private async Task PutStreamObject(PrmPutObject args) { - var payload = parameters.Payload!; - var header = parameters.Header!; - - var sessionToken = await GetOrCreateSession(ctx); + var ctx = args.Context!; + var payload = args.Payload!; + var header = args.Header!; var hdr = header.ToGrpcMessage(); hdr.OwnerId = Context.Owner.ToGrpcMessage(); @@ -270,20 +302,21 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C } }; - initRequest.AddMetaHeader(); - initRequest.AddObjectSessionToken( - sessionToken, - hdr.ContainerId, - oid, + var sessionToken = await GetOrCreateSession(args, ctx); + + sessionToken.CreateObjectTokenContext( + new Address { ContainerId = hdr.ContainerId, ObjectId = oid }, ObjectSessionContext.Types.Verb.Put, Context.Key ); + initRequest.AddMetaHeader(args.XHeaders, sessionToken); + initRequest.Sign(Context.Key); using var stream = await PutObjectInit(initRequest, ctx); - var bufferSize = parameters.BufferMaxSize > 0 ? parameters.BufferMaxSize : Constants.ObjectChunkSize; + var bufferSize = args.BufferMaxSize > 0 ? args.BufferMaxSize : Constants.ObjectChunkSize; if (payload.CanSeek) { @@ -307,12 +340,13 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C { Body = new PutRequest.Types.Body { - Chunk = ByteString.CopyFrom(buffer[..bytesCount]), + Chunk = ByteString.CopyFrom(buffer.AsSpan()[..bytesCount]), }, VerifyHeader = null }; chunkRequest.Sign(Context.Key); + await stream.Write(chunkRequest); } @@ -388,13 +422,13 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C return new SearchReader(call); } - private async Task GetOrCreateSession(Context ctx) + private async ValueTask GetOrCreateSession(ISessionToken args, Context ctx) { - if (string.IsNullOrEmpty(ctx.SessionToken)) + if (args.SessionToken is null) { - return await Context.Client.CreateSessionInternalAsync(uint.MaxValue, ctx); + return await Context.Client.CreateSessionInternalAsync(new PrmCreateSession(uint.MaxValue, ctx)); } - - return Convert.FromBase64String(ctx.SessionToken).DeserializeSessionToken(); + + return new Session.SessionToken().Deserialize(args.SessionToken.Token); } } diff --git a/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs b/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs index c8c3c47..64c6597 100644 --- a/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs +++ b/src/FrostFS.SDK.ClientV2/Services/SessionServiceProvider.cs @@ -1,6 +1,6 @@ using System.Threading.Tasks; - using FrostFS.SDK.ClientV2.Mappers.GRPC; +using FrostFS.SDK.ClientV2.Parameters; using FrostFS.Session; namespace FrostFS.SDK.ClientV2; @@ -15,41 +15,41 @@ internal class SessionServiceProvider : ContextAccessor _sessionServiceClient = sessionServiceClient; } - internal async Task CreateSessionAsync(ulong expiration, Context ctx) + internal async Task CreateSessionAsync(PrmCreateSession args) { var request = new CreateRequest { Body = new CreateRequest.Types.Body { OwnerId = Context.Owner.ToGrpcMessage(), - Expiration = expiration + Expiration = args.Expiration } }; - request.AddMetaHeader(); + request.AddMetaHeader(args.XHeaders); request.Sign(Context.Key); - var token = await CreateSession(request, ctx); - - return token; + return await CreateSession(request, args.Context!); } internal async Task CreateSession(CreateRequest request, Context ctx) { - var resp = await _sessionServiceClient!.CreateAsync(request, null, ctx.Deadline, ctx.CancellationToken); + var response = await _sessionServiceClient!.CreateAsync(request, null, ctx.Deadline, ctx.CancellationToken); + + Verifier.CheckResponse(response); return new SessionToken { Body = new SessionToken.Types.Body { - Id = resp.Body.Id, - SessionKey = resp.Body.SessionKey, + Id = response.Body.Id, + SessionKey = response.Body.SessionKey, OwnerId = request.Body.OwnerId, Lifetime = new SessionToken.Types.Body.Types.TokenLifetime { Exp = request.Body.Expiration, - Iat = resp.MetaHeader.Epoch, - Nbf = resp.MetaHeader.Epoch, + Iat = response.MetaHeader.Epoch, + Nbf = response.MetaHeader.Epoch, } } }; diff --git a/src/FrostFS.SDK.ClientV2/Tools/Object.cs b/src/FrostFS.SDK.ClientV2/Tools/Object.cs index 0a0290e..1bc66a3 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/Object.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/Object.cs @@ -13,6 +13,12 @@ public static class ObjectExtensions return obj; } + public static FrostFsObject SetPayload(this FrostFsObject obj, byte[] bytes) + { + obj.Payload = bytes; + return obj; + } + public static FrostFsObject AddAttribute(this FrostFsObject obj, string key, string value) { obj.AddAttribute(new ObjectAttribute(key, value)); @@ -45,9 +51,6 @@ public static class ObjectExtensions public static FrostFsObject CalculateObjectId(this FrostFsObject obj) { - if (obj.Payload == null) - throw new MissingFieldException("Payload cannot be null"); - if (obj.Header == null) throw new MissingFieldException("Header cannot be null"); diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectReader.cs b/src/FrostFS.SDK.ClientV2/Tools/ObjectReader.cs similarity index 100% rename from src/FrostFS.SDK.ClientV2/Services/ObjectReader.cs rename to src/FrostFS.SDK.ClientV2/Tools/ObjectReader.cs diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectStreamer.cs b/src/FrostFS.SDK.ClientV2/Tools/ObjectStreamer.cs similarity index 100% rename from src/FrostFS.SDK.ClientV2/Services/ObjectStreamer.cs rename to src/FrostFS.SDK.ClientV2/Tools/ObjectStreamer.cs diff --git a/src/FrostFS.SDK.ClientV2/Services/ObjectTools.cs b/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs similarity index 98% rename from src/FrostFS.SDK.ClientV2/Services/ObjectTools.cs rename to src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs index 48a1275..fb7f987 100644 --- a/src/FrostFS.SDK.ClientV2/Services/ObjectTools.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/ObjectTools.cs @@ -93,7 +93,7 @@ internal class ObjectTools(ClientEnvironment ctx) : ContextAccessor (ctx) return new Checksum { Type = ChecksumType.Sha256, - Sum = ByteString.CopyFrom(data.Sha256()) + Sum = ByteString.CopyFrom(data.Sha256()) }; } } \ No newline at end of file diff --git a/src/FrostFS.SDK.ClientV2/Tools/RequestConstructor.cs b/src/FrostFS.SDK.ClientV2/Tools/RequestConstructor.cs index 6e2a5dc..dccb35f 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/RequestConstructor.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/RequestConstructor.cs @@ -1,4 +1,7 @@ +using System.Collections.Specialized; +using System.Linq; using System.Security.Cryptography; + using FrostFS.Refs; using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ModelsV2; @@ -9,36 +12,38 @@ namespace FrostFS.SDK.ClientV2; public static class RequestConstructor { - public static void AddMetaHeader(this IRequest request, RequestMetaHeader? metaHeader = null) + public static void AddMetaHeader(this IRequest request, NameValueCollection? xHeaders, Session.SessionToken? sessionToken = null) { - if (request.MetaHeader is not null) return; - metaHeader ??= MetaHeader.Default().ToGrpcMessage(); - request.MetaHeader = metaHeader; + if (request.MetaHeader is not null) + return; + + request.MetaHeader = MetaHeader.Default().ToGrpcMessage(); + + if (sessionToken != null) + request.MetaHeader.SessionToken = sessionToken; + + if (xHeaders != null && xHeaders.Count > 0) + request.MetaHeader.XHeaders.AddRange( + xHeaders.Cast().SelectMany(key => xHeaders.GetValues(key), + (k, v) => new XHeader { Key = k, Value = v })); } - public static void AddObjectSessionToken( - this IRequest request, - Session.SessionToken sessionToken, - ContainerID cid, - ObjectID oid, + public static void CreateObjectTokenContext(this Session.SessionToken sessionToken, + Address address, ObjectSessionContext.Types.Verb verb, ECDsa key) { - if (request.MetaHeader.SessionToken is not null) - return; + ObjectSessionContext.Types.Target target = new() { Container = address.ContainerId }; - request.MetaHeader.SessionToken = sessionToken; - var ctx = new ObjectSessionContext + if (address.ObjectId != null) + target.Objects.Add(address.ObjectId); + + sessionToken.Body.Object = new() { - Target = new ObjectSessionContext.Types.Target - { - Container = cid, - Objects = { oid } - }, + Target = target, Verb = verb }; - request.MetaHeader.SessionToken.Body.Object = ctx; - request.MetaHeader.SessionToken.Signature = key.SignMessagePart(request.MetaHeader.SessionToken.Body); + sessionToken.Signature = key.SignMessagePart(sessionToken.Body); } } diff --git a/src/FrostFS.SDK.ClientV2/Services/SearchReader.cs b/src/FrostFS.SDK.ClientV2/Tools/SearchReader.cs similarity index 100% rename from src/FrostFS.SDK.ClientV2/Services/SearchReader.cs rename to src/FrostFS.SDK.ClientV2/Tools/SearchReader.cs diff --git a/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs b/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs index 6e65c02..2b36a64 100644 --- a/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs +++ b/src/FrostFS.SDK.ClientV2/Tools/Verifier.cs @@ -102,7 +102,7 @@ public static class Verifier var status = resp.MetaHeader.Status.ToModel(); if (!status.IsSuccess) - throw new ApplicationException(status.ToString()); + throw new ResponseException(status); } /// diff --git a/src/FrostFS.SDK.ModelsV2/Context.cs b/src/FrostFS.SDK.ModelsV2/Context.cs index b1038ec..4ac90a8 100644 --- a/src/FrostFS.SDK.ModelsV2/Context.cs +++ b/src/FrostFS.SDK.ModelsV2/Context.cs @@ -12,8 +12,6 @@ public class Context() public CancellationToken CancellationToken { get; set; } = default; public TimeSpan Timeout { get; set; } = default; - public string SessionToken { get; set; } = string.Empty; - public DateTime? Deadline => Timeout.Ticks > 0 ? DateTime.UtcNow.Add(Timeout) : null; public Action? Callback { get; set; } diff --git a/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj b/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj index be180b0..d4993b7 100644 --- a/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj +++ b/src/FrostFS.SDK.ModelsV2/FrostFS.SDK.ModelsV2.csproj @@ -18,4 +18,9 @@ + + + <_Parameter1>FrostFS.SDK.ClientV2 + + diff --git a/src/FrostFS.SDK.ModelsV2/Object/FrostFsObject.cs b/src/FrostFS.SDK.ModelsV2/Object/FrostFsObject.cs index 4cbfaae..fa2586a 100644 --- a/src/FrostFS.SDK.ModelsV2/Object/FrostFsObject.cs +++ b/src/FrostFS.SDK.ModelsV2/Object/FrostFsObject.cs @@ -6,32 +6,56 @@ namespace FrostFS.SDK.ModelsV2; public class FrostFsObject { - public FrostFsObject(ObjectId objectId, ObjectHeader header, byte[] payload) + /// + /// Creates new instance from ObjectHeader + /// + /// + public FrostFsObject(ObjectHeader header) { - ObjectId = objectId; - Payload = payload; Header = header; } - public FrostFsObject(ContainerId container, byte[] payload, ObjectType objectType = ObjectType.Regular) + /// + /// Creates new instance with specified parameters + /// + /// + /// + public FrostFsObject(ContainerId container, ObjectType objectType = ObjectType.Regular) { - Payload = payload; - Header = new ObjectHeader(containerId: container, type: objectType, attributes: []); + Header = new ObjectHeader(containerId: container, type: objectType); } + /// + /// Header contains metadata for the object + /// + /// public ObjectHeader Header { get; set; } + /// + /// The value is calculated internally as a hash of ObjectHeader. Do not use pre-calculated value is the object has been changed. + /// public ObjectId? ObjectId { get; internal set; } - - public byte[] Payload { get; set; } + /// + /// The size of payload cannot exceed MaxObjectSize value from NetworkSettings + /// Used only for PutSingleObject method + /// + /// Buffer for output data + public byte[] Payload { get; set; } = []; + + /// + /// A payload is obtained via stream reader + /// + /// Reader for received data public IObjectReader? ObjectReader { get; set; } - public Signature? Signature { get; set; } - + /// + /// Applied only for the last Object in chain in case of manual multipart uploading + /// + /// Parent for multipart object public void SetParent(LargeObject largeObject) { if (Header?.Split == null) @@ -41,7 +65,7 @@ public class FrostFsObject } } -public class LargeObject(ContainerId container) : FrostFsObject(container, []) +public class LargeObject(ContainerId container) : FrostFsObject(container) { private readonly SHA256 payloadHash = SHA256.Create(); @@ -66,11 +90,11 @@ public class LargeObject(ContainerId container) : FrostFsObject(container, []) public class LinkObject : FrostFsObject { - public LinkObject(ContainerId containerId, SplitId splitId, LargeObject largeObject) : base (containerId, []) + public LinkObject(ContainerId containerId, SplitId splitId, LargeObject largeObject) : base (containerId) { Header!.Split = new Split(splitId) { - ParentHeader = largeObject.Header + ParentHeader = largeObject.Header }; } } \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/PutObjectParameters.cs b/src/FrostFS.SDK.ModelsV2/PutObjectParameters.cs index 69a5a99..0a8d64c 100644 --- a/src/FrostFS.SDK.ModelsV2/PutObjectParameters.cs +++ b/src/FrostFS.SDK.ModelsV2/PutObjectParameters.cs @@ -1,14 +1 @@ -using System.IO; - namespace FrostFS.SDK.ModelsV2; - -public class PutObjectParameters -{ - public ObjectHeader? Header { get; set; } - - public Stream? Payload { get; set; } - - public bool ClientCut { get; set; } - - public int BufferMaxSize { get; set; } -} \ No newline at end of file diff --git a/src/FrostFS.SDK.ModelsV2/SessionToken.cs b/src/FrostFS.SDK.ModelsV2/SessionToken.cs index 6fde99e..e7da09a 100644 --- a/src/FrostFS.SDK.ModelsV2/SessionToken.cs +++ b/src/FrostFS.SDK.ModelsV2/SessionToken.cs @@ -1,8 +1,11 @@ namespace FrostFS.SDK.ModelsV2; -public class SessionToken(byte[] sessionKey, byte[] id) +public class SessionToken { - public byte[] Id { get; } = id; + internal SessionToken(byte[] token) + { + Token = token; + } - public byte[] SessionKey { get; } = sessionKey; + public byte[] Token { get; private set; } } diff --git a/src/FrostFS.SDK.Tests/ContainerTest.cs b/src/FrostFS.SDK.Tests/ContainerTest.cs index 498a47a..9293579 100644 --- a/src/FrostFS.SDK.Tests/ContainerTest.cs +++ b/src/FrostFS.SDK.Tests/ContainerTest.cs @@ -8,10 +8,10 @@ using FrostFS.SDK.ModelsV2.Netmap; using FrostFS.SDK.ModelsV2.Enums; using Google.Protobuf; using FrostFS.SDK.ClientV2.Interfaces; +using FrostFS.SDK.ClientV2.Parameters; namespace FrostFS.SDK.Tests; - public abstract class ContainerTestsBase { protected readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; @@ -38,12 +38,12 @@ public abstract class ContainerTestsBase protected IFrostFSClient GetClient() { return ClientV2.Client.GetTestInstance( - Settings, - null, - new NetworkMocker(this.key).GetMock().Object, - new SessionMocker(this.key).GetMock().Object, - Mocker.GetMock().Object, - new ObjectMocker(this.key).GetMock().Object); + Settings, + null, + new NetworkMocker(this.key).GetMock().Object, + new SessionMocker(this.key).GetMock().Object, + Mocker.GetMock().Object, + new ObjectMocker(this.key).GetMock().Object); } } @@ -52,7 +52,9 @@ public class ContainerTest : ContainerTestsBase [Fact] public async void CreateContainerTest() { - var result = await GetClient().CreateContainerAsync(new ModelsV2.Container(BasicAcl.PublicRW, Mocker.PlacementPolicy)); + var param = new PrmCreateContainer(new ModelsV2.Container(BasicAcl.PublicRW, Mocker.PlacementPolicy)); + + var result = await GetClient().CreateContainerAsync(param); Assert.NotNull(result); Assert.NotNull(result.Value); @@ -66,7 +68,7 @@ public class ContainerTest : ContainerTestsBase Mocker.Acl = BasicAcl.PublicRO; - var result = await GetClient().GetContainerAsync(cid); + var result = await GetClient().GetContainerAsync(new PrmGetContainer(cid)); Assert.NotNull(result); Assert.Equal(Mocker.Acl, result.BasicAcl); @@ -99,9 +101,10 @@ public class ContainerTest : ContainerTestsBase [Fact] public async void DeleteContainerAsyncTest() { + Mocker.ReturnContainerRemoved = true; var cid = new ContainerId(Base58.Encode(Mocker.ContainerGuid.ToBytes())); - await GetClient().DeleteContainerAsync(cid); + await GetClient().DeleteContainerAsync(new PrmDeleteContainer(cid)); Assert.Single(Mocker.Requests); diff --git a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs index 2188795..aff566d 100644 --- a/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs +++ b/src/FrostFS.SDK.Tests/Mocks/ContainerServiceMocks/GetContainerMock.cs @@ -77,6 +77,21 @@ public class ContainerMocker(string key) : ContainerServiceBase(key) getResponse.VerifyHeader = GetResponseVerificationHeader(getResponse); + var getNoContainerResponse = new GetResponse + { + Body = new (), + MetaHeader = new ResponseMetaHeader + { + Status = new Status.Status + { + Code = 3072, + Message = "container not found" + } + } + }; + + getNoContainerResponse.VerifyHeader = GetResponseVerificationHeader(getNoContainerResponse); + mock.Setup(x => x.GetAsync( It.IsAny(), It.IsAny(), @@ -86,12 +101,22 @@ public class ContainerMocker(string key) : ContainerServiceBase(key) { Verifier.CheckRequest(r); + if (ReturnContainerRemoved) + { + return new AsyncUnaryCall( + Task.FromResult(getNoContainerResponse), + Task.FromResult(ResponseMetaData), + () => new Grpc.Core.Status(StatusCode.NotFound, string.Empty), + () => ResponseMetaData, + () => { }); + } + return new AsyncUnaryCall( Task.FromResult(getResponse), Task.FromResult(ResponseMetaData), () => new Grpc.Core.Status(StatusCode.OK, string.Empty), () => ResponseMetaData, - () => { }); + () => { }); }); var listResponse = new ListResponse @@ -154,6 +179,8 @@ public class ContainerMocker(string key) : ContainerServiceBase(key) return mock; } + public bool ReturnContainerRemoved { get; set; } + public List ContainerIds { get; set; } = []; public List> Requests { get; set; } = []; diff --git a/src/FrostFS.SDK.Tests/ObjectTest.cs b/src/FrostFS.SDK.Tests/ObjectTest.cs index d524284..2513908 100644 --- a/src/FrostFS.SDK.Tests/ObjectTest.cs +++ b/src/FrostFS.SDK.Tests/ObjectTest.cs @@ -2,6 +2,7 @@ using FrostFS.Refs; using FrostFS.SDK.ClientV2; using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Mappers.GRPC; +using FrostFS.SDK.ClientV2.Parameters; using FrostFS.SDK.Cryptography; using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2.Enums; @@ -76,7 +77,7 @@ public class ObjectTest : ObjectTestsBase Timeout = TimeSpan.FromSeconds(2) }; - var result = await client.GetObjectAsync(ContainerId, objectId, context); + var result = await client.GetObjectAsync(new PrmGetObject(ContainerId, objectId) { Context = context }); Assert.NotNull(result); @@ -97,7 +98,7 @@ public class ObjectTest : ObjectTestsBase var bytes = new byte[1024]; rnd.NextBytes(bytes); - var param = new PutObjectParameters + var param = new PrmPutObject { Header = Mocker.ObjectHeader, Payload = new MemoryStream(bytes), @@ -132,7 +133,7 @@ public class ObjectTest : ObjectTestsBase byte[] bytes = File.ReadAllBytes(@".\..\..\..\TestData\cat.jpg"); var fileLength = bytes.Length; - var param = new PutObjectParameters + var param = new PrmPutObject { Header = Mocker.ObjectHeader, Payload = new MemoryStream(bytes), @@ -196,7 +197,7 @@ public class ObjectTest : ObjectTestsBase { Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); - await GetClient().DeleteObjectAsync(ContainerId, Mocker.ObjectId); + await GetClient().DeleteObjectAsync(new PrmDeleteObject(ContainerId, Mocker.ObjectId)); var request = Mocker.DeleteRequests.FirstOrDefault(); Assert.NotNull(request); @@ -209,7 +210,7 @@ public class ObjectTest : ObjectTestsBase { Mocker.ObjectId = new ObjectID { Value = ByteString.CopyFrom(SHA256.HashData(Encoding.UTF8.GetBytes("test"))) }.ToModel(); - var response = await GetClient().GetObjectHeadAsync(ContainerId, Mocker.ObjectId); + var response = await GetClient().GetObjectHeadAsync(new PrmGetObjectHead(ContainerId, Mocker.ObjectId)); var request = Mocker.HeadRequests.FirstOrDefault(); Assert.NotNull(request); diff --git a/src/FrostFS.SDK.Tests/SmokeTests.cs b/src/FrostFS.SDK.Tests/SmokeTests.cs index ad9f5ee..ba7e32c 100644 --- a/src/FrostFS.SDK.Tests/SmokeTests.cs +++ b/src/FrostFS.SDK.Tests/SmokeTests.cs @@ -4,24 +4,30 @@ using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2.Enums; using FrostFS.SDK.ModelsV2.Netmap; +using FrostFS.SDK.Cryptography; + using Grpc.Core; using Microsoft.Extensions.Options; using Grpc.Core.Interceptors; using System.Diagnostics; +using static FrostFS.Session.SessionToken.Types.Body; +using FrostFS.SDK.ClientV2.Parameters; + namespace FrostFS.SDK.SmokeTests; public class SmokeTests { + private static PrmWait lightWait = new (100, 1); private readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; private readonly string url = "http://172.29.238.97:8080"; [Fact] public async void NetworkMapTest() { - using var fsClient = Client.GetInstance(GetOptions(this.key, this.url)); + using var client = Client.GetInstance(GetOptions(this.key, this.url)); - var result = await fsClient.GetNetmapSnapshotAsync(); + var result = await client.GetNetmapSnapshotAsync(); Assert.True(result.Epoch > 0); Assert.Single(result.NodeInfoCollection); @@ -38,9 +44,9 @@ public class SmokeTests [Fact] public async void NodeInfoTest() { - using var fsClient = Client.GetInstance(GetOptions(this.key, this.url)); + using var client = Client.GetInstance(GetOptions(this.key, this.url)); - var result = await fsClient.GetNodeInfoAsync(); + var result = await client.GetNodeInfoAsync(); Assert.Equal(2, result.Version.Major); Assert.Equal(13, result.Version.Minor); @@ -51,80 +57,74 @@ public class SmokeTests } [Fact] - public async void NodeInfo_Statictics_Test() + public async void NodeInfo_Statistics_Test() { var ctx = new Context { Callback = (cs) => Console.WriteLine($"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds") }; - using var fsClient = Client.GetInstance(GetOptions(this.key, this.url)); + using var client = Client.GetInstance(GetOptions(this.key, this.url)); - var result = await fsClient.GetNodeInfoAsync(); + var result = await client.GetNodeInfoAsync(); } - [Theory] - [InlineData(1)] - [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB - [InlineData(6 * 1024 * 1024 + 100)] - public async void SimpleScenarioTest(int objectSize) + [Fact] + public async void GetSessionTest() { - using var fsClient = Client.GetInstance(GetOptions(this.key, this.url)); + using var client = Client.GetInstance(GetOptions(this.key, this.url)); - await Cleanup(fsClient); + var token = await client.CreateSessionAsync(new PrmCreateSession(100)); - var containerId = await fsClient.CreateContainerAsync( - new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1))), - new Context - { - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - } - ); + var session = new Session.SessionToken().Deserialize(token.Token); + + var ecdsaKey = this.key.LoadWif(); + var owner = OwnerId.FromKey(ecdsaKey); - var context = new Context - { - Timeout = TimeSpan.FromSeconds(10), - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - }; + var ownerHash = Base58.Decode(owner.Value); + + Assert.NotNull(session); + Assert.Null(session.Body.Container); + Assert.Null(session.Body.Object); + Assert.Equal(16, session.Body.Id.Length); + Assert.Equal(100ul, session.Body.Lifetime.Exp); + Assert.Equal(ownerHash, session.Body.OwnerId.Value); + Assert.Equal(33, session.Body.SessionKey.Length); + Assert.Equal(ContextOneofCase.None, session.Body.ContextCase); + } - var container = await GetContainer(fsClient, containerId, context); + [Fact] + public async void CreateObjectWithSessionToken() + { + using var client = Client.GetInstance(GetOptions(this.key, this.url)); + + await Cleanup(client); - Assert.NotNull(container); + var token = await client.CreateSessionAsync(new PrmCreateSession(int.MaxValue)); - var bytes = GetRandomBytes(objectSize); + var createContainerParam = new PrmCreateContainer( + new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1)))); - var param = new PutObjectParameters + createContainerParam.XHeaders.Add("key1", "value1"); + + var containerId = await client.CreateContainerAsync(createContainerParam); + + var bytes = GetRandomBytes(1024); + + var param = new PrmPutObject { Header = new ObjectHeader( containerId: containerId, type: ObjectType.Regular, new ObjectAttribute("fileName", "test")), Payload = new MemoryStream(bytes), - ClientCut = false + ClientCut = false, + SessionToken = token }; + + var objectId = await client.PutObjectAsync(param); - var objectId = await fsClient.PutObjectAsync(param, new Context - { - Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) - }); - - var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test"); - - bool hasObject = false; - await foreach (var objId in fsClient.SearchObjectsAsync(containerId, [filter])) - { - hasObject = true; - - var objHeader = await fsClient.GetObjectHeadAsync(containerId, objectId); - Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); - Assert.Single(objHeader.Attributes); - Assert.Equal("fileName", objHeader.Attributes.First().Key); - Assert.Equal("test", objHeader.Attributes.First().Value); - } - - Assert.True(hasObject); - - var @object = await fsClient.GetObjectAsync(containerId, objectId!); + var @object = await client.GetObjectAsync(new PrmGetObject(containerId, objectId)); var downloadedBytes = new byte[@object.Header.PayloadLength]; MemoryStream ms = new(downloadedBytes); @@ -137,11 +137,177 @@ public class SmokeTests Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); - await Cleanup(fsClient); + await Cleanup(client); + } - await Task.Delay(2000); + [Theory] + [InlineData(1)] + [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB + [InlineData(6 * 1024 * 1024 + 100)] + public async void SimpleScenarioTest(int objectSize) + { + using var client = Client.GetInstance(GetOptions(this.key, this.url)); + + await Cleanup(client); - await foreach (var _ in fsClient.ListContainersAsync()) + bool callbackInvoked = false; + var ctx = new Context + { + Timeout = TimeSpan.FromSeconds(20), + Callback = new((CallStatistics cs) => + { + callbackInvoked = true; + Assert.True(cs.ElapsedMicroSeconds > 0); + }) + }; + + var createContainerParam = new PrmCreateContainer( + new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1)))) + { + Context = ctx + }; + + var containerId = await client.CreateContainerAsync(createContainerParam); + + var container = await client.GetContainerAsync(new PrmGetContainer(containerId,ctx)); + Assert.NotNull(container); + Assert.True(callbackInvoked); + + var bytes = GetRandomBytes(objectSize); + + var param = new PrmPutObject + { + Header = new ObjectHeader( + containerId: containerId, + type: ObjectType.Regular, + new ObjectAttribute("fileName", "test")), + Payload = new MemoryStream(bytes), + ClientCut = false, + Context = new Context + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + } + }; + + var objectId = await client.PutObjectAsync(param); + + var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test"); + + bool hasObject = false; + await foreach (var objId in client.SearchObjectsAsync(new PrmSearchObject(containerId) { Filters = [filter] })) + { + hasObject = true; + + var objHeader = await client.GetObjectHeadAsync(new PrmGetObjectHead(containerId, objectId)); + Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.Single(objHeader.Attributes); + Assert.Equal("fileName", objHeader.Attributes.First().Key); + Assert.Equal("test", objHeader.Attributes.First().Value); + } + + Assert.True(hasObject); + + var @object = await client.GetObjectAsync(new PrmGetObject(containerId, objectId)); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + byte[]? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk); + } + + Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); + + await Cleanup(client); + + await foreach (var _ in client.ListContainersAsync()) + { + Assert.Fail("Containers exist"); + } + } + + [Theory] + [InlineData(1)] + [InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB + [InlineData(6 * 1024 * 1024 + 100)] + public async void SimpleScenarioWithSessionTest(int objectSize) + { + using var client = Client.GetInstance(GetOptions(this.key, this.url)); + + var token = await client.CreateSessionAsync(new PrmCreateSession(int.MaxValue)); + + await Cleanup(client); + + var ctx = new Context + { + Timeout = TimeSpan.FromSeconds(20), + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }; + + var createContainerParam = new PrmCreateContainer( + new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1)))) + { + Context = ctx + }; + + var containerId = await client.CreateContainerAsync(createContainerParam); + + var container = await client.GetContainerAsync(new PrmGetContainer(containerId,ctx)); + Assert.NotNull(container); + + var bytes = GetRandomBytes(objectSize); + + var param = new PrmPutObject + { + Header = new ObjectHeader( + containerId: containerId, + type: ObjectType.Regular, + new ObjectAttribute("fileName", "test")), + Payload = new MemoryStream(bytes), + ClientCut = false, + Context = new Context + { + Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0)) + }, + SessionToken = token + }; + + var objectId = await client.PutObjectAsync(param); + + var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test"); + + bool hasObject = false; + await foreach (var objId in client.SearchObjectsAsync(new PrmSearchObject(containerId) { Filters = [filter], SessionToken = token })) + { + hasObject = true; + + var objHeader = await client.GetObjectHeadAsync(new PrmGetObjectHead(containerId, objectId) { SessionToken = token }); + Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); + Assert.Single(objHeader.Attributes); + Assert.Equal("fileName", objHeader.Attributes.First().Key); + Assert.Equal("test", objHeader.Attributes.First().Value); + } + + Assert.True(hasObject); + + var @object = await client.GetObjectAsync(new PrmGetObject(containerId, objectId) { SessionToken = token }); + + var downloadedBytes = new byte[@object.Header.PayloadLength]; + MemoryStream ms = new(downloadedBytes); + + byte[]? chunk = null; + while ((chunk = await @object.ObjectReader!.ReadChunk()) != null) + { + ms.Write(chunk); + } + + Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); + + await Cleanup(client); + + await foreach (var _ in client.ListContainersAsync()) { Assert.Fail("Containers exist"); } @@ -155,13 +321,16 @@ public class SmokeTests [InlineData(2 * 64 * 1024 * 1024 + 256)] public async void ClientCutScenarioTest(int objectSize) { - using var fsClient = Client.GetInstance(GetOptions(this.key, this.url)); + using var client = Client.GetInstance(GetOptions(this.key, this.url)); - await Cleanup(fsClient); + await Cleanup(client); - var cnt = new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1))); + var createContainerParam = new PrmCreateContainer(new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1)))) + { + WaitParams = lightWait + }; - var containerId = await fsClient.CreateContainerAsync(cnt); + var containerId = await client.CreateContainerAsync(createContainerParam); var context = new Context { @@ -169,13 +338,13 @@ public class SmokeTests Interceptors = new([new MetricsInterceptor()]) }; - var container = await GetContainer(fsClient, containerId, context); + var container = await client.GetContainerAsync(new PrmGetContainer(containerId, context)); Assert.NotNull(container); byte[] bytes = GetRandomBytes(objectSize); - var param = new PutObjectParameters + var param = new PrmPutObject { Header = new ObjectHeader( containerId: containerId, @@ -185,16 +354,16 @@ public class SmokeTests ClientCut = true }; - var objectId = await fsClient.PutObjectAsync(param); + var objectId = await client.PutObjectAsync(param); var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test"); bool hasObject = false; - await foreach (var objId in fsClient.SearchObjectsAsync(containerId, [filter])) + await foreach (var objId in client.SearchObjectsAsync(new PrmSearchObject(containerId, filter))) { hasObject = true; - var objHeader = await fsClient.GetObjectHeadAsync(containerId, objectId); + var objHeader = await client.GetObjectHeadAsync(new PrmGetObjectHead(containerId, objectId)); Assert.Equal((ulong)bytes.Length, objHeader.PayloadLength); Assert.Single(objHeader.Attributes); Assert.Equal("fileName", objHeader.Attributes.First().Key); @@ -203,7 +372,7 @@ public class SmokeTests Assert.True(hasObject); - var @object = await fsClient.GetObjectAsync(containerId, objectId!); + var @object = await client.GetObjectAsync(new PrmGetObject(containerId, objectId)); var downloadedBytes = new byte[@object.Header.PayloadLength]; MemoryStream ms = new(downloadedBytes); @@ -216,14 +385,23 @@ public class SmokeTests Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); - await Cleanup(fsClient); + await Cleanup(client); - await Task.Delay(2000); + var deadline = DateTime.UtcNow.Add(TimeSpan.FromSeconds(5)); - await foreach (var _ in fsClient.ListContainersAsync()) + IAsyncEnumerator? enumerator = null; + do { - Assert.Fail("Containers exist"); + if (deadline <= DateTime.UtcNow) + { + Assert.Fail("Containers exist"); + break; + } + + enumerator = client.ListContainersAsync().GetAsyncEnumerator(); + await Task.Delay(500); } + while (await enumerator!.MoveNextAsync()); } private static byte[] GetRandomBytes(int size) @@ -243,36 +421,16 @@ public class SmokeTests }); } - static async Task Cleanup(IFrostFSClient fsClient) + static async Task Cleanup(IFrostFSClient client) { - await foreach (var cid in fsClient.ListContainersAsync()) + await foreach (var cid in client.ListContainersAsync()) { - await fsClient.DeleteContainerAsync(cid); - } - } - - static async Task GetContainer(IFrostFSClient fsClient, ContainerId id, Context ctx) - { - while (true) - { - try - { - await Task.Delay(100); - return await fsClient.GetContainerAsync(id, ctx); - } - catch (ApplicationException) - { - if (DateTime.UtcNow >= ctx.Deadline) - throw new TimeoutException(); - } - catch (RpcException) - { - throw; - } + await client.DeleteContainerAsync(new PrmDeleteContainer(cid) { WaitParams = lightWait }); } } } + public class MetricsInterceptor() : Interceptor { public override AsyncUnaryCall AsyncUnaryCall(