|
@@ -0,0 +1,307 @@
|
|
|
|
|
+package com.zd.base.files.bigupload.service.impl;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+import com.zd.base.files.bigupload.dto.FileChunkDTO;
|
|
|
|
|
+import com.zd.base.files.bigupload.dto.FileChunkResultDTO;
|
|
|
|
|
+import com.zd.base.files.bigupload.response.error.BusinessErrorCode;
|
|
|
|
|
+import com.zd.base.files.bigupload.response.error.BusinessException;
|
|
|
|
|
+import com.zd.base.files.bigupload.service.IUploadService;
|
|
|
|
|
+import com.zd.base.files.bigupload.utils.FileUtils;
|
|
|
|
|
+import org.apache.tomcat.util.http.fileupload.IOUtils;
|
|
|
|
|
+import org.slf4j.Logger;
|
|
|
|
|
+import org.slf4j.LoggerFactory;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Autowired;
|
|
|
|
|
+import org.springframework.beans.factory.annotation.Value;
|
|
|
|
|
+import org.springframework.data.redis.core.RedisTemplate;
|
|
|
|
|
+import org.springframework.stereotype.Service;
|
|
|
|
|
+import org.springframework.util.StringUtils;
|
|
|
|
|
+
|
|
|
|
|
+import java.io.*;
|
|
|
|
|
+import java.net.UnknownHostException;
|
|
|
|
|
+import java.util.*;
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+/**
|
|
|
|
|
+ * @Author donggaosheng
|
|
|
|
|
+ * @Date 2021/1/17 10:24
|
|
|
|
|
+ * @Version 1.0
|
|
|
|
|
+ * @Modify XJLZ
|
|
|
|
|
+ **/
|
|
|
|
|
+@Service
|
|
|
|
|
+@SuppressWarnings("all")
|
|
|
|
|
+public class UploadServiceImpl implements IUploadService {
|
|
|
|
|
+
|
|
|
|
|
+ private static final String FILE_PREFIX = "bigfile:";
|
|
|
|
|
+
|
|
|
|
|
+ private Logger logger = LoggerFactory.getLogger(UploadServiceImpl.class);
|
|
|
|
|
+
|
|
|
|
|
+ @Autowired
|
|
|
|
|
+ private RedisTemplate<String, Object> redisTemplate;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 域名或本机访问地址
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${file.domain}")
|
|
|
|
|
+ public String domain;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传文件存储在本地的根路径
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${file.path}")
|
|
|
|
|
+ private String uploadFolder;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 资源映射路径 前缀
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${file.prefix}")
|
|
|
|
|
+ public String filePrefix;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 资源映射路径 前缀
|
|
|
|
|
+ */
|
|
|
|
|
+ @Value("${file.bigFile}")
|
|
|
|
|
+ public String bigFile;
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查文件是否存在,如果存在则跳过该文件的上传,如果不存在,返回需要上传的分片集合
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param chunkDTO
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public FileChunkResultDTO checkChunkExist(FileChunkDTO chunkDTO) throws BusinessException {
|
|
|
|
|
+ //1.检查文件是否已上传过
|
|
|
|
|
+ //1.1)检查在磁盘中是否存在
|
|
|
|
|
+ String fileFolderPath = getFileFolderPath(chunkDTO.getIdentifier());
|
|
|
|
|
+ logger.info("fileFolderPath-->{}", fileFolderPath);
|
|
|
|
|
+ String filePath = getFilePath(chunkDTO.getIdentifier(), chunkDTO.getFilename());
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ boolean exists = file.exists();
|
|
|
|
|
+ //1.2)检查Redis中是否存在,并且所有分片已经上传完成。
|
|
|
|
|
+ Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(FILE_PREFIX + chunkDTO.getIdentifier(), "uploaded");
|
|
|
|
|
+ if (uploaded != null && uploaded.size() == chunkDTO.getTotalChunks() && exists) {
|
|
|
|
|
+ return new FileChunkResultDTO(true);
|
|
|
|
|
+ }
|
|
|
|
|
+ File fileFolder = new File(fileFolderPath);
|
|
|
|
|
+ if (!fileFolder.exists()) {
|
|
|
|
|
+ boolean mkdirs = fileFolder.mkdirs();
|
|
|
|
|
+ logger.info("准备工作,创建文件夹,fileFolderPath:{},mkdirs:{}", fileFolderPath, mkdirs);
|
|
|
|
|
+ }
|
|
|
|
|
+ // 断点续传,返回已上传的分片
|
|
|
|
|
+ return new FileChunkResultDTO(false, uploaded);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 上传分片
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param chunkDTO
|
|
|
|
|
+ */
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public void uploadChunk(FileChunkDTO chunkDTO) throws BusinessException {
|
|
|
|
|
+ //分块的目录
|
|
|
|
|
+ String chunkFileFolderPath = getChunkFileFolderPath(chunkDTO.getIdentifier());
|
|
|
|
|
+ logger.info("分块的目录 -> {}", chunkFileFolderPath);
|
|
|
|
|
+ File chunkFileFolder = new File(chunkFileFolderPath);
|
|
|
|
|
+ if (!chunkFileFolder.exists()) {
|
|
|
|
|
+ boolean mkdirs = chunkFileFolder.mkdirs();
|
|
|
|
|
+ logger.info("创建分片文件夹:{}", mkdirs);
|
|
|
|
|
+ }
|
|
|
|
|
+ //写入分片
|
|
|
|
|
+ try (
|
|
|
|
|
+ InputStream inputStream = chunkDTO.getFile().getInputStream();
|
|
|
|
|
+ FileOutputStream outputStream = new FileOutputStream(new File(chunkFileFolderPath + chunkDTO.getChunkNumber()))
|
|
|
|
|
+ ) {
|
|
|
|
|
+ IOUtils.copy(inputStream, outputStream);
|
|
|
|
|
+ logger.info("文件标识:{},chunkNumber:{}", chunkDTO.getIdentifier(), chunkDTO.getChunkNumber());
|
|
|
|
|
+ //将该分片写入redis
|
|
|
|
|
+ long size = saveToRedis(chunkDTO);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ throw new BusinessException(BusinessErrorCode.INVALID_PARAMETER, e.getMessage());
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ @Override
|
|
|
|
|
+ public String mergeChunk(String identifier, String fileName, Integer totalChunks) throws BusinessException, IOException {
|
|
|
|
|
+ String suffix = fileName.substring(fileName.lastIndexOf("."));
|
|
|
|
|
+ if(null==suffix){
|
|
|
|
|
+ throw new RuntimeException("文件格式有误");
|
|
|
|
|
+ }
|
|
|
|
|
+ fileName= UUID.randomUUID().toString()+suffix;
|
|
|
|
|
+ return mergeChunks(identifier, fileName, totalChunks);
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 合并分片
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param identifier
|
|
|
|
|
+ * @param filename
|
|
|
|
|
+ */
|
|
|
|
|
+ private String mergeChunks(String identifier,String filename, Integer totalChunks) throws IOException {
|
|
|
|
|
+ String chunkFileFolderPath = getChunkFileFolderPath(identifier);
|
|
|
|
|
+ String prefixFileFolderPath = getPrefixFileFolderPath(identifier);
|
|
|
|
|
+ String filePath = getFilePath(identifier, filename);
|
|
|
|
|
+ String accessPath = getNetWorkPath(filename);
|
|
|
|
|
+ // 检查分片是否都存在
|
|
|
|
|
+ if (checkChunks(chunkFileFolderPath, totalChunks)) {
|
|
|
|
|
+ File chunkFileFolder = new File(chunkFileFolderPath);
|
|
|
|
|
+ File mergeFile = new File(filePath);
|
|
|
|
|
+ if (!mergeFile.exists()) {
|
|
|
|
|
+ mergeFile.createNewFile();
|
|
|
|
|
+ }
|
|
|
|
|
+ File[] chunks = chunkFileFolder.listFiles();
|
|
|
|
|
+ //排序
|
|
|
|
|
+ List fileList = Arrays.asList(chunks);
|
|
|
|
|
+ Collections.sort(fileList, (Comparator<File>) (o1, o2) -> {
|
|
|
|
|
+ return Integer.parseInt(o1.getName()) - (Integer.parseInt(o2.getName()));
|
|
|
|
|
+ });
|
|
|
|
|
+ try {
|
|
|
|
|
+ RandomAccessFile randomAccessFileWriter = new RandomAccessFile(mergeFile, "rw");
|
|
|
|
|
+ byte[] bytes = new byte[1024];
|
|
|
|
|
+ for (File chunk : chunks) {
|
|
|
|
|
+ RandomAccessFile randomAccessFileReader = new RandomAccessFile(chunk, "r");
|
|
|
|
|
+ int len;
|
|
|
|
|
+ while ((len = randomAccessFileReader.read(bytes)) != -1) {
|
|
|
|
|
+ randomAccessFileWriter.write(bytes, 0, len);
|
|
|
|
|
+ }
|
|
|
|
|
+ randomAccessFileReader.close();
|
|
|
|
|
+ }
|
|
|
|
|
+ randomAccessFileWriter.close();
|
|
|
|
|
+ FileUtils.delFolder(prefixFileFolderPath);
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return null;
|
|
|
|
|
+ }
|
|
|
|
|
+ return accessPath;
|
|
|
|
|
+ }
|
|
|
|
|
+ return accessPath;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ private String getAcessPath(String prefix, String filename) {
|
|
|
|
|
+ if (!StringUtils.isEmpty(prefix)) {
|
|
|
|
|
+ return prefix + "/" + FileUtils.getFormatter() + "/" + filename;
|
|
|
|
|
+ }
|
|
|
|
|
+ return FileUtils.getFormatter() + "/" + filename;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private String getNetWorkPath(String fileName) throws UnknownHostException {
|
|
|
|
|
+ /*String urlPrefix = domain;
|
|
|
|
|
+ String localIP = "127.0.0.1";
|
|
|
|
|
+ if (urlPrefix.contains(localIP)) {
|
|
|
|
|
+ String ip = InetAddress.getLocalHost().getHostAddress();;
|
|
|
|
|
+ urlPrefix = urlPrefix.replace(localIP, ip);
|
|
|
|
|
+ }
|
|
|
|
|
+ String url = urlPrefix + filePrefix+"/"+bigFile+"/"+ FileUtils.getFormatter() + "/" + fileName;*/
|
|
|
|
|
+ String url = filePrefix+"/"+bigFile+"/"+ FileUtils.getFormatter() + "/" + fileName;
|
|
|
|
|
+ return url.replace("//","/").replace("./",".//");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 检查分片是否都存在
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param chunkFileFolderPath
|
|
|
|
|
+ * @param totalChunks
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ private boolean checkChunks(String chunkFileFolderPath, Integer totalChunks) {
|
|
|
|
|
+ try {
|
|
|
|
|
+ for (int i = 1; i <= totalChunks + 1; i++) {
|
|
|
|
|
+ File file = new File(chunkFileFolderPath + File.separator + i);
|
|
|
|
|
+ if (file.exists()) {
|
|
|
|
|
+ continue;
|
|
|
|
|
+ } else {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ }
|
|
|
|
|
+ } catch (Exception e) {
|
|
|
|
|
+ return false;
|
|
|
|
|
+ }
|
|
|
|
|
+ return true;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 分片写入Redis
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param chunkDTO
|
|
|
|
|
+ */
|
|
|
|
|
+ private synchronized long saveToRedis(FileChunkDTO chunkDTO) {
|
|
|
|
|
+ Set<Integer> uploaded = (Set<Integer>) redisTemplate.opsForHash().get(FILE_PREFIX + chunkDTO.getIdentifier(), "uploaded");
|
|
|
|
|
+ if (uploaded == null) {
|
|
|
|
|
+ uploaded = new HashSet<>(Arrays.asList(chunkDTO.getChunkNumber()));
|
|
|
|
|
+ HashMap<String, Object> objectObjectHashMap = new HashMap<>();
|
|
|
|
|
+ objectObjectHashMap.put("uploaded", uploaded);
|
|
|
|
|
+ objectObjectHashMap.put("totalChunks", chunkDTO.getTotalChunks());
|
|
|
|
|
+ objectObjectHashMap.put("totalSize", chunkDTO.getTotalSize());
|
|
|
|
|
+ objectObjectHashMap.put("path", chunkDTO.getFilename());
|
|
|
|
|
+ redisTemplate.opsForHash().putAll(FILE_PREFIX + chunkDTO.getIdentifier(), objectObjectHashMap);
|
|
|
|
|
+ } else {
|
|
|
|
|
+ uploaded.add(chunkDTO.getChunkNumber());
|
|
|
|
|
+ redisTemplate.opsForHash().put(FILE_PREFIX + chunkDTO.getIdentifier(), "uploaded", uploaded);
|
|
|
|
|
+ }
|
|
|
|
|
+ return uploaded.size();
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 得到文件的绝对路径
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param identifier
|
|
|
|
|
+ * @param filename
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getFilePath(String identifier, String filename) {
|
|
|
|
|
+ String ext = filename.substring(filename.lastIndexOf("."));
|
|
|
|
|
+ String filePath=uploadFolder +"/"+bigFile+"/"+ FileUtils.getFormatter();
|
|
|
|
|
+ File file = new File(filePath);
|
|
|
|
|
+ if (!file.exists()) {
|
|
|
|
|
+ file.mkdirs();
|
|
|
|
|
+ }
|
|
|
|
|
+ filePath=filePath+"/" + filename;
|
|
|
|
|
+ return filePath.replace("//","/");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 得到文件的相对路径
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param identifier
|
|
|
|
|
+ * @param filename
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getFileRelativelyPath(String identifier, String filename) {
|
|
|
|
|
+ String ext = filename.substring(filename.lastIndexOf("."));
|
|
|
|
|
+ return "/" + identifier.substring(0, 1) + "/" +
|
|
|
|
|
+ identifier.substring(1, 2) + "/" +
|
|
|
|
|
+ identifier + "/" + identifier
|
|
|
|
|
+ + ext;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 得到分块文件所属的目录
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param identifier
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getChunkFileFolderPath(String identifier) {
|
|
|
|
|
+ return getFileFolderPath(identifier) + "chunks" + File.separator;
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+
|
|
|
|
|
+ private String getPrefixFileFolderPath(String identifier) {
|
|
|
|
|
+ String prefixFile=uploadFolder +"/"+filePrefix+"/"+bigFile +"/"+identifier.substring(0, 1);
|
|
|
|
|
+ return prefixFile.replace("//","/");
|
|
|
|
|
+ }
|
|
|
|
|
+
|
|
|
|
|
+ /**
|
|
|
|
|
+ * 得到文件所属的目录
|
|
|
|
|
+ *
|
|
|
|
|
+ * @param identifier
|
|
|
|
|
+ * @return
|
|
|
|
|
+ */
|
|
|
|
|
+ private String getFileFolderPath(String identifier) {
|
|
|
|
|
+ String fileFolderPath=uploadFolder+"/"+filePrefix+"/"+bigFile+"/"+identifier.substring(0, 1) + File.separator +
|
|
|
|
|
+ identifier.substring(1, 2) + File.separator +
|
|
|
|
|
+ identifier + File.separator;
|
|
|
|
|
+ return fileFolderPath.replace("//","/");
|
|
|
|
|
+ }
|
|
|
|
|
+}
|