FastDfs分布式文件存储系统

FastDfs分布式文件存储系统

FastDfs 是一个开源的高性能分布式文件系统(DFS)。 它的主要功能包括:文件存储,文件同步和文件访问,以及高容量和负载平衡。主要解决了海量数据存储问题,特别适合以中小文件(建议范围:4KB < file_size <500MB)为载体的在线服务。组件Tracker负责文件管理负载均衡操作,控制中心,可多个。Storage负责文件操作,可多组。

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

FastDFS为互联网量身定制,充分考虑了冗余备份、负载均衡、线性扩容等机制,并注重高可用、高性能等指标,使用FastDFS很容易搭建一套高性能的文件服务器集群提供文件上传、下载等服务。

FastDFS 架构包括 Tracker server 和 Storage server。客户端请求 Tracker server 进行文件上传、下载,通过Tracker server 调度最终由 Storage server 完成文件上传和下载。

Tracker server 作用是负载均衡和调度,通过 Tracker server 在文件上传时可以根据一些策略找到Storage server 提供文件上传服务。可以将 tracker 称为追踪服务器或调度服务器。Storage server 作用是文件存储,客户端上传的文件最终存储在 Storage 服务器上,Storageserver 没有实现自己的文件系统而是利用操作系统的文件系统来管理文件。可以将storage称为存储服务器。

FastDfs分布式文件存储系统_第1张图片

FastDfs分布式文件存储系统_第2张图片

客户端上传文件后存储服务器将文件 ID 返回给客户端,此文件 ID 用于以后访问该文件的索引信息。文件索引信息包括:组名,虚拟磁盘路径,数据两级目录,文件名。

在这里插入图片描述

组名:文件上传后所在的 storage 组名称,在文件上传成功后有storage 服务器返回,需要客户端自行保存。

虚拟磁盘路径:storage 配置的虚拟路径,与磁盘选项store_path*对应。如果配置了

store_path0 则是 M00,如果配置了 store_path1 则是 M01,以此类推。

数据两级目录:storage 服务器在每个虚拟磁盘路径下创建的两级目录,用于存储数据

文件。

文件名:与文件上传时不同。是由存储服务器根据特定信息生成,文件名包含:源存储

服务器 IP 地址、文件创建时间戳、文件大小、随机数和文件拓展名等信息。

FastDfs分布式文件存储系统_第3张图片

参考配置服务器地址:https://blog.csdn.net/bondsui/article/details/90115486(fastdfs+nginx)

参考:https://blog.csdn.net/qq_37759106/article/details/82981023

docker安装fastdfs

无法使用

#下载
docker pull qbanxiaoli/fastdfs
 
#启动 ip地址和80的nginx端口
docker run -d --restart=always --privileged=true --net=host --name=fastdfs -e IP=192.168.169.133 -e WEB_PORT=80 -v ${HOME}/fastdfs:/var/local/fdfs qbanxiaoli/fastdfs

#测试,上传文件
docker exec -it fastdfs /bin/bash
echo "Hello FastDFS!">index.html
fdfs_test /etc/fdfs/client.conf upload index.html

#成功如下,可以访问链接

FastDfs分布式文件存储系统_第4张图片

23000端口是storage服务的端口,在storage.conf里配置,如果服务器无法访问,storage服务连接超时,请设置安全组和防火墙开放。注意这里结合nginx80默认端口,也需要开发80端口安全组和防火墙,80端口默认不用输入端口。22122是tracker的端口,必需开放。

用这个安装,可行,服务器也不会错,但没有nginx

服务器环境:

docker exec -it fastdfs /bin/bash

#查看报错日志
cd /var/local/fdfs/storage/logs/
cat storaged.log

#修改fastdfs的storage或tracker配置文件
cd /etc/fdfs/
vi storage.conf
http.server_port=8888是网页访问图片的端口号

vi tracker.conf

23000端口是storage服务的端口,在storage.conf里配置,如果服务器无法访问,storage服务连接超时,请设置安全组和防火墙开放。22122是tracker的端口,必需开放,23000和nginx不需要也行的,看情况。

具体安装如下:

docker image pull delron/fastdfs

#运行tracker
docker run -dti --network=host --name tracker -v /var/fdfs/tracker:/var/fdfs delron/fastdfs tracker

#运行storage,注意这里的ip不要写成127.0.0.1
docker run -dti --network=host --name storage -e TRACKER_SERVER=192.168.169.135:22122 -v /var/fdfs/storage:/var/fdfs delron/fastdfs storage

cd /etc/fdfs/
vi storage.conf
http.server_port=8888是网页访问图片的端口号

#查看文件存放路径
cd /var/local/fdfs/storage/data/

所以服务器至少开放2个tcp端口,一个是22122和8888,其它看情况。
FastDfs分布式文件存储系统_第5张图片
FastDfs分布式文件存储系统_第6张图片

安装FastDFS镜像+nginx

如果一开始连接不上,请暴露22122的授权组为所有0.0.0.0/0,它好像会连接其他ip来生成storage.其实需要暴露给我本地开发和连接服务器本身2个ip,所以需要先设置0.0.0.0/0,但是部署了jar就只需要暴露给服务器本身。

23000/22122/80都要暴露。80暴露给测试ip使用。

3个看情况,都可以。如果未部署微服务时,23000需要暴露给本机开发,22122的授权组为所有0.0.0.0/0。已经部署的话,23000和22122又只需要暴露给服务器本身:

FastDfs分布式文件存储系统_第7张图片

强烈推荐有效,有效,有效,加nginx

拉取镜像

docker pull morunchang/fastdfs

运行tracker

docker run -d --name tracker --net=host morunchang/fastdfs sh tracker.sh

运行storage

docker run -d --name storage --net=host -e TRACKER_IP=192.168.169.133:22122 -e GROUP_NAME=group1 morunchang/fastdfs sh storage.sh
  • 使用的网络模式是–net=host, 192.168.169.133是宿主机的IP
  • group1是组名,即storage的组
  • 如果想要增加新的storage服务器,再次运行该命令,注意更换 新组名
#配置文件的存放路径
cd /etc/fdfs

store_path0=/data/fast_data

#文件存放路径,日志
cd /data/fast_data/data

配置Nginx

Nginx在这里主要提供对FastDFS图片访问的支持,Docker容器中已经集成了Nginx,我们需要修改nginx的配置,进入storage的容器内部,修改nginx.conf

docker exec -it storage  /bin/bash
#注意主机名不变

进入后

vi /etc/nginx/conf/nginx.conf

server里面添加以下内容:

location ~ /M00 {
     root /data/fast_data/data;
     ngx_fastdfs_module;
}

禁止缓存(很重要,文件删除了,用户不用自己清除浏览器缓存,不然文件删除了还可以访问,存在浏览器缓存中):

//添加这句
add_header Cache-Control no-store;

//如下
location ~ /M00 {
     add_header Cache-Control no-store;
     root /data/fast_data/data;
     ngx_fastdfs_module;
}

注意,这里每次访问的端口是8080端口,访问的端口其实是storage容器的nginx端口,如果想修改该端口可以直接进入到storage容器,然后修改即可。

docker exec -it storage  /bin/bash
vi /etc/nginx/conf/nginx.conf

