FastDfs分布式文件存储系统

news2024/11/14 5:39:26

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图片

在这里插入图片描述

效果:

在这里插入图片描述

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.coloradmin.cn/o/131855.html

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

基于Springboot+Mybatis+mysql+vue+html校园招聘管理系统

基于SpringbootMybatismysqlvuehtml校园招聘管理系统二、系统介绍三、功能展示1.首页2.个人中心(学生端)3.简历信息管理&#xff08;学生端&#xff09;4.应聘信息(学生端)5.企业信息&#xff08;企业&#xff09;6.招聘信息管理&#xff08;企业&#xff09;7.应聘信息管理&am…

谷粒学院——Day15【微信支付】

❤ 作者主页&#xff1a;Java技术一点通的博客 ❀ 个人介绍&#xff1a;大家好&#xff0c;我是Java技术一点通&#xff01;(&#xffe3;▽&#xffe3;)~* &#x1f34a; 记得关注、点赞、收藏、评论⭐️⭐️⭐️ &#x1f4e3; 认真学习&#xff0c;共同进步&#xff01;&am…

【观察】美达电器:以数字化重塑质量管理体系,构筑车企新“护城河”

在汽车行业&#xff0c;越来越多的企业走上数字化转型道路&#xff0c;运用数字化手段&#xff0c;从产品研发、生产制造、供应链管理等方面优化内部协同&#xff0c;从而降低管理成本&#xff0c;提升市场竞争力。美达电器(重庆)有限公司&#xff08;以下简称美达电器&#xf…

day17-缓冲流转换流序列化流打印流Properties

day17_JAVAOOP 课程目标 1. 【理解】什么是缓冲流 2. 【掌握】缓冲流的使用 3. 【理解】转换流 4. 【理解】序列化流 5. 【理解】打印流 6. 【掌握】Properties集合的使用缓冲流 ​ 前期我们学习了基本的一些流&#xff0c;作为IO流的入门&#xff0c;今天我们要见识一些更强…

babylon.js魔方建模

本文主要内容可能和babylon并无太紧密的关联&#xff0c; 主要是对旋转&#xff08; 空间想象力 &#xff09;的练习。 本来想写个魔方练练&#xff0c;就想着顺便练练baboly. 结果反正是最重要的交互逻辑没有实现。 标题已经说明了本文的主题是建模&#xff0c;也就是说&…

ArcGIS基础实验操作100例--实验29矢量数据空间校正

本实验专栏参考自汤国安教授《地理信息系统基础实验操作100例》一书 实验平台&#xff1a;ArcGIS 10.6 实验数据&#xff1a;请访问实验1&#xff08;传送门&#xff09; 高级编辑篇--实验29 矢量数据空间校正 目录 一、实验背景 二、实验数据 三、实验步骤 &#xff08;1&…

android中service实现原理分析

前言&#xff1a; 一开始的目标是解决各种各样的ANR问题的&#xff0c;我们知道&#xff0c;ANR总体上分有四种类型&#xff0c;这四种类型有三种是和四大组件相对应的&#xff0c;所以&#xff0c;如果想了解ANR发生的根因&#xff0c;对安卓四大组件的实现流程是必须要了解的…

Odoo 16 企业版手册 - 库存管理之产品管理

产品管理 记录与产品相关的每个方面对于有效维护库存至关重要。Odoo 库存模块使您可以在数据库中配置新产品&#xff0c;这些产品将有效跟踪和监控所有操作&#xff0c;以加强各自产品的库存管理。库存模块中的产品配置过程与销售和购买模块的流程几乎相似。您将在库存的主菜单…

一步一步学爬虫(4)数据存储之CSV文件存储

一步一步学爬虫&#xff08;4&#xff09;数据存储之CSV文件存储4.3 CSV文件存储4.3.1 写入4.3.2 读取4.3.3 总结4.3 CSV文件存储 CSV&#xff0c;全称Comma-Separated Values&#xff0c;中文叫做逗号分隔值或字符分隔值&#xff0c;其文件以纯文本形式存储表格数据。CSV文件…

java.lang.OutOfMemoryError: GC overhead limit exceeded问题分析及解决

