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