FastDfs分布式文件存储系统_第8张图片

修改成80端口,url不用写入端口即可访问。8080可能被tomat占用。

服务器安全组记得开放80端口。

FastDfs分布式文件存储系统_第9张图片

退出容器

exit

重启storage容器

docker restart storage

查看启动容器docker ps

9f2391f73d97 morunchang/fastdfs "sh storage.sh" 12 minutes ago Up 12 seconds storage
e22a3c7f95ea morunchang/fastdfs "sh tracker.sh" 13 minutes ago Up 13 minutes tracker

开启启动设置

docker update --restart=always tracker
docker update --restart=always storage

Springboot集成

使用这个就好了,有用,有用,有用

依赖
# 不加集群

<fastdfs.version>1.26.2fastdfs.version>
<dependency>
    <groupId>com.github.tobatogroupId>
    <artifactId>fastdfs-clientartifactId>
    <version>${fastdfs.version}version>
dependency>
配置
# 分布式文件系统fastdfs配置
fdfs:
  # socket连接超时时长
  soTimeout: 1500
  # 连接tracker服务器超时时长
  connectTimeout: 600
  pool:
    # 从池中借出的对象的最大数目
    max-total: 153
    # 获取连接时的最大等待毫秒数100
    max-wait-millis: 102
  # 缩略图生成参数,可选
  thumbImage:
    width: 150
    height: 150
  # 跟踪服务器tracker_server请求地址,支持多个,这里只有一个,如果有多个在下方加- x.x.x.x:port
  trackerList:
    - 192.168.169.135:22122

  # 存储服务器storage_server访问地址,如果没nginx则需要加http.stotage_server端口
  web-server-url: http://192.168.169.135/
  
#不支持了
#spring:
#  http:
#    multipart:
#      max-file-size: 100MB # 最大支持文件大小
#      max-request-size: 100MB # 最大支持请求大小
启动类加入
@Import(FdfsClientConfig.class)
@EnableMBeanExport(registration = RegistrationPolicy.IGNORE_EXISTING)
工具类
import com.github.tobato.fastdfs.conn.FdfsWebServer;
import com.github.tobato.fastdfs.domain.StorePath;
import com.github.tobato.fastdfs.proto.storage.DownloadByteArray;
import com.github.tobato.fastdfs.service.FastFileStorageClient;
import org.apache.commons.io.FilenameUtils;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;

import java.io.*;

@Component
public class FastDFSClient {

    private static Logger log = LoggerFactory.getLogger(FastDFSClient.class);

    private static FastFileStorageClient fastFileStorageClient;

    private static FdfsWebServer fdfsWebServer;

    @Autowired
    public void setFastDFSClient(FastFileStorageClient fastFileStorageClient, FdfsWebServer fdfsWebServer) {
        FastDFSClient.fastFileStorageClient = fastFileStorageClient;
        FastDFSClient.fdfsWebServer = fdfsWebServer;
    }

