Skip to content

Commit f282f40

Browse files
committed
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.
1 parent 5b8382d commit f282f40

14 files changed

Lines changed: 268 additions & 90 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: 56 additions & 43 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,44 +31,33 @@ 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

@@ -72,31 +71,45 @@ public SftpWriteRequest(uint protocolVersion,
7271
Action<SftpStatusResponse> statusAction)
7372
: base(protocolVersion, requestId, statusAction)
7473
{
75-
Handle = handle;
76-
ServerFileOffset = serverFileOffset;
77-
Data = data;
78-
Offset = offset;
79-
Length = length;
74+
_buffer = new SftpWriteRequestBuffer(handle, serverFileOffset, data.AsSpan(offset, length))
75+
{
76+
RequestId = requestId,
77+
};
8078
}
8179

82-
protected override void LoadData()
80+
public SftpWriteRequest(uint protocolVersion,
81+
SftpWriteRequestBuffer buffer,
82+
Action<SftpStatusResponse> statusAction)
83+
: base(protocolVersion, buffer.RequestId, statusAction)
8384
{
84-
base.LoadData();
85+
_buffer = buffer;
86+
}
8587

86-
Handle = ReadBinary();
87-
ServerFileOffset = ReadUInt64();
88-
Data = ReadBinary();
89-
Offset = 0;
90-
Length = Data.Length;
88+
protected override void LoadData()
89+
{
90+
throw new NotImplementedException();
9191
}
9292

9393
protected override void SaveData()
9494
{
95-
base.SaveData();
95+
throw new NotImplementedException();
96+
}
97+
98+
protected override void WriteBytes(SshDataStream stream)
99+
{
100+
var activeBuffer = GetBytes();
101+
102+
stream.Write(activeBuffer.Array, activeBuffer.Offset, activeBuffer.Count);
103+
}
104+
105+
public new ArraySegment<byte> GetBytes()
106+
{
107+
var activeBuffer = _buffer.ActiveBytes;
108+
109+
// Write SFTP packet length.
110+
BinaryPrimitives.WriteInt32BigEndian(activeBuffer.AsSpan(), activeBuffer.Count - 4);
96111

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

src/Renci.SshNet/Sftp/SftpSession.cs

Lines changed: 28 additions & 9 deletions
Original file line numberDiff line numberDiff line change
@@ -87,8 +87,16 @@ public async Task ChangeDirectoryAsync(string path, CancellationToken cancellati
8787

8888
internal void SendMessage(SftpMessage sftpMessage)
8989
{
90-
var data = sftpMessage.GetBytes();
91-
SendData(data);
90+
if (sftpMessage is SftpWriteRequest writeRequest)
91+
{
92+
var data = writeRequest.GetBytes();
93+
SendData(data.Array, data.Offset, data.Count);
94+
}
95+
else
96+
{
97+
var data = sftpMessage.GetBytes();
98+
SendData(data);
99+
}
92100
}
93101

94102
/// <inheritdoc/>
@@ -579,20 +587,31 @@ public void RequestWrite(byte[] handle,
579587
byte[] data,
580588
int offset,
581589
int length,
590+
AutoResetEvent wait)
591+
{
592+
var buffer = new SftpWriteRequestBuffer(handle, serverOffset, data.AsSpan(offset, length));
593+
594+
RequestWrite(buffer, wait, writeCompleted: null);
595+
}
596+
597+
/// <inheritdoc/>
598+
public void RequestWrite(SftpWriteRequestBuffer buffer, Action<SftpStatusResponse> writeCompleted)
599+
{
600+
RequestWrite(buffer, wait: null, writeCompleted);
601+
}
602+
603+
private void RequestWrite(SftpWriteRequestBuffer buffer,
582604
AutoResetEvent wait,
583-
Action<SftpStatusResponse> writeCompleted = null)
605+
Action<SftpStatusResponse> writeCompleted)
584606
{
585607
Debug.Assert((wait is null) != (writeCompleted is null), "Should have one parameter or the other.");
586608

587609
SftpException exception = null;
588610

611+
buffer.RequestId = NextRequestId;
612+
589613
var request = new SftpWriteRequest(ProtocolVersion,
590-
NextRequestId,
591-
handle,
592-
serverOffset,
593-
data,
594-
offset,
595-
length,
614+
buffer,
596615
response =>
597616
{
598617
if (writeCompleted is not null)

0 commit comments

Comments
 (0)