[#1] Add object Get operation + code quality

Signed-off-by: Ivan Pchelintsev <i.pchelintsev@yadro.com>
This commit is contained in:
Ivan Pchelintsev 2024-05-16 17:31:48 +03:00
parent 9aa93d123d
commit b307c2c899
17 changed files with 182 additions and 99 deletions

View file

@ -2,6 +2,7 @@ using System.Security.Cryptography;
using FrostFS.Container; using FrostFS.Container;
using FrostFS.Netmap; using FrostFS.Netmap;
using FrostFS.Object; using FrostFS.Object;
using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.Cryptography; using FrostFS.SDK.Cryptography;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;

View file

@ -5,7 +5,7 @@ using DeleteResponse = FrostFS.Container.DeleteResponse;
using GetResponse = FrostFS.Container.GetResponse; using GetResponse = FrostFS.Container.GetResponse;
using PutResponse = FrostFS.Container.PutResponse; using PutResponse = FrostFS.Container.PutResponse;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2.Interfaces;
public interface IFrostFSClient public interface IFrostFSClient
{ {
@ -14,6 +14,7 @@ public interface IFrostFSClient
Task<GetResponse> GetContainerAsync(ContainerID containerId); Task<GetResponse> GetContainerAsync(ContainerID containerId);
Task<DeleteResponse> DeleteContainerAsync(ContainerID containerId); Task<DeleteResponse> DeleteContainerAsync(ContainerID containerId);
Task<HeadResponse> GetObjectHeadAsync(ContainerID containerId, ObjectID objectId); Task<HeadResponse> GetObjectHeadAsync(ContainerID containerId, ObjectID objectId);
Task<Object.PutResponse> PutObjectAsync(Object.Header header, Stream payload); Task<Object.Object> GetObjectAsync(ContainerID containerId, ObjectID objectId);
Task<Object.PutResponse> PutObjectAsync(Header header, Stream payload);
Task<Object.DeleteResponse> DeleteObjectAsync(ContainerID containerId, ObjectID objectId); Task<Object.DeleteResponse> DeleteObjectAsync(ContainerID containerId, ObjectID objectId);
} }

View file

@ -19,14 +19,14 @@ public static class ContainerMapper
public static ModelsV2.Container ToModel(this Container.Container container) public static ModelsV2.Container ToModel(this Container.Container container)
{ {
var basicAclName = Enum.GetName(typeof(BasicACL), container.BasicAcl); var basicAclName = Enum.GetName(typeof(BasicAcl), container.BasicAcl);
if (basicAclName is null) if (basicAclName is null)
{ {
throw new ArgumentException($"Unknown BasicACL rule. Value: '{container.BasicAcl}'."); throw new ArgumentException($"Unknown BasicACL rule. Value: '{container.BasicAcl}'.");
} }
return new ModelsV2.Container( return new ModelsV2.Container(
Enum.Parse<BasicACL>(basicAclName), Enum.Parse<BasicAcl>(basicAclName),
container.PlacementPolicy.ToModel() container.PlacementPolicy.ToModel()
) )
{ {

View file

@ -24,12 +24,9 @@ public static class PlacementPolicyMapper
public static ModelsV2.Netmap.PlacementPolicy ToModel(this PlacementPolicy placementPolicy) public static ModelsV2.Netmap.PlacementPolicy ToModel(this PlacementPolicy placementPolicy)
{ {
var replicas = new List<Replica>(); return new ModelsV2.Netmap.PlacementPolicy(
foreach (var replica in placementPolicy.Replicas) placementPolicy.Unique,
{ placementPolicy.Replicas.Select(replica => replica.ToModel()).ToArray()
replicas.Add(replica.ToModel()); );
}
return new ModelsV2.Netmap.PlacementPolicy(placementPolicy.Unique, replicas.ToArray());
} }
} }

View file

@ -52,16 +52,10 @@ public static class ObjectHeadMapper
throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'."); throw new ArgumentException($"Unknown ObjectType. Value: '{header.ObjectType}'.");
} }
var attributes = new List<ObjectAttribute>();
foreach (var attribute in header.Attributes)
{
attributes.Add(attribute.ToModel());
}
return new ObjectHeader( return new ObjectHeader(
ContainerId.FromHash(header.ContainerId.Value.ToByteArray()), ContainerId.FromHash(header.ContainerId.Value.ToByteArray()),
Enum.Parse<ModelsV2.Enums.ObjectType>(objTypeName), Enum.Parse<ModelsV2.Enums.ObjectType>(objTypeName),
attributes.ToArray() header.Attributes.Select(attribute => attribute.ToModel()).ToArray()
) )
{ {
Size = (long)header.PayloadLength, Size = (long)header.PayloadLength,
@ -69,3 +63,16 @@ public static class ObjectHeadMapper
}; };
} }
} }
public static class ObjectMapper
{
public static ModelsV2.Object ToModel(this Object.Object obj)
{
return new ModelsV2.Object
{
Header = obj.Header.ToModel(),
ObjectId = ObjectId.FromHash(obj.ObjectId.Value.ToByteArray()),
Payload = obj.Payload.ToByteArray()
};
}
}