    /**
     * @param multipartFile 文件对象
     * @return 返回文件地址
     * @author qbanxiaoli
     * @description 上传文件
     */
    public static String uploadFile(MultipartFile multipartFile) {
        try {
            StorePath storePath = fastFileStorageClient.uploadFile(multipartFile.getInputStream(), multipartFile.getSize(), FilenameUtils.getExtension(multipartFile.getOriginalFilename()), null);
            return storePath.getFullPath();
        } catch (IOException e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param multipartFile 图片对象
     * @return 返回图片地址
     * @author qbanxiaoli
     * @description 上传缩略图
     */
    public static String uploadImageAndCrtThumbImage(MultipartFile multipartFile) {
        try {
            StorePath storePath = fastFileStorageClient.uploadImageAndCrtThumbImage(multipartFile.getInputStream(), multipartFile.getSize(), FilenameUtils.getExtension(multipartFile.getOriginalFilename()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param file 文件对象
     * @return 返回文件地址
     * @author qbanxiaoli
     * @description 上传文件
     */
    public static String uploadFile(File file) {
        try {
            FileInputStream inputStream = new FileInputStream(file);
            StorePath storePath = fastFileStorageClient.uploadFile(inputStream, file.length(), FilenameUtils.getExtension(file.getName()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param file 图片对象
     * @return 返回图片地址
     * @author qbanxiaoli
     * @description 上传缩略图
     */
    public static String uploadImageAndCrtThumbImage(File file) {
        try {
            FileInputStream inputStream = new FileInputStream(file);
            StorePath storePath = fastFileStorageClient.uploadImageAndCrtThumbImage(inputStream, file.length(), FilenameUtils.getExtension(file.getName()), null);
            return storePath.getFullPath();
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

    /**
     * @param bytes         byte数组
     * @param fileExtension 文件扩展名
     * @return 返回文件地址
     * @author qbanxiaoli
     * @description 将byte数组生成一个文件上传
     */
    public static String uploadFile(byte[] bytes, String fileExtension) {
        ByteArrayInputStream stream = new ByteArrayInputStream(bytes);
        StorePath storePath = fastFileStorageClient.uploadFile(stream, bytes.length, fileExtension, null);
        return storePath.getFullPath();
    }

    /**
     * @param fileUrl 文件访问地址
     * @param file    文件保存路径
     * @author qbanxiaoli
     * @description 下载文件
     */
    public static boolean downloadFile(String fileUrl, File file) {
        try {
            StorePath storePath = StorePath.praseFromUrl(fileUrl);
            byte[] bytes = fastFileStorageClient.downloadFile(storePath.getGroup(), storePath.getPath(), new DownloadByteArray());
            FileOutputStream stream = new FileOutputStream(file);
            stream.write(bytes);
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
        return true;
    }

    /**
     * @param fileUrl 文件访问地址
     * @author qbanxiaoli
     * @description 删除文件
     */
    public static boolean deleteFile(String fileUrl) {
        if (StringUtils.isEmpty(fileUrl)) {
            return false;
        }
        try {
            StorePath storePath = StorePath.praseFromUrl(fileUrl);
            fastFileStorageClient.deleteFile(storePath.getGroup(), storePath.getPath());
        } catch (Exception e) {
            log.error(e.getMessage());
            return false;
        }
        return true;
    }

    // 封装文件完整URL地址
    public static String getResAccessUrl(String path) {
        String url = fdfsWebServer.getWebServerUrl() + path;
        log.info("上传文件地址为:\n" + url);
        return url;
    }
    
    // 封装图片地址的前缀
    public static String getFrontUrl() {
        String url = String.valueOf(fdfsWebServer.getWebServerUrl());
        log.info("封装图片地址的前缀:\n" + url);
        return url;
    }

}
配置类
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;

/**
 * FastDfs描扫配置
 */
@Configuration
@ComponentScan("com.lead.news.configuration.fastdfs")//配置的处的包名
public class FdConfig {
}

测试
    @Test
    public void Upload() {
        String fileUrl = this.getClass().getResource("/test.png").getPath();
        File file = new File(fileUrl);
        String str = FastDFSClient.uploadFile(file);
        FastDFSClient.getResAccessUrl(str);
    }

    @Test
    public void Delete() {
        FastDFSClient.deleteFile("group1/M00/00/00/wKiph146XvSAPjfuAAWUNhTgPOk116.png");
    }

上传图片文件serviceimpl

mapper存图片地址或图片id到数据库

@RequestParam("file") MultipartFile file

@Slf4j

    @Autowired
    private Mapper mapper;

    @Override
    public ResponseResult uploadPicture(MultipartFile multipartFile) {
        // 上传时的文件名全部
        String originFileName = multipartFile.getOriginalFilename();
        // 后缀
        String extName = originFileName.substring(originFileName.lastIndexOf(".") + 1);
        // 判断是不是图片
        if(!extName.matches("(gif|png|jpg|jpeg)")) {
            // "文件格式错误"
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_IMAGE_FORMAT_ERROR);
        }
        String fileId = null;
        //上传图片获得文件id
        try {
            // 上传到fastDFSClient
            fileId = FastDFSClient.uploadFile(multipartFile);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("user {} upload file {} to fastDFS error, error info:n",
                    user.getId(),
                    originFileName, e.getMessage());
            // "服务器内部问题"
            return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
        }
        //上传成功保存到数据库,图片地址,类型
        Class class = new Class();
        //保存图片地址或图片id到数据库
        class.setUrl(fileId);
        mapper.insert(class);
        //设置返回值则是完整地址
        wmMaterial.setUrl(FastDFSClient.getResAccessUrl(fileId));
        return ResponseResult.okResult(class);
    }

删除图片文件serviceimpl
@Slf4j

    @Autowired
    private Mapper mapper;

    @Autowired
    private Mapper2 mapper2;

    public ResponseResult delPicture(WmMaterialDto dto) {
        if (dto == null || dto.getId() == null) {
            //"参数无效"
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        //删除fastDFS上的文件数据
        Class class = mapper.selectByPrimaryKey(dto.getId());
        if (class == null) {
            //"不存在数据"
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID);
        }
        // 图片与其他关联表,被引用,则不可删除,变量是图片数据的主键
        Example example = new Example(Class2.class);
        example.createCriteria().andEqualTo("变量",dto.getId());
        int count = mapper2.selectCountByExample(example);
        if (count > 0) {
            return ResponseResult.errorResult(AppHttpCodeEnum.PARAM_INVALID,
                    "当前图片被引用");
        }
        String fileId = mapper.getUrl();
        try {
            // 传入图片id
            FastDFSClient.deleteFile(fileId);
        } catch (Exception e) {
            e.printStackTrace();
            log.error("user {} delete file {} from fastDFS error, error info:n",
                    user.getId(),
                    fileId, e.getMessage());
            return ResponseResult.errorResult(AppHttpCodeEnum.SERVER_ERROR);
        }
        //删除数据库记录
        mapper.deleteByPrimaryKey(dto.getId());
        return ResponseResult.okResult(AppHttpCodeEnum.SUCCESS);
    }

文件service

工具类修改下载方法:

    /**
     * @param fileUrl 文件访问地址
     * @author qbanxiaoli
     * @description 下载文件
     */
    public static byte[] downloadFile(String fileUrl) {
        try {
            StorePath storePath = StorePath.praseFromUrl(fileUrl);
            byte[] bytes = fastFileStorageClient.downloadFile(storePath.getGroup(), storePath.getPath(), new DownloadByteArray());
            //将字节数组转换成字节输入流
            //return new ByteArrayInputStream(bytes);
            return bytes;
        } catch (Exception e) {
            log.error(e.getMessage());
            return null;
        }
    }

service:

import com.wzq.da.chuang.commons.dto.ResponseResult;
import com.wzq.da.chuang.file.utils.FastDFSClient;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;

@Service
@Slf4j
public class FileService {

    public ResponseResult<String> upload(MultipartFile multipartFile) {
        // 上传时的文件名全部
        String originFileName = multipartFile.getOriginalFilename();
        // 后缀
        String extName = originFileName.substring(originFileName.lastIndexOf(".") + 1);
        // 判断是不是符合要求的文件后缀
        if(!extName.matches("(png|jpg|jpeg|doc|docx|pdf|pptx|xls|xlsx|txt|md)")) {
            // "文件格式错误"
            return new ResponseResult<String>(ResponseResult.CodeStatus.FAIL,"文件格式错误",null);
        }
        String fileId = null;
        //上传图片获得文件id
        try {
            // 上传到fastDFSClient
            fileId = FastDFSClient.uploadFile(multipartFile);
        } catch (Exception e) {
            e.printStackTrace();
            // "服务器内部问题"
            return new ResponseResult<String>(ResponseResult.CodeStatus.FAIL,"服务器内部问题",null);
        }
        fileId = FastDFSClient.getFrontUrl()+fileId;
        return new ResponseResult<String>(ResponseResult.CodeStatus.OK,"上传文件成功",fileId);
    }


    public ResponseResult<Void> delete(String fileUrl) throws Exception {
        try {
            // 传入图片id
            boolean b = FastDFSClient.deleteFile(fileUrl);
            if (b){
                return new ResponseResult<Void>(ResponseResult.CodeStatus.OK,"删除文件成功");
            }
            return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"服务器内部问题");
        } catch (Exception e) {
            e.printStackTrace();
            return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"服务器内部问题");
        }
    }

    public byte[] download(String fileUrl) throws Exception {
        return FastDFSClient.downloadFile(fileUrl);
    }
}

文件controller

其实文档文件下载直接浏览器访问链接就可以了,但是图片类的文件无法下载,得用输入流。

有问题,就是文件名下载下来变了(随机数)。可以在服务消费方,把文件名存入数据库,前端也只显示数据库的名字,一起返回给前端。消费者调用文件服务提供方的下载controller时可以带名字(包括后缀)和路径过来。名字只是为了重新赋值为下载的文件名,具体实现如下:

这是直接访问url下载的效果,名字不是原来的那个。

FastDfs分布式文件存储系统_第10张图片

前端设置名字即可,类似这样:

request({
      url:'http://47.113.80.250:9003/report/admin/excel',
      method:'POST',
       responseType: 'blob',
         headers: {
       'Content-Type': 'application/json'
     }
    }).then(res => {
      const content = res;
      const blob = new Blob([content],{type:'application/ms-excel'});
     const fileName = 'year年国家级、省级大学生创新创业训练项目中期检查验收结果.xlsx';//下载文件名称
     const elink = document.createElement('a');
     elink.download = fileName;
     elink.style.display = 'none';
     elink.href = URL.createObjectURL(blob);
     document.body.appendChild(elink);
     elink.click();
     URL.revokeObjectURL(elink.href); // 释放URL 对象
     document.body.removeChild(elink);
    })

import com.wzq.da.chuang.commons.dto.ResponseResult;
import com.wzq.da.chuang.file.service.impl.FileService;
import com.wzq.da.chuang.file.utils.FastDFSClient;
import com.wzq.da.chuang.model.dto.file.DownloadDto;
import org.springframework.util.StringUtils;
import org.springframework.web.bind.annotation.CrossOrigin;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import javax.servlet.ServletOutputStream;
import javax.servlet.http.HttpServletResponse;
import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;
import java.net.URLEncoder;
import java.util.Map;

@CrossOrigin(origins = "*", maxAge = 3600)
@RestController
public class FileController {

    @Resource
    private FileService fileService;

    /***
     * 文件上传
     * @return
     */
    @PostMapping(value = "/upload")
    public ResponseResult<String> upload(@RequestParam("file") MultipartFile file) throws Exception {
        if (file != null){
            return fileService.upload(file);
        }
        return new ResponseResult<String>(ResponseResult.CodeStatus.FAIL,"文件为空",null);
    }

    /***
     * 文件删除
     * @return
     */
    @PostMapping(value = "/delete")
    public ResponseResult<Void> delete(@RequestBody Map<String,String> url) throws Exception {
        if (!StringUtils.isEmpty(url.get("fileUrl"))){
            //去掉文件路径前缀
            String fileUrl = DeleteFrontUrl(url.get("fileUrl"));
            return fileService.delete(fileUrl);
        }
        return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"文件url为空");
    }

    /***
     * 文件下载
     * @return
     */
    @PostMapping(value = "/download")
    public ResponseResult<ByteArrayInputStream> download(@RequestBody DownloadDto downloadDto, HttpServletResponse response) throws Exception {
        if (downloadDto!=null && !StringUtils.isEmpty(downloadDto.getFileName())
        && !StringUtils.isEmpty(downloadDto.getFileUrl()) ){
            try {
                //去掉文件路径前缀
                String fileUrl = DeleteFrontUrl(downloadDto.getFileUrl());
                byte[] bytes = fileService.download(fileUrl);
                // 需要在上传的时候保存文件名。下载的时候使用对应的格式,消费者传名字(包括后缀)和路径过来,路径去掉前缀
                response.setHeader("Content-disposition", "attachment;filename=" + URLEncoder.encode(downloadDto.getFileName(), "UTF-8"));
                response.setCharacterEncoding("UTF-8");
                ServletOutputStream outputStream = null;
                try {
                    outputStream = response.getOutputStream();
                    outputStream.write(bytes);
                    return new ResponseResult<ByteArrayInputStream>(ResponseResult.CodeStatus.OK,"下载成功",null);
                } catch (IOException e) {
                    e.printStackTrace();
                } finally {
                    try {
                        assert outputStream != null;
                        outputStream.flush();
                        outputStream.close();
                    } catch (IOException e) {
                        e.printStackTrace();
                    }
                }
                return new ResponseResult<ByteArrayInputStream>(ResponseResult.CodeStatus.FAIL,"服务器内部错误",null);
            }catch (Exception e){
                e.printStackTrace();
                return new ResponseResult<ByteArrayInputStream>(ResponseResult.CodeStatus.FAIL,"服务器内部错误",null);
            }
        }
        return new ResponseResult<ByteArrayInputStream>(ResponseResult.CodeStatus.FAIL,"文件参数不足",null);
    }

    //去掉文件路径前缀
    public static String DeleteFrontUrl(String fileUrl){
        fileUrl = fileUrl.replace(FastDFSClient.getFrontUrl(),"");
        return fileUrl;
    }

//    public static void main(String[] args) {
//        String s = DeleteFrontUrl("http://47.113.80.250/group1/M00/00/00/rBJg-l6NpqKAT-baAATuTCSFI4s96.docx");
//        System.out.println(s);
//    }

}

dto:

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@AllArgsConstructor
@NoArgsConstructor
@ApiModel(description = "下载文件dto",value = "下载文件dto")
public class DownloadDto implements Serializable {

    private static final long serialVersionUID = -434377331557361280L;

    @ApiModelProperty(value = "文件路径",required = true)
    private String fileUrl;

    @ApiModelProperty(value = "文件名",required = true)
    private String fileName;

}


FastDfs分布式文件存储系统_第11张图片

FastDfs分布式文件存储系统_第12张图片

FastDfs分布式文件存储系统_第13张图片

以上是文件服务提供方,下是服务者,可feign或http。

消费方调用

启动类注入:

    @Bean(name = "restTemplate")
    public RestTemplate restTemplate() {
        return new RestTemplate();
    }

重写ByteArrayResource,让response携带文件上传。

import org.springframework.core.io.ByteArrayResource;
import org.springframework.web.multipart.MultipartFile;

import java.io.IOException;

/**
 * 参考:
 * https://github.com/spring-projects/spring-framework/issues/18147
 *
 * @author May
 * @since 2020/1/15 15:32
 */
public class MultipartFileResource extends ByteArrayResource {

    private String filename;

    public MultipartFileResource(MultipartFile multipartFile) throws IOException {
        super(multipartFile.getBytes());
        this.filename = multipartFile.getOriginalFilename();
    }

    @Override
    public String getFilename() {
        return this.filename;
    }
}

controller访问文件微服务

    @Resource
    private RestTemplate restTemplate;   

    /**
     * 文件上传,可多个
     * @param files
     * @return
     */
    @PostMapping("/file/insert")
    @ApiOperation(value = "文件上传,可多个,在中期报告信息生成后发送,如果是修改中期报告,重新上传文件也是这个接口")
    @PreAuthorize("hasAnyAuthority('ReportFileInsert','Student')") // 资源权限
    public ResponseResult<Void> fileInsert(@RequestParam("files") MultipartFile[] files,@RequestParam("reportId") Long reportId) throws IOException {
        if (files != null && files.length > 0 && !StringUtils.isEmpty(reportId)){

            for (MultipartFile file : files) {

                // 1 body
                //方法一:ByteArrayResource
                ByteArrayResource resource = new MultipartFileResource(file);
                // 方法二:接受到文件流后先暂时持久化到本地临时文件夹,然后转发,本地生成临时文件的,最好不使用
                //FileSystemResource resource = new FileSystemResource(new File("文件本地磁盘路径"));
                MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
                body.add("file", resource);

                //2、 headers
                HttpHeaders headers = new HttpHeaders();

                //设置 接受的类型
                ArrayList<MediaType> acceptMediaTypes = new ArrayList<>();
                acceptMediaTypes.add(MediaType.APPLICATION_JSON);

                //2.1 设置header 等效 headers.add("Accept", APPLICATION_JSON.toString());
                headers.setAccept(acceptMediaTypes);

                //2.2 设置header 是表单提交 等效
                //headers.setContentType(MediaType.MULTIPART_FORM_DATA);
                headers.setContentType(MediaType.parseMediaType("multipart/form-data;charset=UTF-8"));

                //3、构造请求体 body+header
                HttpEntity<Object> requestEntity = new HttpEntity<>(body, headers);

                //4、发送请求
                ResponseResult responseResult = restTemplate.postForObject("http://localhost:9002/upload", requestEntity ,ResponseResult.class);

                if (responseResult == null){
                    return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"文件上传失败");
                }
                System.out.println(responseResult.getCode()+":"+ResponseResult.CodeStatus.OK);
                if (!responseResult.getCode().equals(ResponseResult.CodeStatus.OK)){
                    return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"文件上传失败");
                }
                String url = (String) responseResult.getData();
                MFile mFile = new MFile();
                mFile.setFUrl(url);
                mFile.setReportId(reportId);
                mFile.setFName(file.getOriginalFilename());//文件名
                mFileService.insertSelective(mFile);
            }
            return new ResponseResult<Void>(ResponseResult.CodeStatus.OK,"文件上传成功");
        }
        return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"参数不足");
    }



    /**
     * 文件上传,一个
     * @param file
     * @return
     */
    @PostMapping("/file/insert/one")
    @ApiOperation(value = "文件上传,一个一个,我不知道前端要怎么传,多个文件的方法也有,单个文件的话,是这个")
    @PreAuthorize("hasAnyAuthority('ReportFileInsertOne','Student')") // 资源权限
    public ResponseResult<Void> fileInsertOne(@RequestParam("file") MultipartFile file,@RequestParam("reportId") Long reportId) throws IOException {
        if (file != null && !StringUtils.isEmpty(reportId)){

            // 1 body
            //方法一:ByteArrayResource
            ByteArrayResource resource = new MultipartFileResource(file);
            // 方法二:接受到文件流后先暂时持久化到本地临时文件夹,然后转发,本地生成临时文件的,最好不使用
            //FileSystemResource resource = new FileSystemResource(new File("文件本地磁盘路径"));
            MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
            body.add("file", resource);

            //2、 headers
            HttpHeaders headers = new HttpHeaders();

            //设置 接受的类型
            ArrayList<MediaType> acceptMediaTypes = new ArrayList<>();
            acceptMediaTypes.add(MediaType.APPLICATION_JSON);

            //2.1 设置header 等效 headers.add("Accept", APPLICATION_JSON.toString());
            headers.setAccept(acceptMediaTypes);

            //2.2 设置header 是表单提交 等效
            //headers.setContentType(MediaType.MULTIPART_FORM_DATA);
            headers.setContentType(MediaType.parseMediaType("multipart/form-data;charset=UTF-8"));

            //3、构造请求体 body+header
            HttpEntity<Object> requestEntity = new HttpEntity<>(body, headers);

            //4、发送请求
            ResponseResult responseResult = restTemplate.postForObject("http://localhost:9002/upload", requestEntity ,ResponseResult.class);

            if (responseResult == null){
                return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"文件上传失败");
            }
            System.out.println(responseResult.getCode()+":"+ResponseResult.CodeStatus.OK);
            if (!responseResult.getCode().equals(ResponseResult.CodeStatus.OK)){
                return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"文件上传失败");
            }
            String url = (String) responseResult.getData();
            MFile mFile = new MFile();
            mFile.setFUrl(url);
            mFile.setReportId(reportId);
            mFile.setFName(file.getOriginalFilename());//文件名
            mFileService.insertSelective(mFile);

            return new ResponseResult<Void>(ResponseResult.CodeStatus.OK,"文件上传成功");
        }
        return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"参数不足");
    }



    /**
     * 文件删除
     * @param fileId
     * @return
     */
    @PostMapping("/file/delete")
    @ApiOperation(value = "文件删除,一个一个删,点击文件后的删除按钮,就删除了对应该中期报告的一个文件,给我fileId")
    @PreAuthorize("hasAnyAuthority('ReportFileDelete','Student')") // 资源权限
    public ResponseResult<Void> fileDelete(@RequestBody Map<String,String> fileId) {
        if (fileId!=null && !StringUtils.isEmpty(fileId.get("fileId"))){

            String id = fileId.get("fileId");
            MFile mFile = mFileService.selectByPrimaryKey(Long.valueOf(id));

            if (mFile == null || StringUtils.isEmpty(mFile.getFUrl())){
                return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"文件不存在");
            }

            MReport mReport = mReportService.selectByPrimaryKey(mFile.getReportId());
            if (mReport.getTApproval().equals(2)){
                return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"中期报告导师已通过,无法修改");
            }

            String fUrl = mFile.getFUrl();
            //删除文件
            Map<String, String> map = new HashMap<>();
            map.put("fileUrl",fUrl);
            ResponseResult responseResult = restTemplate.postForObject("http://localhost:9002/delete", map, ResponseResult.class);
            if (responseResult!=null && responseResult.getCode().equals(ResponseResult.CodeStatus.OK)) {
                //删除数据库纪录
                mFileService.deleteByPrimaryKey(id);
                return new ResponseResult<Void>(ResponseResult.CodeStatus.OK,"删除成功");
            }
            return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"删除失败");
        }
        return new ResponseResult<Void>(ResponseResult.CodeStatus.FAIL,"参数为空");
    }

