// Copyright (c) .NET Foundation. All rights reserved. // Licensed under the Apache License, Version 2.0. See License.txt in the project root for license information. using System.Diagnostics.Contracts; using System.IO; using System.Linq; using System.Net.Http.Headers; using System.Net.Http.Internal; using System.Threading.Tasks; using System.Web.Http; namespace System.Net.Http { /// /// implementation which provides a byte range view over a stream used to generate HTTP /// 206 (Partial Content) byte range responses. The supports one or more /// byte ranges regardless of whether the ranges are consecutive or not. If there is only one range then a /// single partial response body containing a Content-Range header is generated. If there are more than one /// ranges then a multipart/byteranges response is generated where each body part contains a range indicated /// by the associated Content-Range header field. /// public class ByteRangeStreamContent : HttpContent { private const string SupportedRangeUnit = "bytes"; private const string ByteRangesContentSubtype = "byteranges"; private const int DefaultBufferSize = 4096; private const int MinBufferSize = 1; private readonly Stream _content; private readonly long _start; private readonly HttpContent _byteRangeContent; private bool _disposed; /// /// implementation which provides a byte range view over a stream used to generate HTTP /// 206 (Partial Content) byte range responses. If none of the requested ranges overlap with the current extend /// of the selected resource represented by the parameter then an /// is thrown indicating the valid Content-Range of the content. /// /// The stream over which to generate a byte range view. /// The range or ranges, typically obtained from the Range HTTP request header field. /// The media type of the content stream. public ByteRangeStreamContent(Stream content, RangeHeaderValue range, string mediaType) : this(content, range, new MediaTypeHeaderValue(mediaType), DefaultBufferSize) { } /// /// implementation which provides a byte range view over a stream used to generate HTTP /// 206 (Partial Content) byte range responses. If none of the requested ranges overlap with the current extend /// of the selected resource represented by the parameter then an /// is thrown indicating the valid Content-Range of the content. /// /// The stream over which to generate a byte range view. /// The range or ranges, typically obtained from the Range HTTP request header field. /// The media type of the content stream. /// The buffer size used when copying the content stream. public ByteRangeStreamContent(Stream content, RangeHeaderValue range, string mediaType, int bufferSize) : this(content, range, new MediaTypeHeaderValue(mediaType), bufferSize) { } /// /// implementation which provides a byte range view over a stream used to generate HTTP /// 206 (Partial Content) byte range responses. If none of the requested ranges overlap with the current extend /// of the selected resource represented by the parameter then an /// is thrown indicating the valid Content-Range of the content. /// /// The stream over which to generate a byte range view. /// The range or ranges, typically obtained from the Range HTTP request header field. /// The media type of the content stream. public ByteRangeStreamContent(Stream content, RangeHeaderValue range, MediaTypeHeaderValue mediaType) : this(content, range, mediaType, DefaultBufferSize) { } /// /// implementation which provides a byte range view over a stream used to generate HTTP /// 206 (Partial Content) byte range responses. If none of the requested ranges overlap with the current extend /// of the selected resource represented by the parameter then an /// is thrown indicating the valid Content-Range of the content. /// /// The stream over which to generate a byte range view. /// The range or ranges, typically obtained from the Range HTTP request header field. /// The media type of the content stream. /// The buffer size used when copying the content stream. public ByteRangeStreamContent(Stream content, RangeHeaderValue range, MediaTypeHeaderValue mediaType, int bufferSize) { if (content == null) { throw Error.ArgumentNull("content"); } if (!content.CanSeek) { throw Error.Argument("content", Properties.Resources.ByteRangeStreamNotSeekable, typeof(ByteRangeStreamContent).Name); } if (range == null) { throw Error.ArgumentNull("range"); } if (mediaType == null) { throw Error.ArgumentNull("mediaType"); } if (bufferSize < MinBufferSize) { throw Error.ArgumentMustBeGreaterThanOrEqualTo("bufferSize", bufferSize, MinBufferSize); } if (!range.Unit.Equals(SupportedRangeUnit, StringComparison.OrdinalIgnoreCase)) { throw Error.Argument("range", Properties.Resources.ByteRangeStreamContentNotBytesRange, range.Unit, SupportedRangeUnit); } try { // If we have more than one range then we use a multipart/byteranges content type as wrapper. // Otherwise we use a non-multipart response. if (range.Ranges.Count > 1) { // Create Multipart content and copy headers to this content MultipartContent rangeContent = new MultipartContent(ByteRangesContentSubtype); _byteRangeContent = rangeContent; foreach (RangeItemHeaderValue rangeValue in range.Ranges) { try { ByteRangeStream rangeStream = new ByteRangeStream(content, rangeValue); HttpContent rangeBodyPart = new StreamContent(rangeStream, bufferSize); rangeBodyPart.Headers.ContentType = mediaType; rangeBodyPart.Headers.ContentRange = rangeStream.ContentRange; rangeContent.Add(rangeBodyPart); } catch (ArgumentOutOfRangeException) { // We ignore range errors until we check that we have at least one valid range } } // If no overlapping ranges were found then stop if (!rangeContent.Any()) { ContentRangeHeaderValue actualContentRange = new ContentRangeHeaderValue(content.Length); string msg = Error.Format(Properties.Resources.ByteRangeStreamNoneOverlap, range.ToString()); throw new InvalidByteRangeException(actualContentRange, msg); } } else if (range.Ranges.Count == 1) { try { ByteRangeStream rangeStream = new ByteRangeStream(content, range.Ranges.First()); _byteRangeContent = new StreamContent(rangeStream, bufferSize); _byteRangeContent.Headers.ContentType = mediaType; _byteRangeContent.Headers.ContentRange = rangeStream.ContentRange; } catch (ArgumentOutOfRangeException) { ContentRangeHeaderValue actualContentRange = new ContentRangeHeaderValue(content.Length); string msg = Error.Format(Properties.Resources.ByteRangeStreamNoOverlap, range.ToString()); throw new InvalidByteRangeException(actualContentRange, msg); } } else { throw Error.Argument("range", Properties.Resources.ByteRangeStreamContentNoRanges); } // Copy headers from byte range content so that we get the right content type etc. _byteRangeContent.Headers.CopyTo(Headers); _content = content; _start = content.Position; } catch { if (_byteRangeContent != null) { _byteRangeContent.Dispose(); } throw; } } protected override Task SerializeToStreamAsync(Stream stream, TransportContext context) { // Reset stream to start position _content.Position = _start; // Copy result to output return _byteRangeContent.CopyToAsync(stream); } protected override bool TryComputeLength(out long length) { long? contentLength = _byteRangeContent.Headers.ContentLength; if (contentLength.HasValue) { length = contentLength.Value; return true; } length = -1; return false; } protected override void Dispose(bool disposing) { Contract.Assert(_byteRangeContent != null); if (disposing) { if (!_disposed) { _byteRangeContent.Dispose(); _content.Dispose(); _disposed = true; } } base.Dispose(disposing); } } }