SpringBoot 整合 FastDFS 实现文件上传打包下载

FastDFS

  • FastDFS是一个开源的轻量级分布式文件系统,它对文件进行管理,功能包括:文件存储、文件同步、文件访问(文件上传、文件下载)等,解决了大容量存储和负载均衡的问题。特别适合以文件为载体的在线服务,如相册网站、视频网站等等。

1.FastDFS的安装

  • 参照 baidu | Google
  • docker 安装

2.使用fastdfs-client操作FastDFS

1
2
3
4
5
6
7
8
9
10
11
12
<!-- fastDFS -->
<dependency>
<groupId>com.github.tobato</groupId>
<artifactId>fastdfs-client</artifactId>
<version>1.26.7</version>
</dependency>

<dependency>
<groupId>commons-io</groupId>
<artifactId>commons-io</artifactId>
<version>2.6</version>
</dependency>

3.添加FastDFS的配置

  • 可xml 可 bean 可yaml(此处使用)
1
2
3
4
5
6
7
8
9
10
11
# fastDFS 配置
fdfs:
soTimeout: 1500 #socket连接超时时长
connectTimeout: 600 #连接tracker服务器超时时长
resHost: 10.6.11.xxx
storagePort: 23000
thumbImage: #缩略图生成参数,可选
width: 150
height: 150
trackerList: #TrackerList参数,支持多个,我这里只有一个,如果有多个在下方加- x.x.x.x:port
- 10.16.11.xxx:22122

4.加载FastDFS的配置信息

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
package kid1999.upload.config;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
* @desc: fastDFS配置类
* @auther: kid1999
* @date: 2019/12/19 19:23
**/
@Component
@Data
public class FastDfsConfig {
@Value("${fdfs.resHost}")
private String resHost;

@Value("${fdfs.storagePort}")
private String storagePort;
}

5.配置自己需要的FastDFS操作

  • 如上传,下载,查看信息,删除 …
  • 更多操作 参考源码中的测试案例
    1
    2
    3
    4
    5
    6
    7
    8
    9
    10
    11
    12
    13
    14
    15
    16
    17
    18
    19
    20
    21
    22
    23
    24
    25
    26
    27
    28
    29
    30
    31
    32
    33
    34
    35
    36
    37
    38
    39
    40
    41
    42
    43
    44
    45
    46
    47
    48
    49
    50
    51
    52
    53
    54
    55
    56
    57
    58
    59
    60
    61
    62
    63
    64
    65
    66
    67
    68
    69
    70
    71
    72
    73
    74
    75
    76
    77
    78
    79
    80
    81
    82
    83
    84
    85
    86
    87
    88
    89
    90
    91
    92
    93
    94
    95
    96
    97
    98
    99
    100
    101
    102
    103
    104
    105
    106
    107
    108
    109
    110
    111
    112
    113
    114
    115
    116
    117
    118
    119
    package kid1999.upload.utils;

    import com.github.tobato.fastdfs.domain.fdfs.FileInfo;
    import com.github.tobato.fastdfs.domain.fdfs.StorePath;
    import com.github.tobato.fastdfs.domain.proto.storage.DownloadByteArray;
    import com.github.tobato.fastdfs.exception.FdfsUnsupportStorePathException;
    import com.github.tobato.fastdfs.service.FastFileStorageClient;
    import kid1999.upload.config.FastDfsConfig;
    import lombok.extern.slf4j.Slf4j;
    import org.apache.commons.io.FilenameUtils;
    import org.apache.commons.lang3.StringUtils;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Component;
    import org.springframework.web.multipart.MultipartFile;

    import java.io.IOException;

    /**
    * @author kid1999
    * @desc:
    * @auther: kid1999
    * @date: 2019/12/19 19:25
    **/

    @Slf4j
    @Component
    public class FastDFSClientUtils {
    @Autowired
    private FastFileStorageClient storageClient;

    @Autowired
    private FastDfsConfig appConfig; // 项目参数配置

    /**
    * 上传文件
    * @param file 文件对象
    * @return 文件访问地址
    * @throws IOException
    */
    public String uploadFile(MultipartFile file) throws IOException {
    StorePath storePath = storageClient.uploadFile(file.getInputStream(),file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()),null);
    return getResAccessUrl(storePath);
    }

    /**
    * 查询文件信息
    * @param fileUrl
    * @return FileInfo 远程文件信息
    */
    public FileInfo getFileInfo(String fileUrl){
    if (StringUtils.isEmpty(fileUrl)) {
    return null;
    }
    StorePath storePath = StorePath.parseFromUrl(fileUrl);
    return storageClient.queryFileInfo(storePath.getGroup(), storePath.getPath());
    }



    /**
    * 删除文件
    * @param fileUrl 文件访问地址
    * @return
    */
    public void deleteFile(String fileUrl) {
    if (StringUtils.isEmpty(fileUrl)) {
    return;
    }
    try {
    StorePath storePath = StorePath.parseFromUrl(fileUrl);
    storageClient.deleteFile(storePath.getGroup(), storePath.getPath());
    } catch (FdfsUnsupportStorePathException e) {
    log.warn(e.getMessage());
    }
    }

    /**
    * 下载文件
    * @param fileUrl 文件访问地址
    * @return
    */
    public byte[] downloadFile(String fileUrl){
    if(StringUtils.isEmpty(fileUrl)){
    return null;
    }
    try {
    StorePath storePath = StorePath.parseFromUrl(fileUrl);
    DownloadByteArray callback = new DownloadByteArray();
    byte[] content = storageClient.downloadFile(storePath.getGroup(), storePath.getPath(), callback);
    return content;
    }catch (Exception e){
    log.warn(e.getMessage());
    }
    return null;
    }

    /**
    * 更新文件(复写)
    * @param file
    * @param oldFileUrl 原文件
    * @return 文件路径
    * @throws IOException
    */
    public String updateFile(MultipartFile file,String oldFileUrl) throws IOException {
    deleteFile(oldFileUrl);
    StorePath storePath = storageClient.uploadFile(file.getInputStream(),file.getSize(), FilenameUtils.getExtension(file.getOriginalFilename()),null);
    return getResAccessUrl(storePath);
    }

    /**
    * 封装完整URL地址
    * @param storePath
    */
    private String getResAccessUrl(StorePath storePath) {
    String fileUrl = "http://" + appConfig.getResHost()
    + ":" + appConfig.getStoragePort() + "/" + storePath.getFullPath();
    return fileUrl;
    }
    }