消费方封装

文件上传封装,返回上传成功的文件id

/**
 * Title:文件通用工具类
 * Description:
 * @author WZQ
 * @version 1.0.0
 * @date 2020/7/20
 */
public class FileUtils {

    /**
     * 文件后缀判断
     * @param originFileName 上传时的文件名全部
     * @return
     */
    public static boolean verificationFile(String originFileName){
        // 后缀
        String extName = originFileName.substring(originFileName.lastIndexOf(".") + 1);
        // 判断是不是图片、文档、excel、pdf、压缩包等文件后缀
        if(!extName.matches("(tif|gif|png|jpg|jpeg|bmp|doc|docx|pdf|xls|xlsx|rar|zip)")) {
            // "文件格式错误"
            return false;
        }
        return true;
    }

    /**
     * 图片后缀判断
     * @param originFileName 上传时的文件名全部
     * @return
     */
    public static boolean verificationImage(String originFileName){
        // 后缀
        String extName = originFileName.substring(originFileName.lastIndexOf(".") + 1);
        // 判断是不是图片、文档、excel、pdf、压缩包等文件后缀
        if(!extName.matches("(png|jpg|jpeg)")) {
            // "文件格式错误"
            return false;
        }
        return true;
    }
}


import com.ye.competition.commons.constant.SystemConstant;
import com.ye.competition.commons.dto.ResponseResult;
import com.ye.competition.commons.utils.FileUtils;
import com.ye.competition.model.pojos.File;
import com.ye.competition.web.config.MultipartFileResource;
import com.ye.competition.web.service.FileService;
import org.apache.commons.lang3.StringUtils;
import org.springframework.core.io.ByteArrayResource;
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.stereotype.Component;
import org.springframework.util.LinkedMultiValueMap;
import org.springframework.util.MultiValueMap;
import org.springframework.web.client.RestTemplate;
import org.springframework.web.multipart.MultipartFile;