View file

@ -3,6 +3,7 @@ using FrostFS.Refs;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
using FrostFS.Session; using FrostFS.Session;
using Version = FrostFS.SDK.ModelsV2.Version;
namespace FrostFS.SDK.ClientV2; namespace FrostFS.SDK.ClientV2;

View file

@ -18,14 +18,14 @@ namespace FrostFS.SDK.ClientV2
public static byte[] SignRFC6979(this ECDsa key, byte[] data) public static byte[] SignRFC6979(this ECDsa key, byte[] data)
{ {
var digest = new Sha256Digest(); var digest = new Sha256Digest();
var secp256r1 = SecNamedCurves.GetByName("secp256r1"); var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var ec_parameters = new ECDomainParameters(secp256r1.Curve, secp256r1.G, secp256r1.N); var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N);
var private_key = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ec_parameters); var privateKey = new ECPrivateKeyParameters(new BigInteger(1, key.PrivateKey()), ecParameters);
var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); var signer = new ECDsaSigner(new HMacDsaKCalculator(digest));
var hash = new byte[digest.GetDigestSize()]; var hash = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, 0, data.Length); digest.BlockUpdate(data, 0, data.Length);
digest.DoFinal(hash, 0); digest.DoFinal(hash, 0);
signer.Init(true, private_key); signer.Init(true, privateKey);
var rs = signer.GenerateSignature(hash); var rs = signer.GenerateSignature(hash);
var signature = new byte[RFC6979SignatureSize]; var signature = new byte[RFC6979SignatureSize];
var rbytes = rs[0].ToByteArrayUnsigned(); var rbytes = rs[0].ToByteArrayUnsigned();
@ -67,11 +67,11 @@ namespace FrostFS.SDK.ClientV2
public static Signature SignMessagePart(this ECDsa key, IMessage? data) public static Signature SignMessagePart(this ECDsa key, IMessage? data)
{ {
var data2sign = data is null ? Array.Empty<byte>() : data.ToByteArray(); var data2Sign = data is null ? Array.Empty<byte>() : data.ToByteArray();
var sig = new Signature var sig = new Signature
{ {
Key = ByteString.CopyFrom(key.PublicKey()), Key = ByteString.CopyFrom(key.PublicKey()),
Sign = ByteString.CopyFrom(key.SignData(data2sign)), Sign = ByteString.CopyFrom(key.SignData(data2Sign)),
}; };
return sig; return sig;
} }

View file

