[#17] Client: Add extra parameter
All checks were successful
DCO / DCO (pull_request) Successful in 47s

API methods' parameters types with optional session, polling settings, xHeaders etc. and corresponding handlers have been added

Signed-off-by: Pavel Gross <p.gross@yadro.com>
This commit is contained in:
Pavel Gross 2024-07-18 15:08:53 +03:00 committed by p.gross
parent 00a1e9412f
commit 7b9c19f37c
42 changed files with 1054 additions and 386 deletions

View file

@ -2,6 +2,7 @@
using FrostFS.Netmap; using FrostFS.Netmap;
using FrostFS.Object; using FrostFS.Object;
using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Parameters;
using FrostFS.SDK.Cryptography; using FrostFS.SDK.Cryptography;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ModelsV2.Netmap; using FrostFS.SDK.ModelsV2.Netmap;
@ -96,7 +97,8 @@ public class Client : IFrostFSClient
channel: channel, channel: channel,
version: new Version(2, 13)); version: new Version(2, 13));
CheckFrostFsVersionSupport(); // TODO: define timeout logic
CheckFrostFsVersionSupport(new Context { Timeout = TimeSpan.FromSeconds(20)});
} }
public void Dispose() public void Dispose()
@ -115,105 +117,106 @@ public class Client : IFrostFSClient
} }
#region ContainerImplementation #region ContainerImplementation
public Task<ModelsV2.Container> GetContainerAsync(ContainerId containerId, Context? ctx = null) public Task<ModelsV2.Container> GetContainerAsync(PrmGetContainer args)
{ {
var service = GetContainerService(ref ctx); var service = GetContainerService(args);
return service.GetContainerAsync(containerId, ctx!); return service.GetContainerAsync(args);
} }
public IAsyncEnumerable<ContainerId> ListContainersAsync(Context? ctx = default) public IAsyncEnumerable<ContainerId> ListContainersAsync(PrmListContainer? args = null)
{ {
var service = GetContainerService(ref ctx); args = args ?? new PrmListContainer();
return service.ListContainersAsync(ctx!); var service = GetContainerService(args);
return service.ListContainersAsync(args);
} }
public Task<ContainerId> CreateContainerAsync(ModelsV2.Container container, Context? ctx = null) public Task<ContainerId> CreateContainerAsync(PrmCreateContainer args)
{ {
var service = GetContainerService(ref ctx); var service = GetContainerService(args);
return service.CreateContainerAsync(container, ctx!); return service.CreateContainerAsync(args);
} }
public Task DeleteContainerAsync(ContainerId containerId, Context? ctx = default) public Task DeleteContainerAsync(PrmDeleteContainer args)
{ {
var service = GetContainerService(ref ctx); var service = GetContainerService(args);
return service.DeleteContainerAsync(containerId, ctx!); return service.DeleteContainerAsync(args);
} }
#endregion #endregion
#region NetworkImplementation #region NetworkImplementation
public Task<NetmapSnapshot> GetNetmapSnapshotAsync(Context? ctx = default) public Task<NetmapSnapshot> GetNetmapSnapshotAsync(PrmGetNetmapSnapshot? args)
{ {
var service = GetNetmapService(ref ctx); args ??= new PrmGetNetmapSnapshot();
return service.GetNetmapSnapshotAsync(ctx!); var service = GetNetmapService(args);
return service.GetNetmapSnapshotAsync(args);
} }
public Task<ModelsV2.Netmap.NodeInfo> GetNodeInfoAsync(Context? ctx = default) public Task<ModelsV2.Netmap.NodeInfo> GetNodeInfoAsync(PrmGetNodeInfo? args)
{ {
var service = GetNetmapService(ref ctx); args ??= new PrmGetNodeInfo();
return service.GetLocalNodeInfoAsync(ctx!); var service = GetNetmapService(args);
return service.GetLocalNodeInfoAsync(args);
} }
public Task<NetworkSettings> GetNetworkSettingsAsync(Context? ctx = default) public Task<NetworkSettings> GetNetworkSettingsAsync(PrmGetNetworkSettings? args)
{ {
var service = GetNetmapService(ref ctx); args ??= new PrmGetNetworkSettings();
return service.GetNetworkSettingsAsync(ctx!); var service = GetNetmapService(args);
return service.GetNetworkSettingsAsync(args.Context!);
} }
#endregion #endregion
#region ObjectImplementation #region ObjectImplementation
public Task<ObjectHeader> GetObjectHeadAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) public Task<ObjectHeader> GetObjectHeadAsync(PrmGetObjectHead args)
{ {
var service = GetObjectService(ref ctx); var service = GetObjectService(args);
return service.GetObjectHeadAsync(containerId, objectId, ctx!); return service.GetObjectHeadAsync(args);
} }
public Task<FrostFsObject> GetObjectAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) public Task<FrostFsObject> GetObjectAsync(PrmGetObject args)
{ {
var service = GetObjectService(ref ctx); var service = GetObjectService(args);
return service.GetObjectAsync(containerId, objectId, ctx!); return service.GetObjectAsync(args);
} }
public Task<ObjectId> PutObjectAsync(PutObjectParameters putObjectParameters, Context? ctx = default) public Task<ObjectId> PutObjectAsync(PrmPutObject args)
{ {
var service = GetObjectService(ref ctx); var service = GetObjectService(args);
return service.PutObjectAsync(putObjectParameters, ctx!); return service.PutObjectAsync(args);
} }
public Task<ObjectId> PutSingleObjectAsync(FrostFsObject obj, Context? ctx = default) public Task<ObjectId> PutSingleObjectAsync(PrmPutSingleObject args)
{ {
var service = GetObjectService(ref ctx); var service = GetObjectService(args);
return service.PutSingleObjectAsync(obj, ctx!); return service.PutSingleObjectAsync(args);
} }
public Task DeleteObjectAsync(ContainerId containerId, ObjectId objectId, Context? ctx = default) public Task DeleteObjectAsync(PrmDeleteObject args)
{ {
var service = GetObjectService(ref ctx); var service = GetObjectService(args);
return service.DeleteObjectAsync(containerId, objectId, ctx!); return service.DeleteObjectAsync(args);
} }
public IAsyncEnumerable<ObjectId> SearchObjectsAsync( public IAsyncEnumerable<ObjectId> SearchObjectsAsync(PrmSearchObject args)
ContainerId containerId,
IEnumerable<ObjectFilter> filters,
Context? ctx = default)
{ {
var service = GetObjectService(ref ctx); var service = GetObjectService(args);
return service.SearchObjectsAsync(containerId, filters, ctx!); return service.SearchObjectsAsync(args);
} }
#endregion #endregion
#region SessionImplementation #region SessionImplementation
public async Task<ModelsV2.SessionToken> CreateSessionAsync(ulong expiration, Context? ctx = null) public async Task<ModelsV2.SessionToken> CreateSessionAsync(PrmCreateSession args)
{ {
var session = await CreateSessionInternalAsync(expiration, ctx); var session = await CreateSessionInternalAsync(args);
var token = session.Serialize(); var token = session.Serialize();
return new ModelsV2.SessionToken([], token); return new ModelsV2.SessionToken(token);
} }
public Task<Session.SessionToken> CreateSessionInternalAsync(ulong expiration, Context? ctx = null) internal Task<Session.SessionToken> CreateSessionInternalAsync(PrmCreateSession args)
{ {
var service = GetSessionService(ref ctx); var service = GetSessionService(args);
return service.CreateSessionAsync(expiration, ctx!); return service.CreateSessionAsync(args);
} }
#endregion #endregion
@ -226,8 +229,9 @@ public class Client : IFrostFSClient
private async void CheckFrostFsVersionSupport(Context? ctx = default) private async void CheckFrostFsVersionSupport(Context? ctx = default)
{ {
var service = GetNetmapService(ref ctx); var args = new PrmGetNodeInfo(ctx);
var localNodeInfo = await service.GetLocalNodeInfoAsync(ctx!); var service = GetNetmapService(args);
var localNodeInfo = await service.GetLocalNodeInfoAsync(args);
if (!localNodeInfo.Version.IsSupported(ClientCtx.Version)) 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) if (isDisposed)
throw new Exception("Client is disposed."); throw new Exception("Client is disposed.");
ctx ??= new Context(); ctx.Context ??= new Context();
CallInvoker? callInvoker = null; 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); callInvoker = AddInvoker(callInvoker, interceptor);
} }
} }
if (ctx.Callback != null) if (ctx.Context.Callback != null)
callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Callback)); callInvoker = AddInvoker(callInvoker, new MetricsInterceptor(ctx.Context.Callback));
return callInvoker; 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 var client = NetmapServiceClient ?? (callInvoker != null
? new NetmapService.NetmapServiceClient(callInvoker) ? new NetmapService.NetmapServiceClient(callInvoker)
: new NetmapService.NetmapServiceClient(ClientCtx.Channel)); : new NetmapService.NetmapServiceClient(ClientCtx.Channel));
@ -279,9 +283,9 @@ public class Client : IFrostFSClient
return new NetmapServiceProvider(client, ClientCtx); 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 var client = SessionServiceClient ?? (callInvoker != null
? new SessionService.SessionServiceClient(callInvoker) ? new SessionService.SessionServiceClient(callInvoker)
: new SessionService.SessionServiceClient(ClientCtx.Channel)); : new SessionService.SessionServiceClient(ClientCtx.Channel));
@ -289,9 +293,9 @@ public class Client : IFrostFSClient
return new SessionServiceProvider(client, ClientCtx); 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 var client = ContainerServiceClient ?? (callInvoker != null
? new ContainerService.ContainerServiceClient(callInvoker) ? new ContainerService.ContainerServiceClient(callInvoker)
: new ContainerService.ContainerServiceClient(ClientCtx.Channel)); : new ContainerService.ContainerServiceClient(ClientCtx.Channel));
@ -299,9 +303,9 @@ public class Client : IFrostFSClient
return new ContainerServiceProvider(client, ClientCtx); 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 var client = ObjectServiceClient ?? (callInvoker != null
? new ObjectService.ObjectServiceClient(callInvoker) ? new ObjectService.ObjectServiceClient(callInvoker)
: new ObjectService.ObjectServiceClient(ClientCtx.Channel)); : new ObjectService.ObjectServiceClient(ClientCtx.Channel));

