Net Core 使用Mongodb操作文件(上传,下载)
Net Core 使用Mongodb操作文件(上传,下载)
1.Mongodb GridFS 文件操作帮助类。
GridFS 介绍 https://baike.baidu.com/item/GridFS/6342715?fr=aladdin
DLL源码:https://gitee.com/chenjianhua1985/mongodb-client-encapsulation
DLL文件:链接:https://pan.baidu.com/s/1SpWDtaXjavalJQav89UE4A?pwd=yceu 提取码:yceu
/// <summary> /// MongoDB 文件 操作类 /// </summary> public abstract class MongoFileRepository : IMongoFileRepository { #region GridFS 介绍 // MongodB使用两个集合来存储GridFS文件,一个是fs.files,另一个是fs.chunks // fs.files这个集合中存储的是每一个上传到数据库的文档的信息 // fs.chunks这个集合存储的是上传文件的内容。一个chunk相当于一个文档(大文件被拆分成多个有序的chunk) // GridFS中的bucket这个概念指代的是fs.files和fs.chunks的组合 // 它的工作原理是: // 在GridFS存储文件是将文件分块存储,文件会按照256KB的大小分割成多个块进行存储 // GridFS使用两个集合 // (collection)存储文件,一个集合是chunks, 用于存储文件的二进制数据 // 一个集合是files,用于存储文件的元数据信息(文件名称、块大小、上传时间等信息) // 从GridFS中读取文件要对文件的各各块进行组装、合并 // GridFSBucket 用于打开下载流对象 #endregion /// <summary> /// 库 名 /// </summary> public abstract string DataBaseName { get; } /// <summary> /// 配置类型 /// </summary> public virtual DBConfigTypeEnum DBConfigType { get; set; } = DBConfigTypeEnum.WriteDB; /// <summary> /// 根据 Id 获取内部信息 /// </summary> /// <param name="id"></param> /// <returns></returns> public ObjectId GetInternalId(string id) { if (!ObjectId.TryParse(id, out ObjectId internalId)) internalId = ObjectId.Empty; return internalId; } /// <summary> /// 根据 Id 获取文件 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<GridFSFileInfo> GetFileById(string id) { // 通过系统 Id 筛选出包含 _id 的文件数据 var filter = Builders<GridFSFileInfo>.Filter.Eq("_id", GetInternalId(id)); return await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).Find(filter).FirstOrDefaultAsync(); } /// <summary> /// 根据 Id 获取文件 -- 异步 /// </summary> /// <param name="id">文件Id</param> /// <returns></returns> public async Task<GridFSFileInfo> GetFileById(ObjectId id) { var filter = Builders<GridFSFileInfo>.Filter.Eq("_id", id); return await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).Find(filter).FirstOrDefaultAsync(); } /// <summary> /// 上传文件 -- 异步 /// </summary> /// <param name="fileName">文件名称</param> /// <param name="source">流</param> /// <returns></returns> public async Task<ObjectId> UploadFile(string fileName, Stream source) { var id = await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).UploadFromStreamAsync(fileName, source); return id; } /// <summary> /// 根据Id 下载文件流可搜索 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStreamSeekable(string id) { var options = new GridFSDownloadOptions { Seekable = true }; return await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).OpenDownloadStreamAsync(GetInternalId(id), options); } /// <summary> /// 根据Id 下载文件流可搜索 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStreamSeekable(ObjectId id) { var options = new GridFSDownloadOptions { Seekable = true }; return await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).OpenDownloadStreamAsync(id, options); } /// <summary> /// 根据 Id 下载文件流 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStream(string id) { return await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).OpenDownloadStreamAsync(GetInternalId(id)); } /// <summary> /// 下载 Id 获取文件流 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task<GridFSDownloadStream<ObjectId>> DownloadFileStream(ObjectId id) { return await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).OpenDownloadStreamAsync(id); } /// <summary> /// 根据Id删除文件 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task DeleteFile(string id) { await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).DeleteAsync(GetInternalId(id)); } /// <summary> /// 根据Id删除文件 -- 异步 /// </summary> /// <param name="id"></param> /// <returns></returns> public async Task DeleteFile(ObjectId id) { await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).DeleteAsync(id); } /// <summary> /// 根据Id和文件名称移除文件 -- 异步 /// </summary> /// <param name="id"></param> /// <param name="newFilename"></param> /// <returns></returns> public async Task RenameFile(string id, string newFilename) { await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).RenameAsync(GetInternalId(id), newFilename); } /// <summary> /// 根据Id和文件名称移除文件 --异步 /// </summary> /// <param name="id"></param> /// <param name="newFilename"></param> /// <returns></returns> public async Task RenameFile(ObjectId id, string newFilename) { await MongoDBClient.GetGridFSBucketInstance(DataBaseName, DBConfigType).RenameAsync(id, newFilename); } }
在项目的使用实例
本人比较喜欢封装好再用。所以将上面的帮助类封装成了一个DLL文件,这样在项目中可以直接引用。
项目结构
API层有一个mongoDBConfig.json这个是客户端连接配置文件。
[ { "DbName": "PMFiles", "ReadConnectionString": "mongodb://192.168.10.200:27017", "WriteConnectionString": "mongodb://192.168.10.200:27017" }, { "DbName": "Test", "ReadConnectionString": "mongodb://192.168.10.200:27017", "WriteConnectionString": "mongodb://192.168.10.200:27017" } ]
使用很简单,在要引用的类库中直接引用DLL文件就可以。
下面是具体的实例代码:
1.数据访问层
/// <summary> /// MongodbFile处理接口 /// </summary> public interface IFileRepository : IMongoFileRepository { }
这里创建的接口是空的,没有要扩展的方法,常用的接口都在 IMongoFileRepository 基类接口中定义了.
实现类:
/// <summary> /// Mongodb 文件 数据库 操作类 /// </summary> public class FileRepository : MongoFileRepository , IFileRepository { public override string DataBaseName => "PMFiles"; }
这里创建的实现类也是空的,没有要扩展的方法,常用的接口都在 MongoFileRepository基类中实现了.
注意: 这里重写了DataBaseName这里一定要重写基类的数据库名称。
2.业务层
/// <summary> /// 文件 操作 业务逻辑层接口 /// </summary> public interface IFileService : IMongoFileRepository { }
实现类
这里主要是调用数据访问层的实例来实现功能。
[AutoInject(typeof(IFileService), InjectType.Scope)]
这里的服务注入用的是自动注入。可以改成手动注册。手动注册时可以删除类上的 AutoInject 标签
/// <summary> /// 文件操作服务 /// </summary> [AutoInject(typeof(IFileService), InjectType.Scope)] public class FileService : IFileService { private readonly IFileRepository _srviceFile = RepositoryIocFactory.GetRegisterImp<IFileRepository>(); public Task DeleteFile(string id) { return _srviceFile.DeleteFile(id); } public Task DeleteFile(ObjectId id) { return _srviceFile.DeleteFile(id); } public Task<GridFSDownloadStream<ObjectId>> DownloadFileStream(string id) { return _srviceFile.DownloadFileStream(id); } public Task<GridFSDownloadStream<ObjectId>> DownloadFileStream(ObjectId id) { return _srviceFile.DownloadFileStream(id); } public Task<GridFSDownloadStream<ObjectId>> DownloadFileStreamSeekable(string id) { return _srviceFile.DownloadFileStreamSeekable(id); } public Task<GridFSDownloadStream<ObjectId>> DownloadFileStreamSeekable(ObjectId id) { return _srviceFile.DownloadFileStreamSeekable(id); } public Task<GridFSFileInfo> GetFileById(string id) { return _srviceFile.GetFileById(id); } public Task<GridFSFileInfo> GetFileById(ObjectId id) { return _srviceFile.GetFileById(id); } public ObjectId GetInternalId(string id) { return _srviceFile.GetInternalId(id); } public Task RenameFile(string id, string newFilename) { return _srviceFile.RenameFile(id, newFilename); } public Task RenameFile(ObjectId id, string newFilename) { return _srviceFile.RenameFile(id, newFilename); } public Task<ObjectId> UploadFile(string fileName, Stream source) { return _srviceFile.UploadFile(fileName, source); } }
1、API层的实现
AIP层主要的接口有 上传 和 根据ID下载文件,以文件流对象返回数据。
/// <summary> /// MongoDB文件上传 /// </summary> public class FileUploadController : BaseApiController { private IFileService serviceFile; public FileUploadController(IFileService fileService) { serviceFile = fileService; } /// <summary> /// 上传文件 /// </summary> /// <returns></returns> [HttpPost, Route("UploadFileAsync")] [DisableRequestSizeLimit] [DisableFormValueModelBinding] public async Task<AjaxResultPageModel> UploadFileAsync() { var rspModel = new AjaxResultPageModel(); var fileIds = new List<string>(); //检查ContentType if (!MultipartRequestHelper.IsMultipartContentType(Request.ContentType)) { rspModel.Warning("内容类型不能为空"); } else { var _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 serviceFile.UploadFile(currentFile.FileName, section.Body); fileIds.Add(id.ToString()); } } section = await reader.ReadNextSectionAsync(); } rspModel.Success(fileIds); } return rspModel; } /// <summary> /// 下载文件 /// </summary> [HttpGet,Route("Download")] public async Task<IActionResult> Download(string id) { var fileInfo = await serviceFile.GetFileById(id); if (fileInfo == null) { return NotFound(); } else { return File(await serviceFile.DownloadFileStream(fileInfo.Id), "application/octet-stream", fileInfo.Filename); } } }
前端实现:
前端使用vue组件
<template> <div class="home"> <el-card class="box-card-top" shadow="never"> <el-row :gutter="100"> <el-col :span="2"> <div class="user-avatar"> <p class="user-avatar-text">{{ avatar }}</p> </div> </el-col> <el-col :span="22"> <div class="text"> <h3>{{ holler }} {{ username }}</h3> <p> 桂树青春百里疆,鹧鸪啼彻午阴凉. 延平津上峰如削,剑去江空水自长. --剑道·尘心 </p> <p> 活在当下,着眼未来. 没有一往无前的觉悟.就不配握紧手中的双刀.空有大志, 却没有实际行动.天下万般兵刃.唯有过往,伤人最深.如果真相带来痛苦. 谎言只会雪上加霜. --LOL·亚索 </p> </div> </el-col> </el-row> <!-- <h2>首页界面</h2> <div class="hello"> --> <!-- <el-input v-model="user" type="text" /> --> <!-- <div id="message" v-html="remsg"></div> --> <!-- <div id="el-input"> <el-input id="chatbox" @keyup.native.enter="handle" type="textarea" :rows="1" placeholder="请输入内容" v-model="msg"></el-input> </div> <el-button size="small" style="display:inline-block;" icon="el-icon-s-promotion" type="suceess" @click="handle" plain></el-button> --> <!-- </div> --> </el-card> <el-row :gutter="20"> <el-col :span="4"> <el-card class="box-card-center" shadow="never"> <el-upload :action="action" :file-list="modeList1" :http-request="modeUpload1" :multiple="true" :before-remove="handleRemove1" > <el-button size="small" type="primary">上传</el-button> </el-upload> <el-button @click="upload1">点击上传文件</el-button> <br/> <el-button @click="fileUpload">点击上传文件1</el-button> </el-card> </el-col> <el-col :span="4"> <el-card class="box-card-center" shadow="never"></el-card> </el-col> <el-col :span="4"> <el-card class="box-card-center" shadow="never"></el-card> </el-col> </el-row> </div> </template> <script> import axios from 'axios' import config from "../../config/index"; import service from "../../api/UploadFiles/index"; export default { data() { return { username: null, holler: "欢迎回来 — ", avatar: "", action: "", //上传文件的接口 mode1: {}, modeList1:[], fd1: new FormData() }; }, created() { this.username = JSON.parse( sessionStorage.getItem(config.localStorageKey) ).userName; this.avatar = this.username.slice(0, 1); }, methods: { modeUpload1: function(item) { // console.log(item.file); // this.mode1 = item.file // const isIMG = // item.file.type === 'image/jpg' || // item.file.type === 'image/jpeg' || // item.file.type === 'image/png' // const isLt = item.file.size / 1024 / 2000 <= 1 // if (!isIMG) { // this.error='文件格式有误\n请上传后缀为jpg\\png的图片' // } // if (!isLt) { // // console.log(file.size) // this.error='上传头像图片大小不能超过500KB!' // } // if(isIMG&&isLt){ this.fd1.append('files', item.file); //这里是自己把文件对象获取到,并存储起来 //console.log("modeUpload1-> addFile:",item.file); //} }, upload1: function() { console.log("upload1-> 11,files.length=",this.fd1.getAll('files').length); if(this.fd1.getAll('files').length===0){//formdata.getAll()返回的是一个file的数组(当然这里是你之前的参数得是文件) this.error='请先上传文件' }else { console.log("upload1-> post"); axios.post('https://localhost:5001/adminapi/FileUpload/UploadFileAsync', this.fd1, { // headers: { 'Content-Type': 'multipart/form-data' } }).then(response => { console.log("upload1->",response.data); }) } }, handleRemove1(file, fileList) { let files = this.fd1.getAll('files'); this.fd1.delete('files'); let len = files.length for(var i=0;i<len;i++){ if(files[i].uid!=file.uid){ //uid应该是可以唯一标志文件的,如果不能,就是我,会死 this.fd1.append('files',files[i]) } } }, //上传方法 fileUpload(uploader) { //let form = new FormData(); //form.append("files", uploader.file); //console.log(this.fd1); //console.log("--------------------------"); //console.log(uploader) service.uploadFlie(this.fd1 // onUploadProgress: (progressEvent) => { // //这一步是展示上传的进度条,不展示也行,根据自身需求决定 // let percent = ((progressEvent.loaded / progressEvent.total) * 100) | 0; // uploader.onProgress({ percent: percent }); //调用uploader的进度回调 // }, ).then((res) => { console.log(res); if (res.success) { this.$message({ message: "上传成功", type: "success", }); } else { this.$message.error("上传失败,"+res.message); } }) .catch((err) => { this.$message.error(err); }); }, }, }; /* import * as signalR from "@microsoft/signalr"; let hubUrl = "https://localhost:44360/chatHub";//"https://localhost:44367/chatHub";//https://localhost:44318/chatHub; https://localhost:44367/chatHub //.net core 版本中默认不会自动重连,需手动调用 withAutomaticReconnect const connection = new signalR.HubConnectionBuilder().withAutomaticReconnect().withUrl(hubUrl).build(); connection.start().catch(err => alert(err.message)); export default { name: "Im", mounted() { var _this = this; console.log('hubUrl:',hubUrl); //实现Show方法 OK connection.on("Show", function(username, message) { _this.remsg = _this.remsg + "<br>" + username + ":" + message; console.log('Show:',message); }); //实现ConnectResponse方法 //connection.on("ChatHubReceiveMsg", function(username, message) { // _this.remsg = _this.remsg + "<br>" + username + ":" + message; // console.log('ChatHubReceiveMsg:',message); //}); //实现DisconnectResponse方法 //connection.on("DisconnectResponse", function(username, message) { // _this.remsg = _this.remsg + "<br>" + username + ":" + message; // console.log('DisconnectResponse:',message); //}); }, data() { return { user: "cjh", msg: "", remsg: "" }; }, methods: { handle: function() { if(this.msg.trim()==""){ alert("不能发送空白消息"); return; } //调用后端方法 SendMsg 传入参数 OK connection.invoke("SendMsg", this.user, this.msg); //connection.invoke("PublicSendMsg", this.msg); this.msg = ""; } } }; */ </script> <style lang="less" scoped> .home { margin: 20px; .box-card-top { min-height: 160px; .user-avatar { width: 100px; height: 100px; margin-left: 50px; margin-top: 5px; border-radius: 50%; background-image: linear-gradient(#cdeefa, #b4b4fc); // background-color: #b4b4fc; .user-avatar-text { font-size: 40px; text-align: center; padding-top: 22px; color: #383838; } } .text h3 { margin-top: 20px; margin-bottom: 10px; font-size: 20px; } .text p { font-size: 16px; user-select: none; } } .box-card-center { width: 350px; height: 300px; margin-top: 20px; } } h1, h2 { font-weight: normal; } ul { list-style-type: none; padding: 0; } li { display: inline-block; margin: 0 10px; } a { color: #42b983; } #el-input { display: inline-block; width: 96%; float: left; } #message { overflow-y: auto; text-align: left; border: #42b983 solid 1px; height: 500px; } </style>
跨域问题处理。
requestFile.js
注意上图的类型只能是这个类型,只能一个类型,不能多。
//interceptors.js // vue axios配置 发起请求加载loading请求结束关闭loading // http request 请求拦截器,有token值则配置上token值 import axios from 'axios' import common from '../utils/common' // import { API_BASE } from '../config/config'; // axios.defaults.baseURL = API_BASE; // api base_url,设置前缀不存在 // const BASE_URL=""; const service = axios.create({ baseURL: '', timeout: 60000, // 请求超时时间 headers: { // Authorization: Authorization, 'Content-Type': 'multipart/form-data' } }); // Content-Type: application/wasmsql-wasm.wasm // http请求拦截器 service.interceptors.request.use( config => { let token = common.getToken(); //console.log('token:', token); if (token) { // bus.$emit('toggleloading', true)//显示loading // Loading.service(options); //如果token存在 config.headers['Authorization'] = `Bearer ${token}`; //console.log('token 1:', token); } return config; }, error => { Promise.reject(error); } ) // http response 服务器响应拦截器, // 这里拦截401错误,并重新跳入登页重新获取token service.interceptors.response.use( response => { if (response.status === 200) { //通讯成功 // Toast.clear(); // console.log(response); /************* * response.data.status === 0 错误 * response.data.status === 100 成功 * response.data.status === 401 token过期 * * *************/ // response.data.head.errorCode // bus.$emit('toggleloading', false)//隐藏loading if (response.data.state == 401) {//未授权 //如果是token过期,跳转至登录 // console.log("401"); //common.goLogin(); message.error("未授权 请联系管理员!"); //store.commit('setToken', ''); } else if (response.data.state == 0) { // Message.error(response.data.message); return response.data; } else { // util.goLogin(); return response.data; } } }, error => { //请求失败 // ; const response = error.response; if (response.status === 401) { // Toast.fail(response.data.message); message.error("未授权 请联系管理员!"); //util.goLogin(); } else if (response.status === 403) { $router.push({ name: '403' }); } else { // Toast.fail(response.data.message ? response.data.message : '系统错误请联系管理员'); // message.error({ // message: '无服务,请联系管理员' // }); } return Promise.reject(error); } ); export default service;
API js
import http from '../../utils/http' export default { // 上传文件 uploadFileAsync:(params) => { return http.postMongoDBFile("adminapi/FileUpload/UploadFileAsync",params); }, // 下载文件 download:(params) => { return http.postMongoDBdownloadFile("adminapi/FileUpload/Download?id="+params) } };
完成。