@ -24,19 +24,19 @@ namespace FrostFS.SDK.ClientV2 {
return rs; return rs;
} }
public static bool VerifyRFC6979(this byte[] public_key, byte[] data, byte[] sig) public static bool VerifyRFC6979(this byte[] publicKey, byte[] data, byte[] sig)
{ {
if (public_key is null || data is null || sig is null) return false; if (publicKey is null || data is null || sig is null) return false;
var rs = DecodeSignature(sig); var rs = DecodeSignature(sig);
var digest = new Sha256Digest(); var digest = new Sha256Digest();
var signer = new ECDsaSigner(new HMacDsaKCalculator(digest)); var signer = new ECDsaSigner(new HMacDsaKCalculator(digest));
var secp256r1 = SecNamedCurves.GetByName("secp256r1"); var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var ec_parameters = new ECDomainParameters(secp256r1.Curve, secp256r1.G, secp256r1.N); var ecParameters = new ECDomainParameters(secp256R1.Curve, secp256R1.G, secp256R1.N);
var bc_public_key = new ECPublicKeyParameters(secp256r1.Curve.DecodePoint(public_key), ec_parameters); var bcPublicKey = new ECPublicKeyParameters(secp256R1.Curve.DecodePoint(publicKey), ecParameters);
var hash = new byte[digest.GetDigestSize()]; var hash = new byte[digest.GetDigestSize()];
digest.BlockUpdate(data, 0, data.Length); digest.BlockUpdate(data, 0, data.Length);
digest.DoFinal(hash, 0); digest.DoFinal(hash, 0);
signer.Init(false, bc_public_key); signer.Init(false, bcPublicKey);
return signer.VerifySignature(hash, rs[0], rs[1]); return signer.VerifySignature(hash, rs[0], rs[1]);
} }
@ -54,8 +54,8 @@ namespace FrostFS.SDK.ClientV2 {
{ {
if (sig is null || sig.Key is null || sig.Sign is null) return false; if (sig is null || sig.Key is null || sig.Sign is null) return false;
using var key = sig.Key.ToByteArray().LoadPublicKey(); using var key = sig.Key.ToByteArray().LoadPublicKey();
var data2verify = data is null ? Array.Empty<byte>() : data.ToByteArray(); var data2Verify = data is null ? Array.Empty<byte>() : data.ToByteArray();
return key.VerifyData(data2verify, sig.Sign.ToByteArray()); return key.VerifyData(data2Verify, sig.Sign.ToByteArray());
} }
public static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification) public static bool VerifyMatryoskaLevel(IMessage body, IMetaHeader meta, IVerificationHeader verification)
@ -65,8 +65,7 @@ namespace FrostFS.SDK.ClientV2 {
if (!verification.OriginSignature.VerifyMessagePart(origin)) return false; if (!verification.OriginSignature.VerifyMessagePart(origin)) return false;
if (origin is null) if (origin is null)
return verification.BodySignature.VerifyMessagePart(body); return verification.BodySignature.VerifyMessagePart(body);
if (verification.BodySignature is not null) return false; return verification.BodySignature is null && VerifyMatryoskaLevel(body, meta.GetOrigin(), origin);
return VerifyMatryoskaLevel(body, meta.GetOrigin(), origin);
} }
public static bool Verify(this IVerificableMessage message) public static bool Verify(this IVerificableMessage message)

View file

@ -65,25 +65,4 @@ public partial class Client
request.Sign(_key); request.Sign(_key);
return await _containerServiceClient.DeleteAsync(request); return await _containerServiceClient.DeleteAsync(request);
} }
// private void PrepareContainerSessionToken(RequestMetaHeader meta, SessionToken sessionToken, ContainerID? cid,
// ContainerSessionContext.Types.Verb verb)
// {
// if (meta.SessionToken is not null) return;
// meta.SessionToken = sessionToken;
// var ctx = new ContainerSessionContext
// {
// Verb = verb
// };
// if (cid is null)
// {
// ctx.Wildcard = true;
// }
// else
// {
// ctx.ContainerId = cid;
// }
// meta.SessionToken.Body.Container = ctx;
// meta.SessionToken.Signature = _key.SignMessagePart(meta.SessionToken.Body);
// }
} }

View file

