package com.lc.ibps.cloud.file.service.impl;
import com.lc.ibps.api.base.context.CurrentContext;
import com.lc.ibps.api.base.file.FileInfo;
import com.lc.ibps.base.core.encrypt.EncryptUtil;
import com.lc.ibps.base.core.util.BeanUtils;
import com.lc.ibps.base.core.util.MapUtil;
import com.lc.ibps.base.core.util.string.StringUtil;
import com.lc.ibps.cloud.file.provider.FfmpegProvider;
import com.lc.ibps.cloud.file.util.FtpUtil;
import com.lc.ibps.cloud.redis.config.AppConfig;
import com.lc.ibps.cloud.redis.utils.RedisUtil;
import com.lc.ibps.components.upload.constants.FileParam;
import com.lc.ibps.components.upload.constants.SaveType;
import com.lc.ibps.components.upload.impl.AbstractUploadService;
import com.lc.ibps.components.upload.util.UploadUtil;
import org.apache.commons.io.IOUtils;
import org.apache.commons.net.ftp.FTPClient;
import org.apache.commons.net.ftp.FTPFile;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.*;
import java.util.*;
import java.util.concurrent.TimeUnit;
/**
* FTP文件服务实现
*
* <pre>
*
* 构建组:iform-comp-file-server
* 作者:eddy
* 邮箱:xuq@ankepower.com
* 日期:2018年5月22日-上午9:38:17
* 版权:广州安可信息技术有限公司版权所有
* </pre>
*/
public class UploadServiceFtpImpl<T extends FileInfo> extends AbstractUploadService<T> {
private static final Logger logger = LoggerFactory.getLogger(UploadServiceFtpImpl.class);
private static final String KEY = SaveType.ftp.name();
private static final Short FAIL_SIGN = 1;
private static final String FILEINFO_FILEPATH = "fileInfo.filePath";
private static final String FILEINFO_ID = "fileInfo.id";
private CurrentContext currentContext;
private AppConfig appConfig;
private FTPClient ftpClient;
private final static String ffmpegVideType = "mp4";
@Autowired
FfmpegProvider ffmpegProvider;
@Autowired
public void setCurrentContext(CurrentContext currentContext) {
this.currentContext = currentContext;
}
@Autowired
public void setAppConfig(AppConfig appConfig) {
this.appConfig = appConfig;
}
@Autowired
public void setFTPClient(FTPClient ftpClient) {
this.ftpClient = ftpClient;
}
@Override
public String getSaveType() {
return SaveType.ftp.name();
}
@Override
public T uploadFile(InputStream is, Map<String, Object> params) throws Exception {
T fileInfo = fileInfoPersistenceService.initFileInfo();
//总文件md5
String fileMd5 = MapUtil.getString(params, FileParam.FILE_MD5);
String originalFileName = MapUtil.getString(params, FileParam.ORIGINAL_FILE_NAME);
String chunk = MapUtil.getString(params, FileParam.CHUNK);
String chunkSize = MapUtil.getString(params, FileParam.CHUNK_SIZE);
byte[] byteArray = IOUtils.toByteArray(is);
String chunkMd5 = EncryptUtil.encryptMd5(byteArray);
fileInfo = this.getFileInfo(fileInfo, params);
fileInfo.setMd5(chunkMd5);
fileInfo.setIsFail(FAIL_SIGN);
String filePath = "";
if (StringUtil.isNotBlank(fileMd5) && StringUtil.isNotBlank(chunk)) {
//pcz 分片上传 分块文件上传路径规则 为 原路径/总文件md5(前端返回的随机字符)//文件 分块文件名暂时为 原文件名 + .{chunk}. +
String chunkPath = getChunkFilePath(fileMd5);
String chunkFileName = getChunkFileName(chunk);
filePath = FtpUtil.uploadChunkByBytes(ftpClient, "", chunkPath, chunkFileName, byteArray);
fileInfo.setFilePath(filePath);
} else {
filePath = FtpUtil.uploadByBytes(ftpClient, "", fileInfo.getMd5(), originalFileName, byteArray);
fileInfo.setFilePath(filePath);
}
fileInfo.setIsFail(null);
//分片上传的小分片,暂时不入库,否则系统管理中,附件数据太多
if (StringUtil.isNotBlank(filePath) && StringUtil.isBlank(chunk)) {
fileInfo = fileInfoPersistenceService.save(fileInfo, params);
}
return fileInfo;
}
@Override
public void deleteFile(String[] deleteIds) throws Exception {
// 获取对应实体类
for (int i = 0; i < deleteIds.length; i++) {
String deleteId = deleteIds[i];
T fileInfo = fileInfoPersistenceService.getLoaclUpload(deleteId);
if (BeanUtils.isEmpty(fileInfo)) {
logger.warn("根据主键Id:{},获取不到对应的实体", deleteId);
continue;
}
if (fileInfoPersistenceService.isUnique(fileInfo)) {
FtpUtil.remove(ftpClient, "", fileInfo.getMd5(), fileInfo.getFilePath());
}
fileInfoPersistenceService.deleteInfo(deleteId);
}
}
@Override
public T downloadFile(String downloadId) throws Exception {
// 获取对应实体类
T loaclUpload = fileInfoPersistenceService.getLoaclUpload(downloadId);
if (BeanUtils.isEmpty(loaclUpload)) {
throw new Exception("根据主键Id:" + downloadId + ",获取不到对应的实体");
}
byte[] readByte = FtpUtil.downloadToBytes(ftpClient, loaclUpload.getFilePath());
loaclUpload.setFileBytes(readByte);
return loaclUpload;
}
@Override
public T saveFile(Map<String, Object> params) throws Exception {
T fileInfo = this.getFileInfo(params);
if (isDuplicate()) {
this.transferTo(fileInfo);
}
// 调用 使用者实现方法 实现数据持久化
return fileInfoPersistenceService.save(fileInfo, params);
}
@Override
public boolean checkFileExists(T fileInfo) throws Exception {
T info = fileInfoPersistenceService.getByIsFail(fileInfo.getMd5());
String redisKey = null;
if (BeanUtils.isNotEmpty(info)) {
redisKey = appConfig.getRedisKey(KEY, currentContext.getCurrentUserAccount(), info.getMd5());
RedisUtil.redisTemplate.opsForHash().put(redisKey, FILEINFO_FILEPATH, info.getFilePath());
RedisUtil.redisTemplate.opsForHash().put(redisKey, FILEINFO_ID, info.getId());
RedisUtil.redisTemplate.expire(redisKey, FtpUtil.getExpireTimeSeconds(), TimeUnit.SECONDS);
return false;
}
try {
FtpUtil.stat(ftpClient, "", fileInfo.getMd5(), fileInfo.getFilePath());
redisKey = appConfig.getRedisKey(KEY, currentContext.getCurrentUserAccount(), fileInfo.getMd5());
RedisUtil.redisTemplate.opsForHash().put(redisKey, FILEINFO_FILEPATH, fileInfo.getFilePath());
RedisUtil.redisTemplate.expire(redisKey, FtpUtil.getExpireTimeSeconds(), TimeUnit.SECONDS);
return true;
} catch (Exception e) {
}
return false;
}
@Override
public boolean checkChunk(Map<String, Object> params) throws Exception {
// TODO Auto-generated method stub
return false;
}
@Override
public T mergeChunks(Map<String, Object> params) throws Exception {
// md5为分块存储最后路径/md5/
String fileMd5 = (String) params.get(FileParam.FILE_MD5);
String chunkPath = FtpUtil.getChunkPath(null, true, fileMd5);
String originFileName = (String) params.get(FileParam.ORIGINAL_FILE_NAME);
//登陆ftpClient
ftpClient = FtpUtil.connectFtp();
//获取所有分块文件
FTPFile[] files = ftpClient.listFiles(chunkPath);
//获取分块名称
String[] chunkFiles = getChunkFiles(files);
T fileInfo = this.getFileInfo(params);
//未合并分块与未切割视频
fileInfo.setIsFail((short) 1);
fileInfo = fileInfoPersistenceService.save(fileInfo, params);
T finalFileInfo = fileInfo;
Thread thread = new Thread(() -> {
try {
//异步合并分块文件
InputStream fileInputStream = FtpUtil.mergeChunkFilesAndUploadToFtp(ftpClient, finalFileInfo, chunkFiles, chunkPath, originFileName);
//如果是需要视频流播放的格式,分割视屏流
if (ffmpegVideType.equals(finalFileInfo.getExt())) {
ffmpegProvider.splitVideo(finalFileInfo, fileInputStream);
}
//合并分块与切割视频
finalFileInfo.setIsFail((short) 0);
} catch (Exception e) {
//合并分块成功切割视频
finalFileInfo.setIsFail((short) 1);
logger.info("merge file or ffmpeg error ", e.getMessage());
}
fileInfoPersistenceService.update(finalFileInfo, new HashMap<>());
}, "mergerffmpegThread");
thread.start();
return fileInfo;
}
/* 私有方法 */
private T getFileInfo(T fileInfo, Map<String, Object> params) throws Exception {
fileInfo = UploadUtil.getFileInfo(fileInfo, params);
return fileInfo;
}
/**
* 获取新的文件信息
*
* @param params
* @return
* @throws Exception
*/
private T getFileInfo(Map<String, Object> params) throws Exception {
String fileMd5 = (String)params.get(FileParam.FILE_MD5);
T fileInfo = fileInfoPersistenceService.initFileInfo();
String redisKey = appConfig.getRedisKey(KEY, currentContext.getCurrentUserAccount(), fileMd5);
Object filePath = RedisUtil.redisTemplate.opsForHash().get(redisKey, FILEINFO_FILEPATH);
if (BeanUtils.isNotEmpty(filePath)) {
fileInfo.setFilePath(filePath.toString());
}
return getFileInfo(fileInfo, params);
}
/**
* 文件转存
*
* @param fileInfo
* @throws Exception
*/
private void transferTo(FileInfo fileInfo) throws Exception {
InputStream readed = FtpUtil.download(ftpClient, fileInfo.getFilePath());
String filePath = FtpUtil.upload(ftpClient, "", fileInfo.getMd5(), fileInfo.getFileName(), readed);
fileInfo.setFilePath(filePath);
}
/**
private String getFileChunkPath(String fileMd5, String chunk) {
return UploadUtil.getAbsolutePath(new String[] {fileMd5, chunk});
}
**/
private static String[] getChunkFiles(FTPFile[] files) {
// 假设分块文件名形如 "file.part1", "file.part2", ...
String baseName = null;
String[] chunkFiles = new String[files.length];
for(int i =0;i<files.length;i++) {
String fileName = files[i].getName();
chunkFiles[i] = fileName;
}
//给分片排序
return Arrays.stream(chunkFiles).filter(s -> s != null).sorted(
(a,b)-> {
Integer aInt = Integer.parseInt(a.substring(0, a.indexOf(".")));
Integer bInt = Integer.parseInt(b.substring(0, b.indexOf(".")));
return Integer.compare(aInt, bInt);
})
.toArray(String[]::new);
}
private String getChunkFilePath(String filePath) {
//分片上传 分块文件上传路径规则 为 /chunk/总文件md5/文件 分块文件名暂时为 原文件名 + .{chunk}. +
return "/chunk/"+filePath;
}
private String getChunkFileName(String chunk) {
// 分块文件名暂时为 原文件名 + .{chunk}. + 如 分片1 1.1.part
return chunk + "." + chunk+"." + "part";
}
}