View file

@ -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;
}
}

View file

@ -1,52 +1,50 @@
using System; using System;
using System.Collections.Generic; using System.Collections.Generic;
using System.Threading.Tasks; using System.Threading.Tasks;
using FrostFS.SDK.ClientV2.Parameters;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ModelsV2.Netmap; using FrostFS.SDK.ModelsV2.Netmap;
namespace FrostFS.SDK.ClientV2.Interfaces; namespace FrostFS.SDK.ClientV2.Interfaces;
public interface IFrostFSClient : IDisposable public interface IFrostFSClient : IDisposable
{ {
#region Network #region Network
Task<NetmapSnapshot> GetNetmapSnapshotAsync(Context? context = default); Task<NetmapSnapshot> GetNetmapSnapshotAsync(PrmGetNetmapSnapshot? args = null);
Task<NodeInfo> GetNodeInfoAsync(Context? context = default); Task<NodeInfo> GetNodeInfoAsync(PrmGetNodeInfo? args = null);
Task<NetworkSettings> GetNetworkSettingsAsync(Context? context = default); Task<NetworkSettings> GetNetworkSettingsAsync(PrmGetNetworkSettings? args = null);
#endregion #endregion
#region Session #region Session
Task<SessionToken> CreateSessionAsync(ulong expiration, Context? context = default); Task<SessionToken> CreateSessionAsync(PrmCreateSession args);
#endregion #endregion
#region Container #region Container
Task<ModelsV2.Container> GetContainerAsync(ContainerId containerId, Context? context = default); Task<ModelsV2.Container> GetContainerAsync(PrmGetContainer args);
IAsyncEnumerable<ContainerId> ListContainersAsync(Context? context = default); IAsyncEnumerable<ContainerId> ListContainersAsync(PrmListContainer? args = null);
Task<ContainerId> CreateContainerAsync(ModelsV2.Container container, Context? context = default); Task<ContainerId> CreateContainerAsync(PrmCreateContainer args);
Task DeleteContainerAsync(ContainerId containerId, Context? context = default); Task DeleteContainerAsync(PrmDeleteContainer args);
#endregion #endregion
#region Object #region Object
Task<ObjectHeader> GetObjectHeadAsync(ContainerId containerId, ObjectId objectId, Context? context = default); Task<ObjectHeader> GetObjectHeadAsync(PrmGetObjectHead args);
Task<FrostFsObject> GetObjectAsync(ContainerId containerId, ObjectId objectId, Context? context = default); Task<FrostFsObject> GetObjectAsync(PrmGetObject args);
Task<ObjectId> PutObjectAsync(PutObjectParameters putObjectParameters, Context? context = default); Task<ObjectId> PutObjectAsync(PrmPutObject putObjectParameters);
Task<ObjectId> PutSingleObjectAsync(FrostFsObject obj, Context? context = default); Task<ObjectId> PutSingleObjectAsync(PrmPutSingleObject args);
Task DeleteObjectAsync(ContainerId containerId, ObjectId objectId, Context? context = default); Task DeleteObjectAsync(PrmDeleteObject args);
IAsyncEnumerable<ObjectId> SearchObjectsAsync(ContainerId cid, IEnumerable<ObjectFilter> filters, Context? context = default); IAsyncEnumerable<ObjectId> SearchObjectsAsync(PrmSearchObject args);
#endregion #endregion
#region Tools #region Tools
ObjectId CalculateObjectId(ObjectHeader header); ObjectId CalculateObjectId(ObjectHeader header);
#endregion #endregion
} }

View file

@ -2,13 +2,13 @@ using FrostFS.SDK.ModelsV2;
namespace FrostFS.SDK.ClientV2.Mappers.GRPC; 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( return new FrostFsObject(obj.Header.ToModel())
ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()), {
obj.Header.ToModel(), ObjectId = ObjectId.FromHash(obj.ObjectId.Value.ToByteArray())
obj.Payload.ToByteArray()); };
} }
} }

View file

@ -4,7 +4,7 @@ namespace FrostFS.SDK.ClientV2;
public static class SessionMapper 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()]; byte[] bytes = new byte[token.CalculateSize()];
CodedOutputStream stream = new(bytes); CodedOutputStream stream = new(bytes);
@ -13,11 +13,9 @@ public static class SessionMapper
return bytes; 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); token.MergeFrom(bytes);
return token; return token;
} }
} }

