FastDfs分布式文件存储系统

49 篇文章 2 订阅
36 篇文章 1 订阅

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称为存储服务器。

在这里插入图片描述

在这里插入图片描述

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

在这里插入图片描述

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

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

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

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

文件。

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

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

在这里插入图片描述

参考配置服务器地址: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

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

在这里插入图片描述

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镜像+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又只需要暴露给服务器本身:

在这里插入图片描述

强烈推荐有效,有效,有效,加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

在这里插入图片描述

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

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

在这里插入图片描述

退出容器

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文件存储 -->
<fastdfs.version>1.26.2</fastdfs.version>
<dependency>
    <groupId>com.github.tobato</groupId>
    <artifactId>fastdfs-client</artifactId>
    <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下载的效果,名字不是原来的那个。

在这里插入图片描述

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

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;

}


在这里插入图片描述

在这里插入图片描述

在这里插入图片描述

以上是文件服务提供方,下是服务者,可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 =
                    "<p class=\"form-group row mt-4\">\n" +
                    "  <b class=\"col-sm-2\"></b>\n" +
                    "  <b class=\"square\"></b>\n" +
                    "  <a target=\"_blank\" href=\"http://192.168.169.133:8012/onlinePreview?url=" + data.data.url + "\"" +
                    "     style=\"margin-right: 15px\">" + data.data.name + "</a>\n" +
                    "  <a class=\"text-primary\" href=\"javascript:void(0);\"" +
                    "     οnclick=\"fileDownload('" + data.data.url + "', '" + data.data.name + "')\"\n" +
                    "     style=\"margin-right: 15px\">下载</a>\n" +
                    "  <a target=\"_blank\" class=\"text-primary\"\n" +
                    "     href=\"http://192.168.169.133:8012/onlinePreview?url=" + data.data.url + "\"\n" +
                    "     style=\"margin-right: 15px\">预览</a>\n" +
                    "  <a class=\"text-primary\"\n" +
                    "     href=\"javascript:void(0);\" οnclick=\"fileDelete(this, '" + data.data.id + "')\"\n" +
                    "     style=\"margin-right: 15px\">删除</a>\n" +
                    "</p>";
                $("#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文件存储 -->
<fastdfs.version>1.27.0.0</fastdfs.version>
<dependency>
    <groupId>net.oschina.zcx7878</groupId>
    <artifactId>fastdfs-client-java</artifactId>
    <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];
    }
}

在这里插入图片描述

选择post请求方式

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

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

在这里插入图片描述

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

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

在这里插入图片描述

测试

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是定义后台接口链接

<template>
     <div class="upload_pic" >
              <el-form  status-icon label-width="100px">
                <img :src="upload_img_url" class="upload_pic_show" />
                <el-form-item label="用户图片" prop="logo">
                  <el-upload ref="myUpload" action="" :auto-upload="false">
                    <el-button size="small" type="primary">点击选择图片</el-button>
                  </el-upload>
                </el-form-item>
                <el-form-item>
              <el-button type="primary" @click="fnUpload" size="small">开始上传</el-button>
            </el-form-item>
        </el-form>
     </div>
</template>
<script>
import { uploadImg } from  '@/api/publish'
export default {
  name:"upload",
  props:['imgChange'],
  data () {
      return  {
         upload_img_url:require('@/assets/pic_bg.png'),
      }
  },
  methods:{
      //上传图片
     async fnUpload () {
        let files = document.querySelector('.el-upload .el-upload__input').files ;
        if(files && files.length) {
          let fd = new FormData();
          fd.append('file', files[0],files[0].name);
          let result = await uploadImg(fd)
          this.$message({message:'上传成功',type:'success'}) && (this.upload_img_url = result.data.url)
          debugger;
          this.imgChange && this.imgChange(result.url) //调用上层的方法 通知数据变化
        }else{
           this.$message({message:"请选择一张图片",type:"warning"})
        }
      }
  }
}
</script>

<style>
 .upload_pic_show{
    display:block;
    width:240px;
    height:180px;
    margin:15px auto 10px;
  }
</style>


pic图片

在这里插入图片描述

效果:

在这里插入图片描述

  • 2
    点赞
  • 7
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 4
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论 4
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

wzq_55552

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值