This commit is contained in:
2026-06-15 18:18:16 +08:00
parent 97c9fba14e
commit 2b9f134e5f
4164 changed files with 386922 additions and 79 deletions

View File

@@ -0,0 +1,23 @@
namespace Best.HTTP.Request.Upload
{
/// <summary>
/// Provides constants representing different, special body lengths for HTTP requests with upload streams.
/// </summary>
public static class BodyLengths
{
/// <summary>
/// The <see cref="UploadStreamBase"/>'s length is unknown and the plugin have to send data with '<c>chunked</c>' transfer-encoding.
/// </summary>
public const long UnknownWithChunkedTransferEncoding = -2;
/// <summary>
/// The <see cref="UploadStreamBase"/>'s length is unknown and the plugin have to send data as-is, without any encoding.
/// </summary>
public const long UnknownRaw = -1;
/// <summary>
/// No content to send.
/// </summary>
public const long NoBody = 0;
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 517183824f3e02745ba784c260724b60
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,178 @@
using System;
using System.Collections.Concurrent;
using System.Threading;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Request.Upload
{
/// <summary>
/// A specialized upload stream designed to handle data that's generated on-the-fly or periodically.
/// </summary>
/// <remarks>
/// This implementation is designed to handle scenarios where data may not always be immediately available for upload.
/// The request will remain active until the <see cref="Complete"/> method is invoked, ensuring that data can continue to be fed into the stream even if it's temporarily empty during a Read operation.
/// </remarks>
public sealed class DynamicUploadStream : UploadStreamBase
{
/// <summary>
/// Gets the length of the upload stream.
/// </summary>
/// <remarks>
/// This implementation returns a constant value of <c>-1</c>, indicating that the length of the data to be uploaded is unknown. When the processing connection encounters this value, it should utilize chunked uploading to handle the data transfer.
/// </remarks>
/// <value>The constant value of <c>-1</c>, representing unknown length.</value>
public override long Length
=> BodyLengths.UnknownWithChunkedTransferEncoding;
/// <summary>
/// Gets the length of data currently buffered and ready for upload.
/// </summary>
/// <value>The length of buffered data in bytes.</value>
public long BufferedLength
=> this._bufferedLength;
private long _bufferedLength;
private bool _isCompleted;
private ConcurrentQueue<BufferSegment> _chunks = new ConcurrentQueue<BufferSegment>();
private BufferSegment _current;
private string _contentType;
/// <summary>
/// Initializes a new instance of the DynamicUploadStream class with an optional content type.
/// </summary>
/// <param name="contentType">The MIME type of the content to be uploaded. Defaults to "<c>application/octet-stream</c>" if not specified.</param>
/// <remarks>
/// This constructor allows the caller to specify the content type of the data to be uploaded. If not provided, it defaults to a general binary data type.
/// </remarks>
public DynamicUploadStream(string contentType = "application/octet-stream")
=> this._contentType = contentType;
/// <summary>
/// Sets the necessary headers before sending the request.
/// </summary>
public override void BeforeSendHeaders(HTTPRequest request)
=> request.SetHeader("content-type", this._contentType);
/// <summary>
/// Prepares the stream before the request body is sent.
/// </summary>
public override void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler)
=> base.BeforeSendBody(request, threadSignaler);
/// <summary>
/// Reads data from the stream to be uploaded.
/// </summary>
/// <remarks>
/// The returned value indicates the state of the stream:
/// <list type="bullet">
/// <item><term>-1</term><description>More data is expected in the future, but isn't currently available. When new data is ready, the IThreadSignaler must be notified.</description></item>
/// <item><term>0</term><description>The stream has been closed and no more data will be provided.</description></item>
/// <item><description>Otherwise it returns with the number bytes copied to the buffer.</description></item>
/// </list>
/// Note: A zero return value can come after a -1 return, indicating a transition from waiting to completion.
/// </remarks>
public override int Read(byte[] buffer, int offset, int count)
{
int readCount = 0;
while (readCount < count && (_current != BufferSegment.Empty || _chunks.TryDequeue(out _current)))
{
int copyCount = Math.Min(count - readCount, _current.Count);
Array.Copy(_current.Data, _current.Offset, buffer, offset, copyCount);
readCount += copyCount;
offset += copyCount;
if (_current.Offset + copyCount >= _current.Count)
{
BufferPool.Release(_current);
_current = BufferSegment.Empty;
}
else
{
_current = _current.Slice(_current.Offset + copyCount);
}
}
if (!this._isCompleted && readCount == 0)
return UploadReadConstants.WaitForMore;
Interlocked.Add(ref this._bufferedLength, -readCount);
return readCount;
}
/// <summary>
/// Writes data to the stream, making it available for upload.
/// </summary>
/// <remarks>
/// After writing data to the stream using this method, the connection is signaled that data is available to send.
/// </remarks>
/// <param name="buffer">The array of unsigned bytes from which to copy count bytes to the current stream.</param>
/// <param name="offset">The zero-based byte offset in buffer at which to begin copying bytes to the current stream.</param>
/// <param name="count">The number of bytes to be written to the current stream.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to write after the stream has been marked as complete.</exception>
public override void Write(byte[] buffer, int offset, int count)
{
if (buffer == null)
throw new ArgumentNullException(nameof(buffer));
if (this._isCompleted)
throw new InvalidOperationException("Complete() already called on the stream!");
var localCopy = BufferPool.Get(count, true, base.Signaler?.Context);
Array.Copy(buffer, 0, localCopy, offset, count);
Write(localCopy.AsBuffer(count));
}
/// <summary>
/// Writes a segment of data to the stream, making it available for upload.
/// </summary>
/// <param name="segment">A segment of data to be written to the stream.</param>
/// <exception cref="InvalidOperationException">Thrown when trying to write after the stream has been marked as complete.</exception>
/// <remarks>
/// After writing a segment to the stream using this method, the connection is signaled that data is available to send.
/// </remarks>
public void Write(BufferSegment segment)
{
if (this._isCompleted)
throw new InvalidOperationException("Complete() already called on the stream!");
if (segment.Data == null)
return;
this._chunks.Enqueue(segment);
Interlocked.Add(ref this._bufferedLength, segment.Count);
this.Signaler?.SignalThread(SignalHandlerTypes.Signal);
}
/// <summary>
/// Marks the stream as complete, signaling that no more data will be added.
/// </summary>
/// <remarks>
/// All remaining buffered data will be sent to the server.
/// </remarks>
public void Complete()
{
if (this._isCompleted)
throw new InvalidOperationException("Complete() already called on the stream!");
this._isCompleted = true;
base.Signaler?.SignalThread(SignalHandlerTypes.Signal);
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => true;
public override long Position { get => throw new System.NotImplementedException(); set => throw new System.NotImplementedException(); }
public override long Seek(long offset, System.IO.SeekOrigin origin) => throw new System.NotImplementedException();
public override void SetLength(long value) => throw new System.NotImplementedException();
public override void Flush() { }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: ebb54a5ce9460664f826e2ff8b5ae38f
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,8 @@
fileFormatVersion: 2
guid: e43c3ab19497cb84089ea5d4aefa8591
folderAsset: yes
DefaultImporter:
externalObjects: {}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,271 @@
using System;
using System.Collections.Generic;
using System.IO;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
using UnityEngine;
using static Best.HTTP.Hosts.Connections.HTTP1.Constants;
namespace Best.HTTP.Request.Upload.Forms
{
/// <summary>
/// An <see cref="UploadStreamBase"/> based implementation of the <c>multipart/form-data</c> Content-Type. It's very memory-effective, streams are read into memory in chunks.
/// </summary>
/// <remarks>
/// <para>The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
/// <list type="bullet">
/// <item>
/// <term>Less than zero(<c>-1</c>) value </term>
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
/// </item>
/// <item>
/// <term>Zero (<c>0</c>)</term>
/// <description> means that the stream is closed, no more data can be expected.</description>
/// </item>
/// </list>
/// A zero value to signal stream closure can follow a less than zero value.</para>
/// </remarks>
public sealed class MultipartFormDataStream : UploadStreamBase
{
/// <summary>
/// Gets the length of this multipart/form-data stream.
/// </summary>
public override long Length { get => this._length; }
private long _length;
/// <summary>
/// A random boundary generated in the constructor.
/// </summary>
private string boundary;
private Queue<StreamList> fields = new Queue<StreamList>(1);
private StreamList currentField;
/// <summary>
/// Initializes a new instance of the MultipartFormDataStream class.
/// </summary>
public MultipartFormDataStream()
{
var hash = new Hash128();
hash.Append(this.GetHashCode());
this.boundary = $"com.Tivadar.Best.HTTP.boundary.{hash}";
}
/// <summary>
/// Initializes a new instance of the MultipartFormDataStream class with a custom boundary.
/// </summary>
public MultipartFormDataStream(string boundary)
{
this.boundary = boundary;
}
public override void BeforeSendHeaders(HTTPRequest request)
{
request.SetHeader("Content-Type", $"multipart/form-data; boundary=\"{this.boundary}\"");
var boundaryStream = new BufferPoolMemoryStream();
boundaryStream.WriteLine("--" + this.boundary + "--");
boundaryStream.Position = 0;
this.fields.Enqueue(new StreamList(boundaryStream));
if (this._length >= 0)
this._length += boundaryStream.Length;
}
/// <summary>
/// Adds a textual field to the multipart/form-data stream.
/// </summary>
/// <param name="fieldName">The name of the field.</param>
/// <param name="value">The textual value of the field.</param>
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
public MultipartFormDataStream AddField(string fieldName, string value)
=> AddField(fieldName, value, System.Text.Encoding.UTF8);
/// <summary>
/// Adds a textual field to the multipart/form-data stream.
/// </summary>
/// <param name="fieldName">The name of the field.</param>
/// <param name="value">The textual value of the field.</param>
/// <param name="encoding">The encoding to use for the value.</param>
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
public MultipartFormDataStream AddField(string fieldName, string value, System.Text.Encoding encoding)
{
var enc = encoding ?? System.Text.Encoding.UTF8;
var byteCount = enc.GetByteCount(value);
var buffer = BufferPool.Get(byteCount, true);
var stream = new BufferPoolMemoryStream();
enc.GetBytes(value, 0, value.Length, buffer, 0);
stream.Write(buffer, 0, byteCount);
stream.Position = 0;
string mime = encoding != null ? "text/plain; charset=" + encoding.WebName : null;
return AddStreamField(fieldName, stream, null, mime);
}
/// <summary>
/// Adds a stream field to the multipart/form-data stream.
/// </summary>
/// <param name="fieldName">The name of the field.</param>
/// <param name="data">The data containing the field data.</param>
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
public MultipartFormDataStream AddField(string fieldName, byte[] data)
=> AddStreamField(fieldName, new MemoryStream(data));
/// <summary>
/// Adds a stream field to the multipart/form-data stream.
/// </summary>
/// <param name="stream">The stream containing the field data.</param>
/// <param name="fieldName">The name of the field.</param>
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream)
=> AddStreamField(fieldName, stream, null, null);
/// <summary>
/// Adds a stream field to the multipart/form-data stream.
/// </summary>
/// <param name="stream">The stream containing the field data.</param>
/// <param name="fieldName">The name of the field.</param>
/// <param name="fileName">The name of the file, if applicable.</param>
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream, string fileName)
=> AddStreamField(fieldName, stream, fileName, null);
/// <summary>
/// Adds a stream field to the multipart/form-data stream.
/// </summary>
/// <param name="stream">The stream containing the field data.</param>
/// <param name="fieldName">The name of the field.</param>
/// <param name="fileName">The name of the file, if applicable.</param>
/// <param name="mimeType">The MIME type of the content.</param>
/// <returns>The MultipartFormDataStream instance for method chaining.</returns>
public MultipartFormDataStream AddStreamField(string fieldName, System.IO.Stream stream, string fileName, string mimeType)
{
var header = new BufferPoolMemoryStream();
header.WriteLine("--" + this.boundary);
header.WriteLine("Content-Disposition: form-data; name=\"" + fieldName + "\"" + (!string.IsNullOrEmpty(fileName) ? "; filename=\"" + fileName + "\"" : string.Empty));
// Set up Content-Type head for the form.
if (!string.IsNullOrEmpty(mimeType))
header.WriteLine("Content-Type: " + mimeType);
header.WriteLine();
header.Position = 0;
var footer = new BufferPoolMemoryStream();
footer.Write(EOL, 0, EOL.Length);
footer.Position = 0;
// all wrapped streams going to be disposed by the StreamList wrapper.
var wrapper = new StreamList(header, stream, footer);
try
{
if (this._length >= 0)
this._length += wrapper.Length;
}
catch
{
this._length = -1;
}
this.fields.Enqueue(wrapper);
return this;
}
/// <summary>
/// Adds the final boundary to the multipart/form-data stream before sending the request body.
/// </summary>
/// <param name="request">The HTTP request.</param>
/// <param name="threadSignaler">The thread signaler for handling asynchronous operations.</param>
public override void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler)
{
base.BeforeSendBody(request, threadSignaler);
}
/// <summary>
/// Reads data from the multipart/form-data stream into the provided buffer.
/// </summary>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="offset">The starting offset in the buffer.</param>
/// <param name="length">The maximum number of bytes to read.</param>
/// <returns>The number of bytes read into the buffer.</returns>
public override int Read(byte[] buffer, int offset, int length)
{
if (this.currentField == null && this.fields.Count == 0)
return -1;
if (this.currentField == null && this.fields.Count > 0)
this.currentField = this.fields.Dequeue();
int readCount = 0;
do
{
// read from the current stream
int count = this.currentField.Read(buffer, offset + readCount, length - readCount);
if (count > 0)
readCount += count;
else
{
// if the current field's stream is empty, go for the next one.
// dispose the current one first
try
{
this.currentField.Dispose();
}
catch
{ }
// no more fields/streams? exit
if (this.fields.Count == 0)
break;
// grab the next one
this.currentField = this.fields.Dequeue();
}
// exit when we reach the length goal, or there's no more streams to read from
} while (readCount < length && this.fields.Count > 0);
return readCount;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
if (fields != null)
{
foreach (var field in fields)
field.Dispose();
fields.Clear();
fields = null;
}
currentField?.Dispose();
currentField = null;
}
public override bool CanRead { get { return true; } }
public override bool CanSeek { get { return false; } }
public override bool CanWrite { get { return false; } }
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override void Flush() { }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: d54866f088b8c154db3325a2771b58af
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,158 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Request.Upload.Forms
{
/// <summary>
/// Readonly struct to hold key -> value pairs, where the value is either textual or binary.
/// </summary>
readonly struct FormField
{
public readonly string Key;
public readonly string TextValue;
public readonly byte[] BinaryValue;
public FormField(string key, string textValue)
{
this.Key = key;
this.TextValue = textValue;
this.BinaryValue = null;
}
public FormField(string key, byte[] binaryValue)
{
this.Key = key;
this.TextValue = null;
this.BinaryValue = binaryValue;
}
}
/// <summary>
/// An <see cref="UploadStreamBase"/> implementation representing a stream that prepares and sends data as URL-encoded form data in an HTTP request.
/// </summary>
/// <remarks>
/// <para>This stream is used to send data as URL-encoded form data in an HTTP request. It sets the <c>"Content-Type"</c> header to <c>"application/x-www-form-urlencoded"</c>.
/// URL-encoded form data is typically used for submitting form data to a web server. It is commonly used in HTTP POST requests to send data to a server, such as submitting HTML form data.</para>
///
/// <para>The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
/// <list type="bullet">
/// <item>
/// <term>Less than zero(<c>-1</c>) value </term>
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
/// </item>
/// <item>
/// <term>Zero (<c>0</c>)</term>
/// <description> means that the stream is closed, no more data can be expected.</description>
/// </item>
/// </list>
/// A zero value to signal stream closure can follow a less than zero value.</para>
/// <para>While it's possible, it's not advised to send binary data url-encoded!</para>
/// </remarks>
public sealed class UrlEncodedStream : UploadStreamBase
{
private const int EscapeTreshold = 256;
/// <summary>
/// Gets the length of the stream.
/// </summary>
public override long Length { get => this._memoryStream.Length; }
private MemoryStream _memoryStream;
/// <summary>
/// A list that holds the form's fields.
/// </summary>
private List<FormField> _fields = new List<FormField>();
/// <summary>
/// Sets up the HTTP request by adding the <c>"Content-Type"</c> header as <c>"application/x-www-form-urlencoded"</c>.
/// </summary>
/// <param name="request">The HTTP request.</param>
public override void BeforeSendHeaders(HTTPRequest request)
{
request.SetHeader("Content-Type", "application/x-www-form-urlencoded");
StringBuilder sb = StringBuilderPool.Get(_fields.Count * 4);
// Create a "field1=value1&field2=value2" formatted string
for (int i = 0; i < _fields.Count; ++i)
{
var field = _fields[i];
if (i > 0)
sb.Append("&");
sb.Append(EscapeString(field.Key));
sb.Append("=");
if (!string.IsNullOrEmpty(field.TextValue) || field.BinaryValue == null)
sb.Append(EscapeString(field.TextValue));
else
// If forced to this form type with binary data, we will create a base64 encoded string from it.
sb.Append(Convert.ToBase64String(field.BinaryValue, 0, field.BinaryValue.Length));
}
this._memoryStream = new MemoryStream(Encoding.UTF8.GetBytes(StringBuilderPool.ReleaseAndGrab(sb)));
}
/// <summary>
/// Adds binary data to the form. It is not advised to send binary data with an URL-encoded form due to the conversion cost of binary to text conversion.
/// </summary>
/// <param name="fieldName">The name of the field.</param>
/// <param name="content">The binary data content.</param>
/// <returns>The UrlEncodedStream instance for method chaining.</returns>
public UrlEncodedStream AddBinaryData(string fieldName, byte[] content)
{
_fields.Add(new FormField(fieldName, content));
return this;
}
public UrlEncodedStream AddField(string fieldName, string value)
{
_fields.Add(new FormField(fieldName, value));
return this;
}
public override int Read(byte[] buffer, int offset, int count) => this._memoryStream.Read(buffer, offset, count);
private static string EscapeString(string originalString)
{
if (originalString.Length < EscapeTreshold)
return Uri.EscapeDataString(originalString);
else
{
int loops = originalString.Length / EscapeTreshold;
StringBuilder sb = StringBuilderPool.Get(loops); //new StringBuilder(loops);
for (int i = 0; i <= loops; i++)
sb.Append(i < loops ?
Uri.EscapeDataString(originalString.Substring(EscapeTreshold * i, EscapeTreshold)) :
Uri.EscapeDataString(originalString.Substring(EscapeTreshold * i)));
return StringBuilderPool.ReleaseAndGrab(sb);
}
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._memoryStream?.Dispose();
this._memoryStream = null;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
public override void SetLength(long value) => throw new NotImplementedException();
public override void Write(byte[] buffer, int offset, int count) => throw new NotImplementedException();
public override void Flush() => throw new NotImplementedException();
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 0a92cdbf082f89b42b5db8ba2a373498
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,101 @@
using System;
using System.IO;
using System.Text;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Request.Upload
{
/// <summary>
/// An <see cref="UploadStreamBase"/> implementation to convert and upload the object as JSON data. It sets the <c>"Content-Type"</c> header to <c>"application/json; charset=utf-8"</c>.
/// </summary>
/// <typeparam name="T">The type of the object to be converted to JSON.</typeparam>
/// <remarks>
/// <para>This stream keeps a reference to the object until the preparation in <see cref="BeforeSendHeaders"/>. This means, changes to the object after passing it to the constructor will be reflected in the sent data too.</para>
/// <para>The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
/// <list type="bullet">
/// <item>
/// <term>Less than zero(<c>-1</c>) value </term>
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
/// </item>
/// <item>
/// <term>Zero (<c>0</c>)</term>
/// <description> means that the stream is closed, no more data can be expected.</description>
/// </item>
/// </list>
/// A zero value to signal stream closure can follow a less than zero value.</para>
/// </remarks>
public sealed class JSonDataStream<T> : UploadStreamBase
{
public override long Length { get => this._innerStream.Length; }
private BufferPoolMemoryStream _innerStream;
private T _objToJson;
/// <summary>
/// Initializes a new instance of the <see cref="JSonDataStream{T}"/> class with the specified object.
/// </summary>
/// <param name="obj">The object to be converted to JSON and uploaded.</param>
public JSonDataStream(T obj) => this._objToJson = obj;
/// <summary>
/// Called before sending out the request's headers. It sets the <c>"Content-Type"</c> header to <c>"application/json; charset=utf-8"</c>.
/// </summary>
/// <param name="request">The HTTP request.</param>
public override void BeforeSendHeaders(HTTPRequest request)
{
request.SetHeader("Content-Type", "application/json; charset=utf-8");
if (this._innerStream != null)
{
this._innerStream.Position = 0;
return;
}
var json = Best.HTTP.JSON.LitJson.JsonMapper.ToJson(this._objToJson);
this._objToJson = default;
var byteLength = Encoding.UTF8.GetByteCount(json);
var buffer = BufferPool.Get(byteLength, true);
Encoding.UTF8.GetBytes(json, 0, json.Length, buffer, 0);
this._innerStream = new BufferPoolMemoryStream(byteLength);
this._innerStream.Write(buffer, 0, byteLength);
this._innerStream.Position = 0;
BufferPool.Release(buffer);
}
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
/// </summary>
/// <param name="buffer">An array of bytes. When this method returns, the buffer contains the specified byte array with the values between <paramref name="offset"/> and ( <paramref name="offset"/> + <paramref name="count"/> - 1) replaced by the bytes read from the current source.</param>
/// <param name="offset">The zero-based byte offset in <paramref name="buffer"/> at which to begin storing the data read from the current stream.</param>
/// <param name="count">The maximum number of bytes to be read from the current stream.</param>
/// <returns>The total number of bytes read into the buffer. This can be less than the number of bytes requested if that many bytes are not currently available, or zero (0) if the end of the stream has been reached.</returns>
public override int Read(byte[] buffer, int offset, int count) => this._innerStream.Read(buffer, offset, count);
/// <summary>
/// Releases the unmanaged resources used by the <see cref="JSonDataStream{T}"/> and optionally releases the managed resources.
/// </summary>
/// <param name="disposing"><c>true</c> to release both managed and unmanaged resources; <c>false</c> to release only unmanaged resources.</param>
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._objToJson = default;
this._innerStream?.Dispose();
this._innerStream = null;
}
public override bool CanRead => true;
public override bool CanSeek => false;
public override bool CanWrite => false;
public override long Position { get => throw new NotImplementedException(); set => throw new NotImplementedException(); }
public override void Flush() { }
public override long Seek(long offset, SeekOrigin origin) => throw new NotImplementedException();
public override void SetLength(long value) { throw new NotImplementedException(); }
public override void Write(byte[] buffer, int offset, int count) { throw new NotImplementedException(); }
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 4843451c7126126409eefb9945ddd4a6
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant:

View File

@@ -0,0 +1,84 @@
using Best.HTTP.Hosts.Connections;
namespace Best.HTTP.Request.Upload
{
// [Request Creation] --> [Request Queued] --> [UploadStream.SetupRequestHeaders call] --> [Send Request Headers] --> [UploadStream.PrepareToSend call] --> [UploadStream.Read to Send Request Body] -> [Dispose UploadStream]
public static class UploadReadConstants
{
public static int WaitForMore = -1;
public static int Completed = 0;
}
/// <summary>
/// Abstract class to serve as a base for non-conventional streams used in HTTP requests.
/// </summary>
/// <remarks>
/// The return value of <see cref="System.IO.Stream.Read(byte[], int, int)"/> is treated specially in the plugin:
/// <list type="bullet">
/// <item>
/// <term>Less than zero(<c>-1</c>)</term>
/// <description> indicates that no data is currently available but more is expected in the future. In this case, when new data becomes available the IThreadSignaler object must be signaled.</description>
/// </item>
/// <item>
/// <term>Zero (<c>0</c>)</term>
/// <description> means that the stream is closed, no more data can be expected.</description>
/// </item>
/// <item><description>Otherwise it must return with the number bytes copied to the buffer.</description></item>
/// </list>
/// A zero value to signal stream closure can follow a less than zero value.
/// </remarks>
public abstract class UploadStreamBase : System.IO.Stream
{
/// <summary>
/// Gets the <see cref="IThreadSignaler"/> object for signaling when new data is available.
/// </summary>
public IThreadSignaler Signaler { get; private set; }
/// <summary>
/// Length in bytes that the stream will upload.
/// </summary>
/// <remarks>
/// The return value of Length is treated specially in the plugin:
/// <list type="bullet">
/// <item><term>-2</term><description>The stream's length is unknown and the plugin have to send data <c>with 'chunked' transfer-encoding</c>.</description></item>
/// <item><term>-1</term><description>The stream's length is unknown and the plugin have to send data <c>as-is, without any encoding</c>.</description></item>
/// <item><term>0</term><description>No content to send. The content-length header will contain zero (<c>0</c>).</description></item>
/// <item><term>>0</term><description>Length of the content is known, will be sent <c>as-is, without any encoding</c>. The content-length header will contain zero (<c>0</c>).</description></item>
/// </list>
/// Constants for the first three points can be found in <see cref="Best.HTTP.Request.Upload.BodyLengths"/>.
/// </remarks>
public override long Length => throw new System.NotImplementedException();
/// <summary>
/// Called before sending out the request's headers. Perform content processing to calculate the final length if possible.
/// In this function the implementor can set headers and other parameters to the request.
/// </summary>
/// <remarks>Typically called on a thread.</remarks>
/// <param name="request">The <see cref="HTTPRequest"/> associated with the stream.</param>
public abstract void BeforeSendHeaders(HTTPRequest request);
/// <summary>
/// Called just before sending out the request's body, and saves the <see cref="IThreadSignaler"/> for signaling when new data is available.
/// </summary>
/// <param name="request">The HTTPRequest associated with the stream.</param>
/// <param name="threadSignaler">The <see cref="IThreadSignaler"/> object to be used for signaling.</param>
/// <remarks>Typically called on a separate thread.</remarks>
/// <summary>
/// Called just before sending out the request's body, saves the <see cref="IThreadSignaler"/> that can be used for signaling when new data is available.
/// </summary>
/// <param name="request">The HTTPRequest associated with the stream.</param>
/// <param name="threadSignaler">The <see cref="IThreadSignaler"/> object to be used for signaling.</param>
/// <remarks>Typically called on a separate thread.</remarks>
public virtual void BeforeSendBody(HTTPRequest request, IThreadSignaler threadSignaler) => this.Signaler = threadSignaler;
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this.Signaler = null;
}
}
}

View File

@@ -0,0 +1,11 @@
fileFormatVersion: 2
guid: 5585905c4d10eba4ca0fe7d011d06b71
MonoImporter:
externalObjects: {}
serializedVersion: 2
defaultReferences: []
executionOrder: 0
icon: {instanceID: 0}
userData:
assetBundleName:
assetBundleVariant: