From 07f955e48e91e84ec6f5a768a76cb9ae8b848096 Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=8C=BF=E4=BA=BA=E6=98=93?= Date: Sat, 7 Dec 2024 19:09:01 +0800 Subject: [PATCH] =?UTF-8?q?=E6=B7=BB=E5=8A=A0=E6=96=AD=E7=82=B9=E7=BB=AD?= =?UTF-8?q?=E4=BC=A0=E6=96=87=E4=BB=B6=E7=BB=93=E6=9E=9C=E5=92=8C=E6=89=A7?= =?UTF-8?q?=E8=A1=8C=E5=99=A8=E6=94=AF=E6=8C=81?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit 引入了多个文件以支持断点续传功能: - `ActionContextExtension.cs`:定义了 `SetContentDispositionHeaderInline` 扩展方法。 - `ControllerExtensions.cs`:定义了多个 `ResumeFile` 扩展方法。 - `IResumeFileResult.cs`:定义了 `IResumeFileResult` 接口。 - `ResumeFileContentResult.cs`:实现了支持断点续传的文件内容结果。 - `ResumeFileStreamResult.cs`:实现了支持断点续传的文件流结果。 - `ResumePhysicalFileResult.cs`:实现了支持断点续传的本地物理文件结果。 - `ResumeVirtualFileResult.cs`:实现了支持断点续传的虚拟路径文件结果。 - `ResumeFileContentResultExecutor.cs`:实现了 `ResumeFileContentResult` 的执行器。 - `ResumeFileStreamResultExecutor.cs`:实现了 `ResumeFileStreamResult` 的执行器。 - `ResumePhysicalFileResultExecutor.cs`:实现了 `ResumePhysicalFileResult` 的执行器。 - `ResumeVirtualFileResultExecutor.cs`:实现了 `ResumeVirtualFileResult` 的执行器。 --- .../Extensions/ActionContextExtension.cs | 33 ++++ .../Extensions/ControllerExtensions.cs | 157 ++++++++++++++++++ .../ResumeFileContentResultExecutor.cs | 34 ++++ .../ResumeFileStreamResultExecutor.cs | 35 ++++ .../ResumePhysicalFileResultExecutor.cs | 34 ++++ .../ResumeVirtualFileResultExecutor.cs | 31 ++++ .../ResumeFileResult/IResumeFileResult.cs | 17 ++ .../ResumeFileContentResult.cs | 45 +++++ .../ResumeFileStreamResult.cs | 45 +++++ .../ResumePhysicalFileResult.cs | 45 +++++ .../ResumeVirtualFileResult.cs | 45 +++++ 11 files changed, 521 insertions(+) create mode 100644 Pek.AspNetCore/Extensions/ActionContextExtension.cs create mode 100644 Pek.AspNetCore/Extensions/ControllerExtensions.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileContentResultExecutor.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileStreamResultExecutor.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/Executor/ResumePhysicalFileResultExecutor.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/Executor/ResumeVirtualFileResultExecutor.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/IResumeFileResult.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/ResumeFileContentResult.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/ResumeFileStreamResult.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/ResumePhysicalFileResult.cs create mode 100644 Pek.AspNetCore/ResumeFileResult/ResumeVirtualFileResult.cs diff --git a/Pek.AspNetCore/Extensions/ActionContextExtension.cs b/Pek.AspNetCore/Extensions/ActionContextExtension.cs new file mode 100644 index 0000000..418ac1d --- /dev/null +++ b/Pek.AspNetCore/Extensions/ActionContextExtension.cs @@ -0,0 +1,33 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.Net.Http.Headers; + +using Pek.ResumeFileResult; + +namespace Pek; + +/// +/// ResumeFileHelper +/// +public static class ActionContextExtension +{ + /// + /// 设置响应头ContentDispositionHeader + /// + /// + /// + public static void SetContentDispositionHeaderInline(this ActionContext context, IResumeFileResult result) + { + context.HttpContext.Response.Headers[HeaderNames.AccessControlExposeHeaders] = HeaderNames.ContentDisposition; + if (string.IsNullOrEmpty(result.FileDownloadName)) + { + var contentDisposition = new ContentDispositionHeaderValue("inline"); + + if (!string.IsNullOrWhiteSpace(result.FileInlineName)) + { + contentDisposition.SetHttpFileName(result.FileInlineName); + } + + context.HttpContext.Response.Headers[HeaderNames.ContentDisposition] = contentDisposition.ToString(); + } + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/Extensions/ControllerExtensions.cs b/Pek.AspNetCore/Extensions/ControllerExtensions.cs new file mode 100644 index 0000000..e758547 --- /dev/null +++ b/Pek.AspNetCore/Extensions/ControllerExtensions.cs @@ -0,0 +1,157 @@ +using Microsoft.AspNetCore.Mvc; + +using Pek.Mime; +using Pek.ResumeFileResult; + +namespace Pek; + +/// +/// Controller扩展方法 +/// +public static class ControllerExtensions +{ + private static readonly IMimeMapper _mimeMapper = new MimeMapper(); + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 文件二进制流 + /// Content-Type + /// 下载的文件名 + /// + public static ResumeFileContentResult ResumeFile(this ControllerBase controller, Byte[] fileContents, String contentType, String fileDownloadName) => ResumeFile(controller, fileContents, contentType, fileDownloadName, null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 文件二进制流 + /// 下载的文件名 + /// + public static ResumeFileContentResult ResumeFile(this ControllerBase controller, Byte[] fileContents, String fileDownloadName) => ResumeFile(controller, fileContents, _mimeMapper.GetMimeFromPath(fileDownloadName), fileDownloadName, null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 文件二进制流 + /// Content-Type + /// 下载的文件名 + /// ETag + /// + public static ResumeFileContentResult ResumeFile(this ControllerBase controller, Byte[] fileContents, String? contentType, String fileDownloadName, String? etag) + { + return new ResumeFileContentResult(fileContents, contentType, etag) + { + FileDownloadName = fileDownloadName + }; + } + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 文件二进制流 + /// Content-Type + /// 下载的文件名 + /// + public static ResumeFileStreamResult ResumeFile(this ControllerBase controller, FileStream fileStream, String contentType, String fileDownloadName) => ResumeFile(controller, fileStream, contentType, fileDownloadName, null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 文件二进制流 + /// 下载的文件名 + /// + public static ResumeFileStreamResult ResumeFile(this ControllerBase controller, FileStream fileStream, String fileDownloadName) => ResumeFile(controller, fileStream, _mimeMapper.GetMimeFromPath(fileDownloadName), fileDownloadName, null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 文件二进制流 + /// Content-Type + /// 下载的文件名 + /// ETag + /// + public static ResumeFileStreamResult ResumeFile(this ControllerBase controller, FileStream fileStream, String? contentType, String fileDownloadName, String? etag) + { + return new ResumeFileStreamResult(fileStream, contentType, etag) + { + FileDownloadName = fileDownloadName + }; + } + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 服务端本地文件的虚拟路径 + /// Content-Type + /// 下载的文件名 + /// + public static ResumeVirtualFileResult ResumeFile(this ControllerBase controller, String virtualPath, String contentType, String fileDownloadName) => ResumeFile(controller, virtualPath, contentType, fileDownloadName, null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 服务端本地文件的虚拟路径 + /// 下载的文件名 + /// + public static ResumeVirtualFileResult ResumeFile(this ControllerBase controller, String virtualPath, String fileDownloadName) => ResumeFile(controller, virtualPath, _mimeMapper.GetMimeFromPath(virtualPath), fileDownloadName, null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 服务端本地文件的虚拟路径 + /// Content-Type + /// 下载的文件名 + /// ETag + /// + public static ResumeVirtualFileResult ResumeFile(this ControllerBase controller, String virtualPath, String? contentType, String fileDownloadName, String? etag) + { + return new ResumeVirtualFileResult(virtualPath, contentType, etag) + { + FileDownloadName = fileDownloadName + }; + } + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 服务端本地文件的物理路径 + /// Content-Type + /// 下载的文件名 + /// + public static ResumePhysicalFileResult ResumePhysicalFile(this ControllerBase controller, String physicalPath, String contentType, String fileDownloadName) => ResumePhysicalFile(controller, physicalPath, contentType, fileDownloadName, etag: null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 服务端本地文件的物理路径 + /// 下载的文件名 + /// + public static ResumePhysicalFileResult ResumePhysicalFile(this ControllerBase controller, String physicalPath, String fileDownloadName) => ResumePhysicalFile(controller, physicalPath, _mimeMapper.GetMimeFromPath(physicalPath), fileDownloadName, etag: null); + + /// + /// 可断点续传和多线程下载的FileResult + /// + /// + /// 服务端本地文件的物理路径 + /// Content-Type + /// 下载的文件名 + /// ETag + /// + public static ResumePhysicalFileResult ResumePhysicalFile(this ControllerBase controller, String physicalPath, String? contentType, String fileDownloadName, String? etag) + { + return new ResumePhysicalFileResult(physicalPath, contentType, etag) + { + FileDownloadName = fileDownloadName + }; + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileContentResultExecutor.cs b/Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileContentResultExecutor.cs new file mode 100644 index 0000000..6c65e3a --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileContentResultExecutor.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.AspNetCore.Mvc; + +namespace Pek.ResumeFileResult.Executor; + +/// +/// 断点续传文件FileResult执行器 +/// +internal class ResumeFileContentResultExecutor : FileContentResultExecutor, IActionResultExecutor +{ + /// + /// 构造函数 + /// + /// + public ResumeFileContentResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) + { + } + + /// + /// 执行Result + /// + /// + /// + /// + public virtual Task ExecuteAsync(ActionContext context, ResumeFileContentResult result) + { + ArgumentNullException.ThrowIfNull(context); + + ArgumentNullException.ThrowIfNull(result); + + context.SetContentDispositionHeaderInline(result); + return base.ExecuteAsync(context, result); + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileStreamResultExecutor.cs b/Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileStreamResultExecutor.cs new file mode 100644 index 0000000..2a4660c --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/Executor/ResumeFileStreamResultExecutor.cs @@ -0,0 +1,35 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Pek.ResumeFileResult.Executor; + +/// +/// 可断点续传的FileStreamResult执行器 +/// +internal class ResumeFileStreamResultExecutor : FileStreamResultExecutor, IActionResultExecutor +{ + /// + /// 构造函数 + /// + /// + public ResumeFileStreamResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) + { + } + + /// + /// 执行Result + /// + /// + /// + /// + public virtual Task ExecuteAsync(ActionContext context, ResumeFileStreamResult result) + { + ArgumentNullException.ThrowIfNull(context); + + ArgumentNullException.ThrowIfNull(result); + + context.SetContentDispositionHeaderInline(result); + + return base.ExecuteAsync(context, result); + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/Executor/ResumePhysicalFileResultExecutor.cs b/Pek.AspNetCore/ResumeFileResult/Executor/ResumePhysicalFileResultExecutor.cs new file mode 100644 index 0000000..7750216 --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/Executor/ResumePhysicalFileResultExecutor.cs @@ -0,0 +1,34 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Pek.ResumeFileResult.Executor; + +/// +/// 通过本地文件的可断点续传的FileResult执行器 +/// +internal class ResumePhysicalFileResultExecutor : PhysicalFileResultExecutor, IActionResultExecutor +{ + /// + /// 构造函数 + /// + /// + public ResumePhysicalFileResultExecutor(ILoggerFactory loggerFactory) : base(loggerFactory) + { + } + + /// + /// 执行Result + /// + /// + /// + /// + public virtual Task ExecuteAsync(ActionContext context, ResumePhysicalFileResult result) + { + ArgumentNullException.ThrowIfNull(context); + + ArgumentNullException.ThrowIfNull(result); + + context.SetContentDispositionHeaderInline(result); + return base.ExecuteAsync(context, result); + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/Executor/ResumeVirtualFileResultExecutor.cs b/Pek.AspNetCore/ResumeFileResult/Executor/ResumeVirtualFileResultExecutor.cs new file mode 100644 index 0000000..1245a39 --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/Executor/ResumeVirtualFileResultExecutor.cs @@ -0,0 +1,31 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; + +namespace Pek.ResumeFileResult.Executor; + +/// +/// 使用本地虚拟路径的可断点续传的FileResult +/// +internal class ResumeVirtualFileResultExecutor : VirtualFileResultExecutor, IActionResultExecutor +{ + /// + /// 执行FileResult + /// + /// + /// + /// + public virtual Task ExecuteAsync(ActionContext context, ResumeVirtualFileResult result) + { + ArgumentNullException.ThrowIfNull(context); + + ArgumentNullException.ThrowIfNull(result); + + context.SetContentDispositionHeaderInline(result); + + return base.ExecuteAsync(context, result); + } + + public ResumeVirtualFileResultExecutor(ILoggerFactory loggerFactory, IWebHostEnvironment hostingEnvironment) : base(loggerFactory, hostingEnvironment) + { + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/IResumeFileResult.cs b/Pek.AspNetCore/ResumeFileResult/IResumeFileResult.cs new file mode 100644 index 0000000..8c01c85 --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/IResumeFileResult.cs @@ -0,0 +1,17 @@ +namespace Pek.ResumeFileResult; + +/// +/// 可断点续传的FileResult +/// +public interface IResumeFileResult +{ + /// + /// 文件下载名 + /// + String FileDownloadName { get; set; } + + /// + /// 给响应头的文件名 + /// + String FileInlineName { get; set; } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/ResumeFileContentResult.cs b/Pek.AspNetCore/ResumeFileResult/ResumeFileContentResult.cs new file mode 100644 index 0000000..629e8ee --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/ResumeFileContentResult.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Net.Http.Headers; + +namespace Pek.ResumeFileResult; + +/// +/// 基于Stream的ResumeFileContentResult +/// +public class ResumeFileContentResult : FileContentResult, IResumeFileResult +{ + /// + /// 构造函数 + /// + /// 文件二进制流 + /// Content-Type + /// ETag + public ResumeFileContentResult(Byte[] fileContents, String? contentType, String? etag = null) : this(fileContents, MediaTypeHeaderValue.Parse(contentType), !String.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null) + { + } + + /// + /// 构造函数 + /// + /// 文件二进制流 + /// Content-Type + /// ETag + public ResumeFileContentResult(Byte[] fileContents, MediaTypeHeaderValue contentType, EntityTagHeaderValue? etag = null) : base(fileContents, contentType) + { + EntityTag = etag; + EnableRangeProcessing = true; + } + + /// + public String FileInlineName { get; set; } = String.Empty; + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var executor = context.HttpContext.RequestServices.GetRequiredService>(); + return executor.ExecuteAsync(context, this); + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/ResumeFileStreamResult.cs b/Pek.AspNetCore/ResumeFileResult/ResumeFileStreamResult.cs new file mode 100644 index 0000000..e9aeafb --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/ResumeFileStreamResult.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Net.Http.Headers; + +namespace Pek.ResumeFileResult; + +/// +/// 基于Stream的ResumeFileStreamResult +/// +public class ResumeFileStreamResult : FileStreamResult, IResumeFileResult +{ + /// + /// 构造函数 + /// + /// 文件流 + /// Content-Type + /// ETag + public ResumeFileStreamResult(FileStream fileStream, String? contentType, String? etag = null) : this(fileStream, MediaTypeHeaderValue.Parse(contentType), !String.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null) + { + } + + /// + /// 构造函数 + /// + /// 文件流 + /// Content-Type + /// ETag + public ResumeFileStreamResult(FileStream fileStream, MediaTypeHeaderValue contentType, EntityTagHeaderValue? etag = null) : base(fileStream, contentType) + { + EntityTag = etag; + EnableRangeProcessing = true; + } + + /// + public String FileInlineName { get; set; } = String.Empty; + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var executor = context.HttpContext.RequestServices.GetRequiredService>(); + return executor.ExecuteAsync(context, this); + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/ResumePhysicalFileResult.cs b/Pek.AspNetCore/ResumeFileResult/ResumePhysicalFileResult.cs new file mode 100644 index 0000000..7810051 --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/ResumePhysicalFileResult.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Net.Http.Headers; + +namespace Pek.ResumeFileResult; + +/// +/// 基于本地物理路径的ResumePhysicalFileResult +/// +public class ResumePhysicalFileResult : PhysicalFileResult, IResumeFileResult +{ + /// + /// 基于本地物理路径的ResumePhysicalFileResult + /// + /// 文件全路径 + /// Content-Type + /// ETag + public ResumePhysicalFileResult(String fileName, String? contentType, String? etag = null) : this(fileName, MediaTypeHeaderValue.Parse(contentType), !String.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null) + { + } + + /// + /// 基于本地物理路径的ResumePhysicalFileResult + /// + /// 文件全路径 + /// Content-Type + /// ETag + public ResumePhysicalFileResult(String fileName, MediaTypeHeaderValue contentType, EntityTagHeaderValue? etag = null) : base(fileName, contentType) + { + EntityTag = etag; + EnableRangeProcessing = true; + } + + /// + public String FileInlineName { get; set; } = String.Empty; + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + ArgumentNullException.ThrowIfNull(context); + + var executor = context.HttpContext.RequestServices.GetRequiredService>(); + return executor.ExecuteAsync(context, this); + } +} \ No newline at end of file diff --git a/Pek.AspNetCore/ResumeFileResult/ResumeVirtualFileResult.cs b/Pek.AspNetCore/ResumeFileResult/ResumeVirtualFileResult.cs new file mode 100644 index 0000000..2184003 --- /dev/null +++ b/Pek.AspNetCore/ResumeFileResult/ResumeVirtualFileResult.cs @@ -0,0 +1,45 @@ +using Microsoft.AspNetCore.Mvc; +using Microsoft.AspNetCore.Mvc.Infrastructure; +using Microsoft.Net.Http.Headers; + +namespace Pek.ResumeFileResult; + +/// +/// 基于服务器虚拟路径路径的ResumePhysicalFileResult +/// +public class ResumeVirtualFileResult : VirtualFileResult, IResumeFileResult +{ + /// + /// 基于服务器虚拟路径路径的ResumePhysicalFileResult + /// + /// 文件全路径 + /// Content-Type + /// ETag + public ResumeVirtualFileResult(String fileName, String? contentType, String? etag = null) : this(fileName, MediaTypeHeaderValue.Parse(contentType), !String.IsNullOrEmpty(etag) ? EntityTagHeaderValue.Parse(etag) : null) + { + } + + /// + /// 基于服务器虚拟路径路径的ResumePhysicalFileResult + /// + /// 文件全路径 + /// Content-Type + /// ETag + public ResumeVirtualFileResult(String fileName, MediaTypeHeaderValue contentType, EntityTagHeaderValue? etag = null) : base(fileName, contentType) + { + EntityTag = etag; + EnableRangeProcessing = true; + } + + /// + public String FileInlineName { get; set; } = String.Empty; + + /// + public override Task ExecuteResultAsync(ActionContext context) + { + ArgumentNullException.ThrowIfNull(context, nameof(context)); + + var executor = context.HttpContext.RequestServices.GetRequiredService>(); + return executor.ExecuteAsync(context, this); + } +} \ No newline at end of file