using System; using System.Buffers; using System.IO; using System.Runtime.InteropServices; using System.Threading; using System.Threading.Tasks; using Jellyfin.Api.Models.PlaybackDtos; using MediaBrowser.Common.Extensions; using MediaBrowser.Controller.Library; using MediaBrowser.Model.IO; namespace Jellyfin.Api.Helpers { /// /// Progressive file copier. /// public class ProgressiveFileCopier { private readonly TranscodingJobDto? _job; private readonly string? _path; private readonly CancellationToken _cancellationToken; private readonly IDirectStreamProvider? _directStreamProvider; private readonly TranscodingJobHelper _transcodingJobHelper; private long _bytesWritten; /// /// Initializes a new instance of the class. /// /// The path to copy from. /// The transcoding job. /// Instance of the . /// The cancellation token. public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken) { _path = path; _job = job; _cancellationToken = cancellationToken; _transcodingJobHelper = transcodingJobHelper; } /// /// Initializes a new instance of the class. /// /// Instance of the interface. /// The transcoding job. /// Instance of the . /// The cancellation token. public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken) { _directStreamProvider = directStreamProvider; _job = job; _cancellationToken = cancellationToken; _transcodingJobHelper = transcodingJobHelper; } /// /// Gets or sets a value indicating whether allow read end of file. /// public bool AllowEndOfFile { get; set; } = true; /// /// Gets or sets copy start position. /// public long StartPosition { get; set; } /// /// Write source stream to output. /// /// Output stream. /// Cancellation token. /// A . public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken) { using var linkedCancellationTokenSource = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken); cancellationToken = linkedCancellationTokenSource.Token; try { if (_directStreamProvider != null) { await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false); return; } var fileOptions = FileOptions.SequentialScan; var allowAsyncFileRead = false; // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039 if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows)) { fileOptions |= FileOptions.Asynchronous; allowAsyncFileRead = true; } if (_path == null) { throw new ResourceNotFoundException(nameof(_path)); } await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions); var eofCount = 0; const int EmptyReadLimit = 20; if (StartPosition > 0) { inputStream.Position = StartPosition; } while (eofCount < EmptyReadLimit || !AllowEndOfFile) { var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false); if (bytesRead == 0) { if (_job == null || _job.HasExited) { eofCount++; } await Task.Delay(100, cancellationToken).ConfigureAwait(false); } else { eofCount = 0; } } } finally { if (_job != null) { _transcodingJobHelper.OnTranscodeEndRequest(_job); } } } private async Task CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken) { var array = ArrayPool.Shared.Rent(IODefaults.CopyToBufferSize); try { int bytesRead; int totalBytesRead = 0; if (readAsync) { bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); } else { bytesRead = source.Read(array, 0, array.Length); } while (bytesRead != 0) { var bytesToWrite = bytesRead; if (bytesToWrite > 0) { await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false); _bytesWritten += bytesRead; totalBytesRead += bytesRead; if (_job != null) { _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten); } } if (readAsync) { bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false); } else { bytesRead = source.Read(array, 0, array.Length); } } return totalBytesRead; } finally { ArrayPool.Shared.Return(array); } } } }