一、错误重现 2022-12-29 10:12:07.210 ERROR 73511 --- [nio-8001-exec-6] o.a.c.c.C.[.[.[/].[dispatcherServlet] : Servlet.service() for servlet [dispatcherServlet] in context with path [] threw exception [Handler dispatch failed; nested exception is java.…

SQL刷题宝典-MySQL速通力扣困难题

&#x1f4e2;作者&#xff1a; 小小明-代码实体 &#x1f4e2;博客主页&#xff1a;https://blog.csdn.net/as604049322 &#x1f4e2;欢迎点赞 &#x1f44d; 收藏 ⭐留言 &#x1f4dd; 欢迎讨论&#xff01; 本手册目录&#xff1a; 文章目录前言Markdown导入数据库python脚…

奇安信 工业互联网安全发展与实践 报告 学习笔记一 欢迎扶正

声明 本文是学习2021工业互联网安全发展与实践分析报告. 下载地址而整理的学习笔记,分享出来希望更多人受益,如果存在侵权请及时联系我们 主要观点 工业系统安全漏洞数量增长显著放缓&#xff0c;但超高危漏洞数量却大幅增加。统计显示&#xff0c;2021年&#xff0c;国内外…

Linux 软件包管理器 yum

1.什么是软件包 在Linux下安装软件&#xff0c;一个通常的办法是下载到程序的源代码&#xff0c;并进行编译&#xff0c;得到可执行程序。但是这样太麻烦了&#xff0c;于是有些人把一些常用的软件提前编译好, 做成软件包(可以理解成windows上的安装程序)放在一个服务器上&…

再见2022

大家好&#xff0c;我是bigsai&#xff0c;好久不见。看了上一篇更新时间&#xff0c;大概已经停更近10个月(呜呜后面还会坚持的)&#xff0c;在2022的最后一天&#xff0c;这一篇也算是对这一年做个总结。期间也收到一些朋友的问候和鼓励&#xff0c;确实自己在读研期间的前两…

山东大学2022-2023非关系型数据库(Nosql)期末考试

写在前面的话&#xff1a; 今年线上开卷考试&#xff0c;Nosql考试软工&#xff08;限选课&#xff09;和大数据&#xff08;必修课&#xff09;是一套试题&#xff0c;因此大数据所学的许多内容考试并无涉及。考察点主要以学过的四类Nosql数据库的相关知识为主。 试题如下&…

引用量超1400的经典语义分割方法BiSeNet解读

今天给大家分享语义分割领域非常经典的一篇论文&#xff1a;BiSeNet&#xff0c;该论文发表在了ECCV2018上&#xff0c;引用量超过1400。 开源代码地址&#xff1a;https://github.com/ycszen/TorchSeg 1.动机 语义分割任务&#xff0c;即为图片的每个像素分配一个标签&#…

嵌入式 程序调试之gdb+gdbserver+vscode可视化调试

嵌入式 程序调试之gdbgdbservervscode可视化调试 一、简述 记--使用过visual studio的都知道&#xff0c;它的单步调试真的好用&#xff0c;可以直接在源码下断点&#xff0c;实时查看内存变量、寄存器等相关信息。嵌入式linux开发多用的是gdb, 都是命令行执行的&#xff0c;毕…

python特殊数据类型应用(1)字典类型

目录python中特殊数据类型应用&#xff08;1&#xff09;字典类型字典类型定义字典类型注意事项字典类型的访问python中特殊数据类型应用&#xff08;1&#xff09;字典类型 python作为最流行的几种开发语言之一&#xff0c;在数据类型上和传统的c、c和java等有很大的不同&…

Typora使用方法

自用&#xff0c;有错误请谅解 tpora破解版使用学习使用&#xff1a; 链接&#xff1a;https://pan.baidu.com/s/1Wj46k3iVIzr-7kwQstp9nQ 提取码&#xff1a;2sa8 来源教程网址&#xff1a;Typora一款 Markdown 编辑器和阅读器 记得更改图片位置&#xff0c;以后就是相对路径…

babel及其使用

什么是Babel&#xff1f; Babel 是一个工具链&#xff0c;由大量的工具包组成&#xff0c;接下来我们逐步了解。主要用于将 ECMAScript 2015 版本的代码转换为向后兼容的 JavaScript 语法&#xff0c;以便能够运行在当前和旧版本的浏览器或其他环境中。 核心库 babel/core B…