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