View file

@ -0,0 +1,11 @@
namespace FrostFS.SDK.ClientV2.Parameters;
public interface IContext
{
/// <summary>
/// The method call can be extended with additional behavior like canceling by timeout or user's request,
/// callbacks, interceptors.
/// </summary>
/// <value>Additional parameters for calling the method</value>
Context? Context { get; set; }
}

View file

@ -0,0 +1,14 @@
using FrostFS.SDK.ModelsV2;
namespace FrostFS.SDK.ClientV2.Parameters;
public interface ISessionToken
{
/// <summary>
/// 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.
/// </summary>
/// <value>Instance of the session obtained from the server</value>
SessionToken? SessionToken { get; set; }
}

View file

@ -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;
/// <summary>
/// Since the container becomes available with some delay, it needs to poll the container status
/// </summary>
/// <value>Rules for polling the result</value>
public PrmWait? WaitParams { get; set; }
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = context;
}

View file

@ -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;
/// <summary>
/// Since the container is removed with some delay, it needs to poll the container status
/// </summary>
/// <value>Rules for polling the result</value>
public PrmWait? WaitParams { get; set; }
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = ctx;
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
/// <inheritdoc />
public SessionToken? SessionToken { get; set; }
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = ctx;
}

View file

@ -0,0 +1,14 @@
using System.Collections.Specialized;
namespace FrostFS.SDK.ClientV2.Parameters;
public sealed class PrmGetNetmapSnapshot(Context? context = default) : IContext
{
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = context;
}

View file

@ -0,0 +1,14 @@
using System.Collections.Specialized;
namespace FrostFS.SDK.ClientV2.Parameters;
public sealed class PrmGetNetworkSettings(Context? context = default) : IContext
{
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = context;
}

View file

@ -0,0 +1,14 @@
using System.Collections.Specialized;
namespace FrostFS.SDK.ClientV2.Parameters;
public sealed class PrmGetNodeInfo(Context? context = default) : IContext
{
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = context;
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
/// <inheritdoc />
public SessionToken? SessionToken { get; set; }
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
/// <inheritdoc />
public SessionToken? SessionToken { get; set; }
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
}

View file

@ -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
{
/// <summary>
/// Need to provide values like <c>ContainerId</c> and <c>ObjectType</c> to create and object.
/// Optional parameters ike <c>Attributes</c> can be provided as well.
/// </summary>
/// <value>Header with required parameters to create an object</value>
public ObjectHeader? Header { get; set; }
/// <summary>
/// A stream with source data
/// </summary>
public Stream? Payload { get; set; }
/// <summary>
/// Object size is limited. In the data exceeds the limit, the object will be splitted.
/// If the parameter is <c>true</c>, the client side cut is applied. Otherwise, the data is transferred
/// as a stream and will be cut on server side.
/// </summary>
/// <value>Is client cut is applied</value>
public bool ClientCut { get; set; }
/// <summary>
/// Overrides default size of the buffer for stream transferring.
/// </summary>
/// <value>Size of the buffer</value>
public int BufferMaxSize { get; set; }
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
/// <inheritdoc />
public SessionToken? SessionToken { get; set; }
}

View file

@ -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;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; } = context;
/// <inheritdoc />
public SessionToken? SessionToken { get; set; }
}

View file

@ -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;
/// <summary>
/// Defines the search criteria
/// </summary>
/// <value>Collection of filters</value>
public IEnumerable<ObjectFilter> Filters { get; set; } = filters;
/// <summary>
/// FrostFS request X-Headers
/// </summary>
public NameValueCollection XHeaders { get; set; } = [];
/// <inheritdoc />
public Context? Context { get; set; }
/// <inheritdoc />
public SessionToken? SessionToken { get; set; }
}

View file

@ -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);
}
}

View file

@ -6,6 +6,10 @@ using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Cryptography; using FrostFS.SDK.Cryptography;
using System.Collections.Generic; using System.Collections.Generic;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.Refs;
using System;
using FrostFS.SDK.ClientV2.Parameters;
using System.Collections.Specialized;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
@ -19,28 +23,21 @@ internal class ContainerServiceProvider : ContextAccessor
containerServiceClient = service; containerServiceClient = service;
} }
internal async Task<ModelsV2.Container> GetContainerAsync(ContainerId cid, Context context) internal async Task<ModelsV2.Container> GetContainerAsync(PrmGetContainer args)
{ {
var request = new GetRequest GetRequest request = GetContainerRequest(args.ContainerId.ToGrpcMessage(), args.XHeaders);
{
Body = new GetRequest.Types.Body
{
ContainerId = cid.ToGrpcMessage()
},
};
request.AddMetaHeader(); var response = await containerServiceClient.GetAsync(request, null, args.Context!.Deadline, args.Context.CancellationToken);
request.Sign(Context.Key);
var response = await containerServiceClient.GetAsync(request, null, context.Deadline, context.CancellationToken);
Verifier.CheckResponse(response); Verifier.CheckResponse(response);
return response.Body.Container.ToModel(); return response.Body.Container.ToModel();
} }
internal async IAsyncEnumerable<ContainerId> ListContainersAsync(Context ctx) internal async IAsyncEnumerable<ContainerId> ListContainersAsync(PrmListContainer args)
{ {
var ctx = args.Context!;
var request = new ListRequest var request = new ListRequest
{ {
Body = new ListRequest.Types.Body Body = new ListRequest.Types.Body
@ -49,7 +46,7 @@ internal class ContainerServiceProvider : ContextAccessor
} }
}; };
request.AddMetaHeader(); request.AddMetaHeader(args.XHeaders);
request.Sign(Context.Key); request.Sign(Context.Key);
var response = await containerServiceClient.ListAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await containerServiceClient.ListAsync(request, null, ctx.Deadline, ctx.CancellationToken);
@ -62,9 +59,10 @@ internal class ContainerServiceProvider : ContextAccessor
} }
} }
internal async Task<ContainerId> CreateContainerAsync(ModelsV2.Container container, Context ctx) internal async Task<ContainerId> CreateContainerAsync(PrmCreateContainer args)
{ {
var grpcContainer = container.ToGrpcMessage(); var ctx = args.Context!;
var grpcContainer = args.Container.ToGrpcMessage();
grpcContainer.OwnerId = Context.Owner.ToGrpcMessage(); grpcContainer.OwnerId = Context.Owner.ToGrpcMessage();
grpcContainer.Version = Context.Version.ToGrpcMessage(); grpcContainer.Version = Context.Version.ToGrpcMessage();
@ -73,37 +71,113 @@ internal class ContainerServiceProvider : ContextAccessor
Body = new PutRequest.Types.Body Body = new PutRequest.Types.Body
{ {
Container = grpcContainer, Container = grpcContainer,
Signature = Context.Key.SignRFC6979(grpcContainer), Signature = Context.Key.SignRFC6979(grpcContainer)
} }
}; };
request.AddMetaHeader();
request.AddMetaHeader(args.XHeaders);
request.Sign(Context.Key); request.Sign(Context.Key);
var response = await containerServiceClient.PutAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await containerServiceClient.PutAsync(request, null, ctx.Deadline, ctx.CancellationToken);
Verifier.CheckResponse(response); Verifier.CheckResponse(response);
await WaitForContainer(WaitExpects.Exists, response.Body.ContainerId, args.WaitParams, ctx);
return new ContainerId(Base58.Encode(response.Body.ContainerId.Value.ToByteArray())); 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 var request = new DeleteRequest
{ {
Body = new DeleteRequest.Types.Body Body = new DeleteRequest.Types.Body
{ {
ContainerId = cid.ToGrpcMessage(), ContainerId = args.ContainerId.ToGrpcMessage(),
Signature = Context.Key.SignRFC6979(cid.ToGrpcMessage().Value) Signature = Context.Key.SignRFC6979(args.ContainerId.ToGrpcMessage().Value)
} }
}; };
request.AddMetaHeader(); request.AddMetaHeader(args.XHeaders);
request.Sign(Context.Key); request.Sign(Context.Key);
var response = await containerServiceClient.DeleteAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await containerServiceClient.DeleteAsync(request, null, ctx.Deadline, ctx.CancellationToken);
await WaitForContainer(WaitExpects.Removed, request.Body.ContainerId, args.WaitParams, ctx);
Verifier.CheckResponse(response); 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<Task> 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);
}
}
}
} }

