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 storagecd /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/fdfsstore_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: 600pool:# 从池中借出的对象的最大数目max-total: 153# 获取连接时的最大等待毫秒数100max-wait-millis: 102# 缩略图生成参数,可选thumbImage:width: 150height: 150# 跟踪服务器tracker_server请求地址,支持多个,这里只有一个,如果有多个在下方加- x.x.x.x:porttrackerList:- 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;@Autowiredpublic 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 {}
测试
    @Testpublic void Upload() {String fileUrl = this.getClass().getResource("/test.png").getPath();File file = new File(fileUrl);String str = FastDFSClient.uploadFile(file);FastDFSClient.getResAccessUrl(str);}@Testpublic void Delete() {FastDFSClient.deleteFile("group1/M00/00/00/wKiph146XvSAPjfuAAWUNhTgPOk116.png");}
上传图片文件serviceimpl

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

@RequestParam("file") MultipartFile file

@Slf4j@Autowiredprivate Mapper mapper;@Overridepublic 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;//上传图片获得文件idtry {// 上传到fastDFSClientfileId = 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@Autowiredprivate Mapper mapper;@Autowiredprivate 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 {// 传入图片idFastDFSClient.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;//上传图片获得文件idtry {// 上传到fastDFSClientfileId = 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 {// 传入图片idboolean 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 {@Resourceprivate 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();}@Overridepublic String getFilename() {return this.filename;}
}

controller访问文件微服务

    @Resourceprivate 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//方法一:ByteArrayResourceByteArrayResource resource = new MultipartFileResource(file);// 方法二:接受到文件流后先暂时持久化到本地临时文件夹,然后转发,本地生成临时文件的,最好不使用//FileSystemResource resource = new FileSystemResource(new File("文件本地磁盘路径"));MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();body.add("file", resource);//2、 headersHttpHeaders 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+headerHttpEntity<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//方法一:ByteArrayResourceByteArrayResource resource = new MultipartFileResource(file);// 方法二:接受到文件流后先暂时持久化到本地临时文件夹,然后转发,本地生成临时文件的,最好不使用//FileSystemResource resource = new FileSystemResource(new File("文件本地磁盘路径"));MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();body.add("file", resource);//2、 headersHttpHeaders 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+headerHttpEntity<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 {@Resourceprivate RestTemplate restTemplate;@Resourceprivate 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//方法一:ByteArrayResourceByteArrayResource resource = new MultipartFileResource(file);// 方法二:接受到文件流后先暂时持久化到本地临时文件夹,然后转发,本地生成临时文件的,最好不使用//FileSystemResource resource = new FileSystemResource(new File("文件本地磁盘路径"));MultiValueMap<String, Object> body = new LinkedMultiValueMap<>();body.add("file", resource);//2、 headersHttpHeaders 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+headerHttpEntity<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);//数据,OKreturn new ResponseResult<File>(ResponseResult.CodeStatus.OK, "", f);}}

使用:

 @Resourceprivate FileRestTemplate fileRestTemplate;@PostMapping("/upload/file")@ResponseBodypublic 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,    // 必须是falsedata: 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,    // 必须是falsedata: 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: 50MBmax-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: 10MBapplication: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、gifprivate 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;}@Overridepublic 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地址,大小,看FileInforeturn 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 {//获取StorageClientStorageClient 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 {//获取StorageClientStorageClient 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{//获取TrackerServerTrackerServer trackerServer = getTrackerServer();//通过TrackerServer创建StorageClientStorageClient 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 {//封装一个FastDFSFileFastDFSFile 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信息,这里只有一个,是0System.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图片

效果:

