分分钟搞定一个文件存储服务,了解一下 基于MongoDB GridFS

愿天堂没有BUG

共 12576字,需浏览 26分钟

 ·

2021-09-06 20:09

grid中文释义为网格,顾名思义,GridFS是一个网格式的文件存储规范,GridFS将文件分成多个块,每个块作为一个单独的文档。默认情况下,每块大小默认255kB,意味着除了最后一个块之外,文档被分成多个255kB大小的块存储。GridFS使用两个集合存储文件信息,一个存储文件内容(fs.chunks),一个存储文件的元数据(fs.files)。GridFS的推出也是为了解决单个Document不能超过4M(新版为16M)而推出的,可以通过配置修改单个文件块的大小,这样可以避免小于4M(16M)的文件被分块,提高小文件读写性能。

图解


我们上传一个普通文件到GridFS中进行存储,会产生两个集合,fs.files和fs.chunks,这两个集合是一对多的关系,fs.files中的_id对应了fs.chunks中的files_id,fs.files记录对应多条fs.chunks记录;fs.files主要存储文件的描述信息(大小、名称、格式等等),以在fs.chunks表中有一个n字段,代表这一块的顺序,GridFS在读取文件时候是将文件所有的chunks读取到,然后按照n字段排序,将每个chunk中的二进制内容拼到一起,这样文件内容就还原了,当然这个操作都是MongoDB驱动帮我们去完成的。

  • fs.files字段及描述(可拓展字段)

字段描述
_id文件id,建议存储时候使用uuid生成,_id默认是MongoDB生成的,对应Java中时一个对象结构,不利与交互
filename文件名称
chunkSize单个chunk的大小(模式255kb(261120b))
uploadDate上传时间
aliases别名
md5文件验证,保证安全
length文件大小

fs.file集合除了这些默认的字段信息,还可以在业务上进行拓展,比如添加uploadUser(上传用户),suffix(文件后缀名),方便文件管理页面查询时候进行类型筛选

  • fs.chunks字段及描述(不可拓展)

字段描述
_id唯一id,MongoDB自动生成
files_id对应fs.files中的_id
data文件块二进制内容
n当前chunk的序号

应用

框架

<!-- springboot -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>1.5.7.RELEASE</version>
</parent>
<!-- mongodb -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-mongodb</artifactId>
</dependency>
复制代码

操作实现

为了方便各位阅读,将业务处理都放在接口中

上传

接口实现

