Skip to content

Commit fa98d59

Browse files
authored
Use the read buffer in UploadFile for the SFTP write packets (sshnet#1798)
* Use the read buffer in UploadFile for the SFTP write packets In SftpClient.UploadFile, a buffer is allocated to read from the given stream, and for each read, another array is allocated for the SFTP write packet (which consists of that data prepended with headers). This change effectively leaves space at the start of the buffer for the headers such that it can be used to assemble the packets without that per-packet array allocation. There are cleaner/more general ways to do this (e.g. for all packet types, leave space for the SSH headers as well), but this gets the most impact for about as much effort as I can be bothered with. * Rent from pool
1 parent 41053cb commit fa98d59

14 files changed

Lines changed: 323 additions & 124 deletions

src/Renci.SshNet/Sftp/ISftpSession.cs

Lines changed: 9 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -4,6 +4,7 @@
44
using System.Threading.Tasks;
55

66
using Renci.SshNet.Common;
7+
using Renci.SshNet.Sftp.Requests;
78
using Renci.SshNet.Sftp.Responses;
89

910
namespace Renci.SshNet.Sftp
@@ -325,14 +326,19 @@ internal interface ISftpSession : ISubsystemSession
325326
/// <param name="offset">the zero-based offset in <paramref name="data" /> at which to begin taking bytes to write.</param>
326327
/// <param name="length">The length (in bytes) of the data to write.</param>
327328
/// <param name="wait">The wait event handle if needed.</param>
328-
/// <param name="writeCompleted">The callback to invoke when the write has completed.</param>
329329
void RequestWrite(byte[] handle,
330330
ulong serverOffset,
331331
byte[] data,
332332
int offset,
333333
int length,
334-
AutoResetEvent wait,
335-
Action<SftpStatusResponse> writeCompleted = null);
334+
AutoResetEvent wait);
335+
336+
/// <summary>
337+
/// Performs SSH_FXP_WRITE request.
338+
/// </summary>
339+
/// <param name="buffer">The buffer.</param>
340+
/// <param name="writeCompleted">The callback to invoke when the write has completed.</param>
341+
void RequestWrite(SftpWriteRequestBuffer buffer, Action<SftpStatusResponse> writeCompleted);
336342

337343
/// <summary>
338344
/// Asynchronouly performs a <c>SSH_FXP_WRITE</c> request.
Lines changed: 47 additions & 50 deletions
Original file line numberDiff line numberDiff line change
@@ -1,17 +1,27 @@
11
using System;
2+
using System.Buffers.Binary;
23

4+
using Renci.SshNet.Common;
35
using Renci.SshNet.Sftp.Responses;
46

