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,226 @@
using System;
using System.Threading;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Response
{
/// <summary>
/// A blocking variant of the <see cref="DownloadContentStream"/> that allows clients to wait for downloaded data when the buffer is empty but not completed.
/// </summary>
/// <remarks>
/// <para>
/// The BlockingDownloadContentStream is a specialized variant of the <see cref="DownloadContentStream"/> designed to provide a blocking mechanism for clients waiting for downloaded data.
/// This class is particularly useful when clients need to read from the stream, but the buffer is temporarily empty due to ongoing downloads.
/// </para>
/// <para>
/// Key Features:
/// <list type="bullet">
/// <item>
/// <term>Blocking Data Retrieval</term>
/// <description>Provides a blocking <see cref="Take()"/> method that allows clients to wait for data if the buffer is empty but not yet completed.</description>
/// </item>
/// <item>
/// <term>Timeout Support</term>
/// <description>The <see cref="Take(TimeSpan)"/> method accepts a timeout parameter, allowing clients to set a maximum wait time for data availability.</description>
/// </item>
/// <item>
/// <term>Exception Handling</term>
/// <description>Handles exceptions and errors that occur during download, ensuring that clients receive any relevant exception information.</description>
/// </item>
/// </list>
/// </para>
/// <para>
/// Clients can use the <see cref="Take()"/> method to retrieve data from the stream, and if the buffer is empty, the method will block until new data is downloaded or a timeout occurs.
/// This blocking behavior is particularly useful in scenarios where clients need to consume data sequentially but can't proceed until data is available.
/// </para>
/// <para>
/// When the download is completed or if an error occurs during download, this stream allows clients to inspect the completion status and any associated exceptions, just like the base <see cref="DownloadContentStream"/>.
/// </para>
/// </remarks>
public sealed class BlockingDownloadContentStream : DownloadContentStream
{
private AutoResetEvent _are = new AutoResetEvent(false);
/// <summary>
/// Initializes a new instance of the <see cref="BlockingDownloadContentStream"/> class.
/// </summary>
/// <param name="response">The HTTP response associated with this download stream.</param>
/// <param name="maxBuffered">The maximum size of the internal buffer.</param>
/// <param name="bufferAvailableHandler">Handler for notifying when buffer space becomes available.</param>
public BlockingDownloadContentStream(HTTPResponse response, long maxBuffered, IDownloadContentBufferAvailable bufferAvailableHandler)
: base(response, maxBuffered, bufferAvailableHandler)
{
}
/// <summary>
/// Attempts to retrieve a downloaded content-segment from the stream, blocking if necessary until a segment is available.
/// </summary>
/// <param name="segment">When this method returns, contains the <see cref="BufferSegment"/> instance representing the data, if available; otherwise, contains the value of <see cref="BufferSegment.Empty"/>. This parameter is passed uninitialized.</param>
/// <returns><c>true</c> if a segment could be retrieved; otherwise, <c>false</c>.</returns>
/// <remarks>
/// <para>
/// The TryTake function provides a blocking approach to retrieve data from the stream.
/// If the stream has data available, it immediately returns the data.
/// If there's no data available, the method will block until new data is downloaded or the buffer is marked as completed.
/// </para>
/// <para>
/// This method is designed for scenarios where clients need to read from the stream sequentially and are willing to wait until data is available.
/// It ensures that clients receive data as soon as it becomes available, without having to repeatedly check or poll the stream.
/// </para>
/// </remarks>
public override bool TryTake(out BufferSegment segment)
{
segment = BufferSegment.Empty;
while (!base.IsCompleted && segment == BufferSegment.Empty)
segment = Take();
return segment != BufferSegment.Empty;
}
/// <summary>
/// Returns with a download content-segment. If the stream is currently empty but not completed the execution is blocked until new data downloaded.
/// A segment is an arbitrary length array of bytes the plugin could read in one operation, it can range from couple of bytes to kilobytes.
/// </summary>
/// <returns>A BufferSegment holding a reference to the byte[] containing the downloaded data, offset and count of bytes in the array.</returns>
/// <exception cref="ObjectDisposedException">The stream is disposed.</exception>
/// <exception cref="InvalidOperationException">The stream is empty and marked as completed.</exception>
public BufferSegment Take() => Take(TimeSpan.FromMilliseconds(-1));
/// <summary>
/// Returns with a download content-segment. If the stream is currently empty but not completed the execution is blocked until new data downloaded or the timeout is reached.
/// A segment is an arbitrary length array of bytes the plugin could read in one operation, it can range from couple of bytes to kilobytes.
/// </summary>
/// <param name="timeout">A TimeSpan that represents the number of milliseconds to wait, or a TimeSpan that represents -1 milliseconds to wait indefinitely.</param>
/// <returns>A BufferSegment holding a reference to the byte[] containing the downloaded data, offset and count of bytes in the array. In case of a timeout, BufferSegment.Empty returned.</returns>
/// <exception cref="ObjectDisposedException">The stream is disposed.</exception>
/// <exception cref="InvalidOperationException">The stream is empty and marked as completed.</exception>
public BufferSegment Take(TimeSpan timeout)
{
this.IsDetached = true;
while (!base.IsCompleted)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
if (this._exceptionInfo != null)
this._exceptionInfo.Throw();
if (this._segments.TryDequeue(out var segment) && segment.Count > 0)
{
#pragma warning disable 0197
Interlocked.Add(ref base._length, -segment.Count);
#pragma warning restore
this._bufferAvailableHandler?.BufferAvailable(this);
return segment;
}
if (base.IsCompleted)
throw new InvalidOperationException("The stream is empty and marked as completed!");
if (WaitForEvent(timeout)) /*!this._are.WaitOne(timeout)*/
return BufferSegment.Empty;
}
return BufferSegment.Empty;
}
/// <summary>
/// Reads a sequence of bytes from the current stream and advances the position within the stream by the number of bytes read.
/// </summary>
/// <remarks>
/// <para>
/// This override of the <see cref="Read"/> method provides blocking behavior, meaning if there are no bytes available in the stream, the method will block until new data is downloaded or until the stream completes. Once data is available, or if the stream completes, the method will return with the number of bytes read.
/// </para>
/// <para>
/// This behavior ensures that consumers of the stream can continue reading data sequentially, even if the stream's internal buffer is temporarily empty due to ongoing downloads.
/// </para>
/// </remarks>
/// <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 if the end of the stream is reached.
/// </returns>
public override int Read(byte[] buffer, int offset, int count)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
int readCount = base.Read(buffer, offset, count);
while (readCount == 0 && !IsCompleted)
{
//this._are?.WaitOne();
WaitForEvent(TimeSpan.FromMilliseconds(-1));
readCount = base.Read(buffer, offset, count);
}
return readCount;
}
public override void Write(BufferSegment segment)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
base.Write(segment);
this._are?.Set();
}
internal override void CompleteAdding(Exception error)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
base.CompleteAdding(error);
this._are?.Set();
}
/// <summary>
/// Instead of calling WaitOne once for the total duration of the timeout,
/// periodically check whether we are disposed or not.
/// </summary>
private bool WaitForEvent(TimeSpan timeoutTS)
{
const int CHECK_PERIOD = 100;
var resetEvent = this._are;
if (this._isDisposed || resetEvent == null)
throw new ObjectDisposedException(GetType().FullName);
var timeout = (int)timeoutTS.TotalMilliseconds;
if (timeout < 0)
timeout = int.MaxValue;
while (!this._isDisposed && timeout > 0)
{
int waitTime = Math.Min((int)timeout, CHECK_PERIOD);
// there's a race condition between checking _isDisposed and using resetEvent.
// There are two cases:
// 1.) resetEvent is already disposed when calling WaitOne in case it will throw an exception
// 2.) resetEvent will be disposed while blocking in WaitOne and in this case it will unblock and exit from the while cycle because of _isDisposed is true
if (resetEvent.WaitOne(waitTime))
return true;
timeout -= CHECK_PERIOD;
}
return false;
}
protected override void Dispose(bool disposing)
{
base.Dispose(disposing);
this._are?.Dispose();
this._are = null;
}
}
}