View file

@ -7,6 +7,7 @@ using NodeInfo = FrostFS.SDK.ModelsV2.Netmap.NodeInfo;
using FrostFS.SDK.ModelsV2.Netmap; using FrostFS.SDK.ModelsV2.Netmap;
using static FrostFS.Netmap.NetworkConfig.Types; using static FrostFS.Netmap.NetworkConfig.Types;
using FrostFS.SDK.ClientV2.Parameters;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
@ -39,14 +40,15 @@ internal class NetmapServiceProvider : ContextAccessor
return settings; return settings;
} }
internal async Task<NodeInfo> GetLocalNodeInfoAsync(Context ctx) internal async Task<NodeInfo> GetLocalNodeInfoAsync(PrmGetNodeInfo args)
{ {
var ctx = args.Context!;
var request = new LocalNodeInfoRequest var request = new LocalNodeInfoRequest
{ {
Body = new LocalNodeInfoRequest.Types.Body { } Body = new LocalNodeInfoRequest.Types.Body { }
}; };
request.AddMetaHeader(); request.AddMetaHeader(args.XHeaders);
request.Sign(Context.Key); request.Sign(Context.Key);
var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await netmapServiceClient.LocalNodeInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken);
@ -58,12 +60,9 @@ internal class NetmapServiceProvider : ContextAccessor
internal async Task<NetworkInfoResponse> GetNetworkInfoAsync(Context ctx) internal async Task<NetworkInfoResponse> GetNetworkInfoAsync(Context ctx)
{ {
var request = new NetworkInfoRequest var request = new NetworkInfoRequest();
{
Body = new NetworkInfoRequest.Types.Body { }
};
request.AddMetaHeader(); request.AddMetaHeader(null);
request.Sign(Context.Key); request.Sign(Context.Key);
var response = await netmapServiceClient.NetworkInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await netmapServiceClient.NetworkInfoAsync(request, null, ctx.Deadline, ctx.CancellationToken);
@ -73,14 +72,13 @@ internal class NetmapServiceProvider : ContextAccessor
return response; return response;
} }
internal async Task<NetmapSnapshot> GetNetmapSnapshotAsync(Context ctx) internal async Task<NetmapSnapshot> GetNetmapSnapshotAsync(PrmGetNetmapSnapshot args)
{ {
var request = new NetmapSnapshotRequest var ctx = args.Context!;
{
Body = new NetmapSnapshotRequest.Types.Body { }
};
request.AddMetaHeader(); var request = new NetmapSnapshotRequest();
request.AddMetaHeader(args.XHeaders);
request.Sign(Context.Key); request.Sign(Context.Key);
var response = await netmapServiceClient.NetmapSnapshotAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await netmapServiceClient.NetmapSnapshotAsync(request, null, ctx.Deadline, ctx.CancellationToken);

View file

@ -12,6 +12,8 @@ using FrostFS.SDK.Cryptography;
using FrostFS.Session; using FrostFS.Session;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ClientV2.Extensions; using FrostFS.SDK.ClientV2.Extensions;
using System.Threading;
using FrostFS.SDK.ClientV2.Parameters;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
@ -19,21 +21,30 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
{ {
readonly ObjectTools tools = new(ctx); readonly ObjectTools tools = new(ctx);
internal async Task<ObjectHeader> GetObjectHeadAsync(ContainerId cid, ObjectId oid, Context ctx) internal async Task<ObjectHeader> GetObjectHeadAsync(PrmGetObjectHead args)
{ {
var ctx = args.Context!;
var request = new HeadRequest var request = new HeadRequest
{ {
Body = new HeadRequest.Types.Body Body = new HeadRequest.Types.Body
{ {
Address = new Address Address = new Address
{ {
ContainerId = cid.ToGrpcMessage(), ContainerId = args.ContainerId.ToGrpcMessage(),
ObjectId = oid.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); request.Sign(Context.Key);
var response = await client!.HeadAsync(request, null, ctx.Deadline, ctx.CancellationToken); var response = await client!.HeadAsync(request, null, ctx.Deadline, ctx.CancellationToken);
@ -43,9 +54,9 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
return response.Body.Header.Header.ToModel(); return response.Body.Header.Header.ToModel();
} }
internal async Task<FrostFsObject> GetObjectAsync(ContainerId cid, ObjectId oid, Context ctx) internal async Task<FrostFsObject> GetObjectAsync(PrmGetObject args)
{ {
var sessionToken = await GetOrCreateSession(ctx); var ctx = args.Context!;
var request = new GetRequest var request = new GetRequest
{ {
@ -53,41 +64,49 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
{ {
Address = new Address Address = new Address
{ {
ContainerId = cid.ToGrpcMessage(), ContainerId = args.ContainerId.ToGrpcMessage(),
ObjectId = oid.ToGrpcMessage() ObjectId = args.ObjectId.ToGrpcMessage()
} }
} }
}; };
request.AddMetaHeader(); var sessionToken = await GetOrCreateSession(args, ctx);
request.AddObjectSessionToken(
sessionToken, sessionToken.CreateObjectTokenContext(
cid.ToGrpcMessage(), request.Body.Address,
oid.ToGrpcMessage(),
ObjectSessionContext.Types.Verb.Get, ObjectSessionContext.Types.Verb.Get,
Context.Key Context.Key);
);
request.AddMetaHeader(args.XHeaders, sessionToken);
request.Sign(Context.Key); request.Sign(Context.Key);
return await GetObject(request, ctx); 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 var request = new DeleteRequest
{ {
Body = new DeleteRequest.Types.Body Body = new DeleteRequest.Types.Body
{ {
Address = new Address Address = new Address
{ {
ContainerId = cid.ToGrpcMessage(), ContainerId = args.ContainerId.ToGrpcMessage(),
ObjectId = oid.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); request.Sign(Context.Key);
var response = await client.DeleteAsync(request, null, ctx.Deadline, ctx.CancellationToken); 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); Verifier.CheckResponse(response);
} }
internal async IAsyncEnumerable<ObjectId> SearchObjectsAsync( internal async IAsyncEnumerable<ObjectId> SearchObjectsAsync(PrmSearchObject args)
ContainerId cid,
IEnumerable<ObjectFilter> filters,
Context ctx)
{ {
var ctx = args.Context!;
var request = new SearchRequest var request = new SearchRequest
{ {
Body = new SearchRequest.Types.Body Body = new SearchRequest.Types.Body
{ {
ContainerId = cid.ToGrpcMessage(), ContainerId = args.ContainerId.ToGrpcMessage(),
Filters = { }, Filters = { },
Version = 1 // TODO: clarify this param 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()));
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.AddMetaHeader();
request.Sign(Context.Key); request.Sign(Context.Key);
var objectsIds = SearchObjects(request, ctx); var objectsIds = SearchObjects(request, ctx);
@ -123,25 +148,24 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
} }
} }
internal Task<ObjectId> PutObjectAsync(PutObjectParameters parameters, Context ctx) internal Task<ObjectId> PutObjectAsync(PrmPutObject args)
{ {
if (parameters.Header == null) if (args.Header == null)
throw new ArgumentException("Value cannot be null", nameof(parameters.Header)); throw new ArgumentException("Value cannot be null", nameof(args.Header));
if (parameters.Payload == null) if (args.Payload == null)
throw new ArgumentException("Value cannot be null", nameof(parameters.Payload)); throw new ArgumentException("Value cannot be null", nameof(args.Payload));
if (parameters.ClientCut) if (args.ClientCut)
return PutClientCutObject(parameters, ctx); return PutClientCutObject(args);
else else
return PutStreamObject(parameters, ctx); return PutStreamObject(args);
} }
internal async Task<ObjectId> PutSingleObjectAsync(FrostFsObject modelObject, Context ctx) internal async Task<ObjectId> PutSingleObjectAsync(PrmPutSingleObject args)
{ {
var sessionToken = await GetOrCreateSession(ctx); var ctx = args.Context!;
var grpcObject = tools.CreateObject(args.FrostFsObject);
var grpcObject = tools.CreateObject(modelObject);
var request = new PutSingleRequest var request = new PutSingleRequest
{ {
@ -151,14 +175,14 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
} }
}; };
request.AddMetaHeader(); var sessionToken = await GetOrCreateSession(args, ctx);
request.AddObjectSessionToken(
sessionToken, sessionToken.CreateObjectTokenContext(
grpcObject.Header.ContainerId, new Address { ContainerId = grpcObject.Header.ContainerId, ObjectId = grpcObject.ObjectId},
grpcObject.ObjectId,
ObjectSessionContext.Types.Verb.Put, ObjectSessionContext.Types.Verb.Put,
Context.Key Context.Key);
);
request.AddMetaHeader(args.XHeaders, sessionToken);
request.Sign(Context.Key); request.Sign(Context.Key);
@ -169,17 +193,23 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
return ObjectId.FromHash(grpcObject.ObjectId.Value.ToByteArray()); return ObjectId.FromHash(grpcObject.ObjectId.Value.ToByteArray());
} }
private async Task<ObjectId> PutClientCutObject(PutObjectParameters parameters, Context ctx) static readonly AsyncLocal<Session.SessionToken> asyncLocalSession = new ();
private async Task<ObjectId> PutClientCutObject(PrmPutObject args)
{ {
var payloadStream = parameters.Payload!; var ctx = args.Context!;
var header = parameters.Header!; var tokenRaw = await GetOrCreateSession(args, ctx);
var token = new ModelsV2.SessionToken(tokenRaw.Serialize());
var payloadStream = args.Payload!;
var header = args.Header!;
ObjectId? objectId; ObjectId? objectId;
List<ObjectId> sentObjectIds = []; List<ObjectId> sentObjectIds = [];
FrostFsObject? currentObject; FrostFsObject? currentObject;
var networkSettings = await Context.Client.GetNetworkSettingsAsync(ctx); var networkSettings = await Context.Client.GetNetworkSettingsAsync(new PrmGetNetworkSettings(ctx));
var objectSize = (int)networkSettings.MaxObjectSize; var objectSize = (int)networkSettings.MaxObjectSize;
@ -208,27 +238,30 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
split.Previous = sentObjectIds.LastOrDefault(); 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); .SetSplit(split);
if (largeObject.PayloadLength == fullLength) if (largeObject.PayloadLength == fullLength)
break; break;
objectId = await PutSingleObjectAsync(currentObject, ctx); objectId = await PutSingleObjectAsync(new PrmPutSingleObject(currentObject, ctx) { SessionToken = token });
sentObjectIds.Add(objectId!); sentObjectIds.Add(objectId!);
} }
if (sentObjectIds.Count != 0) if (sentObjectIds.Count != 0)
{ {
largeObject.AddAttributes(parameters.Header!.Attributes); largeObject.AddAttributes(args.Header!.Attributes);
largeObject.CalculateHash();
currentObject.SetParent(largeObject); currentObject.SetParent(largeObject);
objectId = await PutSingleObjectAsync(currentObject, ctx); var putSingleObjectParams = new PrmPutSingleObject(currentObject, ctx) { SessionToken = token };
objectId = await PutSingleObjectAsync(putSingleObjectParams);
sentObjectIds.Add(objectId); sentObjectIds.Add(objectId);
var linkObject = new LinkObject(header.ContainerId, split.SplitId, largeObject) var linkObject = new LinkObject(header.ContainerId, split.SplitId, largeObject)
@ -236,22 +269,21 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
linkObject.Header.Attributes.Clear(); linkObject.Header.Attributes.Clear();
_ = await PutSingleObjectAsync(linkObject, ctx); _ = await PutSingleObjectAsync(new PrmPutSingleObject(linkObject, ctx){ SessionToken = token });
return tools.CalculateObjectId(largeObject.Header); 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<ObjectId> PutStreamObject(PutObjectParameters parameters, Context ctx) private async Task<ObjectId> PutStreamObject(PrmPutObject args)
{ {
var payload = parameters.Payload!; var ctx = args.Context!;
var header = parameters.Header!; var payload = args.Payload!;
var header = args.Header!;
var sessionToken = await GetOrCreateSession(ctx);
var hdr = header.ToGrpcMessage(); var hdr = header.ToGrpcMessage();
hdr.OwnerId = Context.Owner.ToGrpcMessage(); hdr.OwnerId = Context.Owner.ToGrpcMessage();
@ -270,20 +302,21 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
} }
}; };
initRequest.AddMetaHeader(); var sessionToken = await GetOrCreateSession(args, ctx);
initRequest.AddObjectSessionToken(
sessionToken, sessionToken.CreateObjectTokenContext(
hdr.ContainerId, new Address { ContainerId = hdr.ContainerId, ObjectId = oid },
oid,
ObjectSessionContext.Types.Verb.Put, ObjectSessionContext.Types.Verb.Put,
Context.Key Context.Key
); );
initRequest.AddMetaHeader(args.XHeaders, sessionToken);
initRequest.Sign(Context.Key); initRequest.Sign(Context.Key);
using var stream = await PutObjectInit(initRequest, ctx); 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) if (payload.CanSeek)
{ {
@ -307,12 +340,13 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
{ {
Body = new PutRequest.Types.Body Body = new PutRequest.Types.Body
{ {
Chunk = ByteString.CopyFrom(buffer[..bytesCount]), Chunk = ByteString.CopyFrom(buffer.AsSpan()[..bytesCount]),
}, },
VerifyHeader = null VerifyHeader = null
}; };
chunkRequest.Sign(Context.Key); chunkRequest.Sign(Context.Key);
await stream.Write(chunkRequest); await stream.Write(chunkRequest);
} }
@ -388,13 +422,13 @@ internal class ObjectServiceProvider(ObjectService.ObjectServiceClient client, C
return new SearchReader(call); return new SearchReader(call);
} }
private async Task<Session.SessionToken> GetOrCreateSession(Context ctx) private async ValueTask<Session.SessionToken> 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);
} }
} }

View file

@ -1,6 +1,6 @@
using System.Threading.Tasks; using System.Threading.Tasks;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ClientV2.Parameters;
using FrostFS.Session; using FrostFS.Session;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;
@ -15,41 +15,41 @@ internal class SessionServiceProvider : ContextAccessor
_sessionServiceClient = sessionServiceClient; _sessionServiceClient = sessionServiceClient;
} }
internal async Task<SessionToken> CreateSessionAsync(ulong expiration, Context ctx) internal async Task<SessionToken> CreateSessionAsync(PrmCreateSession args)
{ {
var request = new CreateRequest var request = new CreateRequest
{ {
Body = new CreateRequest.Types.Body Body = new CreateRequest.Types.Body
{ {
OwnerId = Context.Owner.ToGrpcMessage(), OwnerId = Context.Owner.ToGrpcMessage(),
Expiration = expiration Expiration = args.Expiration
} }
}; };
request.AddMetaHeader(); request.AddMetaHeader(args.XHeaders);
request.Sign(Context.Key); request.Sign(Context.Key);
var token = await CreateSession(request, ctx); return await CreateSession(request, args.Context!);
return token;
} }
internal async Task<SessionToken> CreateSession(CreateRequest request, Context ctx) internal async Task<SessionToken> 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 return new SessionToken
{ {
Body = new SessionToken.Types.Body Body = new SessionToken.Types.Body
{ {
Id = resp.Body.Id, Id = response.Body.Id,
SessionKey = resp.Body.SessionKey, SessionKey = response.Body.SessionKey,
OwnerId = request.Body.OwnerId, OwnerId = request.Body.OwnerId,
Lifetime = new SessionToken.Types.Body.Types.TokenLifetime Lifetime = new SessionToken.Types.Body.Types.TokenLifetime
{ {
Exp = request.Body.Expiration, Exp = request.Body.Expiration,
Iat = resp.MetaHeader.Epoch, Iat = response.MetaHeader.Epoch,
Nbf = resp.MetaHeader.Epoch, Nbf = response.MetaHeader.Epoch,
} }
} }
}; };

View file

@ -13,6 +13,12 @@ public static class ObjectExtensions
return obj; 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) public static FrostFsObject AddAttribute(this FrostFsObject obj, string key, string value)
{ {
obj.AddAttribute(new ObjectAttribute(key, value)); obj.AddAttribute(new ObjectAttribute(key, value));
@ -45,9 +51,6 @@ public static class ObjectExtensions
public static FrostFsObject CalculateObjectId(this FrostFsObject obj) public static FrostFsObject CalculateObjectId(this FrostFsObject obj)
{ {
if (obj.Payload == null)
throw new MissingFieldException("Payload cannot be null");
if (obj.Header == null) if (obj.Header == null)
throw new MissingFieldException("Header cannot be null"); throw new MissingFieldException("Header cannot be null");

View file

@ -1,4 +1,7 @@
using System.Collections.Specialized;
using System.Linq;
using System.Security.Cryptography; using System.Security.Cryptography;
using FrostFS.Refs; using FrostFS.Refs;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
@ -9,36 +12,38 @@ namespace FrostFS.SDK.ClientV2;
public static class RequestConstructor 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; if (request.MetaHeader is not null)
metaHeader ??= MetaHeader.Default().ToGrpcMessage(); return;
request.MetaHeader = metaHeader;
request.MetaHeader = MetaHeader.Default().ToGrpcMessage();
if (sessionToken != null)
request.MetaHeader.SessionToken = sessionToken;
if (xHeaders != null && xHeaders.Count > 0)
request.MetaHeader.XHeaders.AddRange(
xHeaders.Cast<string>().SelectMany(key => xHeaders.GetValues(key),
(k, v) => new XHeader { Key = k, Value = v }));
} }
public static void AddObjectSessionToken( public static void CreateObjectTokenContext(this Session.SessionToken sessionToken,
this IRequest request, Address address,
Session.SessionToken sessionToken,
ContainerID cid,
ObjectID oid,
ObjectSessionContext.Types.Verb verb, ObjectSessionContext.Types.Verb verb,
ECDsa key) ECDsa key)
{ {
if (request.MetaHeader.SessionToken is not null) ObjectSessionContext.Types.Target target = new() { Container = address.ContainerId };
return;
request.MetaHeader.SessionToken = sessionToken; if (address.ObjectId != null)
var ctx = new ObjectSessionContext target.Objects.Add(address.ObjectId);
sessionToken.Body.Object = new()
{ {
Target = new ObjectSessionContext.Types.Target Target = target,
{
Container = cid,
Objects = { oid }
},
Verb = verb Verb = verb
}; };
request.MetaHeader.SessionToken.Body.Object = ctx; sessionToken.Signature = key.SignMessagePart(sessionToken.Body);
request.MetaHeader.SessionToken.Signature = key.SignMessagePart(request.MetaHeader.SessionToken.Body);
} }
} }

View file

@ -102,7 +102,7 @@ public static class Verifier
var status = resp.MetaHeader.Status.ToModel(); var status = resp.MetaHeader.Status.ToModel();
if (!status.IsSuccess) if (!status.IsSuccess)
throw new ApplicationException(status.ToString()); throw new ResponseException(status);
} }
/// <summary> /// <summary>

View file

@ -12,8 +12,6 @@ public class Context()
public CancellationToken CancellationToken { get; set; } = default; public CancellationToken CancellationToken { get; set; } = default;
public TimeSpan Timeout { 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 DateTime? Deadline => Timeout.Ticks > 0 ? DateTime.UtcNow.Add(Timeout) : null;
public Action<CallStatistics>? Callback { get; set; } public Action<CallStatistics>? Callback { get; set; }

View file

@ -18,4 +18,9 @@
<ProjectReference Include="..\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj" /> <ProjectReference Include="..\FrostFS.SDK.Cryptography\FrostFS.SDK.Cryptography.csproj" />
</ItemGroup> </ItemGroup>
<ItemGroup>
<AssemblyAttribute Include="System.Runtime.CompilerServices.InternalsVisibleTo">
<_Parameter1>FrostFS.SDK.ClientV2</_Parameter1>
</AssemblyAttribute>
</ItemGroup>
</Project> </Project>

View file

@ -6,32 +6,56 @@ namespace FrostFS.SDK.ModelsV2;
public class FrostFsObject public class FrostFsObject
{ {
public FrostFsObject(ObjectId objectId, ObjectHeader header, byte[] payload) /// <summary>
/// Creates new instance from <c>ObjectHeader</c>
/// </summary>
/// <param name="header"></param> <summary>
public FrostFsObject(ObjectHeader header)
{ {
ObjectId = objectId;
Payload = payload;
Header = header; Header = header;
} }
public FrostFsObject(ContainerId container, byte[] payload, ObjectType objectType = ObjectType.Regular) /// <summary>
/// Creates new instance with specified parameters
/// </summary>
/// <param name="container"></param>
/// <param name="objectType"></param>
public FrostFsObject(ContainerId container, ObjectType objectType = ObjectType.Regular)
{ {
Payload = payload; Header = new ObjectHeader(containerId: container, type: objectType);
Header = new ObjectHeader(containerId: container, type: objectType, attributes: []);
} }
/// <summary>
/// Header contains metadata for the object
/// </summary>
/// <value></value>
public ObjectHeader Header { get; set; } public ObjectHeader Header { get; set; }
/// <summary>
/// The value is calculated internally as a hash of ObjectHeader. Do not use pre-calculated value is the object has been changed.
/// </summary>
public ObjectId? ObjectId public ObjectId? ObjectId
{ {
get; internal set; get; internal set;
} }
public byte[] Payload { get; set; } /// <summary>
/// The size of payload cannot exceed <c>MaxObjectSize</c> value from <c>NetworkSettings</c>
/// Used only for PutSingleObject method
/// </summary>
/// <value>Buffer for output data</value>
public byte[] Payload { get; set; } = [];
/// <summary>
/// A payload is obtained via stream reader
/// </summary>
/// <value>Reader for received data</value>
public IObjectReader? ObjectReader { get; set; } public IObjectReader? ObjectReader { get; set; }
public Signature? Signature { get; set; } /// <summary>
/// Applied only for the last Object in chain in case of manual multipart uploading
/// </summary>
/// <param name="largeObject">Parent for multipart object</param>
public void SetParent(LargeObject largeObject) public void SetParent(LargeObject largeObject)
{ {
if (Header?.Split == null) 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(); private readonly SHA256 payloadHash = SHA256.Create();
@ -66,7 +90,7 @@ public class LargeObject(ContainerId container) : FrostFsObject(container, [])
public class LinkObject : FrostFsObject 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) Header!.Split = new Split(splitId)
{ {

View file

@ -1,14 +1 @@
using System.IO;
namespace FrostFS.SDK.ModelsV2; 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; }
}

View file

@ -1,8 +1,11 @@
namespace FrostFS.SDK.ModelsV2; namespace FrostFS.SDK.ModelsV2;
public class SessionToken(byte[] sessionKey, byte[] id) public class SessionToken
{ {
public byte[] Id { get; } = id; internal SessionToken(byte[] token)
{
public byte[] SessionKey { get; } = sessionKey; Token = token;
}
public byte[] Token { get; private set; }
} }

View file

@ -8,10 +8,10 @@ using FrostFS.SDK.ModelsV2.Netmap;
using FrostFS.SDK.ModelsV2.Enums; using FrostFS.SDK.ModelsV2.Enums;
using Google.Protobuf; using Google.Protobuf;
using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Parameters;
namespace FrostFS.SDK.Tests; namespace FrostFS.SDK.Tests;
public abstract class ContainerTestsBase public abstract class ContainerTestsBase
{ {
protected readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; protected readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq";
@ -52,7 +52,9 @@ public class ContainerTest : ContainerTestsBase
[Fact] [Fact]
public async void CreateContainerTest() 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);
Assert.NotNull(result.Value); Assert.NotNull(result.Value);
@ -66,7 +68,7 @@ public class ContainerTest : ContainerTestsBase
Mocker.Acl = BasicAcl.PublicRO; Mocker.Acl = BasicAcl.PublicRO;
var result = await GetClient().GetContainerAsync(cid); var result = await GetClient().GetContainerAsync(new PrmGetContainer(cid));
Assert.NotNull(result); Assert.NotNull(result);
Assert.Equal(Mocker.Acl, result.BasicAcl); Assert.Equal(Mocker.Acl, result.BasicAcl);
@ -99,9 +101,10 @@ public class ContainerTest : ContainerTestsBase
[Fact] [Fact]
public async void DeleteContainerAsyncTest() public async void DeleteContainerAsyncTest()
{ {
Mocker.ReturnContainerRemoved = true;
var cid = new ContainerId(Base58.Encode(Mocker.ContainerGuid.ToBytes())); var cid = new ContainerId(Base58.Encode(Mocker.ContainerGuid.ToBytes()));
await GetClient().DeleteContainerAsync(cid); await GetClient().DeleteContainerAsync(new PrmDeleteContainer(cid));
Assert.Single(Mocker.Requests); Assert.Single(Mocker.Requests);

View file

@ -77,6 +77,21 @@ public class ContainerMocker(string key) : ContainerServiceBase(key)
getResponse.VerifyHeader = GetResponseVerificationHeader(getResponse); 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( mock.Setup(x => x.GetAsync(
It.IsAny<GetRequest>(), It.IsAny<GetRequest>(),
It.IsAny<Metadata>(), It.IsAny<Metadata>(),
@ -86,6 +101,16 @@ public class ContainerMocker(string key) : ContainerServiceBase(key)
{ {
Verifier.CheckRequest(r); Verifier.CheckRequest(r);
if (ReturnContainerRemoved)
{
return new AsyncUnaryCall<GetResponse>(
Task.FromResult(getNoContainerResponse),
Task.FromResult(ResponseMetaData),
() => new Grpc.Core.Status(StatusCode.NotFound, string.Empty),
() => ResponseMetaData,
() => { });
}
return new AsyncUnaryCall<GetResponse>( return new AsyncUnaryCall<GetResponse>(
Task.FromResult(getResponse), Task.FromResult(getResponse),
Task.FromResult(ResponseMetaData), Task.FromResult(ResponseMetaData),
@ -154,6 +179,8 @@ public class ContainerMocker(string key) : ContainerServiceBase(key)
return mock; return mock;
} }
public bool ReturnContainerRemoved { get; set; }
public List<byte[]> ContainerIds { get; set; } = []; public List<byte[]> ContainerIds { get; set; } = [];
public List<RequestData<DeleteRequest>> Requests { get; set; } = []; public List<RequestData<DeleteRequest>> Requests { get; set; } = [];

View file

@ -2,6 +2,7 @@ using FrostFS.Refs;
using FrostFS.SDK.ClientV2; using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ClientV2.Interfaces; using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ClientV2.Parameters;
using FrostFS.SDK.Cryptography; using FrostFS.SDK.Cryptography;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ModelsV2.Enums; using FrostFS.SDK.ModelsV2.Enums;
@ -76,7 +77,7 @@ public class ObjectTest : ObjectTestsBase
Timeout = TimeSpan.FromSeconds(2) 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); Assert.NotNull(result);
@ -97,7 +98,7 @@ public class ObjectTest : ObjectTestsBase
var bytes = new byte[1024]; var bytes = new byte[1024];
rnd.NextBytes(bytes); rnd.NextBytes(bytes);
var param = new PutObjectParameters var param = new PrmPutObject
{ {
Header = Mocker.ObjectHeader, Header = Mocker.ObjectHeader,
Payload = new MemoryStream(bytes), Payload = new MemoryStream(bytes),
@ -132,7 +133,7 @@ public class ObjectTest : ObjectTestsBase
byte[] bytes = File.ReadAllBytes(@".\..\..\..\TestData\cat.jpg"); byte[] bytes = File.ReadAllBytes(@".\..\..\..\TestData\cat.jpg");
var fileLength = bytes.Length; var fileLength = bytes.Length;
var param = new PutObjectParameters var param = new PrmPutObject
{ {
Header = Mocker.ObjectHeader, Header = Mocker.ObjectHeader,
Payload = new MemoryStream(bytes), 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(); 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(); var request = Mocker.DeleteRequests.FirstOrDefault();
Assert.NotNull(request); 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(); 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(); var request = Mocker.HeadRequests.FirstOrDefault();
Assert.NotNull(request); Assert.NotNull(request);

View file

@ -4,24 +4,30 @@ using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.SDK.ModelsV2.Enums; using FrostFS.SDK.ModelsV2.Enums;
using FrostFS.SDK.ModelsV2.Netmap; using FrostFS.SDK.ModelsV2.Netmap;
using FrostFS.SDK.Cryptography;
using Grpc.Core; using Grpc.Core;
using Microsoft.Extensions.Options; using Microsoft.Extensions.Options;
using Grpc.Core.Interceptors; using Grpc.Core.Interceptors;
using System.Diagnostics; using System.Diagnostics;
using static FrostFS.Session.SessionToken.Types.Body;
using FrostFS.SDK.ClientV2.Parameters;
namespace FrostFS.SDK.SmokeTests; namespace FrostFS.SDK.SmokeTests;
public class SmokeTests public class SmokeTests
{ {
private static PrmWait lightWait = new (100, 1);
private readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq"; private readonly string key = "KwHDAJ66o8FoLBjVbjP2sWBmgBMGjt7Vv4boA7xQrBoAYBE397Aq";
private readonly string url = "http://172.29.238.97:8080"; private readonly string url = "http://172.29.238.97:8080";
[Fact] [Fact]
public async void NetworkMapTest() 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.True(result.Epoch > 0);
Assert.Single(result.NodeInfoCollection); Assert.Single(result.NodeInfoCollection);
@ -38,9 +44,9 @@ public class SmokeTests
[Fact] [Fact]
public async void NodeInfoTest() 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(2, result.Version.Major);
Assert.Equal(13, result.Version.Minor); Assert.Equal(13, result.Version.Minor);
@ -51,80 +57,74 @@ public class SmokeTests
} }
[Fact] [Fact]
public async void NodeInfo_Statictics_Test() public async void NodeInfo_Statistics_Test()
{ {
var ctx = new Context var ctx = new Context
{ {
Callback = (cs) => Console.WriteLine($"{cs.MethodName} took {cs.ElapsedMicroSeconds} microseconds") 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] [Fact]
[InlineData(1)] public async void GetSessionTest()
[InlineData(3 * 1024 * 1024)] // exactly one chunk size - 3MB
[InlineData(6 * 1024 * 1024 + 100)]
public async void SimpleScenarioTest(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); var token = await client.CreateSessionAsync(new PrmCreateSession(100));
var containerId = await fsClient.CreateContainerAsync( var session = new Session.SessionToken().Deserialize(token.Token);
new ModelsV2.Container(BasicAcl.PublicRW, new PlacementPolicy(true, new Replica(1))),
new Context var ecdsaKey = this.key.LoadWif();
{ var owner = OwnerId.FromKey(ecdsaKey);
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 context = new Context [Fact]
public async void CreateObjectWithSessionToken()
{ {
Timeout = TimeSpan.FromSeconds(10), using var client = Client.GetInstance(GetOptions(this.key, this.url));
Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0))
};
var container = await GetContainer(fsClient, containerId, context); 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( Header = new ObjectHeader(
containerId: containerId, containerId: containerId,
type: ObjectType.Regular, type: ObjectType.Regular,
new ObjectAttribute("fileName", "test")), new ObjectAttribute("fileName", "test")),
Payload = new MemoryStream(bytes), Payload = new MemoryStream(bytes),
ClientCut = false ClientCut = false,
SessionToken = token
}; };
var objectId = await fsClient.PutObjectAsync(param, new Context var objectId = await client.PutObjectAsync(param);
{
Callback = new((CallStatistics cs) => Assert.True(cs.ElapsedMicroSeconds > 0))
});
var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test"); var @object = await client.GetObjectAsync(new PrmGetObject(containerId, objectId));
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 downloadedBytes = new byte[@object.Header.PayloadLength]; var downloadedBytes = new byte[@object.Header.PayloadLength];
MemoryStream ms = new(downloadedBytes); MemoryStream ms = new(downloadedBytes);
@ -137,11 +137,177 @@ public class SmokeTests
Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); 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 foreach (var _ in fsClient.ListContainersAsync()) await Cleanup(client);
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"); Assert.Fail("Containers exist");
} }
@ -155,13 +321,16 @@ public class SmokeTests
[InlineData(2 * 64 * 1024 * 1024 + 256)] [InlineData(2 * 64 * 1024 * 1024 + 256)]
public async void ClientCutScenarioTest(int objectSize) 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 var context = new Context
{ {
@ -169,13 +338,13 @@ public class SmokeTests
Interceptors = new([new MetricsInterceptor()]) Interceptors = new([new MetricsInterceptor()])
}; };
var container = await GetContainer(fsClient, containerId, context); var container = await client.GetContainerAsync(new PrmGetContainer(containerId, context));
Assert.NotNull(container); Assert.NotNull(container);
byte[] bytes = GetRandomBytes(objectSize); byte[] bytes = GetRandomBytes(objectSize);
var param = new PutObjectParameters var param = new PrmPutObject
{ {
Header = new ObjectHeader( Header = new ObjectHeader(
containerId: containerId, containerId: containerId,
@ -185,16 +354,16 @@ public class SmokeTests
ClientCut = true ClientCut = true
}; };
var objectId = await fsClient.PutObjectAsync(param); var objectId = await client.PutObjectAsync(param);
var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test"); var filter = new ObjectFilter(ObjectMatchType.Equals, "fileName", "test");
bool hasObject = false; 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; 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.Equal((ulong)bytes.Length, objHeader.PayloadLength);
Assert.Single(objHeader.Attributes); Assert.Single(objHeader.Attributes);
Assert.Equal("fileName", objHeader.Attributes.First().Key); Assert.Equal("fileName", objHeader.Attributes.First().Key);
@ -203,7 +372,7 @@ public class SmokeTests
Assert.True(hasObject); 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]; var downloadedBytes = new byte[@object.Header.PayloadLength];
MemoryStream ms = new(downloadedBytes); MemoryStream ms = new(downloadedBytes);
@ -216,14 +385,23 @@ public class SmokeTests
Assert.Equal(MD5.HashData(bytes), MD5.HashData(downloadedBytes)); 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<ContainerId>? enumerator = null;
do
{
if (deadline <= DateTime.UtcNow)
{ {
Assert.Fail("Containers exist"); Assert.Fail("Containers exist");
break;
} }
enumerator = client.ListContainersAsync().GetAsyncEnumerator();
await Task.Delay(500);
}
while (await enumerator!.MoveNextAsync());
} }
private static byte[] GetRandomBytes(int size) private static byte[] GetRandomBytes(int size)
@ -243,35 +421,15 @@ 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); await client.DeleteContainerAsync(new PrmDeleteContainer(cid) { WaitParams = lightWait });
}
} }
} }
static async Task<ModelsV2.Container> 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;
}
}
}
}
public class MetricsInterceptor() : Interceptor public class MetricsInterceptor() : Interceptor
{ {