前言:
fastDFS是一个使用十分广泛的分布式文件存储系统,很多公司都在用它。
有幸我的业务中也涉及到,从开始的研究,到后来的使用,中间踩过许多坑,现将我的经验总结,便于大家和我以后查阅,防止踩坑。
参考连接:https://www.mongona.com/blog/5
只需要将下面的111.111.111.111替换为你的服务器的IP即可,其他都不要动。
docker pull delron/fastdfs
docker run -dti --network=host --name tracker -v /var/fdfs/tracker:/var/fdfs delron/fastdfs tracker
docker run -dti --network=host --name storage -e TRACKER_SERVER=111.111.111.111:22122 -v /var/fdfs/storage:/var/fdfs delron/fastdfs storage
application.yml配置参数
# 设置fastdfs
fdfs:
so-timeout: 1501
connect-timeout: 1601
thumb-image:
width: 150
height: 150
tracker-list: ${tracker_list:111.111.111.111:22122} // 用于配置文件调度
file_server_url: ${file_server_url:111.111.111.111:8888} // 自定义,用于从fdfs获取保存文件
文件分片上传不需要这个配置文件,普通文件上传才需要这个配置,将他放到resource同yml平级
connect_timeout=30
network_timeout=60
base_path=/home/fastdfs
log_level=info
use_connection_pool = false
connection_pool_max_idle_time = 3600
load_fdfs_parameters_from_tracker=false
use_storage_id = false
storage_ids_filename = storage_ids.conf
http.tracker_server_port=80
具体项目,文章最后,我会放到github上。
controller
业务需要,附加了许多参数
/**
* 将各种类型的文件,上传到fastdfs
*
* @param formId formId
* @param tenantId 企业id
* @param fieldName 字段名称
* @param imageWidth 图片宽度
* @param imageHeight 图片高度
* @param file 非图片文件
* @return 文件传输对象
* @throws Exception 异常
*/
@PostMapping(value = "/all/upload", consumes = MediaType.MULTIPART_FORM_DATA_VALUE)
@ApiOperation(value = "将各种类型的文件,上传到fastdfs")
@CrossOrigin // 解决前端跨域问题
public BaseVO<FileTransferVo> uploadAllKindsOfFile(
@ApiParam(value = "表单id") @RequestParam(value = "form_id", required = false) Long formId,
@ApiParam(value = "企业id") @RequestParam(value = "tenant_id", required = false) Long tenantId,
@ApiParam(value = "filedName") @RequestParam(value = "field_name", required = false) String fieldName,
@ApiParam(value = "图片长度") @RequestParam(value = "image_width", required = false) Integer imageWidth,
@ApiParam(value = "图片高度") @RequestParam(value = "image_height", required = false) Integer imageHeight,
@ApiParam(value = "文件") @RequestBody MultipartFile file
) {
if (file == null || file.isEmpty()) {
return new BaseVO(BaseVO.ILLEGAL_PARAM_CODE, "参数不合法");
}
//获取文件类型
String originalFilename = file.getOriginalFilename();
String fileType = originalFilename.substring(originalFilename.lastIndexOf(".") + 1);
FieldBaseVO field = null;
if (!StringUtils.isEmpty(fieldName) && !(originalFilename.endsWith("xls") || originalFilename.endsWith("xlsx")
|| originalFilename.endsWith("zip"))) {
field = metadataClient.getField(tenantId, formId, fieldName);
if (field == null) {
return new BaseVO(BaseVO.ILLEGAL_PARAM_CODE, "参数不合法");
}
if ("image".equals(field.getType()) && (imageHeight == null || imageWidth == null)) {
return new BaseVO(BaseVO.ILLEGAL_PARAM_CODE, "参数不合法");
}
FileVO fileVO = (FileVO) field;
if (!CollectionUtils.isEmpty(fileVO.getExtensions()) && !fileVO.getExtensions().contains(fileType)) {
return new BaseVO(BaseVO.ILLEGAL_PARAM_CODE, "参数不合法");
}
if (fileVO.getSize() != null && fileVO.getSize() * KB_NUMBER * KB_NUMBER < file.getSize()) {
return new BaseVO(BaseVO.ILLEGAL_PARAM_CODE, "参数不合法");
}
}
FileTransferVo fileTransferVo = setFileTransfer(field, imageWidth, imageHeight, fileType, file);
if (fileTransferVo != null) {
if (field != null && fileTransferVo.getIsImage() == 1) {
return fileService.uploadImage(fileTransferVo);
} else {
return fileService.uploadFile(fileTransferVo);
}
} else {
return new BaseVO(BaseVO.OTHER_CODE, "将各种类型的文件,上传到fastdfs失败");
}
}
/**
* 重新处理传输数据
*
* @param fieldBaseVO 文件对象
* @param imageWidth 图片宽
* @param imageHeight 图片高
* @param type 文件类型
* @param file 非图片文件
* @return 封装后的文件对象
* @throws Exception 抛出异常
*/
private FileTransferVo setFileTransfer(FieldBaseVO fieldBaseVO, Integer imageWidth, Integer imageHeight, String type,
MultipartFile file) {
FileTransferVo fileTransferVo = new FileTransferVo();
try {
List<Long> longs = snowflakeClient.uniqueIds(TWO);
fileTransferVo.setId(longs.get(0));
fileTransferVo.setUniqueName(longs.get(1).toString());
fileTransferVo.setFileName(file.getOriginalFilename());
fileTransferVo.setFileType(type);
fileTransferVo.setBytes(chunkUtil.toByteArray(file.getInputStream()));
//图片
if (!(fieldBaseVO == null || !"image".equals(fieldBaseVO.getType()))) {
fileTransferVo.setIsImage(1);
fileTransferVo.setImageHeight(imageHeight);
fileTransferVo.setImageWidth(imageWidth);
fileTransferVo.setCompress(((ImageVO) fieldBaseVO).getCompress());
fileTransferVo.setImageString(fastDFSUtil.imageToBase64(file));
fileTransferVo.setInputStream(file.getInputStream());
}
} catch (IOException e) {
log.error("setFileTransfer", e.getMessage());
return null;
}
return fileTransferVo;
}
service实现
import cn.cncommdata.file.vo.FileTransferVo;
import org.csource.common.MyException;
import org.csource.fastdfs.ClientGlobal;
import org.csource.fastdfs.StorageClient;
import org.csource.fastdfs.StorageServer;
import org.csource.fastdfs.TrackerClient;
import org.csource.fastdfs.TrackerServer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.ClassPathResource;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import java.util.Base64;
/**
* 上传图片
*
* @param fileTransferVo 上传图片对象
* @return 上传后返回地址
* @throws Exception 抛出异常
*/
public String uploadImage(FileTransferVo fileTransferVo) throws Exception {
StringBuffer path = new StringBuffer();
try {
String[] strings = getStorageClient().upload_file(inputToByte(fileTransferVo.getInputStream()),
fileTransferVo.getFileType(), null);
path.append(strings[0]).append("/").append(strings[1]);
} catch (MyException e) {
throw new Exception("获取连接失败!");
} catch (IOException e) {
throw new Exception("获取连接失败!");
}
return path.toString();
}
/**
* 获取fastDFS的storageClient
*
* @return storageClient
* @throws Exception 抛出异常
*/
private StorageClient getStorageClient() throws Exception {
// 初始化文件资源
ClassPathResource cpr = new ClassPathResource("fdfs_client.conf");
ClientGlobal.init(cpr.getClassLoader().getResource("fdfs_client.conf").getPath());
// 链接FastDFS服务器,创建tracker和Storage
TrackerClient trackerClient = new TrackerClient();
// 3、使用 TrackerClient 对象创建连接,获得一个 TrackerServer 对象。
TrackerServer trackerServer = trackerClient.getConnection();
// 4、创建一个 StorageServer 的引用,值为 null
StorageServer storageServer = null;
// 5、创建一个 StorageClient 对象,需要两个参数 TrackerServer 对象、StorageServer 的引用
return new StorageClient(trackerServer, storageServer);
}
/**
* inputStream 转 bytes数组
*
* @param inStream 流
* @return bytes数组
* @throws IOException 抛出异常
*/
public byte[] inputToByte(InputStream inStream)
throws IOException {
final int len = 100;
ByteArrayOutputStream swapStream = new ByteArrayOutputStream();
byte[] buff = new byte[len];
int rc = 0;
while ((rc = inStream.read(buff, 0, len)) > 0) {
swapStream.write(buff, 0, rc);
}
byte[] bytes = swapStream.toByteArray();
return bytes;
}
package cn.cncommdata.file.vo;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.ser.std.ToStringSerializer;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.InputStream;
import java.io.Serializable;
import java.util.List;
/**
* @author: leimin
* @description:
* @create: on 2019/03/26
**/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class FileTransferVo implements Serializable {
/**
* 主键id
*/
@JsonSerialize(using = ToStringSerializer.class)
private Long id;
/**
* 文件名称
*/
private String fileName;
/**
* 文件唯一名称
*/
private String uniqueName;
/**
* 缩略图
*/
private String thumbnail;
/**
* 文件保存路径
*/
private String path;
/**
* 创建时间
*/
private Long createTime;
/**
* 用于接收文件的bytes[]数组/图片base64string
*/
private String imageString;
/**
* 用于接收文件的输入流
*/
private InputStream inputStream;
/**
* 生成缩略图的最大边长,等比缩放
*/
private int compress;
/**
* 传输图片的限制大小,超出提示
*/
private int imageMaxMemory;
/**
* 前端传图片宽度
*/
private int imageWidth;
/**
* 前端传图片高度
*/
private int imageHeight;
/**
* 文件类型,例如:jpg,mp4等。
*/
private String fileType;
/**
* 用于解析文件是不是图片,0:否,1:是
*/
private int isImage;
/**
* 用于封装,分片
*/
private List<byte[]> bytes;
/**
* 是否上传成功,0:否,1:是
*/
private int isUpload;
}
# 将文件按10m大小等分分片
upload:
chunkSize: 10485760 # 10M
@Override
public BaseVO<FileTransferVo> uploadFile(FileTransferVo fileTransferVo) {
//1.redis记录分片信息,当前分片下标
String uploadKey = "fileKey:" + fileTransferVo.getId();
String currChunkKey = uploadKey + ":currChunkKey";
try {
//3.分片上传
uploadChunk(fileTransferVo, uploadKey, currChunkKey);
} catch (Exception e) {
log.info(e.getMessage());
return new BaseVO(BaseVO.OTHER_CODE, "上传文件失败!");
}
//设置当前时间戳
fileTransferVo.setCreateTime(System.currentTimeMillis());
fileTransferVo.setInputStream(null);
fileTransferVo.setImageString(null);
fileTransferVo.setBytes(null);
return new BaseVO(BaseVO.SUCCESS_CODE, "操作成功!", fileTransferVo);
}
/**
* 上传文件的分片信息,所有分片上传完成后保存到数据库
*
* @param fileTransferVo 上传对象
* @param uploadKey 上传key
* @param currChunkKey 分片下标
* @return 上传对象
* @throws Exception 抛出异常
*/
private FileTransferVo uploadChunk(FileTransferVo fileTransferVo, String uploadKey, String currChunkKey) throws Exception {
String fdfsPathKey = uploadKey + ":fdfsPathKey";
StorePath path = new StorePath();
//1.循环遍历文件,开始上传,
List<byte[]> list = fileTransferVo.getBytes();
for (int i = 0; i < list.size(); i++) {
if (returnActualLength(list.get(i)) == 0 && i < list.size() - 1) {
throw new Exception("传输的数据有问题!");
}
//获取当前上传分片编号
Object cNum = redisTemplate.opsForValue().get(currChunkKey);
int chunkCurrNumber = cNum != null ? (int) cNum : 0;
if (i == chunkCurrNumber) {
//2.开始上传分片
InputStream inputStream = new ByteArrayInputStream(list.get(i));
if (i == 0) {
path = appendFileStorageClient.uploadAppenderFile("group1", inputStream, list.get(i).length,
FileUtil.extName(fileTransferVo.getFileName()));
if (path == null) {
throw new Exception("获取远程文件路径出错!");
}
redisTemplate.opsForValue().set(fdfsPathKey, path.getPath());
} else {
String noGroupPath = (String) redisTemplate.opsForValue().get(fdfsPathKey);
if (StringUtils.isEmpty(noGroupPath)) {
throw new Exception("无法获取上传远程服务器文件出错!");
}
appendFileStorageClient.modifyFile("group1", noGroupPath, inputStream, list.get(i).length,
(long) chunkCurrNumber * chunkSize);
}
//3.一个分片上传完成
if (i + 1 < list.size()) {
redisTemplate.opsForValue().set(currChunkKey, "" + (i + 1));
} else if (i + 1 == list.size()) {
fileTransferVo.setPath(path.getGroup() + "/" + path.getPath());
//拼接访问ip地址
fileTransferVo.setPath(fileServerUrl + "/" + fileTransferVo.getPath());
saveFileTransferInDB(fileTransferVo);
// System.out.println(fileTransferVo.getPath());
}
}
}
//设置当前时间戳
fileTransferVo.setCreateTime(System.currentTimeMillis());
return fileTransferVo;
}
https://github.com/LM917178900/cnFile