View File

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

View File

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

View File

@@ -0,0 +1,98 @@
using System;
using Best.HTTP.Shared.Streams;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Response.Decompression
{
public sealed class BrotliDecompressor : IDecompressor
{
#if ((NET_STANDARD_2_1 || UNITY_2021_2_OR_NEWER) && (UNITY_EDITOR || (!UNITY_WEBGL && !(ENABLE_MONO && UNITY_ANDROID)))) && !BESTHTTP_DISABLE_BROTLI
private BufferSegmentStream decompressorInputStream;
private BufferPoolMemoryStream decompressorOutputStream;
private System.IO.Compression.BrotliStream decompressorStream;
byte[] copyBuffer = null;
#endif
private int _minLengthToDecompress;
public static bool IsSupported()
{
// Not enabled under android with the mono runtime
#if ((NET_STANDARD_2_1 || UNITY_2021_2_OR_NEWER) && (UNITY_EDITOR || (!UNITY_WEBGL && !(ENABLE_MONO && UNITY_ANDROID)))) && !BESTHTTP_DISABLE_BROTLI
return true;
#else
return false;
#endif
}
public BrotliDecompressor(int minLengthToDecompress)
{
this._minLengthToDecompress = minLengthToDecompress;
}
public (BufferSegment decompressed, bool releaseTheOld) Decompress(BufferSegment segment, bool forceDecompress, bool dataCanBeLarger, LoggingContext context)
{
#if ((NET_STANDARD_2_1 || UNITY_2021_2_OR_NEWER) && (UNITY_EDITOR || (!UNITY_WEBGL && !(ENABLE_MONO && UNITY_ANDROID)))) && !BESTHTTP_DISABLE_BROTLI
if (decompressorInputStream == null)
decompressorInputStream = new BufferSegmentStream();
if (segment.Data != null)
decompressorInputStream.Write(segment);
if (!forceDecompress && decompressorInputStream.Length < _minLengthToDecompress)
return (BufferSegment.Empty, false);
if (decompressorStream == null)
{
decompressorStream = new System.IO.Compression.BrotliStream(decompressorInputStream,
System.IO.Compression.CompressionMode.Decompress,
true);
}
if (decompressorOutputStream == null)
decompressorOutputStream = new BufferPoolMemoryStream();
decompressorOutputStream.SetLength(0);
if (copyBuffer == null)
copyBuffer = BufferPool.Get(4 * 1024, true);
int readCount;
int sumReadCount = 0;
while ((readCount = decompressorStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
{
decompressorOutputStream.Write(copyBuffer, 0, readCount);
sumReadCount += readCount;
}
byte[] result = decompressorOutputStream.ToArray(dataCanBeLarger, context);
return (new BufferSegment(result, 0, dataCanBeLarger ? (int)decompressorOutputStream.Length : result.Length), false);
#else
return (BufferSegment.Empty, false);
#endif
}
public void Dispose()
{
#if ((NET_STANDARD_2_1 || UNITY_2021_2_OR_NEWER) && (UNITY_EDITOR || (!UNITY_WEBGL && !(ENABLE_MONO && UNITY_ANDROID)))) && !BESTHTTP_DISABLE_BROTLI
if (decompressorStream != null)
decompressorStream.Dispose();
decompressorStream = null;
if (decompressorInputStream != null)
decompressorInputStream.Dispose();
decompressorInputStream = null;
if (decompressorOutputStream != null)
decompressorOutputStream.Dispose();
decompressorOutputStream = null;
BufferPool.Release(copyBuffer);
copyBuffer = null;
#endif
}
}
}

View File

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

View File

@@ -0,0 +1,72 @@
using Best.HTTP.Shared;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Text;
namespace Best.HTTP.Response.Decompression
{
public static class DecompressorFactory
{
public const int MinLengthToDecompress = 256;
// cached header value
private static string AcceptEncoding = null;
public static void SetupHeaders(HTTPRequest request)
{
if (!request.HasHeader("Accept-Encoding"))
{
if (AcceptEncoding == null)
{
var sb = StringBuilderPool.Get(4);
if (BrotliDecompressor.IsSupported())
sb.Append("br, ");
if (GZipDecompressor.IsSupported)
sb.Append("gzip, ");
if (DeflateDecompressor.IsSupported)
sb.Append("deflate, ");
sb.Append("identity");
AcceptEncoding = StringBuilderPool.ReleaseAndGrab(sb);
}
request.AddHeader("Accept-Encoding", AcceptEncoding);
}
}
public static IDecompressor GetDecompressor(string encoding, LoggingContext context)
{
if (encoding == null)
return null;
switch (encoding.ToLowerInvariant())
{
// https://github.com/Benedicht/BestHTTP-Issues/issues/183
case "none":
case "identity":
case "utf-8":
break;
case "gzip": return new GZipDecompressor(MinLengthToDecompress);
case "deflate": return new DeflateDecompressor(MinLengthToDecompress);
case "br":
if (BrotliDecompressor.IsSupported())
return new BrotliDecompressor(MinLengthToDecompress);
else
goto default;
default:
HTTPManager.Logger.Warning(nameof(DecompressorFactory), $"GetDecompressor - unsupported encoding '{encoding}'!", context);
break;
}
return null;
}
}
}

View File

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

View File

@@ -0,0 +1,109 @@
using System;
using Best.HTTP.Shared.Compression.Zlib;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Response.Decompression
{
public sealed class DeflateDecompressor : IDecompressor
{
private BufferPoolMemoryStream decompressorInputStream;
private BufferPoolMemoryStream decompressorOutputStream;
private DeflateStream decompressorStream;
private int MinLengthToDecompress = 256;
public static bool IsSupported = true;
public DeflateDecompressor(int minLengthToDecompress)
{
this.MinLengthToDecompress = minLengthToDecompress;
}
public (BufferSegment decompressed, bool releaseTheOld) Decompress(BufferSegment segment, bool forceDecompress, bool dataCanBeLarger, LoggingContext context)
{
if (decompressorInputStream == null)
decompressorInputStream = new BufferPoolMemoryStream(segment.Count);
if (segment.Data != null)
decompressorInputStream.Write(segment.Data, segment.Offset, segment.Count);
if (!forceDecompress && decompressorInputStream.Length < MinLengthToDecompress)
return (BufferSegment.Empty, true);
decompressorInputStream.Position = 0;
if (decompressorStream == null)
{
// Had to change from this
// _baseStream = new ZlibBaseStream(stream, mode, level, ZlibStreamFlavor.DEFLATE, leaveOpen);
// this this:
// _baseStream = new ZlibBaseStream(stream, mode, level, ZlibStreamFlavor.ZLIB, leaveOpen);
// Because there are two bytes in the header additionally to what the deflate flavor expects that the zlib one handles fine.
decompressorStream = new DeflateStream(decompressorInputStream,
CompressionMode.Decompress,
CompressionLevel.Default,
true);
decompressorStream.FlushMode = FlushType.Sync;
}
if (decompressorOutputStream == null)
decompressorOutputStream = new BufferPoolMemoryStream();
decompressorOutputStream.SetLength(0);
byte[] copyBuffer = BufferPool.Get(1024, true);
int readCount;
int sumReadCount = 0;
while ((readCount = decompressorStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
{
decompressorOutputStream.Write(copyBuffer, 0, readCount);
sumReadCount += readCount;
}
BufferPool.Release(copyBuffer);
// If no read is done (returned with any data) don't zero out the input stream, as it would delete any not yet used data.
if (sumReadCount > 0)
decompressorStream.SetLength(0);
byte[] result = decompressorOutputStream.ToArray(dataCanBeLarger, context);
return (new BufferSegment(result, 0, dataCanBeLarger ? (int)decompressorOutputStream.Length : result.Length), true);
}
~DeflateDecompressor()
{
Dispose();
}
public void Dispose()
{
if (decompressorInputStream != null)
decompressorInputStream.Dispose();
decompressorInputStream = null;
if (decompressorOutputStream != null)
decompressorOutputStream.Dispose();
decompressorOutputStream = null;
if (decompressorStream != null)
{
// If the decompressor closed before receiving data, or it's incomplete, disposing (eg. closing) it
// throws an execption like this:
// "Missing or incomplete GZIP trailer. Expected 8 bytes, got 0."
try
{
decompressorStream.Dispose();
}
catch
{ }
}
decompressorStream = null;
GC.SuppressFinalize(this);
}
}
}

View File

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

View File

@@ -0,0 +1,104 @@
using System;
using Best.HTTP.Shared.Compression.Zlib;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
using Best.HTTP.Shared.Streams;
namespace Best.HTTP.Response.Decompression
{
public sealed class GZipDecompressor : IDecompressor
{
private BufferPoolMemoryStream decompressorInputStream;
private BufferPoolMemoryStream decompressorOutputStream;
private GZipStream decompressorStream;
private int MinLengthToDecompress = 256;
public static bool IsSupported = true;
public GZipDecompressor(int minLengthToDecompress)
{
this.MinLengthToDecompress = minLengthToDecompress;
}
public (BufferSegment decompressed, bool releaseTheOld) Decompress(BufferSegment segment, bool forceDecompress, bool dataCanBeLarger, LoggingContext context)
{
if (decompressorInputStream == null)
decompressorInputStream = new BufferPoolMemoryStream(segment.Count);
if (segment.Data != null)
decompressorInputStream.Write(segment.Data, segment.Offset, segment.Count);
if (!forceDecompress && decompressorInputStream.Length < MinLengthToDecompress)
return (BufferSegment.Empty, true);
decompressorInputStream.Position = 0;
if (decompressorStream == null)
{
decompressorStream = new GZipStream(decompressorInputStream,
CompressionMode.Decompress,
CompressionLevel.Default,
true);
decompressorStream.FlushMode = FlushType.Sync;
}
if (decompressorOutputStream == null)
decompressorOutputStream = new BufferPoolMemoryStream();
decompressorOutputStream.SetLength(0);
byte[] copyBuffer = BufferPool.Get(1024, true);
int readCount;
int sumReadCount = 0;
while ((readCount = decompressorStream.Read(copyBuffer, 0, copyBuffer.Length)) != 0)
{
decompressorOutputStream.Write(copyBuffer, 0, readCount);
sumReadCount += readCount;
}
BufferPool.Release(copyBuffer);
// If no read is done (returned with any data) don't zero out the input stream, as it would delete any not yet used data.
if (sumReadCount > 0)
decompressorStream.SetLength(0);
byte[] result = decompressorOutputStream.ToArray(dataCanBeLarger, context);
return (new BufferSegment(result, 0, dataCanBeLarger ? (int)decompressorOutputStream.Length : result.Length), true);
}
~GZipDecompressor()
{
Dispose();
}
public void Dispose()
{
if (decompressorInputStream != null)
decompressorInputStream.Dispose();
decompressorInputStream = null;
if (decompressorOutputStream != null)
decompressorOutputStream.Dispose();
decompressorOutputStream = null;
if (decompressorStream != null)
{
// If the decompressor closed before receiving data, or it's incomplete, disposing (eg. closing) it
// throws an execption like this:
// "Missing or incomplete GZIP trailer. Expected 8 bytes, got 0."
try
{
decompressorStream.Dispose();
}
catch
{ }
}
decompressorStream = null;
GC.SuppressFinalize(this);
}
}
}

View File

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

View File

@@ -0,0 +1,12 @@
using System;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
namespace Best.HTTP.Response.Decompression
{
public interface IDecompressor : IDisposable
{
(BufferSegment decompressed, bool releaseTheOld) Decompress(BufferSegment segment, bool forceDecompress, bool dataCanBeLarger, LoggingContext context);
}
}

View File

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

View File

@@ -0,0 +1,292 @@
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Shared;
using Best.HTTP.Shared.PlatformSupport.Memory;
using System;
using System.Collections.Concurrent;
using System.IO;
using System.Runtime.ExceptionServices;
using System.Threading;
namespace Best.HTTP.Response
{
/// <summary>
/// A read-only stream that the plugin uses to store the downloaded content. This stream is designed to buffer downloaded data efficiently and provide it to consumers.
/// </summary>
/// <remarks>
/// <para>
/// The DownloadContentStream serves as a storage medium for content downloaded during HTTP requests.
/// It buffers the downloaded data in segments and allows clients to read from the buffer as needed.
/// This buffering mechanism is essential for optimizing download performance, especially in scenarios where the download rate may vary or be faster than the rate at which data is consumed.
/// </para>
/// <para>
/// The stream operates in conjunction with the <see cref="IDownloadContentBufferAvailable"/> interface, which is used to signal connections when buffer space becomes available.
/// Connections can then transfer additional data into the buffer for processing.
/// </para>
/// <para>
/// <list type="bullet">
/// <item>
/// <term>Efficient Buffering</term>
/// <description>The stream efficiently buffers downloaded content, ensuring that data is readily available for reading without extensive delays.</description>
/// </item>
/// <item>
/// <term>Dynamic Resizing</term>
/// <description>The internal buffer dynamically resizes to accommodate varying amounts of downloaded data, optimizing memory usage.</description>
/// </item>
/// <item>
/// <term>Asynchronous Signal Handling</term>
/// <description>Asynchronous signaling mechanisms are used to notify connections when buffer space is available, enabling efficient data transfer.</description>
/// </item>
/// <item>
/// <term>Error Handling</term>
/// <description>The stream captures and propagates errors that occur during download, allowing clients to handle exceptions gracefully.</description>
/// </item>
/// <item>
/// <term>Blocking Variant</term>
/// <description>A blocking variant, <see cref="BlockingDownloadContentStream"/>, allows clients to wait for data when the buffer is empty but not completed.</description>
/// </item>
/// </list>
/// </para>
/// <para>
/// Clients can read from this stream using standard stream reading methods, and the stream will release memory segments as data is read.
/// When the download is completed or if an error occurs during download, this stream allows clients to inspect the completion status and any associated exceptions.
/// </para>
/// </remarks>
public class DownloadContentStream : Stream
{
/// <summary>
/// Gets the HTTP response from which this download stream originated.
/// </summary>
public HTTPResponse Response { get; protected set; }
/// <summary>
/// Gets a value indicating whether the download is completed, and there's no more data buffered in the stream to read.
/// </summary>
public virtual bool IsCompleted { get => this._isCompleted && this._segments.Count == 0 && this._currentSegment == BufferSegment.Empty; }
/// <summary>
/// Gets a reference to an exception if the download completed with an error.
/// </summary>
public Exception CompletedWith { get => this._exceptionInfo?.SourceException; }
/// <summary>
/// Gets the length of the buffered data. Because downloads happen in parallel, a <see cref="Read(byte[], int, int)"/> call can return with more data after checking Length.
/// </summary>
public override long Length => Interlocked.Read(ref this._length);
protected long _length;
/// <summary>
/// Gets the maximum size of the internal buffer of this stream.
/// </summary>
/// <remarks>In some cases, the plugin may put more data into the stream than the specified size.</remarks>
public long MaxBuffered { get; protected set; }
/// <summary>
/// Gets a value indicating whether the internal buffer holds at least the <see cref="MaxBuffered"/> amount of data.
/// </summary>
public bool IsFull { get => this.Length >= this.MaxBuffered; }
/// <summary>
/// Gets or sets whether the stream is detached from the <see cref="HTTPRequest"/>/<see cref="HTTPResponse"/> when <see cref="Read(byte[], int, int)"/> is used before the request is finished.
/// When the stream is detached from the response object, their lifetimes are not bound together,
/// meaning that the stream isn't disposed automatically, and the client code is responsible for calling the stream's <see cref="System.IO.Stream.Dispose()"/> function.
/// </summary>
public bool IsDetached
{
get => this._isDetached;
set
{
if (this._isDetached != value)
{
HTTPManager.Logger.Verbose(nameof(DownloadContentStream), $"IsDetached {this._isDetached} => {value}", this.Response?.Context);
this._isDetached = value;
}
}
}
private bool _isDetached;
/// <summary>
/// There are cases where the plugin have to put more data into the buffer than its previously set maximum.
/// For example when the underlying connection is closed, but the content provider still have buffered data,
/// in witch case we have to push all processed data to the user facing download stream.
/// </summary>
public void EmergencyIncreaseMaxBuffered() => this.MaxBuffered = long.MaxValue;
protected IDownloadContentBufferAvailable _bufferAvailableHandler;
protected ConcurrentQueue<BufferSegment> _segments = new ConcurrentQueue<BufferSegment>();
protected BufferSegment _currentSegment = BufferSegment.Empty;
protected bool _isCompleted;
protected ExceptionDispatchInfo _exceptionInfo;
/// <summary>
/// Count of consecutive calls with DoFullCheck that found the stream fully buffered.
/// </summary>
private int _isFullCheckCount;
protected bool _isDisposed;
/// <summary>
/// Initializes a new instance of the DownloadContentStream class.
/// </summary>
/// <param name="response">The HTTP response associated with this download stream.</param>
/// <param name="maxBuffered">The maximum size of the internal buffer.</param>
/// <param name="bufferAvailableHandler">Handler for notifying when buffer space becomes available.</param>
public DownloadContentStream(HTTPResponse response, long maxBuffered, IDownloadContentBufferAvailable bufferAvailableHandler)
{
this.Response = response;
this.MaxBuffered = maxBuffered;
this._bufferAvailableHandler = bufferAvailableHandler;
}
/// <summary>
/// Completes the download stream with an optional error. Called when the download is finished.
/// </summary>
/// <param name="error">The exception that occurred during download, if any.</param>
internal virtual void CompleteAdding(Exception error)
{
if (HTTPManager.Logger.Level <= Shared.Logger.Loglevels.Information)
HTTPManager.Logger.Information(nameof(DownloadContentStream), $"CompleteAdding({error})", this.Response?.Context);
this._isCompleted = true;
this._exceptionInfo = error != null ? ExceptionDispatchInfo.Capture(error) : null;
this._bufferAvailableHandler = null;
}
/// <summary>
/// Tries to remove a downloaded segment from the stream. If the stream is empty, it returns immediately with false.
/// </summary>
/// <param name="segment">A <see cref="BufferSegment"/> containing the reference to a byte[] and the offset and count of the data in the array.</param>
/// <returns><c>true</c> if a downloaded segment was available and could return with, otherwise <c>false</c></returns>
public virtual bool TryTake(out BufferSegment segment)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
this.IsDetached = true;
if (this._segments.TryDequeue(out segment) && segment.Count > 0)
{
Interlocked.Add(ref this._length, -segment.Count);
this._bufferAvailableHandler?.BufferAvailable(this);
return true;
}
return false;
}
/// <summary>
/// A non-blocking Read function. When it returns <c>0</c>, it doesn't mean the download is complete. If the download interrupted before completing, the next Read call can throw an exception.
/// </summary>
/// <param name="buffer">The buffer to read data into.</param>
/// <param name="offset">The zero-based byte offset in the buffer at which to begin copying bytes.</param>
/// <param name="count">The maximum number of bytes to read.</param>
/// <returns>The number of bytes copied to the buffer, or zero if no downloaded data is available at the time of the call.</returns>
/// <exception cref="ObjectDisposedException">If the stream is already disposed.</exception>
public override int Read(byte[] buffer, int offset, int count)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
if (this._exceptionInfo != null)
this._exceptionInfo.Throw();
this.IsDetached = true;
if (this._currentSegment == BufferSegment.Empty)
this._segments.TryDequeue(out this._currentSegment);
int sumReadCount = 0;
while (sumReadCount < count && this._currentSegment != BufferSegment.Empty)
{
int readCount = Math.Min(count - sumReadCount, this._currentSegment.Count);
Array.Copy(this._currentSegment.Data, this._currentSegment.Offset, buffer, offset, readCount);
offset += readCount;
sumReadCount += readCount;
if (this._currentSegment.Count == readCount)
{
BufferPool.Release(this._currentSegment);
if (!this._segments.TryDequeue(out this._currentSegment))
this._currentSegment = BufferSegment.Empty;
}
else
this._currentSegment = this._currentSegment.Slice(this._currentSegment.Offset + readCount);
}
Interlocked.Add(ref this._length, -sumReadCount);
this._bufferAvailableHandler?.BufferAvailable(this);
return sumReadCount;
}
/// <summary>
/// Writes a downloaded data segment to the stream.
/// </summary>
/// <param name="segment">The downloaded data segment to write.</param>
public virtual void Write(BufferSegment segment)
{
if (this._isDisposed)
throw new ObjectDisposedException(GetType().FullName);
if (segment.Count <= 0)
return;
this._segments.Enqueue(segment);
Interlocked.Add(ref this._length, segment.Count);
this._isFullCheckCount = 0;
}
/// <summary>
/// Checks whether the stream is fully buffered and increases a counter if it's full, resetting it otherwise.
/// </summary>
/// <param name="limit">The limit for the full check counter.</param>
/// <returns><c>true</c> if the counter is equal to or larger than the limit parameter; otherwise <c>false</c>.</returns>
internal bool DoFullCheck(int limit)
{
if (IsFull)
_isFullCheckCount++;
else
_isFullCheckCount = 0;
return _isFullCheckCount >= limit;
}
/// <summary>
/// Disposes of the stream, releasing any resources held by it.
/// </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);
if (this._isDisposed)
return;
this._isDisposed = true;
using (var _ = new Unity.Profiling.ProfilerMarker("DownloadContentStream.Dispose").Auto())
{
BufferPool.Release(this._currentSegment);
this._currentSegment = BufferSegment.Empty;
BufferPool.ReleaseBulk(this._segments);
}
}
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() => 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();
}
}

View File

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

View File

@@ -0,0 +1,528 @@
using System;
using System.Collections.Generic;
using System.IO;
using System.Text;
using UnityEngine;
using static Best.HTTP.Response.HTTPStatusCodes;
namespace Best.HTTP
{
using Best.HTTP.Caching;
using Best.HTTP.Hosts.Connections;
using Best.HTTP.Response;
using Best.HTTP.Shared;
using Best.HTTP.Shared.Extensions;
using Best.HTTP.Shared.Logger;
using Best.HTTP.Shared.PlatformSupport.Memory;
/// <summary>
/// Represents an HTTP response received from a remote server, containing information about the response status, headers, and data.
/// </summary>
/// <remarks>
/// <para>
/// The HTTPResponse class represents an HTTP response received from a remote server. It contains information about the response status, headers, and the data content.
/// </para>
/// <para>
/// Key Features:
/// <list type="bullet">
/// <item>
/// <term>Response Properties</term>
/// <description>Provides access to various properties such as <see cref="HTTPVersion"/>, <see cref="StatusCode"/>, <see cref="Message"/>, and more, to inspect the response details.</description>
/// </item>
/// <item>
/// <term>Data Access</term>
/// <description>Allows access to the response data in various forms, including raw bytes, UTF-8 text, and as a <see cref="Texture2D"/> for image data.</description>
/// </item>
/// <item>
/// <term>Header Management</term>
/// <description>Provides methods to add, retrieve, and manipulate HTTP headers associated with the response, making it easy to inspect and work with header information.</description>
/// </item>
/// <item>
/// <term>Caching Support</term>
/// <description>Supports response caching, enabling the storage of downloaded data in local cache storage for future use.</description>
/// </item>
/// <item>
/// <term>Stream Management</term>
/// <description>Manages the download process and data streaming through a <see cref="DownloadContentStream"/> (<see cref="DownStream"/>) to optimize memory usage and ensure efficient handling of large response bodies.</description>
/// </item>
/// </list>
/// </para>
/// </remarks>
public class HTTPResponse : IDisposable
{
#region Public Properties
/// <summary>
/// Gets the version of the HTTP protocol with which the response was received. Typically, this is HTTP/1.1 for local file and cache responses, even if the original response received with a different version.
/// </summary>
public Version HTTPVersion { get; protected set; }
/// <summary>
/// Gets the HTTP status code sent from the server, indicating the outcome of the HTTP request.
/// </summary>
public int StatusCode { get; protected set; }
/// <summary>
/// Gets the message sent along with the status code from the server. This message can add some details, but it's empty for HTTP/2 responses.
/// </summary>
public string Message { get; protected set; }
/// <summary>
/// Gets a value indicating whether the response represents a successful HTTP request. Returns true if the status code is in the range of [200..300[ or 304 (Not Modified).
/// </summary>
public bool IsSuccess { get { return (this.StatusCode >= OK && this.StatusCode < MultipleChoices) || this.StatusCode == NotModified; } }
/// <summary>
/// Gets a value indicating whether the response body is read from the cache.
/// </summary>
public bool IsFromCache { get; internal set; }
/// <summary>
/// Gets the headers sent from the server as key-value pairs. You can use additional methods to manage and retrieve header information.
/// </summary>
/// <remarks>
/// The Headers property provides access to the headers sent by the server in the HTTP response. You can use the following methods to work with headers:
/// <list type="bullet">
/// <item><term><see cref="AddHeader(string, string)"/> </term><description>Adds an HTTP header with the specified name and value to the response headers.</description></item>
/// <item><term><see cref="GetHeaderValues(string)"/> </term><description>Retrieves the list of values for a given header name as received from the server.</description></item>
/// <item><term><see cref="GetFirstHeaderValue(string)"/> </term><description>Retrieves the first value for a given header name as received from the server.</description></item>
/// <item><term><see cref="HasHeaderWithValue(string, string)"/> </term><description>Checks if a header with the specified name and value exists in the response headers.</description></item>
/// <item><term><see cref="HasHeader(string)"/> </term><description>Checks if a header with the specified name exists in the response headers.</description></item>
/// <item><term><see cref="GetRange()"/></term><description>Parses the 'Content-Range' header's value and returns a <see cref="HTTPRange"/> object representing the byte range of the response content.</description></item>
/// </list>
/// </remarks>
public Dictionary<string, List<string>> Headers { get; protected set; }
/// <summary>
/// The data that downloaded from the server. All Transfer and Content encodings decoded if any(eg. chunked, gzip, deflate).
/// </summary>
public byte[] Data
{
get
{
if (this._data != null)
return this._data;
if (this.DownStream == null)
return null;
CheckDisposed();
this._data = new byte[this.DownStream.Length];
try
{
this.DownStream.Read(this._data, 0, this._data.Length);
}
catch (Exception ex)
{
this._data = null;
HTTPManager.Logger.Exception(nameof(HTTPResponse), "get_Data", ex, this.Context);
}
finally
{
this.DownStream.Dispose();
}
return this._data;
}
}
private byte[] _data;
/// <summary>
/// The normal HTTP protocol is upgraded to an other.
/// </summary>
public bool IsUpgraded { get; internal set; }
/// <summary>
/// Cached, converted data.
/// </summary>
protected string dataAsText;
/// <summary>
/// The data converted to an UTF8 string.
/// </summary>
public string DataAsText
{
get
{
if (Data == null)
return string.Empty;
if (!string.IsNullOrEmpty(dataAsText))
return dataAsText;
CheckDisposed();
return dataAsText = Encoding.UTF8.GetString(Data, 0, Data.Length);
}
}
#if !UNITY_SERVER
/// <summary>
/// Cached converted data.
/// </summary>
protected Texture2D texture;
/// <summary>
/// The data loaded to a Texture2D.
/// </summary>
public Texture2D DataAsTexture2D
{
get
{
if (Data == null)
return null;
if (texture != null)
return texture;
CheckDisposed();
texture = new Texture2D(1, 1, TextureFormat.RGBA32, false);
texture.LoadImage(Data, true);
return texture;
}
}
#endif
/// <summary>
/// Reference to the <see cref="DownloadContentStream"/> instance that contains the downloaded data.
/// </summary>
public DownloadContentStream DownStream { get; internal set; }
/// <summary>
/// IProtocol.LoggingContext implementation.
/// </summary>
public LoggingContext Context { get; private set; }
/// <summary>
/// The original request that this response is created for.
/// </summary>
public HTTPRequest Request { get; private set; }
#endregion
protected HTTPCacheContentWriter _cacheWriter;
private bool _isDisposed;
internal HTTPResponse(HTTPRequest request, bool isFromCache)
{
this.Request = request;
this.IsFromCache = isFromCache;
this.Context = new LoggingContext(this);
this.Context.Add("BaseRequest", request.Context);
}
#region Header Management
/// <summary>
/// Adds an HTTP header with the specified name and value to the response headers.
/// </summary>
/// <param name="name">The name of the header.</param>
/// <param name="value">The value of the header.</param>
public void AddHeader(string name, string value) => this.Headers = this.Headers.AddHeader(name, value);
/// <summary>
/// Retrieves the list of values for a given header name as received from the server.
/// </summary>
/// <param name="name">The name of the header.</param>
/// <returns>
/// A list of header values if the header exists and contains values; otherwise, returns <c>null</c>.
/// </returns>
public List<string> GetHeaderValues(string name) => this.Headers.GetHeaderValues(name);
/// <summary>
/// Retrieves the first value for a given header name as received from the server.
/// </summary>
/// <param name="name">The name of the header.</param>
/// <returns>
/// The first header value if the header exists and contains values; otherwise, returns <c>null</c>.
/// </returns>
public string GetFirstHeaderValue(string name) => this.Headers.GetFirstHeaderValue(name);
/// <summary>
/// Checks if a header with the specified name and value exists in the response headers.
/// </summary>
/// <param name="headerName">The name of the header to check.</param>
/// <param name="value">The value to check for in the header.</param>
/// <returns>
/// <c>true</c> if a header with the given name and value exists in the response headers; otherwise, <c>false</c>.
/// </returns>
public bool HasHeaderWithValue(string headerName, string value) => this.Headers.HasHeaderWithValue(headerName, value);
/// <summary>
/// Checks if a header with the specified name exists in the response headers.
/// </summary>
/// <param name="headerName">The name of the header to check.</param>
/// <returns>
/// <c>true</c> if a header with the given name exists in the response headers; otherwise, <c>false</c>.
/// </returns>
public bool HasHeader(string headerName) => this.Headers.HasHeader(headerName);
/// <summary>
/// Parses the <c>'Content-Range'</c> header's value and returns a <see cref="HTTPRange"/> object representing the byte range of the response content.
/// </summary>
/// <remarks>
/// If the server ignores a byte-range-spec because it is syntactically invalid, the server SHOULD treat the request as if the invalid Range header field did not exist.
/// (Normally, this means return a 200 response containing the full entity). In this case because there are no <c>'Content-Range'</c> header values, this function will return <c>null</c>.
/// </remarks>
/// <returns>
/// A <see cref="HTTPRange"/> object representing the byte range of the response content, or <c>null</c> if no '<c>Content-Range</c>' header is found.
/// </returns>
public HTTPRange GetRange()
{
var rangeHeaders = this.Headers.GetHeaderValues("content-range");
if (rangeHeaders == null)
return null;
// A byte-content-range-spec with a byte-range-resp-spec whose last- byte-pos value is less than its first-byte-pos value,
// or whose instance-length value is less than or equal to its last-byte-pos value, is invalid.
// The recipient of an invalid byte-content-range- spec MUST ignore it and any content transferred along with it.
// A valid content-range sample: "bytes 500-1233/1234"
var ranges = rangeHeaders[0].Split(new char[] { ' ', '-', '/' }, StringSplitOptions.RemoveEmptyEntries);
// A server sending a response with status code 416 (Requested range not satisfiable) SHOULD include a Content-Range field with a byte-range-resp-spec of "*".
// The instance-length specifies the current length of the selected resource.
// "bytes */1234"
if (ranges[1] == "*")
return new HTTPRange(int.Parse(ranges[2]));
return new HTTPRange(int.Parse(ranges[1]), int.Parse(ranges[2]), ranges[3] != "*" ? int.Parse(ranges[3]) : -1);
}
#endregion
#region Static Stream Management Helper Functions
internal static string ReadTo(Stream stream, byte blocker)
{
byte[] readBuf = BufferPool.Get(1024, true);
try
{
int bufpos = 0;
int ch = stream.ReadByte();
while (ch != blocker && ch != -1)
{
if (ch > 0x7f) //replaces asciitostring
ch = '?';
//make buffer larger if too short
if (readBuf.Length <= bufpos)
BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, false);
if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
readBuf[bufpos++] = (byte)ch;
ch = stream.ReadByte();
}
while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 1]))
bufpos--;
return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos);
}
finally
{
BufferPool.Release(readBuf);
}
}
internal static BufferSegment ReadToAsByte(Stream stream, byte blocker)
{
var readBuf = BufferPool.Get(1024, true, null);
try
{
int bufpos = 0;
int ch = stream.ReadByte();
while (ch != blocker && ch != -1)
{
if (ch > 0x7f) //replaces asciitostring
ch = '?';
//make buffer larger if too short
if (readBuf.Length <= bufpos + 1)
BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, false);
if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
{
readBuf[bufpos++] = (byte)ch;
readBuf[bufpos++] = 0;
}
ch = stream.ReadByte();
}
while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 2]))
bufpos -= 2;
return readBuf.AsBuffer(bufpos);
}
catch
{
BufferPool.Release(readBuf);
return BufferSegment.Empty;
}
}
internal static string ReadTo(Stream stream, byte blocker1, byte blocker2)
{
byte[] readBuf = BufferPool.Get(1024, true);
try
{
int bufpos = 0;
int ch = stream.ReadByte();
while (ch != blocker1 && ch != blocker2 && ch != -1)
{
if (ch > 0x7f) //replaces asciitostring
ch = '?';
//make buffer larger if too short
if (readBuf.Length <= bufpos)
BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, true);
if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
readBuf[bufpos++] = (byte)ch;
ch = stream.ReadByte();
}
while (bufpos > 0 && char.IsWhiteSpace((char)readBuf[bufpos - 1]))
bufpos--;
return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos);
}
finally
{
BufferPool.Release(readBuf);
}
}
internal static string NoTrimReadTo(Stream stream, byte blocker1, byte blocker2)
{
byte[] readBuf = BufferPool.Get(1024, true);
try
{
int bufpos = 0;
int ch = stream.ReadByte();
while (ch != blocker1 && ch != blocker2 && ch != -1)
{
if (ch > 0x7f) //replaces asciitostring
ch = '?';
//make buffer larger if too short
if (readBuf.Length <= bufpos)
BufferPool.Resize(ref readBuf, readBuf.Length * 2, true, true);
if (bufpos > 0 || !char.IsWhiteSpace((char)ch)) //trimstart
readBuf[bufpos++] = (byte)ch;
ch = stream.ReadByte();
}
return System.Text.Encoding.UTF8.GetString(readBuf, 0, bufpos);
}
finally
{
BufferPool.Release(readBuf);
}
}
#endregion
protected void BeginReceiveContent()
{
CheckDisposed();
if (!Request.DownloadSettings.DisableCache && !IsFromCache)
{
// If caching is enabled and the response not from cache and it's cacheble we will cache the downloaded data
// by writing it to the stream returned by BeginCache
_cacheWriter = HTTPManager.LocalCache?.BeginCache(Request.MethodType, Request.CurrentUri, this.StatusCode, this.Headers, this.Context);
}
}
/// <summary>
/// Add data to the fragments list.
/// </summary>
/// <param name="buffer">The buffer to be added.</param>
/// <param name="pos">The position where we start copy the data.</param>
/// <param name="length">How many data we want to copy.</param>
protected void FeedDownloadedContentChunk(BufferSegment segment)
{
if (segment == BufferSegment.Empty)
return;
CheckDisposed();
_cacheWriter?.Write(segment);
if (!this.Request.DownloadSettings.CacheOnly)
this.DownStream.Write(segment);
else
BufferPool.Release(segment);
}
protected void FinishedContentReceiving()
{
CheckDisposed();
_cacheWriter?.Cache?.EndCache(_cacheWriter, true, this.Context);
_cacheWriter = null;
}
protected void CreateDownloadStream(IDownloadContentBufferAvailable bufferAvailable)
{
if (this.DownStream != null)
this.DownStream.Dispose();
this.DownStream = this.Request.DownloadSettings.DownloadStreamFactory(this.Request, this, bufferAvailable);
HTTPManager.Logger.Information(this.GetType().Name, $"{nameof(DownloadContentStream)} initialized with Maximum Buffer Size: {this.DownStream.MaxBuffered:N0}", this.Context);
// Send download-started event only when the final content is expected (2xx status codes).
// Otherwise, for one request multiple download-started even would be trigger every time it gets redirected.
if (this.StatusCode >= OK && this.StatusCode < MultipleChoices)
RequestEventHelper.EnqueueRequestEvent(new RequestEventInfo(this.Request, RequestEvents.DownloadStarted));
}
protected void CheckDisposed()
{
if (this._isDisposed)
throw new ObjectDisposedException(this.GetType().Name);
}
/// <summary>
/// IDisposable implementation.
/// </summary>
public void Dispose()
{
Dispose(true);
GC.SuppressFinalize(this);
}
protected virtual void Dispose(bool disposing)
{
if (disposing && !this._isDisposed)
{
_cacheWriter?.Cache?.EndCache(_cacheWriter, false, this.Context);
_cacheWriter = null;
if (this.DownStream != null && !this.DownStream.IsDetached)
{
this.DownStream.Dispose();
this.DownStream = null;
}
}
this._isDisposed = true;
}
}
}