import javax.annotation.Resource;
import java.io.IOException;
import java.util.ArrayList;

@Component
public class FileRestTemplate {

    @Resource
    private RestTemplate restTemplate;

    @Resource
    private FileService fileService;

    /**
     * 文件上传
     * @param file
     * @return
     * @throws IOException
     */
    public ResponseResult<File> upload(MultipartFile file) throws IOException {
        if (file == null || file.isEmpty()) {
            return new ResponseResult<File>(ResponseResult.CodeStatus.FAIL, "您还没有文件!");
        }
        String fileName = file.getOriginalFilename();
        if (StringUtils.isBlank(fileName) || !FileUtils.verificationFile(fileName)) {
            return new ResponseResult<File>(ResponseResult.CodeStatus.FAIL, "文件格式不正确!");
        }

        // 1 body
        //方法一:ByteArrayResource
        ByteArrayResource resource = new MultipartFileResource(file);
        // 方法二:接受到文件流后先暂时持久化到本地临时文件夹,然后转发,本地生成临时文件的,最好不使用
        //FileSystemResource resource = new FileSystemResource(new File("文件本地磁盘路径"));
        MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();
        body.add("file", resource);

        //2、 headers
        HttpHeaders headers = new HttpHeaders();

        //设置 接受的类型
        ArrayList<MediaType> acceptMediaTypes = new ArrayList<>();
        acceptMediaTypes.add(MediaType.APPLICATION_JSON);

        //2.1 设置header 等效 headers.add("Accept", APPLICATION_JSON.toString());
        headers.setAccept(acceptMediaTypes);

        //2.2 设置header 是表单提交 等效
        //headers.setContentType(MediaType.MULTIPART_FORM_DATA);
        headers.setContentType(MediaType.parseMediaType("multipart/form-data;charset=UTF-8"));

        //3、构造请求体 body+header
        HttpEntity<Object> requestEntity = new HttpEntity<>(body, headers);

        //4、发送请求,"http://localhost:9002/upload"
        ResponseResult responseResult = restTemplate.postForObject(SystemConstant.FILE_PATH + SystemConstant.FILE_UNLOAD,
                requestEntity, ResponseResult.class);

        if (responseResult == null) {
            return new ResponseResult<File>(ResponseResult.CodeStatus.FAIL, "文件上传失败!");
        }
        System.out.println(responseResult.getCode() + ":" + ResponseResult.CodeStatus.OK);
        if (!responseResult.getCode().equals(ResponseResult.CodeStatus.OK)) {
            return new ResponseResult<File>(ResponseResult.CodeStatus.FAIL, "文件上传失败!");
        }

        //拿到文件路径, 存到数据库
        String url = (String) responseResult.getData();
        File f = new File();
        f.setUrl(url);
        f.setName(fileName);
        fileService.insertSelective(f);

        //数据,OK
        return new ResponseResult<File>(ResponseResult.CodeStatus.OK, "", f);
    }

}