@ -29,10 +29,63 @@ public partial class Client
return await _objectServiceClient.HeadAsync(request); return await _objectServiceClient.HeadAsync(request);
} }
// public async Task<GetResponse> GetObjectAsync(ContainerID cid, ObjectID oid) public async Task<Object.Object> GetObjectAsync(ContainerID cid, ObjectID oid)
// { {
// var sessionToken = await CreateSessionAsync(uint.MaxValue);
// } var request = new GetRequest
{
Body = new GetRequest.Types.Body
{
Raw = false,
Address = new Address
{
ContainerId = cid,
ObjectId = oid
},
}
};
request.AddMetaHeader();
request.AddObjectSessionToken(
sessionToken,
cid,
oid,
ObjectSessionContext.Types.Verb.Get,
_key
);
request.Sign(_key);
return await GetObject(request);
}
private async Task<Object.Object> GetObject(GetRequest request)
{
using var stream = GetObjectInit(request);
var obj = await stream.ReadHeader();
var payload = new byte[obj.Header.PayloadLength];
var offset = 0;
var chunk = await stream.ReadChunk();
while (chunk is not null)
{
chunk.CopyTo(payload, offset);
offset += chunk.Length;
chunk = await stream.ReadChunk();
}
obj.Payload = ByteString.CopyFrom(payload);
return obj;
}
private ObjectReader GetObjectInit(GetRequest initRequest)
{
if (initRequest is null)
{
throw new ArgumentNullException(nameof(initRequest));
}
return new ObjectReader
{
Call = _objectServiceClient.Get(initRequest)
};
}
public async Task<PutResponse> PutObjectAsync(Header header, Stream payload) public async Task<PutResponse> PutObjectAsync(Header header, Stream payload)
{ {
@ -63,7 +116,7 @@ public partial class Client
); );
request.Sign(_key); request.Sign(_key);
using var stream = await InitObject(request); using var stream = await PutObjectInit(request);
var buffer = new byte[Constants.ObjectChunkSize]; var buffer = new byte[Constants.ObjectChunkSize];
var bufferLength = payload.Read(buffer, 0, Constants.ObjectChunkSize); var bufferLength = payload.Read(buffer, 0, Constants.ObjectChunkSize);
while (bufferLength > 0) while (bufferLength > 0)
@ -81,7 +134,7 @@ public partial class Client
return await stream.Close(); return await stream.Close();
} }
private async Task<ObjectStreamer> InitObject(PutRequest initRequest) private async Task<ObjectStreamer> PutObjectInit(PutRequest initRequest)
{ {
if (initRequest is null) if (initRequest is null)
{ {
@ -112,14 +165,47 @@ public partial class Client
} }
} }
internal class ObjectStreamer : IDisposable internal class ObjectReader : IDisposable
{ {
public AsyncClientStreamingCall<PutRequest, PutResponse> Call { get; init; } public AsyncServerStreamingCall<GetResponse> Call { get; init; }
public async Task<Object.Object> ReadHeader()
{
if (!await Call.ResponseStream.MoveNext())
{
throw new InvalidOperationException("unexpect end of stream");
}
var response = Call.ResponseStream.Current;
if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Init)
throw new InvalidOperationException("unexpect message type");
return new Object.Object
{
ObjectId = response.Body.Init.ObjectId,
Header = response.Body.Init.Header,
};
}
public async Task<byte[]?> ReadChunk()
{
if (!await Call.ResponseStream.MoveNext())
{
return null;
}
var response = Call.ResponseStream.Current;
if (response.Body.ObjectPartCase != GetResponse.Types.Body.ObjectPartOneofCase.Chunk)
throw new InvalidOperationException("unexpect message type");
return response.Body.Chunk.ToByteArray();
}
public void Dispose() public void Dispose()
{ {
Call.Dispose(); Call.Dispose();
} }
}
internal class ObjectStreamer : IDisposable
{
public AsyncClientStreamingCall<PutRequest, PutResponse> Call { get; init; }
public async Task Write(PutRequest request) public async Task Write(PutRequest request)
{ {
@ -136,4 +222,9 @@ internal class ObjectStreamer : IDisposable
await Call.RequestStream.CompleteAsync(); await Call.RequestStream.CompleteAsync();
return await Call.ResponseAsync; return await Call.ResponseAsync;
} }
public void Dispose()
{
Call.Dispose();
}
} }

View file

