ResumeFileResult.cs 11 KB

123456789101112131415161718192021222324252627282930313233343536373839404142434445464748495051525354555657585960616263646566676869707172737475767778798081828384858687888990919293949596979899100101102103104105106107108109110111112113114115116117118119120121122123124125126127128129130131132133134135136137138139140141142143144145146147148149150151152153154155156157158159160161162163164165166167168169170171172173174175176177178179180181182183184185186187188189190191192193194195196197198199200201202203204205206207208209210211212213214215216217218219220221222223224225226227228229230231232233234235236237238239240241242243244245246247248249250251252253254255256257258259260261262263264265266267268269270271272273274275276277278279280281282283284285286287288289290291292293294295296297298299300301302303
  1. using Masuit.Tools.Mvc.Internal;
  2. using System;
  3. using System.Globalization;
  4. using System.IO;
  5. using System.Linq;
  6. using System.Net;
  7. using System.Text.RegularExpressions;
  8. using System.Web;
  9. using System.Web.Mvc;
  10. namespace Masuit.Tools.Mvc
  11. {
  12. /// <summary>
  13. /// 扩展自带的FilePathResult来支持断点续传
  14. /// </summary>
  15. public class ResumeFileResult : FilePathResult
  16. {
  17. /// <summary>
  18. /// 由于附加依赖性,所以没使用logger.Log4net。
  19. /// </summary>
  20. public static Action<Exception> LogException;
  21. private readonly Regex _rangePattern = new Regex("bytes=(\\d*)-(\\d*)");
  22. private readonly string _ifNoneMatch;
  23. private readonly string _ifModifiedSince;
  24. private readonly string _ifMatch;
  25. private readonly string _ifUnmodifiedSince;
  26. private readonly string _ifRange;
  27. private readonly string _etag;
  28. private readonly Range _range;
  29. private readonly FileInfo _file;
  30. private readonly string _lastModified;
  31. private readonly bool _rangeRequest;
  32. private readonly string _downloadFileName;
  33. public ResumeFileResult(string fileName, string contentType, HttpRequestBase request) : this(fileName, contentType, request, null)
  34. {
  35. }
  36. public ResumeFileResult(string fileName, string contentType, HttpRequestBase request, string downloadFileName) : this(fileName, contentType, request.Headers[HttpHeaders.IfNoneMatch], request.Headers[HttpHeaders.IfModifiedSince], request.Headers[HttpHeaders.IfMatch], request.Headers[HttpHeaders.IfUnmodifiedSince], request.Headers[HttpHeaders.IfRange], request.Headers[HttpHeaders.Range], downloadFileName)
  37. {
  38. }
  39. public ResumeFileResult(string fileName, string contentType, string ifNoneMatch, string ifModifiedSince, string ifMatch, string ifUnmodifiedSince, string ifRange, string range, string downloadFileName) : base(fileName, contentType)
  40. {
  41. _file = new FileInfo(fileName);
  42. _lastModified = Util.FormatDate(_file.LastWriteTime);
  43. _rangeRequest = range != null;
  44. _range = Range(range);
  45. _etag = Etag();
  46. _ifNoneMatch = ifNoneMatch;
  47. _ifModifiedSince = ifModifiedSince;
  48. _ifMatch = ifMatch;
  49. _ifUnmodifiedSince = ifUnmodifiedSince;
  50. _ifRange = ifRange;
  51. _downloadFileName = downloadFileName;
  52. }
  53. /// <summary>
  54. /// 检查请求中的标头,为响应添加适当的标头
  55. /// </summary>
  56. /// <param name="response"></param>
  57. protected override void WriteFile(HttpResponseBase response)
  58. {
  59. response.AppendHeader(HttpHeaders.Etag, _etag);
  60. response.AppendHeader(HttpHeaders.LastModified, _lastModified);
  61. response.AppendHeader(HttpHeaders.Expires, Util.FormatDate(DateTime.Now));
  62. if (IsNotModified())
  63. {
  64. response.StatusCode = (int)HttpStatusCode.NotModified;
  65. }
  66. else if (IsPreconditionFailed())
  67. {
  68. response.StatusCode = (int)HttpStatusCode.PreconditionFailed;
  69. }
  70. else if (IsRangeNotSatisfiable())
  71. {
  72. response.AppendHeader(HttpHeaders.ContentRange, "bytes */" + _file.Length);
  73. response.StatusCode = (int)HttpStatusCode.RequestedRangeNotSatisfiable;
  74. }
  75. else
  76. {
  77. TransmitFile(response);
  78. }
  79. }
  80. /// <summary>
  81. /// 计算要写入Response的总字节长度
  82. /// </summary>
  83. /// <returns></returns>
  84. protected long ContentLength()
  85. {
  86. return _range.End - _range.Start + 1;
  87. }
  88. /// <summary>
  89. /// 分析If-Range标头并返回:
  90. /// true - 如果必须发送部分内容
  91. /// false - 如果必须发送整个文件
  92. /// spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.27
  93. /// </summary>
  94. /// <returns></returns>
  95. protected bool SendRange()
  96. {
  97. return _rangeRequest && _ifRange == null || _rangeRequest && _ifRange == _etag;
  98. }
  99. /// <summary>
  100. /// 将文件写入响应流,根据请求标头和文件属性添加正确的标头
  101. /// </summary>
  102. /// <param name="response"></param>
  103. protected virtual void TransmitFile(HttpResponseBase response)
  104. {
  105. var contentLength = ContentLength();
  106. response.StatusCode = SendRange() ? (int)HttpStatusCode.PartialContent : (int)HttpStatusCode.OK;
  107. response.AppendHeader(HttpHeaders.ContentLength, contentLength.ToString(CultureInfo.InvariantCulture));
  108. response.AppendHeader(HttpHeaders.AcceptRanges, "bytes");
  109. response.AppendHeader(HttpHeaders.ContentRange, $"bytes {_range.Start}-{_range.End}/{_file.Length}");
  110. if (!string.IsNullOrWhiteSpace(_downloadFileName))
  111. {
  112. response.AddHeader("Content-Disposition", $"attachment;filename=\"{_downloadFileName}\"");
  113. }
  114. try
  115. {
  116. response.TransmitFile(FileName, _range.Start, contentLength);
  117. }
  118. catch (Exception ex)
  119. {
  120. LogException?.Invoke(ex);
  121. }
  122. }
  123. /// <summary>
  124. /// 在以下情况下,范围不可满足:
  125. /// 起点大于文件的总大小
  126. /// 起点小于0
  127. /// 端点等于或大于文件的大小
  128. /// 起点大于终点
  129. /// spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec10.html#sec10.4.17
  130. /// </summary>
  131. /// <returns></returns>
  132. protected bool IsRangeNotSatisfiable()
  133. {
  134. return _range.Start >= _file.Length || _range.Start < 0 || _range.End >= _file.Length || _range.Start > _range.End;
  135. }
  136. /// <summary>
  137. /// 在以下情况下,前提可能会失败
  138. /// 如果匹配(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.24)
  139. /// 标题为空,与etag不匹配
  140. /// 如果未经修改则(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.28)
  141. /// header不为空,与File.LastWriteTime不匹配。
  142. /// 在下载过程中更改文件时可能会发生这种情况。
  143. /// </summary>
  144. /// <returns></returns>
  145. protected bool IsPreconditionFailed()
  146. {
  147. if (_ifMatch != null)
  148. {
  149. return !IsMatch(_ifMatch, _etag);
  150. }
  151. return _ifUnmodifiedSince != null && _ifUnmodifiedSince != _lastModified;
  152. }
  153. /// <summary>
  154. /// 如果有的话,该方法返回true
  155. /// 如果 - 无匹配(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.26)或
  156. /// 或者如果未经修改则(http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.25)
  157. /// 已验证
  158. /// </summary>
  159. /// <returns></returns>
  160. protected bool IsNotModified()
  161. {
  162. if (_ifNoneMatch != null)
  163. {
  164. return IsMatch(_ifNoneMatch, _etag);
  165. }
  166. return _ifModifiedSince != null && _ifModifiedSince == _lastModified;
  167. }
  168. /// <summary>
  169. /// 当前文件的Etag响应头
  170. /// </summary>
  171. /// <returns></returns>
  172. private string Etag()
  173. {
  174. return Util.Etag(_file);
  175. }
  176. private bool IsMatch(string values, string etag)
  177. {
  178. var matches = (values ?? string.Empty).Split(new[]
  179. {
  180. ","
  181. }, StringSplitOptions.RemoveEmptyEntries);
  182. return matches.Any(s => s.Equals("*") || s.Equals(etag));
  183. }
  184. /// <summary>
  185. /// 根据Range标头计算起点和终点
  186. /// Spec: http://www.w3.org/Protocols/rfc2616/rfc2616-sec14.html#sec14.35.1
  187. /// </summary>
  188. /// <param name="range"></param>
  189. /// <returns></returns>
  190. private Range Range(string range)
  191. {
  192. var lastByte = _file.Length - 1;
  193. if (!string.IsNullOrWhiteSpace(range))
  194. {
  195. var matches = _rangePattern.Matches(range);
  196. if (matches.Count != 0)
  197. {
  198. var start = matches[0].Groups[1].Value.ToLong(-1);
  199. var end = matches[0].Groups[2].Value.ToLong(-1);
  200. if (start != -1 || end != -1)
  201. {
  202. if (start == -1)
  203. {
  204. start = _file.Length - end;
  205. end = lastByte;
  206. }
  207. else if (end == -1)
  208. {
  209. end = lastByte;
  210. }
  211. return new Range
  212. {
  213. Start = start,
  214. End = end
  215. };
  216. }
  217. }
  218. return new Range
  219. {
  220. Start = -1,
  221. End = -1
  222. };
  223. }
  224. return new Range
  225. {
  226. Start = 0,
  227. End = lastByte
  228. };
  229. }
  230. /// <summary>
  231. /// 用于支持ResumeFileResult功能的帮助类
  232. /// </summary>
  233. public static class Util
  234. {
  235. /// <summary>
  236. /// Etag响应头
  237. /// </summary>
  238. /// <returns></returns>
  239. public static string Etag(FileInfo file)
  240. {
  241. return Etag(file.FullName, FormatDate(file.LastWriteTime));
  242. }
  243. /// <summary>
  244. /// <see cref="Etag(System.IO.FileInfo)"/>
  245. /// </summary>
  246. /// <param name="fullName"></param>
  247. /// <param name="lastModified"></param>
  248. /// <returns></returns>
  249. public static string Etag(string fullName, string lastModified)
  250. {
  251. return "\"mvc-streaming-" + fullName.GetHashCode() + "-" + fullName.GetHashCode() + "\"";
  252. }
  253. /// <summary>
  254. /// <see cref="Etag(System.IO.FileInfo)"/>
  255. /// </summary>
  256. /// <param name="fullName"></param>
  257. /// <param name="lastWriteTime"></param>
  258. /// <returns></returns>
  259. public static string Etag(string fullName, DateTime lastWriteTime)
  260. {
  261. return Etag(fullName, FormatDate(lastWriteTime));
  262. }
  263. /// <summary>
  264. /// 格式是绝对日期和时间。它必须是RFC 1123日期格式。
  265. /// </summary>
  266. /// <param name="date"></param>
  267. /// <returns></returns>
  268. public static string FormatDate(DateTime date)
  269. {
  270. return date.ToString("R");
  271. }
  272. }
  273. }
  274. }