using System; using System.Collections.Generic; using System.Globalization; using System.IO; using System.Linq; using System.Net; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using MediaBrowser.Model.IO; using MediaBrowser.Model.Services; using Microsoft.Extensions.Logging; using Microsoft.AspNetCore.Http; using Microsoft.Net.Http.Headers; namespace Emby.Server.Implementations.HttpServer { public class FileWriter : IHttpResult { private static readonly CultureInfo UsCulture = CultureInfo.ReadOnly(new CultureInfo("en-US")); private static readonly string[] _skipLogExtensions = { ".js", ".html", ".css" }; private readonly IStreamHelper _streamHelper; private readonly ILogger _logger; private readonly IFileSystem _fileSystem; /// /// The _options /// private readonly IDictionary _options = new Dictionary(); /// /// The _requested ranges /// private List> _requestedRanges; public FileWriter(string path, string contentType, string rangeHeader, ILogger logger, IFileSystem fileSystem, IStreamHelper streamHelper) { if (string.IsNullOrEmpty(contentType)) { throw new ArgumentNullException(nameof(contentType)); } _streamHelper = streamHelper; _fileSystem = fileSystem; Path = path; _logger = logger; RangeHeader = rangeHeader; Headers[HeaderNames.ContentType] = contentType; TotalContentLength = fileSystem.GetFileInfo(path).Length; Headers[HeaderNames.AcceptRanges] = "bytes"; if (string.IsNullOrWhiteSpace(rangeHeader)) { Headers[HeaderNames.ContentLength] = TotalContentLength.ToString(CultureInfo.InvariantCulture); StatusCode = HttpStatusCode.OK; } else { StatusCode = HttpStatusCode.PartialContent; SetRangeValues(); } FileShare = FileShareMode.Read; Cookies = new List(); } private string RangeHeader { get; set; } private bool IsHeadRequest { get; set; } private long RangeStart { get; set; } private long RangeEnd { get; set; } private long RangeLength { get; set; } public long TotalContentLength { get; set; } public Action OnComplete { get; set; } public Action OnError { get; set; } public List Cookies { get; private set; } public FileShareMode FileShare { get; set; } /// /// Gets the options. /// /// The options. public IDictionary Headers => _options; public string Path { get; set; } /// /// Gets the requested ranges. /// /// The requested ranges. protected List> RequestedRanges { get { if (_requestedRanges == null) { _requestedRanges = new List>(); // Example: bytes=0-,32-63 var ranges = RangeHeader.Split('=')[1].Split(','); foreach (var range in ranges) { var vals = range.Split('-'); long start = 0; long? end = null; if (!string.IsNullOrEmpty(vals[0])) { start = long.Parse(vals[0], UsCulture); } if (!string.IsNullOrEmpty(vals[1])) { end = long.Parse(vals[1], UsCulture); } _requestedRanges.Add(new KeyValuePair(start, end)); } } return _requestedRanges; } } public string ContentType { get; set; } public IRequest RequestContext { get; set; } public object Response { get; set; } public int Status { get; set; } public HttpStatusCode StatusCode { get => (HttpStatusCode)Status; set => Status = (int)value; } /// /// Sets the range values. /// private void SetRangeValues() { var requestedRange = RequestedRanges[0]; // If the requested range is "0-", we can optimize by just doing a stream copy if (!requestedRange.Value.HasValue) { RangeEnd = TotalContentLength - 1; } else { RangeEnd = requestedRange.Value.Value; } RangeStart = requestedRange.Key; RangeLength = 1 + RangeEnd - RangeStart; // Content-Length is the length of what we're serving, not the original content var lengthString = RangeLength.ToString(CultureInfo.InvariantCulture); Headers[HeaderNames.ContentLength] = lengthString; var rangeString = $"bytes {RangeStart}-{RangeEnd}/{TotalContentLength}"; Headers[HeaderNames.ContentRange] = rangeString; _logger.LogInformation("Setting range response values for {0}. RangeRequest: {1} Content-Length: {2}, Content-Range: {3}", Path, RangeHeader, lengthString, rangeString); } public async Task WriteToAsync(HttpResponse response, CancellationToken cancellationToken) { try { // Headers only if (IsHeadRequest) { return; } var path = Path; var offset = RangeStart; var count = RangeLength; if (string.IsNullOrWhiteSpace(RangeHeader) || RangeStart <= 0 && RangeEnd >= TotalContentLength - 1) { var extension = System.IO.Path.GetExtension(path); if (extension == null || !_skipLogExtensions.Contains(extension, StringComparer.OrdinalIgnoreCase)) { _logger.LogDebug("Transmit file {0}", path); } offset = 0; count = 0; } await TransmitFile(response.Body, path, offset, count, FileShare, cancellationToken).ConfigureAwait(false); } finally { OnComplete?.Invoke(); } } public async Task TransmitFile(Stream stream, string path, long offset, long count, FileShareMode fileShareMode, CancellationToken cancellationToken) { var fileOpenOptions = FileOpenOptions.SequentialScan; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { fileOpenOptions |= FileOpenOptions.Asynchronous; } using (var fs = _fileSystem.GetFileStream(path, FileOpenMode.Open, FileAccessMode.Read, fileShareMode, fileOpenOptions)) { if (offset > 0) { fs.Position = offset; } if (count > 0) { await _streamHelper.CopyToAsync(fs, stream, count, cancellationToken).ConfigureAwait(false); } else { await fs.CopyToAsync(stream, StreamDefaults.DefaultCopyToBufferSize, cancellationToken).ConfigureAwait(false); } } } } }