使用:

	@Resource
    private FileRestTemplate fileRestTemplate;

	@PostMapping("/upload/file")
    @ResponseBody
    public ResponseResult<File> uploadFile(@RequestParam("file") MultipartFile file,
                                           @RequestParam("progressId") String progressId) throws IOException {
        UserDto userDto = hostHolder.getUser();
        if (userDto == null || userDto.getUser() == null) {
            return new ResponseResult<File>(ResponseResult.CodeStatus.FAIL, "登录失效!");
        }

        Long userId = userDto.getUser().getId();
        //文件上传
        ResponseResult<File> responseResult = fileRestTemplate.upload(file);
        if (!responseResult.getCode().equals(ResponseResult.CodeStatus.OK)) {
            return responseResult;
        }
        if (StringUtils.isEmpty(progressId)){
            return new ResponseResult<File>(ResponseResult.CodeStatus.FAIL, "缺少参数!");
        }

        FileRange fileRange = new FileRange();
        fileRange.setFileId(responseResult.getData().getId());
        fileRange.setProgressId(Long.valueOf(progressId));
        fileRangeService.insertBySelectiveStatus(fileRange);

        return new ResponseResult<File>(ResponseResult.CodeStatus.OK, "上传成功!", responseResult.getData());
    }

前端代码
/**
 * 下载表单提交
 * @param url
 * @param name
 */
function fileDownload(url, name) {
    $("#fileUrl").val(url);
    $("#fileName").val(name);
    document.getElementById('_form').submit();
}

//选择文件后调用,onchange,显示原文件名字
function getImageName(){
    var file = $('#headerImage')[0].files[0];
    if (file === undefined || file == null){
        $('#fileName').html("选择一张图片");
        return false;
    }
    var fileName = $('#headerImage')[0].files[0].name;
    $('#fileName').html(fileName);
}

/**
 * 上传头像图片
 */
function uploadImage() {
    // 创建一个form类型的数据
    var formData = new FormData();
    // console.log(filepath);
    var file = $('#headerImage')[0].files[0];
    if (file === undefined || file == null){
        $('#headerImage').attr("class", "custom-file-input is-invalid");
        $('#uploadMsg').html("请选择文件!");
        return false;
    }
    var fileName = $('#headerImage')[0].files[0].name;
    var size = $('#headerImage')[0].files[0].size;
    if(fileName.match(/\.(png|jpg|jpeg)$/) == null){
        $('#headerImage').attr("class", "custom-file-input is-invalid");
        $('#uploadMsg').html("上传头像必须为图片!");
        return false;
    }
    // 获取上传文件的数据
    formData.append('headerImage', $('#headerImage')[0].files[0]);
    formData.append('userId', $("#userId").val());
    $.ajax({
        url: CONTEXT_PATH +"/user/upload/header",
        type:"POST",
        dataType: "json",
        async: false,
        cache: false, //上传文件无需缓存
        processData: false,   // jQuery不要去处理发送的数据
        contentType:false,    // 必须是false
        data: formData,
        success:function(data){
            if (data.code == 200 || data.code == '200'){
                //成功
                $('#headerImage').attr("class", "custom-file-input");
                alert(data.message);
            }else {
                //失败
                $('#headerImage').attr("class", "custom-file-input is-invalid");
                $('#uploadMsg').html(data.message);
            }
        },
        error:function(e){
            alert("抱歉,服务器异常!");
        }
    });
}

//选择文件后调用,onchange,显示原文件名字
function getFileName(){
    var file = $('#file')[0].files[0];
    if (file === undefined || file == null){
        $('#fName').html("选择文件");
        return false;
    }
    var fileName = $('#file')[0].files[0].name;
    $('#fName').html(fileName);
}

/**
 * 上传文件
 */
