.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); }