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

1,853 阅读6分钟

基于MongoDB GridFS


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

图解


GridFS图解.png

我们上传一个普通文件到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调用示例

Postman上传示例.png

数据库 fs.files

image.png

fs.chunks

image.png

下载

接口实现

@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中自由的拓展字段,无需单独再设计业务表;代码经过测试及优化,有需要的朋友可以拿去应用或者调试

其它

大家感兴趣也可以阅读一下我写得其它文章^!^

不熟悉mongo语法点击这里

MongoDB分表存储查询方案

Redis缓存及本地缓存设计实践

我的主页