function uploadFile(id) {
    // console.log("uploadFile");
    // 创建一个form类型的数据
    var formData = new FormData();
    // console.log(filepath);
    var file = $('#file')[0].files[0];
    if (file === undefined || file == null) {
        $('#file').attr("class", "custom-file-input is-invalid");
        $('#uploadMsg').html("请选择文件!");
        return false;
    }
    var fileName = $('#file')[0].files[0].name;
    var size = $('#file')[0].files[0].size;
    if (fileName.match(/\.(tif|gif|png|jpg|jpeg|bmp|doc|docx|pdf|xls|xlsx)$/) == null) {
        $('#file').attr("class", "custom-file-input is-invalid");
        $('#uploadMsg').html("文件格式不支持!");
        return false;
    }
    // 获取上传文件的数据
    formData.append('file', $('#file')[0].files[0]);
    formData.append('competitionId', id);
    $.ajax({
        url: CONTEXT_PATH + "/file/upload",
        type: "POST",
        dataType: "json",
        async: false,
        cache: false, //上传文件无需缓存
        processData: false,   // jQuery不要去处理发送的数据
        contentType: false,    // 必须是false
        data: formData,
        success: function (data) {
            if (data.code == 200 || data.code == '200') {
                //成功
                $('#file').attr("class", "custom-file-input");
                alert(data.message);
                var html =
                    "

\n" + " \n" + " \n" + " + data.data.url + "\"" + " style=\"margin-right: 15px\">" + data.data.name + "\n" + " + " οnclick=\"fileDownload('" + data.data.url + "', '" + data.data.name + "')\"\n" + " style=\"margin-right: 15px\">下载\n" + " + " href=\"http://192.168.169.133:8012/onlinePreview?url=" + data.data.url + "\"\n" + " style=\"margin-right: 15px\">预览\n" + " + " href=\"javascript:void(0);\" οnclick=\"fileDelete(this, '" + data.data.id + "')\"\n" + " style=\"margin-right: 15px\">删除\n" + "

"
; $("#uploadFiles").append(html); } else { //失败 $('#file').attr("class", "custom-file-input is-invalid"); $('#uploadMsg').html(data.message); } }, error: function (e) { alert("抱歉,服务器异常!"); } }); } /** * 删除文件 */ function fileDelete(obj, id) { if (!confirm("是否确定删除?")) { return; } $.ajax({ url: CONTEXT_PATH + "/file/delete/" + id, type: "POST", dataType: "json", success: function (data) { if (data.code === 200 || data.code === '200') { //成功 $(obj).parent().hide(); } else { //失败 alert(data.message); } }, error: function (e) { alert("抱歉,服务器异常!") } }); }
<div id="uploadFiles">
    <p th:each="file:${files}" class="form-group row mt-4">
        <b class="col-sm-2">b>
        <b class="square">b>
        <a target="_blank" th:text="${file.name}" th:href="@{http://192.168.169.133:8012/onlinePreview(url=${file.url})}" style="margin-right: 15px">a>
        <a class="text-primary" href="javascript:void(0);" th:data-url="${file.url}" th:data-name="${file.name}" onclick="fileDownload(this.getAttribute('data-url'), this.getAttribute('data-name'))" style="margin-right: 15px">下载a>
        <a target="_blank" class="text-primary" th:href="@{http://192.168.169.133:8012/onlinePreview(url=${file.url})}" style="margin-right: 15px">预览a>
        <a class="text-primary" href="javascript:void(0);" th:data-id="${file.id}" onclick="fileDelete(this, this.getAttribute('data-id'))" style="margin-right: 15px">删除a>
    p>
div>

<form id="_form" method="post" action="http://localhost:9002/download">
    <input type="hidden" id="fileUrl" name="fileUrl" value=""/>
    <input type="hidden" id="fileName" name="fileName" value=""/>
form>

注意配置

修改上传文件的最大大小,太小可能传不了

Spring Boot工程嵌入的tomcat限制了请求的文件大小,这一点在Spring Boot的官方文档中有说明,修改如下,消费者和服务提供方都要加:

spring:
  servlet:
    multipart:
      max-file-size: 50MB
      max-request-size: 200MB

Spingboot集成+nginx

依赖
# 加nginx

<fastdfs.version>1.27.0.0fastdfs.version>
<dependency>
    <groupId>net.oschina.zcx7878groupId>
    <artifactId>fastdfs-client-javaartifactId>
    <version>${fastdfs.version}version>
dependency>

FastDFS配置

在resources文件夹下创建fasfDFS的配置文件fdfs_client.conf

connect_timeout=60
network_timeout=60
charset=UTF-8
http.tracker_http_port=8080
tracker_server=192.168.211.132:22122

connect_timeout:连接超时时间,单位为秒。

network_timeout:通信超时时间,单位为秒。发送或接收数据时。假设在超时时间后还不能发送或接收数据,则本次网络通信失败

charset: 字符集

http.tracker_http_port :.tracker的http端口

tracker_server: tracker服务器IP和端口设置

application.yml配置

在resources文件夹下创建application.yml

spring:
  servlet:
    multipart:
      #单个文件大小
      max-file-size: 10MB
      #总上传的数据大小
      max-request-size: 10MB
  application:
    name: file

max-file-size是单个文件大小,max-request-size是设置总上传的数据大小

启动类

这里禁止了DataSource的加载创建,看需不需要。

@SpringBootApplication(exclude={DataSourceAutoConfiguration.class})

文件信息封装

文件上传一般都有文件的名字、文件的内容、文件的扩展名、文件的md5值、文件的作者等相关属性,我们可以创建一个对象封装这些属性,代码如下:

import java.io.Serializable;
import java.util.Arrays;

/**
 * Title:文件上传信息封装
 * Description:
 * @author WZQ
 * @version 1.0.0
 * @date 2020/3/2
 */
public class FastDFSFile implements Serializable {

    //文件名字
    private String name;
    //文件内容
    private byte[] content;
    //文件扩展名:jpg、png、gif
    private String ext;
    //文件MD5摘要值
    private String md5;
    //文件创建作者
    private String author;

    public FastDFSFile(String name, byte[] content, String ext, String md5, String author) {
        this.name = name;
        this.content = content;
        this.ext = ext;
        this.md5 = md5;
        this.author = author;
    }

    public FastDFSFile(String name, byte[] content, String ext) {
        this.name = name;
        this.content = content;
        this.ext = ext;
    }

    public FastDFSFile() {
    }

    public String getName() {
        return name;
    }

    public void setName(String name) {
        this.name = name;
    }

    public byte[] getContent() {
        return content;
    }

    public void setContent(byte[] content) {
        this.content = content;
    }

    public String getExt() {
        return ext;
    }

    public void setExt(String ext) {
        this.ext = ext;
    }

    public String getMd5() {
        return md5;
    }

    public void setMd5(String md5) {
        this.md5 = md5;
    }

    public String getAuthor() {
        return author;
    }

    public void setAuthor(String author) {
        this.author = author;
    }

    @Override
    public String toString() {
        return "FastDFSFile{" +
                "name='" + name + '\'' +
                ", content=" + Arrays.toString(content) +
                ", ext='" + ext + '\'' +
                ", md5='" + md5 + '\'' +
                ", author='" + author + '\'' +
                '}';
    }
}

工具类
import com.changgou.service.file.pojos.FastDFSFile;
import org.csource.common.NameValuePair;
import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.FileInfo;
import org.csource.fastdfs.ServerInfo;
import org.csource.fastdfs.StorageClient;
import org.csource.fastdfs.StorageServer;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerServer;
import org.springframework.core.io.ClassPathResource;

import java.io.ByteArrayInputStream;
import java.io.IOException;
import java.io.InputStream;

/**
 * Title:实现FastDfs文件管理
 * Description:文件上传
 *              文件删除
 *              文件下载
 *              文件信息获取
 *              storage信息获取
 *              tracker信息获取
 * @author WZQ
 * @version 1.0.0
 * @date 2020/3/2
 */
public class FastDFSClient {

    /***
     * 初始化tracker信息
     */
    static {
        try {
            //获取tracker的配置文件fdfs_client.conf的位置
            String filePath = new ClassPathResource("fdfs_client.conf").getPath();
            //加载tracker配置信息
            ClientGlobal.init(filePath);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

    /****
     * 文件上传
     * @param file : 要上传的文件信息封装->FastDFSFile
     * @return String[]
     *          0:文件上传所存储的组名
     *          1:文件存储路径
     */
    public static String[] upload(FastDFSFile file){
        //获取文件作者
        NameValuePair[] meta_list = new NameValuePair[1];
        meta_list[0] =new NameValuePair(file.getAuthor());

        /***
         * 文件上传后的返回值
         * uploadResults[0]:文件上传所存储的组名,例如:group1
         * uploadResults[1]:文件存储路径,例如:M00/00/00/wKjThF0DBzaAP23MAAXz2mMp9oM26.jpeg
         */
        String[] uploadResults = null;
        try {
            //获取StorageClient对象
            StorageClient storageClient = getStorageClient();
            //执行文件上传
            uploadResults = storageClient.upload_file(file.getContent(), file.getExt(), meta_list);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return uploadResults;
    }


    /***
     * 获取文件信息
     * @param groupName:组名 group1
     * @param remoteFileName:文件存储完整名
     * M00/00/00/wKjThF0DBzaAP23MAAXz2mMp9oM26.jpeg
     */
    public static FileInfo getFile(String groupName, String remoteFileName){
        try {
            //获取StorageClient对象
            StorageClient storageClient = getStorageClient();
            //获取文件信息,有ip地址,大小,看FileInfo
            return storageClient.get_file_info(groupName,remoteFileName);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /***
     * 文件下载
     * @param groupName:组名
     * @param remoteFileName:文件存储完整名
     * @return
     */
    public static InputStream downFile(String groupName, String remoteFileName){
        try {
            //获取StorageClient
            StorageClient storageClient = getStorageClient();
            //通过StorageClient下载文件
            byte[] fileByte = storageClient.download_file(groupName, remoteFileName);
            //将字节数组转换成字节输入流
            return new ByteArrayInputStream(fileByte);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /***
     * 文件删除实现
     * @param groupName:组名
     * @param remoteFileName:文件存储完整名
     */
    public static void deleteFile(String groupName,String remoteFileName){
        try {
            //获取StorageClient
            StorageClient storageClient = getStorageClient();
            //通过StorageClient删除文件
            storageClient.delete_file(groupName,remoteFileName);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }


    /***
     * 获取组信息
     * @param groupName :组名
     */
    public static StorageServer getStorages(String groupName){
        try {
            //创建TrackerClient对象
            TrackerClient trackerClient = new TrackerClient();
            //通过TrackerClient获取TrackerServer对象
            TrackerServer trackerServer = trackerClient.getConnection();
            //通过trackerClient获取Storage组信息
            return trackerClient.getStoreStorage(trackerServer,groupName);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /***
     * 根据文件组名和文件存储路径获取Storage服务的IP、端口信息
     * @param groupName :组名
     * @param remoteFileName :文件存储完整名
     */
    public static ServerInfo[] getServerInfo(String groupName, String remoteFileName){
        try {
            //创建TrackerClient对象
            TrackerClient trackerClient = new TrackerClient();
            //通过TrackerClient获取TrackerServer对象
            TrackerServer trackerServer = trackerClient.getConnection();
            //获取服务信息
            return trackerClient.getFetchStorages(trackerServer,groupName,remoteFileName);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return null;
    }

    /***
     * 获取Tracker服务地址
     */
    public static String getTrackerUrl(){
        try {
            //创建TrackerClient对象
            TrackerClient trackerClient = new TrackerClient();
            //通过TrackerClient获取TrackerServer对象
            TrackerServer trackerServer = trackerClient.getConnection();
            //获取Tracker地址
            return "http://"+trackerServer.getInetSocketAddress().getHostString()+":"+ClientGlobal.getG_tracker_http_port(); // 配置文件中获取
        } catch (IOException e) {
            e.printStackTrace();
        }
        return null;
    }

    /***
     * 获取TrackerServer
     */
    public static TrackerServer getTrackerServer() throws Exception{
        //创建TrackerClient对象
        TrackerClient trackerClient = new TrackerClient();
        //通过TrackerClient获取TrackerServer对象
        TrackerServer trackerServer = trackerClient.getConnection();
        return trackerServer;
    }

    /***
     * 获取StorageClient
     * @return
     * @throws Exception
     */
    public static StorageClient getStorageClient() throws Exception{
        //获取TrackerServer
        TrackerServer trackerServer = getTrackerServer();
        //通过TrackerServer创建StorageClient
        StorageClient storageClient = new StorageClient(trackerServer,null);
        return storageClient;
    }
}

controller

具体业务看情况,可以把图片路径存数据库,返回ResponseResult

@RestController
@CrossOrigin
public class FileController {

    /***
     * 文件上传
     * @return
     */
    @PostMapping(value = "/upload")
    public String upload(@RequestParam("file")MultipartFile file) throws Exception {
        //封装一个FastDFSFile
        FastDFSFile fastDFSFile = new FastDFSFile(
                file.getOriginalFilename(), //文件名字
                file.getBytes(),            //文件字节数组
                StringUtils.getFilenameExtension(file.getOriginalFilename()));//文件扩展名

        //文件上传
        String[] uploads = FastDFSClient.upload(fastDFSFile);
        //组装文件上传地址
        return FastDFSClient.getTrackerUrl()+"/"+uploads[0]+"/"+uploads[1];
    }
}

FastDfs分布式文件存储系统_第14张图片

选择post请求方式

填写Headers,不填也行,参数选类型file

Key:Content-Type
Value:multipart/form-data

FastDfs分布式文件存储系统_第15张图片

注意,这里每次访问的端口是8080端口,访问的端口其实是storage容器的nginx端口,如果想修改该端口可以直接进入到storage容器,然后修改即可。

docker exec -it storage  /bin/bash
vi /etc/nginx/conf/nginx.conf

FastDfs分布式文件存储系统_第16张图片

测试

public static void main(String[] args) throws Exception {
        //http://192.168.169.140:8080/group1/M00/00/00/wKipjF5cnK6AJZosAAWUNna0ROw287.png

        //测试获取文件信息
        FileInfo fileInfo = getFile("group1", "M00/00/00/wKipjF5cnK6AJZosAAWUNna0ROw287.png");
        System.out.println(fileInfo.getSourceIpAddr()); // ip地址
        System.out.println(fileInfo.getFileSize()); // 文件大小

        // 文件下载
        InputStream is = downFile("group1", "M00/00/00/wKipjF5cnK6AJZosAAWUNna0ROw287.png");
        // 将文件写入本地磁盘
        FileOutputStream os = new FileOutputStream("D:/1.jpg");
        // 定义缓冲区
        byte[] buffer = new byte[1024];
        while (is.read(buffer)!=-1){
            os.write(buffer);
        }
        os.flush();
        os.close();
        is.close();

        // 文件删除,设置storage禁止缓存
        deleteFile("group1", "M00/00/00/wKipjF5cnK6AJZosAAWUNna0ROw287.png");

        // 获取group1组信息
        StorageServer storageServer = getStorages("group1");
        System.out.println(storageServer.getStorePathIndex());// 获取Storage信息,这里只有一个,是0
        System.out.println(storageServer.getInetSocketAddress().getHostString()); // ip信息

        // 获取Storage的ip信息和端口信息
        ServerInfo[] serverInfo = getServerInfo("group1", "M00/00/00/wKipjF5cnK6AJZosAAWUNna0ROw287.png");
        // 这里Storage只有一个组
        for (ServerInfo info : serverInfo) {
            System.out.println(info.getIpAddr());
            System.out.println(info.getPort());
        }

        // 获取Tracker信息,即是图片前缀
        String trackerUrl = getTrackerUrl();

    }

前台组件

饿了么ui,vue文件

publish是定义后台接口链接







pic图片

FastDfs分布式文件存储系统_第17张图片

效果:

FastDfs分布式文件存储系统_第18张图片

你可能感兴趣的:(java,SpringBoot,java后端,分布式,nginx,微服务,java)