——-这里已经完成正常的CRUD操作——-

——-下面是一些具体案例——-

6.封装上面完成上传文件和批量打包下载

  • 上传文件
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
// 文件上传处理
@PostMapping("/upfile")
@ResponseBody
Result singleFileUpload(@RequestParam("file") MultipartFile file,
@RequestParam("workid") int workid,
@RequestParam("type") String type,
@RequestParam("remarks") String remarks,
@RequestParam("name") String sname,
@RequestParam("studentClass") String classname,
@RequestParam("studentId") String studentno,
Model model,
HttpServletRequest request) {

Result result = new Result();
if (file.isEmpty()){
return Result.fail(400,"文件不允许为空");
}
// 获取来访页面url
String referer = request.getHeader("referer");
if(referer != null){
model.addAttribute("referer",referer);
}else{
model.addAttribute("referer",request.getHeader("host"));
}
// 文件信息处理
String fname = file.getOriginalFilename();
String filename = "";
// 姓名-项目名 学号-项目名 学号-姓名-项目名 班级-姓名-项目名 班级-姓名-学号-项目名
// 处理保存的文件名
switch (type){
case "1" : filename = sname + "-" + fname;break;
case "2" : filename = studentno + "-" + fname;break;
case "3" : filename = studentno + "-" + sname + "-" + fname;break;
case "4" : filename = classname + "-" + sname + "-" + fname;break;
case "5" : filename = classname + "-" + sname + "-" + studentno + "-" + fname;break;
case "6" : filename = sname + "." + fname.substring(fname.lastIndexOf(".") + 1);break;
case "7" : filename = sname + "." + fname.substring(fname.lastIndexOf(".") + 1);break;
}
log.info(sname+ "--" + studentno + "--" + filename );
// 构造student
Student newStudent = new Student();
newStudent.setName(sname);
newStudent.setClassname(classname);
newStudent.setRemarks(remarks);
newStudent.setUptime(new Timestamp(System.currentTimeMillis()));
newStudent.setWorkid(workid);
newStudent.setFilename(filename);

// 先查看是否重复
Student student = studentService.getStudentBySname(workid,sname);

// 文件已提交过了
if(student != null){
try{
newStudent.setId(student.getId()); // 把id带走
String fileUrl = fastDFSClientUtils.updateFile(file,student.getFileurl()); //使用fastDFS写入
newStudent.setFileurl(fileUrl);
studentService.updateStudent(newStudent);
return Result.success("你已经提交过了,上传成功!");
}catch (Exception e){
return Result.fail(400,"文件上传失败!");
}
}

try{
String fileUrl = fastDFSClientUtils.uploadFile(file);
newStudent.setFileurl(fileUrl);
studentService.addStudent(newStudent);
return Result.success("文件上传成功!");
}catch (Exception e){
return Result.fail(400,"文件上传失败!");
}
}
  • 批量打包下载

封装批量打包下载工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
package kid1999.upload.utils;

import kid1999.upload.dto.ZipModel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

import javax.servlet.http.HttpServletResponse;
import java.io.*;
import java.util.List;
import java.util.zip.ZipEntry;
import java.util.zip.ZipOutputStream;