View File

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

View File

@@ -0,0 +1,75 @@
using System;
namespace Best.HTTP.Response
{
// https://developer.mozilla.org/en-US/docs/Web/HTTP/Status/xxx
// where xxx is the numeric value of the status code (100...)
/// <summary>
/// Provides constants representing various HTTP status codes.
/// </summary>
public static class HTTPStatusCodes
{
public const int Continue = 100;
public const int SwitchingProtocols = 101;
public const int Processing = 102;
public const int EarlyHints_Experimental = 103;
public const int OK = 200;
public const int Created = 201;
public const int Accepted = 202;
public const int NonAuthoritativeInformation = 203;
public const int NoContent = 204;
public const int ResetContent = 205;
public const int PartialContent = 206;
public const int MultiStatus = 207;
public const int AlreadyReported = 208;
public const int IMUsed = 226;
public const int MultipleChoices = 300;
public const int MovedPermanently = 301;
public const int Found = 302;
public const int SeeOther = 303;
public const int NotModified = 304;
public const int TemporaryRedirect = 307;
public const int PermanentRedirect = 308;
public const int BadRequest = 400;
public const int Unauthorized = 401;
public const int PaymentRequired = 402;
public const int Forbidden = 403;
public const int NotFound = 404;
public const int MethodNotAllowed = 405;
public const int NotAcceptable = 406;
public const int ProxyAuthenticationRequired = 407;
public const int RequestTimeout = 408;
public const int Conflict = 409;
public const int Gone = 410;
public const int LengthRequired = 411;
public const int PreconditionFailed = 412;
public const int ContentTooLarge = 413;
public const int URITooLong = 414;
public const int UnsupportedMediaType = 415;
public const int RangeNotSatisfiable = 416;
public const int ExpectationFailed = 417;
public const int ImATeapot = 418;
public const int MisdirectedRequest = 421;
public const int UnprocessableContent = 422;
public const int Locked = 423;
public const int FailedDependency = 424;
public const int TooEarly = 425;
public const int UpgradeRequired = 426;
public const int PreconditionRequired = 428;
public const int TooManyRequests = 429;
public const int RequestHeaderFieldsTooLarge = 431;
public const int UnavailableForLegalReasons = 451;
public const int InternalServerError = 500;
public const int NotImplemented = 501;
public const int BadGateway = 502;
public const int ServiceUnavailable = 503;
public const int GatewayTimeout = 504;
public const int HTTPVersionNotSupported = 505;
public const int VariantAlsoNegotiates = 506;
public const int InsufficientStorage = 507;
public const int LoopDetected = 508;
public const int NotExtended = 510;
public const int NetworkAuthenticationRequired = 511;
}
}

View File

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