57
namespace Renci.SshNet.Sftp.Requests
68
{
79
internal sealed class SftpWriteRequest : SftpRequest
810
{
11+
private readonly SftpWriteRequestBuffer _buffer;
12+
913
public override SftpMessageTypes SftpMessageType
1014
{
1115
get { return SftpMessageTypes.Write; }
1216
}
1317

14-
public byte[] Handle { get; private set; }
18+
public ReadOnlySpan<byte> Handle
19+
{
20+
get
21+
{
22+
return _buffer.Handle;
23+
}
24+
}
1525

1626
/// <summary>
1727
/// Gets the zero-based offset (in bytes) relative to the beginning of the file that the write
@@ -21,82 +31,69 @@ public override SftpMessageTypes SftpMessageType
2131
/// The zero-based offset (in bytes) relative to the beginning of the file that the write must
2232
/// start at.
2333
/// </value>
24-
public ulong ServerFileOffset { get; private set; }
34+
public ulong ServerFileOffset
35+
{
36+
get
37+
{
38+
return _buffer.ServerFileOffset;
39+
}
40+
}
2541

2642
/// <summary>
2743
/// Gets the buffer holding the data to write.
2844
/// </summary>
2945
/// <value>
3046
/// The buffer holding the data to write.
3147
/// </value>
32-
public byte[] Data { get; private set; }
33-
34-
/// <summary>
35-
/// Gets the zero-based offset in <see cref="Data" /> at which to begin taking bytes to
36-
/// write.
37-
/// </summary>
38-
/// <value>
39-
/// The zero-based offset in <see cref="Data" /> at which to begin taking bytes to write.
40-
/// </value>
41-
public int Offset { get; private set; }
42-
43-
/// <summary>
44-
/// Gets the length (in bytes) of the data to write.
45-
/// </summary>
46-
/// <value>
47-
/// The length (in bytes) of the data to write.
48-
/// </value>
49-
public int Length { get; private set; }
48+
public ReadOnlySpan<byte> Data
49+
{
50+
get
51+
{
52+
return _buffer.Data.AsSpan(0, _buffer.DataLength);
53+
}
54+
}
5055

5156
protected override int BufferCapacity
5257
{
5358
get
5459
{
55-
var capacity = base.BufferCapacity;
56-
capacity += 4; // Handle length
57-
capacity += Handle.Length; // Handle
58-
capacity += 8; // ServerFileOffset length
59-
capacity += 4; // Data length
60-
capacity += Length; // Data
61-
return capacity;
60+
return _buffer.ActiveBytes.Count;
6261
}
6362
}
6463

6564
public SftpWriteRequest(uint protocolVersion,
66-
uint requestId,
67-
byte[] handle,
68-
ulong serverFileOffset,
69-
byte[] data,
70-
int offset,
71-
int length,
65+
SftpWriteRequestBuffer buffer,
7266
Action<SftpStatusResponse> statusAction)
73-
: base(protocolVersion, requestId, statusAction)
67+
: base(protocolVersion, buffer.RequestId, statusAction)
7468
{
75-
Handle = handle;
76-
ServerFileOffset = serverFileOffset;
77-
Data = data;
78-
Offset = offset;
79-
Length = length;
69+
_buffer = buffer;
8070
}
8171

8272
protected override void LoadData()
8373
{
84-
base.LoadData();
85-
86-
Handle = ReadBinary();
87-
ServerFileOffset = ReadUInt64();
88-
Data = ReadBinary();
89-
Offset = 0;
90-
Length = Data.Length;
74+
throw new NotImplementedException();
9175
}
9276

9377
protected override void SaveData()
9478
{
95-
base.SaveData();
79+
throw new NotImplementedException();
80+
}
81+
82+
protected override void WriteBytes(SshDataStream stream)
83+
{
84+
var activeBuffer = GetBytes();
85+
86+
stream.Write(activeBuffer.Array, activeBuffer.Offset, activeBuffer.Count);
87+
}
88+
89+
public new ArraySegment<byte> GetBytes()
90+
{
91+
var activeBuffer = _buffer.ActiveBytes;
92+
93+
// Write SFTP packet length.
94+
BinaryPrimitives.WriteInt32BigEndian(activeBuffer.AsSpan(), activeBuffer.Count - 4);
9695

97-
WriteBinaryString(Handle);
98-
Write(ServerFileOffset);
99-
WriteBinary(Data, Offset, Length);
96+
return activeBuffer;
10097
}
10198
}
10299
}
Lines changed: 149 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,149 @@
1+
#nullable enable
2+
using System;
3+
using System.Buffers;
4+
using System.Buffers.Binary;
5+
using System.Diagnostics;
6+
7+
namespace Renci.SshNet.Sftp.Requests
8+
{
9+
/// <summary>
10+
/// A helper type that wraps a buffer for SFTP write requests.
11+
/// </summary>
12+
/// <remarks>
13+
/// [Sftp packet length, SftpMessageType, RequestId, Handle length, Handle, Server offset, data length, data].
14+
/// [ 4, 1, 4, 4, ?, 8, 4, ?].
15+
/// </remarks>
16+
internal sealed class SftpWriteRequestBuffer : IDisposable
17+
{
18+
private const int MessageTypeOffset = 4;
19+
private const int RequestIdOffset = MessageTypeOffset + 1;
20+
private const int HandleLengthOffset = RequestIdOffset + 4;
21+
private const int HandleOffset = HandleLengthOffset + 4;
22+
23+
private readonly bool _usePool;
24+
private byte[] _buffer;
25+
26+
public ArraySegment<byte> ActiveBytes
27+
{
28+
get
29+
{
30+
return new(_buffer, 0, HandleOffset + HandleLength + 8 + 4 + DataLength);
31+
}
32+
}
33+
34+
public SftpWriteRequestBuffer(ReadOnlySpan<byte> handle, int dataCapacity, bool usePool = false)
35+
{
36+
Debug.Assert(dataCapacity >= 0);
37+
38+
var totalCapacity = HandleOffset + handle.Length + 8 + 4 + dataCapacity;
39+
40+
_usePool = usePool;
41+
42+
_buffer = usePool
43+
? ArrayPool<byte>.Shared.Rent(totalCapacity)
44+
: new byte[totalCapacity];
45+
46+
_buffer[MessageTypeOffset] = (byte)SftpMessageTypes.Write;
47+
48+
HandleLength = handle.Length;
49+
50+
handle.CopyTo(_buffer.AsSpan(HandleOffset));
51+
}
52+
53+
public SftpWriteRequestBuffer(ReadOnlySpan<byte> handle, ulong serverFileOffset, ReadOnlySpan<byte> data, bool usePool = false)
54+
: this(handle, data.Length, usePool)
55+
{
56+
ServerFileOffset = serverFileOffset;
57+
58+
DataLength = data.Length;
59+
60+
data.CopyTo(Data);
61+
}
62+
63+
public uint RequestId
64+
{
65+
get
66+
{
67+
return BinaryPrimitives.ReadUInt32BigEndian(_buffer.AsSpan(RequestIdOffset));
68+
}
69+
set
70+
{
71+
BinaryPrimitives.WriteUInt32BigEndian(_buffer.AsSpan(RequestIdOffset), value);
72+
}
73+
}
74+
75+
public int HandleLength
76+
{
77+
get
78+
{
79+
return BinaryPrimitives.ReadInt32BigEndian(_buffer.AsSpan(HandleLengthOffset));
80+
}
81+
private init
82+
{
83+
Debug.Assert(value >= 0);
84+
BinaryPrimitives.WriteInt32BigEndian(_buffer.AsSpan(HandleLengthOffset), value);
85+
}
86+
}
87+
88+
public ReadOnlySpan<byte> Handle
89+
{
90+
get
91+
{
92+
return _buffer.AsSpan(HandleOffset, HandleLength);
93+
}
94+
}
95+
96+
public ulong ServerFileOffset
97+
{
98+
get
99+
{
100+
return BinaryPrimitives.ReadUInt64BigEndian(_buffer.AsSpan(HandleOffset + HandleLength));
101+
}
102+
set
103+
{
104+
BinaryPrimitives.WriteUInt64BigEndian(_buffer.AsSpan(HandleOffset + HandleLength), value);
105+
}
106+
}
107+
108+
public int DataLength
109+
{
110+
get
111+
{
112+
return BinaryPrimitives.ReadInt32BigEndian(_buffer.AsSpan(HandleOffset + HandleLength + 8));
113+
}
114+
set
115+
{
116+
Debug.Assert(value >= 0);
117+
Debug.Assert(value <= _buffer.Length - (HandleOffset + HandleLength + 8 + 4));
118+
119+
BinaryPrimitives.WriteInt32BigEndian(_buffer.AsSpan(HandleOffset + HandleLength + 8), value);
120+
}
121+
}
122+
123+
/// <summary>
124+
/// Gets the space available to write as file data. Does not consider <see cref="DataLength"/>.
125+
/// </summary>
126+
public ArraySegment<byte> Data
127+
{
128+
get
129+
{
130+
var offset = HandleOffset + HandleLength + 8 + 4;
131+
return new ArraySegment<byte>(_buffer, offset, _buffer.Length - offset);
132+
}
133+
}
134+
135+
public void Dispose()
136+
{
137+
if (_usePool)
138+
{
139+
var buffer = _buffer;
140+
_buffer = null!;
141+
142+
if (buffer is not null)
143+
{
144+
ArrayPool<byte>.Shared.Return(buffer);
145+
}
146+
}
147+
}
148+
}
149+
}

0 commit comments

Comments
 (0)