/**
* @desc:
* @auther: kid1999
* @date: 2019/12/19 20:21
**/
@Component
@Slf4j
public class ZipUtil {


@Autowired
private FastDFSClientUtils fastDFSClientUtils;

/**
* 压缩文件列表中的文件
*
* @param files
* @param outputStream
* @throws IOException
*/
public void zipFile(List<ZipModel> files, ZipOutputStream outputStream){
try {
int size = files.size();
//压缩列表中的文件
for (int i = 0; i < size; i++) {
ZipModel zipModel = files.get(i);
zipFile(zipModel, outputStream);
}
} catch (IOException e) {
log.error(e.getMessage());
}
}

/**
* 将文件写入到zip文件中
*
* @param zipModel
* @param outputstream
* @throws IOException
*/
public void zipFile(ZipModel zipModel, ZipOutputStream outputstream) throws IOException {
if (zipModel != null && zipModel.getFilePath() != null && zipModel.getFileName() != null) {
log.info(zipModel.getFileName() + ",被下载: " + zipModel.getFilePath());
byte[] content = fastDFSClientUtils.downloadFile(zipModel.getFilePath());
InputStream bInStream = new ByteArrayInputStream(content);
ZipEntry entry = new ZipEntry(zipModel.getFileName());
outputstream.putNextEntry(entry);
final int MAX_BYTE = 10 * 1024 * 1024; //最大的流为10M
long streamTotal = 0; //接受流的容量
int streamNum = 0; //流需要分开的数量
int leaveByte = 0; //文件剩下的字符数
byte[] inOutbyte; //byte数组接受文件的数据

streamTotal = bInStream.available(); //通过available方法取得流的最大字符数
streamNum = (int) Math.floor(streamTotal / MAX_BYTE); //取得流文件需要分开的数量
leaveByte = (int) streamTotal % MAX_BYTE; //分开文件之后,剩余的数量

if (streamNum > 0) {
for (int j = 0; j < streamNum; ++j) {
inOutbyte = new byte[MAX_BYTE];
//读入流,保存在byte数组
bInStream.read(inOutbyte, 0, MAX_BYTE);
outputstream.write(inOutbyte, 0, MAX_BYTE); //写出流
}
}
//写出剩下的流数据
inOutbyte = new byte[leaveByte];
bInStream.read(inOutbyte, 0, leaveByte);
outputstream.write(inOutbyte);
outputstream.closeEntry();
bInStream.close(); //关闭
}
}

/**
* 下载打包的文件
* @param file
* @param response
*/
public void downloadZip(File file, HttpServletResponse response) {
try {
if (!file.exists()) {
file.createNewFile();
}
// 以流的形式下载文件。
BufferedInputStream fis = new BufferedInputStream(new FileInputStream(file.getPath()));
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
fis.close();
// 清空response
response.reset();

OutputStream toClient = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + file.getName());
toClient.write(buffer);
toClient.flush();
toClient.close();
file.delete(); //将生成的服务器端文件删除
} catch (IOException ex) {
ex.printStackTrace();
}
}

}

调用批量打包下载工具

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
package kid1999.upload.controller;

import kid1999.upload.dto.ZipModel;
import kid1999.upload.utils.ZipUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.File;
import java.io.FileOutputStream;
import java.util.ArrayList;
import java.util.List;
import java.util.zip.ZipOutputStream;

@RestController
@Slf4j
public class download {

@Autowired
private ZipUtil zipUtil;

@PostMapping("download")
void download(HttpServletRequest request,
HttpServletResponse response) {
try {
if (request.getParameterValues("filenames") == null) {
response.sendRedirect(request.getHeader("REFERER"));
} else {
List<ZipModel> zipModelList = new ArrayList<>();
String[] filenames = request.getParameterValues("filenames");
for (int i = 0; i < filenames.length; i++) {
String[] files = filenames[i].split(" ");
zipModelList.add(new ZipModel(files[0], files[1]));
}
//todo:设置打包后的文件名
String fileName = "File.zip";
//todo:临时文件目录,用于存储打包的下载文件
String globalUploadPath = request.getSession().getServletContext().getRealPath("/");
String outFilePath = globalUploadPath + File.separator + fileName;
File file = new File(outFilePath);
//文件输出流 压缩流
ZipOutputStream toClient = new ZipOutputStream(new FileOutputStream(file));
//todo:调用通用方法下载fastfds文件,打包成zip文件
zipUtil.zipFile(zipModelList, toClient);
toClient.close();
response.setHeader("content-disposition", "attachment;fileName=" + fileName);
//todo:将zip文件下载下来
zipUtil.downloadZip(file, response);
}
} catch (Exception e) {
log.error(e.getMessage());
}
}
}