C# 如何在ASP.NET Core中提供文件,而它';它还在写
我有一个由后台服务不断写入的日志文件。到目前为止,用户需要能够下载该文件。当我返回一个MVCC# 如何在ASP.NET Core中提供文件,而它';它还在写,c#,asp.net-core,.net-core,asp.net-core-2.0,C#,Asp.net Core,.net Core,Asp.net Core 2.0,我有一个由后台服务不断写入的日志文件。到目前为止,用户需要能够下载该文件。当我返回一个MVCFileResult时,由于内容长度不匹配,我得到一个InvalidOperationException,这可能是因为某些内容在提供服务时已写入文件。提供了一个文件,基本上没有问题,但它通常有一个不完整的最后一行 后台服务基本上是这样做的: var stream = new FileStream(evidenceFilePath, FileMode.Append, FileAccess.Write, Fi
FileResult
时,由于内容长度不匹配,我得到一个InvalidOperationException,这可能是因为某些内容在提供服务时已写入文件。提供了一个文件,基本上没有问题,但它通常有一个不完整的最后一行
后台服务基本上是这样做的:
var stream = new FileStream(evidenceFilePath, FileMode.Append, FileAccess.Write, FileShare.Read);
while (true) // obviously it isn't actually this, but it does happen a lot!
{
var content = "log content\r\n";
stream.Write(Encoding.UTF8.GetBytes(content);
}
以下是控制器动作的一些变化(都有相同的结果):
以下是我尝试上述任一方法时遇到的例外情况:
System.InvalidOperationException: Response Content-Length mismatch: too many bytes written (216072192 of 216059904).
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ThrowTooManyBytesWritten(Int32 count)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.VerifyAndUpdateWrite(Int32 count)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.WriteAsync(ReadOnlyMemory`1 data, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseStream.WriteAsync(Byte[] buffer, Int32 offset, Int32 count, CancellationToken cancellationToken)
at Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(Stream source, Stream destination, Nullable`1 count, Int32 bufferSize, CancellationToken cancel)
at Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.WriteFileAsync(HttpContext context, Stream fileStream, RangeItemHeaderValue range, Int64 rangeLength)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult result)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext context)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State& next, Scope& scope, Object& state, Boolean& isCompleted)
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()
at Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()
at Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext httpContext)
at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext context)
at Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHttpApplication`1 application)
System.InvalidOperationException:响应内容长度不匹配:写入的字节太多(216059904中的216072192个)。
在Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.httprotocol.throwtoomanybyteswrited(Int32计数)
位于Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.VerifyAndUpdateWrite(Int32计数)
位于Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.WriteAsync(ReadOnlyMemory`1数据,CancellationToken CancellationToken)
位于Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpResponseStream.WriteAsync(字节[]缓冲区,Int32偏移量,Int32计数,取消令牌取消令牌)
位于Microsoft.AspNetCore.Http.Extensions.StreamCopyOperation.CopyToAsync(流源、流目标、可为null的`1计数、Int32 bufferSize、CancellationToken cancel)
位于Microsoft.AspNetCore.Mvc.Infrastructure.FileResultExecutorBase.WriteFileAsync(HttpContext上下文、流文件流、RangeItemHeaderValue范围、Int64 rangeLength)
位于Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultAsync(IActionResult)
位于Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResultFilterAsync[TFilter,TFilterAsync]()
位于Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResultExecutedContext)
在Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.ResultNext[TFilter,TFilterAsync](状态和下一步,范围和范围,对象和状态,布尔值和已完成)
在Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeResultFilters()中
在Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeNextResourceFilter()上
位于Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Rethrow(ResourceExecutedContext)
在Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.Next(State&Next、Scope&Scope、Object&State、Boolean&isCompleted)
在Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeFilterPipelineAsync()中
在Microsoft.AspNetCore.Mvc.Internal.ResourceInvoker.InvokeAsync()上
位于Microsoft.AspNetCore.Routing.EndpointMiddleware.Invoke(HttpContext HttpContext)
位于Microsoft.AspNetCore.Routing.EndpointRoutingMiddleware.Invoke(HttpContext HttpContext)
在Microsoft.AspNetCore.Builder.RouterMiddleware.Invoke(HttpContext HttpContext)中
位于Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext上下文)
位于Microsoft.AspNetCore.Session.SessionMiddleware.Invoke(HttpContext上下文)
位于Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext上下文)
位于Microsoft.AspNetCore.StaticFiles.StaticFileMiddleware.Invoke(HttpContext上下文)
在Microsoft.AspNetCore.Authentication.AuthenticationMiddleware.Invoke(HttpContext上下文)中
位于Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext上下文)
位于Microsoft.AspNetCore.Diagnostics.DeveloperExceptionPageMiddleware.Invoke(HttpContext上下文)
位于Microsoft.AspNetCore.Server.Kestrel.Core.Internal.Http.HttpProtocol.ProcessRequests[TContext](IHTTP应用程序`1应用程序)
我不太介意这个例外,但如果不发生的话,我宁愿这样。不过,我确实需要修复不完整的最后一行问题。对我来说,最明显的解决方案是跟踪已写入文件的字节数,并以某种方式只提供前n个字节。但是,我看不到任何简单的方法可以通过
FileResult
和构造它的各种助手方法来实现这一点。文件可能会变得相当大(高达500MB左右),因此在内存中进行缓冲似乎不切实际。文件是非托管资源
因此,当您访问非托管资源(如文件)时,它将通过句柄打开。如果是文件,则打开文件句柄(从内存中重新提取)
所以,我建议(非常通用)编写日志条目的最佳方法是:
打开文件
写入文件
关闭文件
处理(如适用)
简言之,不要让小溪开着
其次,对于控制器,您可以查看MSDN示例,通过控制器提供文件服务。一般来说,您可能会遇到文件锁定问题,因此您需要对此进行规划和补偿。然而,你眼前的问题更容易解决。问题归结为返回一条流。在返回响应时写入该流,因此在创建响应主体时计算的内容长度是错误的 您需要做的是在某个时间点捕获日志,即将其读入
字节[]
。然后,您可以返回该值,而不是流,并且内容长度将正确计算,因为字节[]
在读取后不会更改
using (var stream = new FileStream("C:\\path\\to\\the\\file.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
return File(ms.ToArray(), "text/plain");
}
最后,我编写了一个定制的ActionResult和IActionResultExecutor进行匹配,它们在很大程度上基于MVC和: 问题是——它根据当时的文件长度设置一次
内容长度
头,但如果请求不是范围请求,它将调用StreamCopyOperation.CopyToAsync
,而不传递长度,因此流将被完全复制。有多种方法可以解决这个问题:一个PR,从FileStream
派生来固定长度(不是很简单!),或者插入一个新的IActionResultExecutorusing (var stream = new FileStream("C:\\path\\to\\the\\file.txt", FileMode.Open, FileAccess.Read, FileShare.ReadWrite))
using (var ms = new MemoryStream())
{
await stream.CopyToAsync(ms);
return File(ms.ToArray(), "text/plain");
}
public class PartialFileStreamResult : FileResult
{
Stream stream;
long bytes;
/// <summary>
/// Creates a new <see cref="PartialFileStreamResult"/> instance with
/// the provided <paramref name="fileStream"/> and the
/// provided <paramref name="contentType"/>, which will download the first <paramref name="bytes"/>.
/// </summary>
/// <param name="stream">The stream representing the file</param>
/// <param name="contentType">The Content-Type header for the response</param>
/// <param name="bytes">The number of bytes to send from the start of the file</param>
public PartialFileStreamResult(Stream stream, string contentType, long bytes)
: base(contentType)
{
this.stream = stream ?? throw new ArgumentNullException(nameof(stream));
if (bytes == 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid file length");
}
this.bytes = bytes;
}
/// <summary>
/// Gets or sets the stream representing the file to download.
/// </summary>
public Stream Stream
{
get => stream;
set => stream = value ?? throw new ArgumentNullException(nameof(stream));
}
/// <summary>
/// Gets or sets the number of bytes to send from the start of the file.
/// </summary>
public long Bytes
{
get => bytes;
set
{
if (value == 0)
{
throw new ArgumentOutOfRangeException(nameof(bytes), "Invalid file length");
}
bytes = value;
}
}
/// <inheritdoc />
public override Task ExecuteResultAsync(ActionContext context)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
var executor = context.HttpContext.RequestServices.GetRequiredService<IActionResultExecutor<PartialFileStreamResult>>();
return executor.ExecuteAsync(context, this);
}
}
public class PartialFileStreamResultExecutor : FileResultExecutorBase, IActionResultExecutor<PartialFileStreamResult>
{
public PartialFileStreamResultExecutor(ILoggerFactory loggerFactory)
: base(CreateLogger<PartialFileStreamResultExecutor>(loggerFactory))
{
}
public async Task ExecuteAsync(ActionContext context, PartialFileStreamResult result)
{
if (context == null)
{
throw new ArgumentNullException(nameof(context));
}
if (result == null)
{
throw new ArgumentNullException(nameof(result));
}
using (result.Stream)
{
long length = result.Bytes;
var (range, rangeLength, serveBody) = SetHeadersAndLog(context, result, length, result.EnableRangeProcessing);
if (!serveBody) return;
try
{
var outputStream = context.HttpContext.Response.Body;
if (range == null)
{
await StreamCopyOperation.CopyToAsync(result.Stream, outputStream, length, bufferSize: BufferSize, cancel: context.HttpContext.RequestAborted);
}
else
{
result.Stream.Seek(range.From.Value, SeekOrigin.Begin);
await StreamCopyOperation.CopyToAsync(result.Stream, outputStream, rangeLength, BufferSize, context.HttpContext.RequestAborted);
}
}
catch (OperationCanceledException)
{
// Don't throw this exception, it's most likely caused by the client disconnecting.
// However, if it was cancelled for any other reason we need to prevent empty responses.
context.HttpContext.Abort();
}
}
}
}
services.AddTransient<IActionResultExecutor<PartialFileStreamResult>, PartialFileStreamResultExecutor>();
[HttpGet]
public IActionResult DownloadLog()
{
var (path, bytes) = GetThePathAndTheNumberOfBytesIKnowHaveBeenFlushed();
var stream = new FileStream(path, FileMode.Open, FileAccess.Read, FileShare.ReadWrite); // this ensures that the file can be read while it's still being written
return new PartialFileStreamResult(stream, "text/plain", bytes);
}