@PostMapping("upload")
public String upload(HttpServletRequest request) {
MultipartFile file = ((MultipartHttpServletRequest) request).getFile("upload_file");
try {
GridFS fs = new GridFS(mongoTemplate.getDb());
// 生成文件id
String fileId = UUID.randomUUID().toString().replace("-", "");
// 创建文件对象
GridFSInputFile gif = fs.createFile(file.getInputStream());
gif.setId(fileId);
String filename = file.getOriginalFilename();
// 设置文件名称
gif.setFilename(filename);
// 类型
gif.setContentType(file.getContentType());
// 文件后缀 txt pdf xls...
String suffix = filename.split("\\.")[1];
// 元数据 可以将拓展字段放在这里面
gif.setMetaData(new BasicDBObject("suffix", type));
gif.save();
return fileId;
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}
复制代码

调用示例

上传 木兰辞.txt

唧唧复唧唧,木兰当户织。不闻机杼声,唯闻女叹息。
问女何所思,问女何所忆。女亦无所思,女亦无所忆。昨夜见军帖,可汗大点兵,军书十二卷,卷卷有爷名。阿爷无大儿,木兰无长兄,愿为市鞍马,从此替爷征。
东市买骏马,西市买鞍鞯,南市买辔头,北市买长鞭。旦辞爷娘去,暮宿黄河边,不闻爷娘唤女声,但闻黄河流水鸣溅溅。旦辞黄河去,暮至黑山头,不闻爷娘唤女声,但闻燕山胡骑鸣啾啾。
万里赴戎机,关山度若飞。朔气传金柝,寒光照铁衣。将军百战死,壮士十年归。
归来见天子,天子坐明堂。策勋十二转,赏赐百千强。可汗问所欲,木兰不用尚书郎,愿驰千里足,送儿还故乡。
爷娘闻女来,出郭相扶将;阿姊闻妹来,当户理红妆;小弟闻姊来,磨刀霍霍向猪羊。开我东阁门,坐我西阁床,脱我战时袍,著我旧时裳。当窗理云鬓,对镜帖花黄。出门看火伴,火伴皆惊忙:同行十二年,不知木兰是女郎。
雄兔脚扑朔,雌兔眼迷离;双兔傍地走,安能辨我是雄雌?
复制代码

Postman调用示例

数据库 fs.files

fs.chunks

下载

接口实现

@GetMapping("/download/{fileId}")
public void download(@PathVariable String fileId, HttpServletRequest request, HttpServletResponse response) {
GridFS fs = new GridFS(mongoTemplate.getDb());
// 获取文件对象
GridFSDBFile file = fs.findOne(new BasicDBObject("_id", fileId));
if (file != null) {
// try resurce的方式可以自动关闭流 无需手动处理
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
InputStream is = file.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);) {
// 判断文件类型 设计相关响应头信息 做到多浏览器兼容
String suffix = (String) file.getMetaData().get("suffix");
response.setContentType(SVG.equals(suffix) ? "image/svg+xml;charset=UTF-8" : "application/octet-stream;charset=UTF-8");
response.addHeader("Content-Disposition",
"attachment; " + createContentDisposition(request, file.getFilename()));
response.setContentLength((int) file.getLength());
int length = 0;
byte[] temp = new byte[2048];
while ((length = bis.read(temp)) != -1) {
bos.write(temp, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

/**
* 创建 Content-Disposition
*
* @param request
* @param filename
* @return
*/
private String createContentDisposition(HttpServletRequest request, String filename) {
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
Browser browser = userAgent.getBrowser();
try {
String disposition = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
if (browser.isMatch("firefox") || browser.isMatch("applewebkit") || browser.isMatch("safari")) {
disposition = new String(filename.getBytes(StandardCharsets.UTF_8.name()), StandardCharsets.ISO_8859_1.name());
}
return browser.isMatch("opera") ? "filename*=UTF-8''" + disposition : "filename = "" + disposition + """;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}
private final static Map<String, String> FILE_CONTENT_MAP = new HashMap<>(16);

static {
FILE_CONTENT_MAP.put("doc", "application/msword;charset=UTF-8");
FILE_CONTENT_MAP.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=UTF-8");
FILE_CONTENT_MAP.put("xls", "application/vnd.ms-excel;charset=UTF-8");
FILE_CONTENT_MAP.put("xlt", "application/vnd.ms-excel;charset=UTF-8");
FILE_CONTENT_MAP.put("xla", "application/vnd.ms-excel;charset=UTF-8");
FILE_CONTENT_MAP.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
FILE_CONTENT_MAP.put("xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template;charset=UTF-8");
FILE_CONTENT_MAP.put("xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("xltm", "application/vnd.ms-excel.template.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("xlam", "application/vnd.ms-excel.addin.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("ppt", "application/vnd.ms-powerpoint;charset=UTF-8");
FILE_CONTENT_MAP.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation;charset=UTF-8");
}

private static final String SVG = "svg";
复制代码

调用示例

浏览器填写下载接口地址+文件id即可

删除文件

接口实现

@DeleteMapping("/{fileId}")
public void delete(@PathVariable String fileId) {
GridFS fs = new GridFS(mongoTemplate.getDb());
fs.remove(fileId);
}
复制代码

附录

为了方便有需要的朋友使用及学习,附录了完整代码,好评点个赞(PS:每当我们在网上调研(cv)代码实现,拿回来总是改改改,在这里ctrl+c就完事了)

package com.demo.fsserver.api;

import cn.hutool.http.useragent.Browser;
import cn.hutool.http.useragent.UserAgent;
import cn.hutool.http.useragent.UserAgentUtil;
import com.mongodb.BasicDBObject;
import com.mongodb.gridfs.GridFS;
import com.mongodb.gridfs.GridFSDBFile;
import com.mongodb.gridfs.GridFSInputFile;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.mongodb.core.MongoTemplate;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile;
import org.springframework.web.multipart.MultipartHttpServletRequest;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.net.URLEncoder;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;

@RestController
@RequestMapping("/fs/api")
public class FsController {

@Autowired
private MongoTemplate mongoTemplate;

/**
* 上传
*
* @param request
* @return
*/
@PostMapping("upload")
public String upload(HttpServletRequest request) {
MultipartFile file = ((MultipartHttpServletRequest) request).getFile("upload_file");
try {
GridFS fs = new GridFS(mongoTemplate.getDb());
String fileId = UUID.randomUUID().toString().replace("-", "");
GridFSInputFile gif = fs.createFile(file.getInputStream());
gif.setId(fileId);
String filename = file.getOriginalFilename();
gif.setFilename(filename);
String suffix = filename.split("\.")[1];
gif.setContentType(file.getContentType());
gif.setMetaData(new BasicDBObject("suffix", suffix));
gif.save();
return fileId;
} catch (IOException ex) {
ex.printStackTrace();
}
return null;
}

/**
* 下载文件
*
* @param fileId
* @param request
* @param response
*/
@GetMapping("/download/{fileId}")
public void download(@PathVariable String fileId, HttpServletRequest request, HttpServletResponse response) {
GridFS fs = new GridFS(mongoTemplate.getDb());
GridFSDBFile file = fs.findOne(new BasicDBObject("_id", fileId));
if (file != null) {
try (BufferedOutputStream bos = new BufferedOutputStream(response.getOutputStream());
InputStream is = file.getInputStream();
BufferedInputStream bis = new BufferedInputStream(is);) {
String suffix = (String) file.getMetaData().get("suffix");
response.setContentType(SVG.equals(suffix) ? "image/svg+xml;charset=UTF-8" : "application/octet-stream;charset=UTF-8");
response.addHeader("Content-Disposition",
"attachment; " + createContentDisposition(request, file.getFilename()));
response.setContentLength((int) file.getLength());
int length = 0;
byte[] temp = new byte[2048];
while ((length = bis.read(temp)) != -1) {
bos.write(temp, 0, length);
}
} catch (IOException e) {
e.printStackTrace();
}
}

}

/**
* 创建 Content-Disposition
*
* @param request
* @param filename
* @return
*/
private String createContentDisposition(HttpServletRequest request, String filename) {
UserAgent userAgent = UserAgentUtil.parse(request.getHeader("User-Agent"));
Browser browser = userAgent.getBrowser();
try {
String disposition = URLEncoder.encode(filename, StandardCharsets.UTF_8.name());
if (browser.isMatch("firefox") || browser.isMatch("applewebkit") || browser.isMatch("safari")) {
disposition = new String(filename.getBytes(StandardCharsets.UTF_8.name()), StandardCharsets.ISO_8859_1.name());
}
return browser.isMatch("opera") ? "filename*=UTF-8''" + disposition : "filename = "" + disposition + """;
} catch (UnsupportedEncodingException e) {
e.printStackTrace();
}
return null;
}


/**
* 删除文件
*
* @param fileId
*/
@DeleteMapping("/{fileId}")
public void delete(@PathVariable String fileId) {
GridFS fs = new GridFS(mongoTemplate.getDb());
fs.remove(fileId);
}

private final static Map<String, String> FILE_CONTENT_MAP = new HashMap<>(16);

static {
FILE_CONTENT_MAP.put("doc", "application/msword;charset=UTF-8");
FILE_CONTENT_MAP.put("docx", "application/vnd.openxmlformats-officedocument.wordprocessingml.document;charset=UTF-8");
FILE_CONTENT_MAP.put("xls", "application/vnd.ms-excel;charset=UTF-8");
FILE_CONTENT_MAP.put("xlt", "application/vnd.ms-excel;charset=UTF-8");
FILE_CONTENT_MAP.put("xla", "application/vnd.ms-excel;charset=UTF-8");
FILE_CONTENT_MAP.put("xlsx", "application/vnd.openxmlformats-officedocument.spreadsheetml.sheet;charset=UTF-8");
FILE_CONTENT_MAP.put("xltx", "application/vnd.openxmlformats-officedocument.spreadsheetml.template;charset=UTF-8");
FILE_CONTENT_MAP.put("xlsm", "application/vnd.ms-excel.sheet.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("xltm", "application/vnd.ms-excel.template.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("xlam", "application/vnd.ms-excel.addin.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("xlsb", "application/vnd.ms-excel.sheet.binary.macroEnabled.12;charset=UTF-8");
FILE_CONTENT_MAP.put("ppt", "application/vnd.ms-powerpoint;charset=UTF-8");
FILE_CONTENT_MAP.put("pptx", "application/vnd.openxmlformats-officedocument.presentationml.presentation;charset=UTF-8");
}

private static final String SVG = "svg";
}
复制代码

应用小结

快速开发了三个接口,上传、下载、删除,代码量是不是非常少,在应用到MongoDB的服务中,要实现一个存储服务是如此简单,fs.files可以在mateData中自由的拓展字段,无需单独再设计业务表;代码经过测试及优化,有需要的朋友可以拿去应用或者调试


作者:热黄油啤酒
链接:https://juejin.cn/post/7003629447614038053
来源:掘金
著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。



浏览 30
点赞
评论
收藏
分享

手机扫一扫分享

举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

举报