.Net Core之MongoDB存储文件
MongoDB提供了GridFS来存储文件,我们这里就讨论采用GridFS存储文件的方案
这里主要使用MongoDB.Driver相关库
- MongoDB的一些基本使用:
public class MongoFileRepo
{
private IMongoClient _client;
private IMongoDatabase _db;
private readonly IGridFSBucket bucket;
//注入相关配置
public MongoFileRepo(IOptions<Settings> settings)
{
_client = new MongoClient(settings.Value.ConnectionString_MongoDB);
_db = _client.GetDatabase("MongoDB");
bucket = new GridFSBucket(_db);
}
public ObjectId GetInternalId(string id)
{
if (!ObjectId.TryParse(id, out ObjectId internalId))
internalId = ObjectId.Empty;
return internalId;
}
public async Task<GridFSFileInfo> GetFileById(string id)
{
var filter = Builders<GridFSFileInfo>.Filter.Eq("_id", GetInternalId(id));
return await bucket.Find(filter).FirstOrDefaultAsync();
}
public async Task<GridFSFileInfo> GetFileById(ObjectId id)
{
var filter = Builders<GridFSFileInfo>.Filter.Eq("_id", id);
return await bucket.Find(filter).FirstOrDefaultAsync();
}
public async Task<ObjectId> UploadFile(string fileName, Stream source)
{
var id = await bucket.UploadFromStreamAsync(fileName, source);
return id;
}
public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStreamSeekable(string id)
{
var options = new GridFSDownloadOptions
{
Seekable = true
};
return await bucket.OpenDownloadStreamAsync(GetInternalId(id), options);
}
public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStreamSeekable(ObjectId id)
{
var options = new GridFSDownloadOptions
{
Seekable = true
};
return await bucket.OpenDownloadStreamAsync(id, options);
}
public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStream(string id)
{
return await bucket.OpenDownloadStreamAsync(GetInternalId(id));
}
public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStream(ObjectId id)
{
return await bucket.OpenDownloadStreamAsync(id);
}
public async Task DeleteFile(string id)
{
await bucket.DeleteAsync(GetInternalId(id));
}
public async Task DeleteFile(ObjectId id)
{
await bucket.DeleteAsync(id);
}
public async Task RenameFile(string id, string newFilename)
{
await bucket.RenameAsync(GetInternalId(id), newFilename);
}
public async Task RenameFile(ObjectId id, string newFilename)
{
await bucket.RenameAsync(id, newFilename);
}
}
- MongoDB属于开箱即用,但 .Net Core下的可配置化的文件上传还是没那么简洁的,需要做一些自定义的工作,但是微软官方也是提供了配置方案:
public static class FileStreamingHelper
{
private static readonly FormOptions _defaultFormOptions = new FormOptions();
public static async Task<FormValueProvider> StreamFile(this HttpRequest request, string targetFilePath)
{
if (!MultipartRequestHelper.IsMultipartContentType(request.ContentType))
{
throw new Exception($"Expected a multipart request, but got {request.ContentType}");
}
// Used to accumulate all the form url encoded key value pairs in the
// request.
var formAccumulator = new KeyValueAccumulator();
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
ContentDispositionHeaderValue contentDisposition;
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out contentDisposition);
if (hasContentDispositionHeader)
{
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
FileMultipartSection currentFile = section.AsFileSection();
string time = DateTime.Now.ToString("yyyyMMddHHmmssffff");
string filePath = Path.Combine(targetFilePath, time + "_" + currentFile.FileName);
using (var targetStream = File.Create(filePath))
{
Console.WriteLine("{0} is uploading", currentFile.FileName);
await section.Body.CopyToAsync(targetStream).ConfigureAwait(false);
}
}
else if (MultipartRequestHelper.HasFormDataContentDisposition(contentDisposition))
{
// Content-Disposition: form-data; name="key"
//
// value
// Do not limit the key name length here because the
// multipart headers length limit is already in effect.
var key = HeaderUtilities.RemoveQuotes(contentDisposition.Name);
var encoding = GetEncoding(section);
using (var streamReader = new StreamReader(
section.Body,
encoding,
detectEncodingFromByteOrderMarks: true,
bufferSize: 1024,
leaveOpen: true))
{
// The value length limit is enforced by MultipartBodyLengthLimit
var value = await streamReader.ReadToEndAsync();
if (String.Equals(value, "undefined", StringComparison.OrdinalIgnoreCase))
{
value = String.Empty;
}
formAccumulator.Append(key.Value, value);
if (formAccumulator.ValueCount > _defaultFormOptions.ValueCountLimit)
{
throw new InvalidDataException($"Form key count limit {_defaultFormOptions.ValueCountLimit} exceeded.");
}
}
}
}
// Drains any remaining section body that has not been consumed and
// reads the headers for the next section.
section = await reader.ReadNextSectionAsync();
}
// Bind form data to a model
var formValueProvider = new FormValueProvider(
BindingSource.Form,
new FormCollection(formAccumulator.GetResults()),
CultureInfo.CurrentCulture);
return formValueProvider;
}
private static Encoding GetEncoding(MultipartSection section)
{
MediaTypeHeaderValue mediaType;
var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// most cases.
if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
{
return Encoding.UTF8;
}
return mediaType.Encoding;
}
}
public static class MultipartRequestHelper
{
// Content-Type: multipart/form-data; boundary="----WebKitFormBoundarymx2fSWqWSd0OxQqq"
// The spec says 70 characters is a reasonable limit.
public static string GetBoundary(MediaTypeHeaderValue contentType, int lengthLimit)
{
var boundary = HeaderUtilities.RemoveQuotes(contentType.Boundary).Value;
if (string.IsNullOrWhiteSpace(boundary))
{
throw new InvalidDataException("Missing content-type boundary.");
}
if (boundary.Length > lengthLimit)
{
throw new InvalidDataException(
$"Multipart boundary length limit {lengthLimit} exceeded.");
}
return boundary;
}
public static bool IsMultipartContentType(string contentType)
{
return !string.IsNullOrEmpty(contentType)
&& contentType.IndexOf("multipart/", StringComparison.OrdinalIgnoreCase) >= 0;
}
public static bool HasFormDataContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="key";
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& string.IsNullOrEmpty(contentDisposition.FileName.Value)
&& string.IsNullOrEmpty(contentDisposition.FileNameStar.Value);
}
public static bool HasFileContentDisposition(ContentDispositionHeaderValue contentDisposition)
{
// Content-Disposition: form-data; name="myfile1"; filename="Misc 002.jpg"
return contentDisposition != null
&& contentDisposition.DispositionType.Equals("form-data")
&& (!string.IsNullOrEmpty(contentDisposition.FileName.Value)
|| !string.IsNullOrEmpty(contentDisposition.FileNameStar.Value));
}
public static Encoding GetEncoding(MultipartSection section)
{
MediaTypeHeaderValue mediaType;
var hasMediaTypeHeader = MediaTypeHeaderValue.TryParse(section.ContentType, out mediaType);
// UTF-7 is insecure and should not be honored. UTF-8 will succeed in
// most cases.
if (!hasMediaTypeHeader || Encoding.UTF7.Equals(mediaType.Encoding))
{
return Encoding.UTF8;
}
return mediaType.Encoding;
}
}
[AttributeUsage(AttributeTargets.Class | AttributeTargets.Method)]
public class DisableFormValueModelBindingAttribute : Attribute, IResourceFilter
{
public void OnResourceExecuting(ResourceExecutingContext context)
{
var factories = context.ValueProviderFactories;
factories.RemoveType<FormValueProviderFactory>();
factories.RemoveType<JQueryFormValueProviderFactory>();
}
public void OnResourceExecuted(ResourceExecutedContext context)
{
}
}
- 完成以上准备工作,便可以实现文件上传和下载的接口了:
/// <summary>
/// 上传文件
/// </summary>
/// <returns></returns>
[HttpPost]
[DisableRequestSizeLimit]
[DisableFormValueModelBinding]
public async Task<IActionResult> Upload()
{
//检查ContentType
if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType))
{
return BadRequest("shoule_be_multipart");
}
FormOptions _defaultFormOptions = new FormOptions();
var boundary = MultipartRequestHelper.GetBoundary(MediaTypeHeaderValue.Parse(Request.ContentType), _defaultFormOptions.MultipartBoundaryLengthLimit);
var reader = new MultipartReader(boundary, Request.Body);
var section = await reader.ReadNextSectionAsync();
while (section != null)
{
//把Form的栏位內容逐一取出
var hasContentDispositionHeader = ContentDispositionHeaderValue.TryParse(section.ContentDisposition, out ContentDispositionHeaderValue contentDisposition);
if (hasContentDispositionHeader)
{
//按文件和键值对分类处理
if (MultipartRequestHelper.HasFileContentDisposition(contentDisposition))
{
FileMultipartSection currentFile = section.AsFileSection();
//存储文件到Mongo
var id = await _mongoRepo.UploadFile(currentFile.FileName, section.Body);
}
}
section = await reader.ReadNextSectionAsync();
}
return Ok();
}
/// <summary>
/// 下载文件
/// </summary>
[HttpGet("{id}/")]
public async Task<IActionResult> Download(int id)
{
var fileInfo = await _mongoRepo.GetFileById(id);
if (fileInfo == null)
{
return NotFound();
}
return File(await _mongoRepo.DownloadFileStream(mongoId), "application/octet-stream", fileInfo.Filename);
}