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图片
效果: