ProgressiveFileCopier.cs 7.1 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188
  1. using System;
  2. using System.Buffers;
  3. using System.IO;
  4. using System.Runtime.InteropServices;
  5. using System.Threading;
  6. using System.Threading.Tasks;
  7. using Jellyfin.Api.Models.PlaybackDtos;
  8. using MediaBrowser.Common.Extensions;
  9. using MediaBrowser.Controller.Library;
  10. using MediaBrowser.Model.IO;
  11. namespace Jellyfin.Api.Helpers
  12. {
  13. /// <summary>
  14. /// Progressive file copier.
  15. /// </summary>
  16. public class ProgressiveFileCopier
  17. {
  18. private readonly TranscodingJobDto? _job;
  19. private readonly string? _path;
  20. private readonly CancellationToken _cancellationToken;
  21. private readonly IDirectStreamProvider? _directStreamProvider;
  22. private readonly TranscodingJobHelper _transcodingJobHelper;
  23. private long _bytesWritten;
  24. /// <summary>
  25. /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
  26. /// </summary>
  27. /// <param name="path">The path to copy from.</param>
  28. /// <param name="job">The transcoding job.</param>
  29. /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
  30. /// <param name="cancellationToken">The cancellation token.</param>
  31. public ProgressiveFileCopier(string path, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
  32. {
  33. _path = path;
  34. _job = job;
  35. _cancellationToken = cancellationToken;
  36. _transcodingJobHelper = transcodingJobHelper;
  37. }
  38. /// <summary>
  39. /// Initializes a new instance of the <see cref="ProgressiveFileCopier"/> class.
  40. /// </summary>
  41. /// <param name="directStreamProvider">Instance of the <see cref="IDirectStreamProvider"/> interface.</param>
  42. /// <param name="job">The transcoding job.</param>
  43. /// <param name="transcodingJobHelper">Instance of the <see cref="TranscodingJobHelper"/>.</param>
  44. /// <param name="cancellationToken">The cancellation token.</param>
  45. public ProgressiveFileCopier(IDirectStreamProvider directStreamProvider, TranscodingJobDto? job, TranscodingJobHelper transcodingJobHelper, CancellationToken cancellationToken)
  46. {
  47. _directStreamProvider = directStreamProvider;
  48. _job = job;
  49. _cancellationToken = cancellationToken;
  50. _transcodingJobHelper = transcodingJobHelper;
  51. }
  52. /// <summary>
  53. /// Gets or sets a value indicating whether allow read end of file.
  54. /// </summary>
  55. public bool AllowEndOfFile { get; set; } = true;
  56. /// <summary>
  57. /// Gets or sets copy start position.
  58. /// </summary>
  59. public long StartPosition { get; set; }
  60. /// <summary>
  61. /// Write source stream to output.
  62. /// </summary>
  63. /// <param name="outputStream">Output stream.</param>
  64. /// <param name="cancellationToken">Cancellation token.</param>
  65. /// <returns>A <see cref="Task"/>.</returns>
  66. public async Task WriteToAsync(Stream outputStream, CancellationToken cancellationToken)
  67. {
  68. cancellationToken = CancellationTokenSource.CreateLinkedTokenSource(cancellationToken, _cancellationToken).Token;
  69. try
  70. {
  71. if (_directStreamProvider != null)
  72. {
  73. await _directStreamProvider.CopyToAsync(outputStream, cancellationToken).ConfigureAwait(false);
  74. return;
  75. }
  76. var fileOptions = FileOptions.SequentialScan;
  77. var allowAsyncFileRead = false;
  78. // use non-async filestream along with read due to https://github.com/dotnet/corefx/issues/6039
  79. if (!RuntimeInformation.IsOSPlatform(OSPlatform.Windows))
  80. {
  81. fileOptions |= FileOptions.Asynchronous;
  82. allowAsyncFileRead = true;
  83. }
  84. if (_path == null)
  85. {
  86. throw new ResourceNotFoundException(nameof(_path));
  87. }
  88. await using var inputStream = new FileStream(_path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite, IODefaults.FileStreamBufferSize, fileOptions);
  89. var eofCount = 0;
  90. const int EmptyReadLimit = 20;
  91. if (StartPosition > 0)
  92. {
  93. inputStream.Position = StartPosition;
  94. }
  95. while (eofCount < EmptyReadLimit || !AllowEndOfFile)
  96. {
  97. var bytesRead = await CopyToInternalAsync(inputStream, outputStream, allowAsyncFileRead, cancellationToken).ConfigureAwait(false);
  98. if (bytesRead == 0)
  99. {
  100. if (_job == null || _job.HasExited)
  101. {
  102. eofCount++;
  103. }
  104. await Task.Delay(100, cancellationToken).ConfigureAwait(false);
  105. }
  106. else
  107. {
  108. eofCount = 0;
  109. }
  110. }
  111. }
  112. finally
  113. {
  114. if (_job != null)
  115. {
  116. _transcodingJobHelper.OnTranscodeEndRequest(_job);
  117. }
  118. }
  119. }
  120. private async Task<int> CopyToInternalAsync(Stream source, Stream destination, bool readAsync, CancellationToken cancellationToken)
  121. {
  122. var array = ArrayPool<byte>.Shared.Rent(IODefaults.CopyToBufferSize);
  123. try
  124. {
  125. int bytesRead;
  126. int totalBytesRead = 0;
  127. if (readAsync)
  128. {
  129. bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
  130. }
  131. else
  132. {
  133. bytesRead = source.Read(array, 0, array.Length);
  134. }
  135. while (bytesRead != 0)
  136. {
  137. var bytesToWrite = bytesRead;
  138. if (bytesToWrite > 0)
  139. {
  140. await destination.WriteAsync(array, 0, Convert.ToInt32(bytesToWrite), cancellationToken).ConfigureAwait(false);
  141. _bytesWritten += bytesRead;
  142. totalBytesRead += bytesRead;
  143. if (_job != null)
  144. {
  145. _job.BytesDownloaded = Math.Max(_job.BytesDownloaded ?? _bytesWritten, _bytesWritten);
  146. }
  147. }
  148. if (readAsync)
  149. {
  150. bytesRead = await source.ReadAsync(array, 0, array.Length, cancellationToken).ConfigureAwait(false);
  151. }
  152. else
  153. {
  154. bytesRead = source.Read(array, 0, array.Length);
  155. }
  156. }
  157. return totalBytesRead;
  158. }
  159. finally
  160. {
  161. ArrayPool<byte>.Shared.Return(array);
  162. }
  163. }
  164. }
  165. }