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