这是我之前询问过的关于过于宽泛的问题的后续问题。 Previous Question自定义MultipartFormDataStreamProvider上传后通过WebApi从SQL下载大文件
在那个问题中,我解释说我需要通过将块存储为单独的行来将大文件(1-3GB)上载到数据库。我通过重写MultipartFormDataStreamProvider.GetStream方法来做到这一点。该方法返回了将缓冲区块写入数据库的自定义流。
问题是重写GetStream方法正在将整个请求写入数据库(包括头文件)。它是在保持内存级别平坦的情况下成功写入该数据的,但是当我下载文件时,除了文件内容之外,它将返回下载文件内容中的所有标题信息,因此文件无法打开。
有没有办法在覆盖GetStream方法,只写入文件的内容到数据库而不写头文件?
API
[HttpPost]
[Route("file")]
[ValidateMimeMultipartContentFilter]
public Task<HttpResponseMessage> PostFormData()
{
var provider = new CustomMultipartFormDataStreamProvider();
// Read the form data and return an async task.
var task = Request.Content.ReadAsMultipartAsync(provider).ContinueWith<HttpResponseMessage>(t =>
{
if (t.IsFaulted || t.IsCanceled)
{
Request.CreateErrorResponse(HttpStatusCode.InternalServerError, t.Exception);
}
return Request.CreateResponse(HttpStatusCode.OK);
});
return task;
}
[HttpGet]
[Route("file/{id}")]
public async Task<HttpResponseMessage> GetFile(string id)
{
var result = new HttpResponseMessage()
{
Content = new PushStreamContent(async (outputStream, httpContent, transportContext) =>
{
await WriteDataChunksFromDBToStream(outputStream, httpContent, transportContext, id);
}),
StatusCode = HttpStatusCode.OK
};
result.Content.Headers.ContentType = new MediaTypeHeaderValue("application/zipx");
result.Content.Headers.ContentDisposition = new ContentDispositionHeaderValue("attachment") { FileName = "test response.zipx" };
return result;
}
return new HttpResponseMessage(HttpStatusCode.BadRequest);
}
private async Task WriteDataChunksFromDBToStream(Stream responseStream, HttpContent httpContent, TransportContext transportContext, string fileIdentifier)
{
// PushStreamContent requires the responseStream to be closed
// for signaling it that you have finished writing the response.
using (responseStream)
{
using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString))
{
await myConn.OpenAsync();
using (var myCmd = new SqlCommand("ReadAttachmentChunks", myConn))
{
myCmd.CommandType = System.Data.CommandType.StoredProcedure;
var fileName = new SqlParameter("@Identifier", fileIdentifier);
myCmd.Parameters.Add(fileName);
// Read data back from db in async call to avoid OutOfMemoryException when sending file back to user
using (var reader = await myCmd.ExecuteReaderAsync(CommandBehavior.SequentialAccess))
{
while (await reader.ReadAsync())
{
if (!(await reader.IsDBNullAsync(3)))
{
using (var data = reader.GetStream(3))
{
// Asynchronously copy the stream from the server to the response stream
await data.CopyToAsync(responseStream);
}
}
}
}
}
}
}// close response stream
}
定制MultipartFormDataStreamProvider GetStream方法实现
public override Stream GetStream(HttpContent parent, HttpContentHeaders headers)
{
// For form data, Content-Disposition header is a requirement
ContentDispositionHeaderValue contentDisposition = headers.ContentDisposition;
if (contentDisposition != null)
{
// If we have a file name then write contents out to AWS stream. Otherwise just write to MemoryStream
if (!String.IsNullOrEmpty(contentDisposition.FileName))
{
var identifier = Guid.NewGuid().ToString();
var fileName = contentDisposition.FileName;// GetLocalFileName(headers);
if (fileName.Contains("\\"))
{
fileName = fileName.Substring(fileName.LastIndexOf("\\") + 1).Replace("\"", "");
}
// We won't post process files as form data
_isFormData.Add(false);
var stream = new CustomSqlStream();
stream.Filename = fileName;
stream.Identifier = identifier;
stream.ContentType = headers.ContentType.MediaType;
stream.Description = (_formData.AllKeys.Count() > 0 && _formData["description"] != null) ? _formData["description"] : "";
return stream;
//return new CustomSqlStream(contentDisposition.Name);
}
// We will post process this as form data
_isFormData.Add(true);
// If no filename parameter was found in the Content-Disposition header then return a memory stream.
return new MemoryStream();
}
throw new InvalidOperationException("Did not find required 'Content-Disposition' header field in MIME multipart body part..");
#endregion
}
通过CustomSqlStream称为物流的实现Write方法
public override void Write(byte[] buffer, int offset, int count)
{
//write buffer to database
using (var myConn = new SqlConnection(System.Configuration.ConfigurationManager.ConnectionStrings["TestDB"].ConnectionString)) {
using (var myCmd = new SqlCommand("WriteAttachmentChunk", myConn)) {
myCmd.CommandType = System.Data.CommandType.StoredProcedure;
var pContent = new SqlParameter("@Content", buffer);
myCmd.Parameters.Add(pContent);
myConn.Open();
myCmd.ExecuteNonQuery();
if (myConn.State == System.Data.ConnectionState.Open)
{
myConn.Close();
}
}
}
((ManualResetEvent)_dataAddedEvent).Set();
}
“ReadAttachmentChunks”存储过程获取与插入数据库时排序的db文件相对应的行。因此,代码的工作方式是将这些块拉回,然后异步将其写回到PushStreamContent以返回给用户。
所以我的问题是:
是否有写入文件的只有内容,除了内容相对于头部被上传的方法吗?
任何帮助将不胜感激。谢谢。
是不是讨厌的,因为有一个更好的方法来做到这一点,或者因为你一般不这样做?每次我以前做过大文件传输时,我都会将其写入磁盘并锁定文件存储区,所以我从来没有处理过这样的事情,所以请原谅我,如果我所做的只是愚蠢的。需求是我需要将大文件存储在数据库中(无法使用FileStream),我需要在它到达之前对其进行加密,同时保持较低的内存占用。这个分块的想法是我能想到的唯一方法。 – JakeHova
令人讨厌的部分是,你不能在框架中使用东西来为你做所有肮脏的工作。也许你可以在Nuget上找到第三方的http客户端库,查看源代码,看看它是如何在破解上传流的情况下发挥它的魔力的。 –
看来我现在唯一的问题是请求头包含在我写入数据库的时候。我试过用手工剥离标题的各种方法,通过利用边界值来检测标题的结束位置,但是1)它不适用于非文本文件2)它感觉像这样一个骇人而脆弱的解决方案,应该是什么是一个简单的问题。有关如何在写出标题之前删除标题的想法? – JakeHova