using MediaBrowser.Common.Extensions; using MediaBrowser.Common.IO; using MediaBrowser.Common.Kernel; using System; using System.Collections.Generic; using System.IO; using System.Linq; using System.Threading.Tasks; namespace MediaBrowser.Common.Net.Handlers { /// /// Represents an http handler that serves static content /// public class StaticFileHandler : BaseHandler { /// /// Initializes a new instance of the class. /// /// The kernel. public StaticFileHandler(IKernel kernel) { Initialize(kernel); } /// /// The _path /// private string _path; /// /// Gets or sets the path to the static resource /// /// The path. public string Path { get { if (!string.IsNullOrWhiteSpace(_path)) { return _path; } return QueryString["path"]; } set { _path = value; } } /// /// Gets or sets the last date modified of the resource /// /// The last date modified. public DateTime? LastDateModified { get; set; } /// /// Gets or sets the content type of the resource /// /// The type of the content. public string ContentType { get; set; } /// /// Gets or sets the content type of the resource /// /// The etag. public Guid Etag { get; set; } /// /// Gets or sets the source stream of the resource /// /// The source stream. public Stream SourceStream { get; set; } /// /// Shoulds the compress response. /// /// Type of the content. /// true if XXXX, false otherwise private bool ShouldCompressResponse(string contentType) { // It will take some work to support compression with byte range requests if (IsRangeRequest) { return false; } // Don't compress media if (contentType.StartsWith("audio/", StringComparison.OrdinalIgnoreCase) || contentType.StartsWith("video/", StringComparison.OrdinalIgnoreCase)) { return false; } // Don't compress images if (contentType.StartsWith("image/", StringComparison.OrdinalIgnoreCase)) { return false; } return true; } /// /// Gets or sets the duration of the cache. /// /// The duration of the cache. public TimeSpan? CacheDuration { get; set; } /// /// Gets the total length of the content. /// /// The response info. /// System.Nullable{System.Int64}. protected override long? GetTotalContentLength(ResponseInfo responseInfo) { // If we're compressing the response, content length must be the compressed length, which we don't know if (responseInfo.CompressResponse && ClientSupportsCompression) { return null; } return SourceStream.Length; } /// /// Gets the response info. /// /// Task{ResponseInfo}. protected override Task GetResponseInfo() { var info = new ResponseInfo { ContentType = ContentType ?? MimeTypes.GetMimeType(Path), Etag = Etag, DateLastModified = LastDateModified }; if (SourceStream == null && !string.IsNullOrEmpty(Path)) { // FileShare must be ReadWrite in case someone else is currently writing to it. SourceStream = new FileStream(Path, FileMode.Open, FileAccess.Read, FileShare.Read, StreamDefaults.DefaultFileStreamBufferSize, FileOptions.Asynchronous); } info.CompressResponse = ShouldCompressResponse(info.ContentType); info.SupportsByteRangeRequests = !info.CompressResponse || !ClientSupportsCompression; if (!info.DateLastModified.HasValue && !string.IsNullOrWhiteSpace(Path)) { info.DateLastModified = File.GetLastWriteTimeUtc(Path); } if (CacheDuration.HasValue) { info.CacheDuration = CacheDuration.Value; } if (SourceStream == null && string.IsNullOrEmpty(Path)) { throw new ResourceNotFoundException(); } return Task.FromResult(info); } /// /// Writes the response to output stream. /// /// The stream. /// The response info. /// Total length of the content. /// Task. protected override Task WriteResponseToOutputStream(Stream stream, ResponseInfo responseInfo, long? totalContentLength) { if (IsRangeRequest && totalContentLength.HasValue) { var requestedRange = RequestedRanges.First(); // If the requested range is "0-", we can optimize by just doing a stream copy if (!requestedRange.Value.HasValue) { return ServeCompleteRangeRequest(requestedRange, stream, totalContentLength.Value); } // This will have to buffer a portion of the content into memory return ServePartialRangeRequest(requestedRange.Key, requestedRange.Value.Value, stream, totalContentLength.Value); } return SourceStream.CopyToAsync(stream); } /// /// Disposes the response stream. /// protected override void DisposeResponseStream() { if (SourceStream != null) { SourceStream.Dispose(); } base.DisposeResponseStream(); } /// /// Handles a range request of "bytes=0-" /// This will serve the complete content and add the content-range header /// /// The requested range. /// The response stream. /// Total length of the content. /// Task. private Task ServeCompleteRangeRequest(KeyValuePair requestedRange, Stream responseStream, long totalContentLength) { var rangeStart = requestedRange.Key; var rangeEnd = totalContentLength - 1; var rangeLength = 1 + rangeEnd - rangeStart; // Content-Length is the length of what we're serving, not the original content HttpListenerContext.Response.ContentLength64 = rangeLength; HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); if (rangeStart > 0) { SourceStream.Position = rangeStart; } return SourceStream.CopyToAsync(responseStream); } /// /// Serves a partial range request /// /// The range start. /// The range end. /// The response stream. /// Total length of the content. /// Task. private async Task ServePartialRangeRequest(long rangeStart, long rangeEnd, Stream responseStream, long totalContentLength) { var rangeLength = 1 + rangeEnd - rangeStart; // Content-Length is the length of what we're serving, not the original content HttpListenerContext.Response.ContentLength64 = rangeLength; HttpListenerContext.Response.Headers["Content-Range"] = string.Format("bytes {0}-{1}/{2}", rangeStart, rangeEnd, totalContentLength); SourceStream.Position = rangeStart; // Fast track to just copy the stream to the end if (rangeEnd == totalContentLength - 1) { await SourceStream.CopyToAsync(responseStream).ConfigureAwait(false); } else { // Read the bytes we need var buffer = new byte[Convert.ToInt32(rangeLength)]; await SourceStream.ReadAsync(buffer, 0, buffer.Length).ConfigureAwait(false); await responseStream.WriteAsync(buffer, 0, Convert.ToInt32(rangeLength)).ConfigureAwait(false); } } } }