@ -7,12 +7,11 @@ namespace FrostFS.SDK.Cryptography
[MethodImpl(MethodImplOptions.AggressiveInlining)] [MethodImpl(MethodImplOptions.AggressiveInlining)]
public static byte[] Concat(params byte[][] buffers) public static byte[] Concat(params byte[][] buffers)
{ {
int length = 0; var length = buffers.Sum(buffer => buffer.Length);
for (int i = 0; i < buffers.Length; i++)
length += buffers[i].Length; var dst = new byte[length];
byte[] dst = new byte[length]; var p = 0;
int p = 0; foreach (var src in buffers)
foreach (byte[] src in buffers)
{ {
Buffer.BlockCopy(src, 0, dst, p, src.Length); Buffer.BlockCopy(src, 0, dst, p, src.Length);
p += src.Length; p += src.Length;

View file

@ -22,8 +22,8 @@ public static class KeyExtension
$"{nameof(Compress)} argument isn't uncompressed public key. " + $"{nameof(Compress)} argument isn't uncompressed public key. " +
$"expected length={UncompressedPublicKeyLength}, actual={publicKey.Length}" $"expected length={UncompressedPublicKeyLength}, actual={publicKey.Length}"
); );
var secp256r1 = SecNamedCurves.GetByName("secp256r1"); var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var point = secp256r1.Curve.DecodePoint(publicKey); var point = secp256R1.Curve.DecodePoint(publicKey);
return point.GetEncoded(true); return point.GetEncoded(true);
} }
@ -34,8 +34,8 @@ public static class KeyExtension
$"{nameof(Decompress)} argument isn't compressed public key. " + $"{nameof(Decompress)} argument isn't compressed public key. " +
$"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}" $"expected length={CompressedPublicKeyLength}, actual={publicKey.Length}"
); );
var secp256r1 = SecNamedCurves.GetByName("secp256r1"); var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var point = secp256r1.Curve.DecodePoint(publicKey); var point = secp256R1.Curve.DecodePoint(publicKey);
return point.GetEncoded(false); return point.GetEncoded(false);
} }
@ -115,19 +115,19 @@ public static class KeyExtension
return key.ExportParameters(true).D; return key.ExportParameters(true).D;
} }
public static ECDsa LoadPrivateKey(this byte[] private_key) public static ECDsa LoadPrivateKey(this byte[] privateKey)
{ {
var secp256r1 = SecNamedCurves.GetByName("secp256r1"); var secp256R1 = SecNamedCurves.GetByName("secp256r1");
var public_key = var publicKey =
secp256r1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, private_key)).GetEncoded(false)[1..]; secp256R1.G.Multiply(new Org.BouncyCastle.Math.BigInteger(1, privateKey)).GetEncoded(false)[1..];
var key = ECDsa.Create(new ECParameters var key = ECDsa.Create(new ECParameters
{ {
Curve = ECCurve.NamedCurves.nistP256, Curve = ECCurve.NamedCurves.nistP256,
D = private_key, D = privateKey,
Q = new ECPoint Q = new ECPoint
{ {
X = public_key[..32], X = publicKey[..32],
Y = public_key[32..] Y = publicKey[32..]
} }
}); });
return key; return key;
@ -135,20 +135,20 @@ public static class KeyExtension
public static ECDsa LoadWif(this string wif) public static ECDsa LoadWif(this string wif)
{ {
var private_key = GetPrivateKeyFromWIF(wif); var privateKey = GetPrivateKeyFromWIF(wif);
return LoadPrivateKey(private_key); return LoadPrivateKey(privateKey);
} }
public static ECDsa LoadPublicKey(this byte[] public_key) public static ECDsa LoadPublicKey(this byte[] publicKey)
{ {
var public_key_full = public_key.Decompress()[1..]; var publicKeyFull = publicKey.Decompress()[1..];
var key = ECDsa.Create(new ECParameters var key = ECDsa.Create(new ECParameters
{ {
Curve = ECCurve.NamedCurves.nistP256, Curve = ECCurve.NamedCurves.nistP256,
Q = new ECPoint Q = new ECPoint
{ {
X = public_key_full[..32], X = publicKeyFull[..32],
Y = public_key_full[32..] Y = publicKeyFull[32..]
} }
}); });
return key; return key;

View file

@ -6,11 +6,11 @@ namespace FrostFS.SDK.ModelsV2;
public class Container public class Container
{ {
public Guid Nonce { get; set; } public Guid Nonce { get; set; }
public BasicACL BasicAcl { get; set; } public BasicAcl BasicAcl { get; set; }
public PlacementPolicy PlacementPolicy { get; set; } public PlacementPolicy PlacementPolicy { get; set; }
public Version Version { get; set; } public Version Version { get; set; }
public Container(BasicACL basicAcl, PlacementPolicy placementPolicy) public Container(BasicAcl basicAcl, PlacementPolicy placementPolicy)
{ {
Nonce = Guid.NewGuid(); Nonce = Guid.NewGuid();
BasicAcl = basicAcl; BasicAcl = basicAcl;

View file

@ -4,7 +4,7 @@ namespace FrostFS.SDK.ModelsV2;
public class ContainerId public class ContainerId
{ {
public string Value { get; } public string Value { get; set; }
public ContainerId(string id) public ContainerId(string id)
{ {

View file

@ -2,7 +2,7 @@ using System.ComponentModel;
namespace FrostFS.SDK.ModelsV2.Enums; namespace FrostFS.SDK.ModelsV2.Enums;
public enum BasicACL public enum BasicAcl
{ {
[Description("Basic ACL for private container")] [Description("Basic ACL for private container")]
Private = 0x1C8C8CCC, Private = 0x1C8C8CCC,

View file

@ -37,5 +37,6 @@ public class ObjectHeader
public class Object public class Object
{ {
public ObjectHeader Header { get; set; } public ObjectHeader Header { get; set; }
public Stream Payload { get; set; } public ObjectId ObjectId { get; set; }
public byte[] Payload { get; set; }
} }

View file

@ -1,4 +1,5 @@
using FrostFS.SDK.ClientV2; using FrostFS.SDK.ClientV2;
using FrostFS.SDK.ClientV2.Interfaces;
using FrostFS.SDK.ClientV2.Mappers.GRPC; using FrostFS.SDK.ClientV2.Mappers.GRPC;
using FrostFS.SDK.ModelsV2; using FrostFS.SDK.ModelsV2;
@ -16,14 +17,11 @@ public class FrostFsService
public async Task<ContainerId[]> ListContainersAsync() public async Task<ContainerId[]> ListContainersAsync()
{ {
var containersIds = new List<ContainerId>();
var listContainersResponse = await _client.ListContainersAsync(); var listContainersResponse = await _client.ListContainersAsync();
foreach (var cid in listContainersResponse.Body.ContainerIds)
{
containersIds.Add(ContainerId.FromHash(cid.Value.ToByteArray()));
}
return containersIds.ToArray(); return listContainersResponse.Body.ContainerIds.Select(
cid => ContainerId.FromHash(cid.Value.ToByteArray())
).ToArray();
} }
public async Task<ContainerId> CreateContainerAsync(ModelsV2.Container container) public async Task<ContainerId> CreateContainerAsync(ModelsV2.Container container)
@ -52,6 +50,15 @@ public class FrostFsService
return getObjectHeadResponse.Body.Header.Header.ToModel(); return getObjectHeadResponse.Body.Header.Header.ToModel();
} }
public async Task<ModelsV2.Object> GetObjectAsync(ContainerId containerId, ObjectId objectId)
{
var obj = await _client.GetObjectAsync(
containerId.ToGrpcMessage(),
objectId.ToGrpcMessage()
);
return obj.ToModel();
}
public async Task<ObjectId> PutObjectAsync(ObjectHeader header, Stream payload) public async Task<ObjectId> PutObjectAsync(ObjectHeader header, Stream payload)
{ {
var putObjectResponse = await _client.PutObjectAsync(header.ToGrpcMessage(), payload); var putObjectResponse = await _client.PutObjectAsync(header.ToGrpcMessage(), payload);