FastDfs分布式文件存储系统相关推荐

  1. 搭建FastDFS分布式文件存储系统教程

    转载来源:https://github.com/happyfish100/fastdfs/wiki 搭建FastDFS分布式文件存储系统教程 环境准备 使用的系统软件 名称 说明 centos 7.x ...

  2. Docker部署Fastdfs分布式文件存储系统

    Docker部署Fastdfs分布式文件存储系统 Fastdfs分布式文件存储系统 拉取镜像 部署Tracker和Storage 开放端口 SpringBoot代码示例 Fastdfs分布式文件存储系 ...

  3. 超简单用Docker安装FastDFS分布式文件存储系统

    个人简介 作者是一个来自河源的大三在校生,以下笔记都是作者自学之路的一些浅薄经验,如有错误请指正,将来会不断的完善笔记,帮助更多的Java爱好者入门. Docker安装FastDFS分布式文件存储系统 ...

  4. FastDFS - 分布式文件存储系统(文件存储从此无忧)

    简介 FastDFS 是一个开源的高性能分布式文件系统(DFS). 它的主要功能包括:文件存储,文件同步和文件访问,以及高容量和负载平衡.主要解决了海量数据存储问题,特别适合以中小文件(建议范围:4K ...

  5. FastDFS - 分布式文件存储系统

    目录 一.分布式文件存储 1.分布式文件存储的由来 2.常见的分布式存储框架 二.FastDFS介绍 三.FastDFS安装 1.拉取镜像文件 2.构建Tracker服务 3.构建Storage服务 ...

  6. fastdfs安装_用asp.net core结合fastdfs打造分布式文件存储系统

    今天主要是对开发过程,以及对FastDFS这个通用的分布式文件存储服务的单机及集群安装部署过程做个总结.希望对想要自建分布式文件系统的朋友有所帮助. 什么是FastDFS 这里先简单介绍下分布式文件存 ...

  7. 用asp.net core结合fastdfs打造分布式文件存储系统

    最近被安排开发文件存储微服务,要求是能够通过配置来无缝切换我们公司内部研发的文件存储系统,FastDFS,MongDb GridFS,阿里云OSS,腾讯云OSS等.根据任务紧急度暂时先完成了通过配置来 ...

  8. client mac addr不能开机进不去系统_用asp.net core结合fastdfs打造分布式文件存储系统

    今天主要是对开发过程,以及对FastDFS这个通用的分布式文件存储服务的单机及集群安装部署过程做个总结.希望对想要自建分布式文件系统的朋友有所帮助. 什么是FastDFS 这里先简单介绍下分布式文件存 ...

  9. java电商项目搭建-------分布式文件存储系统(fastDFS)

    人之所以痛苦,那是因为你在成长.--------magic_guo 微服务项目,由于访问量和系统的高可用性能,会将上传的文件图片等存放在搭建的分布式文件存储系统:现在比较流行的文件存储系统有fastD ...

最新文章

  1. 删除json中的指定元素_DeleteFile,VBA中借助Windows Scripting Host删除指定文件
  2. C++判断一个数字是否是某个数字的阶乘(附完整源码)
  3. 南京理工大学计算机学院教授严捍,2019年7月1日学术报告二则(宋巍 教授,南京理工大学;张鹏程 副教授,河海大学)...
  4. c#中将整数转化为字符串_在C#中将字符串转换为字节数组
  5. 由已打开的文件读取数据---read
  6. 设计模式笔记二十一:状态模式
  7. Android AES加密算法及事实上现
  8. 用java实现combin函数_Java8的CompletableFuture
  9. VC++动态链接库(DLL)编程(三)――MFC规则DLL
  10. 七牛底层架构再完善 让服务从单一走向多元
  11. 酷客多小程序携手Richly network Pte Led正式进军新加坡市场
  12. Qt Quick实现的疯狂算数游戏
  13. c语言编程打印格式输出总结
  14. ✿ iBm T60 水货入手了 满意 2005
  15. Python-Selenium自动化登陆QQ空间
  16. java获取中文拼音或拼音首字母
  17. OAS的使用——概述
  18. 华为nova5 pro怎么升级鸿蒙系统,鸿蒙系统终于来了!这几款华为手机都可以升级了...
  19. 连接IBM MQ原因码报2537的错误解决记录
  20. 通信电子电路(2)---使用multisim分析电路小技巧、晶体管等效电路

热门文章

  1. ObiCloth布料缝合
  2. 马云被骗十亿?最后却被百倍奉还。
  3. jar包启动调用外系统文件上传,无法生成文件,报org.springframework.web.client. ResourceAccessException: I/0 error on POST
  4. 入门JAVA第十六天 数据库
  5. 教师节我用Python做了个学生点名系统送给老师当礼物,这回毕业稳了
  6. 列表的join方法,类方法formkeys,删除,集合,深浅拷贝赋值,冒泡排序
  7. Python数据分析学习系列 十三 Python建模库介绍
  8. js后代选择器_后代选择器和子元素选择器的区别
  9. Win10十二月更新系统讲了什么?
  10. python输出到文件里