springboot 大文件分片上传
import cn.hutool.core.lang.Assert;
import cn.hutool.http.ContentType;
import com.fasterxml.jackson.databind.JsonNode;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.node.ObjectNode;
import org.apache.commons.lang3.StringUtils;
import org.apache.tomcat.util.http.fileupload.IOUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.core.io.Resource;
import org.springframework.core.io.UrlResource;
import org.springframework.http.HttpHeaders;
import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile;
import java.io.BufferedReader;
import java.io.BufferedWriter;
import java.io.ByteArrayOutputStream;
import java.io.File;
import java.io.FileInputStream;
import java.io.FileOutputStream;
import java.io.FileReader;
import java.io.FileWriter;
import java.io.IOException;
import java.io.UnsupportedEncodingException;
import java.net.MalformedURLException;
import java.net.URLEncoder;
import java.nio.ByteBuffer;
import java.nio.channels.FileChannel;
import java.nio.file.Files;
import java.nio.file.Path;
import java.nio.file.Paths;
import java.nio.file.StandardCopyOption;
import java.util.ArrayList;
import java.util.Base64;
import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.UUID;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReentrantLock;
import java.util.zip.ZipEntry;
import java.util.zip.ZipInputStream;
/**
* 文件工具类
**/
public class FileUtils
{
private final static Logger LOGGER = LoggerFactory.getLogger(FileUtils.class);
private static final Lock mergeLock = new ReentrantLock();
/**
* 创建文件夹
*
* @param folderPath 文件夹路径
*/
public static void createFolder(String folderPath)
{
File folder = new File(folderPath);
if (!folder.exists())
{
if (!folder.mkdirs())
{
throw new RuntimeException("文件夹创建失败: " + folderPath);
}
}
}
/**
* 上传文件
* @param files 文件列表
* @param targetPath 目标路径
* @return
*/
public static List<Map<String, Object>> uploadFiles(MultipartFile[] files, String targetPath)
{
int size = 0;
for (MultipartFile file : files)
{
size = (int) file.getSize() + size;
}
List<Map<String, Object>> fileInfoList = new ArrayList<>();
for (int i = 0; i < files.length; i++)
{
Map<String, Object> map = new HashMap<>();
String fileName = files[i].getOriginalFilename();
//获取文件后缀
// String afterName = (fileName, ".");
//获取文件前缀
// String prefName = (fileName, ".");
// String fileServiceName = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date()) + i + "_" + prefName
// + "." + afterName;
String fileServiceName = UUID.randomUUID().toString().replace("-", "") + "_" + fileName;
File filePath = new File(targetPath, fileServiceName);
map.put("fileServiceName", fileServiceName);
map.put("fileName", fileName);
map.put("filePath", filePath);
// 判断文件父目录是否存在
if (!filePath.getParentFile().exists())
{
filePath.getParentFile().mkdirs();
}
try
{
files[i].transferTo(filePath);
} catch (IOException e)
{
LOGGER.error("文件上传失败", e);
throw new RuntimeException("文件上传失败");
}
fileInfoList.add(map);
}
return fileInfoList;
}
/**
* 上传文件分片
*
* @param fileChunkVO 包含文件分片信息的VO对象,包括文件、分片号、总分片数、MD5校验码和文件名
* @param path 上传路径的基础路径
*/
public static void uploadChunk(MultipartFile file, FileChunkVO fileChunkVO, String path)
{
// 从VO对象中获取文件、分片信息和文件名
int chunkNumber = fileChunkVO.getChunkNumber();
String md5Check = fileChunkVO.getMd5Check();
Assert.isTrue(chunkNumber >= 0, "chunkNumber参数错误");
// 根据MD5校验码计算文件存储路径,并创建目录
String dirPath = path + md5Check;
File uploadDir = new File(dirPath);
if (!uploadDir.exists())
{
uploadDir.mkdirs();
}
// 创建分片文件并写入数据
File newFile = new File(uploadDir + File.separator + md5Check + "_part" + chunkNumber);
try (FileOutputStream fos = new FileOutputStream(newFile))
{
fos.write(file.getBytes());
} catch (IOException e)
{
LOGGER.error("分片文件上传异常:", e);
}
/*
// 使用Redis记录已上传的分片号
String redisKey = "chunks:" + md5Check;
// 缓存用于判断文件是否完整传输完毕
(redisKey, chunkNumber);
*/
}
/**
* 合并文件块
*
* @param fileChunkVO 包含文件分片信息的VO对象,包括文件、分片号、总分片数、MD5校验码和文件名
* @param path 上传路径的基础路径
*/
public static String mergeChunks(FileChunkVO fileChunkVO, String path)
{
String templateFileName = "";
int totalChunks = fileChunkVO.getTotalChunks();
String md5Check = fileChunkVO.getMd5Check();
String fileName = fileChunkVO.getFileName();
String redisKey = "chunks:" + md5Check;
/*
// 检查是否所有分片都已上传
int size = (redisKey).size();
if (totalChunks != size)
{
return templateFileName;
}
*/
// 如果所有分片都已上传,则合并分片文件
templateFileName = md5Check + "_" + fileName;
String targetPath = path + templateFileName;
String uploadDir = path + md5Check;
// 使用锁确保并发安全性
mergeLock.lock();
try
{
try (FileOutputStream fos = new FileOutputStream(targetPath))
{
for (int i = 0; i < totalChunks; i++)
{
Path chunkFilePath = Paths.get(uploadDir + File.separator, md5Check + "_part" + i);
byte[] chunkBytes = Files.readAllBytes(chunkFilePath);
fos.write(chunkBytes); // 将文件块内容写入目标文件
}
} catch (IOException e)
{
LOGGER.error("合并文件块错误: " + e.getMessage());
} finally
{
// 删除上传目录和清理相关资源
deleteFolder(uploadDir);
/*
// 删除缓存的key
(redisKey);
*/
}
} finally
{
mergeLock.unlock();
}
return templateFileName;
}
/**
* 递归地删除文件夹及其所有内容
*
* @param folderPath 文件夹路径
*/
public static void deleteFolder(String folderPath)
{
File folder = new File(folderPath);
if (folder.exists())
{
File[] files = folder.listFiles();
if (files != null)
{
for (File file : files)
{
if (file.isDirectory())
{
deleteFolder(file.getAbsolutePath());
} else
{
file.delete();
}
}
}
folder.delete();
// ("文件夹{}删除成功!", folderPath);
}
}
/**
* 复制文件夹中的所有内容
*
* @param sourceFolderPath 源文件夹路径 例如:D:/SSS
* @param targetFolderPath 目标文件夹路径 例如:D:/TTT/TTT
* @param newFolderName 新文件夹名称 例如:NNN
* @return 目标文件路径 例如:D:/TTT/TTT/NNN
*/
public static String copyFolderOfAll(String sourceFolderPath, String targetFolderPath, String newFolderName)
{
String destinationFolderPath = targetFolderPath + File.separator + newFolderName;
Path sourcePath = Paths.get(sourceFolderPath);
Path destinationPath = Paths.get(destinationFolderPath);
try
{
// 遍历源文件夹中的所有文件和子文件夹
Files.walk(sourcePath).forEach(source -> {
try
{
// 构建目标文件夹中的对应路径
Path destination = destinationPath.resolve(sourcePath.relativize(source));
if (Files.isDirectory(source))
{
if (!Files.exists(destination))
{
Files.createDirectory(destination);
}
} else
{
Files.copy(source, destination, StandardCopyOption.REPLACE_EXISTING);
}
} catch (IOException e)
{
throw new RuntimeException("复制文件夹失败: " + e.getMessage());
}
});
} catch (Exception e)
{
LOGGER.error("文件夹复制失败: {}", e);
FileUtil.deleteFolder(destinationFolderPath);
throw new RuntimeException("文件夹复制失败");
}
return destinationFolderPath;
}
/**
* 修改JSON文件中指定字段的值
*
* @param filePath 文件路径
* @param fieldName 字段名
* @param newValue 新值
*/
public static void modifyJsonField(String filePath, String fieldName, Object newValue)
{
try
{
ObjectMapper objectMapper = new ObjectMapper();
File file = new File(filePath);
JsonNode jsonNode = objectMapper.readTree(file);
if (jsonNode.isObject())
{
ObjectNode objectNode = (ObjectNode) jsonNode;
objectNode.putPOJO(fieldName, newValue);
objectMapper.writeValue(file, objectNode);
LOGGER.info("Field '" + fieldName + "' in the JSON file has been modified to: " + newValue);
} else
{
LOGGER.info("Invalid JSON format in the file.");
}
} catch (IOException e)
{
throw new RuntimeException(e);
}
}
/**
* 修改文件内容
*
* @param filePath 文件路径
* @param searchString 查找字符串
* @param replacement 替换值
*/
public static void modifyFileContent(String filePath, String searchString, String replacement)
{
try
{
File file = new File(filePath);
BufferedReader reader = new BufferedReader(new FileReader(file));
String line;
StringBuilder content = new StringBuilder();
while ((line = reader.readLine()) != null)
{
if (line.contains(searchString))
{
line = line.replace(searchString, replacement);
}
content.append(line).append("\n");
}
reader.close();
BufferedWriter writer = new BufferedWriter(new FileWriter(file));
writer.write(content.toString());
writer.close();
LOGGER.info("文件内容[" + searchString + "]已被修改为[" + replacement + "]");
} catch (IOException e)
{
throw new RuntimeException(e);
}
}
/**
* 压缩包校验
*
* @param zipFilePath 压缩包文件路径
* @param suffix 文件后缀
* @throws Exception
*/
public static void zipVerify(String zipFilePath, String suffix) throws Exception
{
// 打开压缩文件
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(Paths.get(zipFilePath))))
{
ZipEntry entry = zipInputStream.getNextEntry();
boolean allFilesAreSameSuffix = true;
// 遍历压缩文件中的条目
while (entry != null)
{
String entryName = entry.getName();
if (!entryName.toLowerCase().endsWith(suffix))
{
allFilesAreSameSuffix = false;
break;
}
zipInputStream.closeEntry();
entry = zipInputStream.getNextEntry();
}
// 如果存在非.pak文件则校验失败
if (!allFilesAreSameSuffix)
{
throw new RuntimeException("压缩包中存在非" + suffix + "文件,校验失败");
}
}
}
/**
* 解压缩
*
* @param zipFilePath 压缩包文件路径
* @param destDirectory 目标目录
* @throws IOException
*/
public static List<String> unzip(String zipFilePath, String destDirectory) throws Exception
{
// 文件名列表
List<String> fileNameList = new ArrayList<>();
// 创建目标文件夹
File destDir = new File(destDirectory);
if (!destDir.exists())
{
destDir.mkdirs();
}
// 读取并解压文件
try (ZipInputStream zipInputStream = new ZipInputStream(Files.newInputStream(Paths.get(zipFilePath))))
{
ZipEntry entry = zipInputStream.getNextEntry();
byte[] buffer = new byte[1024];
// 解压文件
while (entry != null)
{
String entryName = entry.getName();
File newFile = new File(destDirectory + File.separator + entryName);
try (FileOutputStream fos = new FileOutputStream(newFile))
{
int len;
while ((len = zipInputStream.read(buffer)) > 0)
{
fos.write(buffer, 0, len);
}
}
zipInputStream.closeEntry();
fileNameList.add(entryName);
entry = zipInputStream.getNextEntry();
}
}
LOGGER.info("解压缩并校验完成");
return fileNameList;
}
/**
* 文件下载
*
* @param filePath 文件路径(必填)
* @param fileName 文件名称(可选)
* @return ResponseEntity
*/
public static ResponseEntity<Resource> download(String filePath, String fileName)
throws MalformedURLException, UnsupportedEncodingException
{
// 加载文件资源
Resource resource = loadFileAsResource(filePath, fileName);
// 避免中文乱码
String encodedFileName = URLEncoder.encode(Objects.requireNonNull(resource.getFilename()), "UTF-8")
.replaceAll("\\+", "%20");
// 确定文件的内容类型
return ResponseEntity.ok().contentType(MediaType.parseMediaType(ContentType.OCTET_STREAM.getValue()))
.header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=" + encodedFileName).body(resource);
}
/**
* 加载文件资源
*
* @param filePath 文件路径(必填)
* @param fileName 文件名称(选填)
* @return Resource
* @throws MalformedURLException URL格式异常
*/
private static Resource loadFileAsResource(String filePath, String fileName) throws MalformedURLException
{
Path path;
if (StringUtils.isEmpty(fileName))
{
path = Paths.get(filePath);
} else
{
path = Paths.get(filePath + File.separator + fileName);
}
Resource resource = new UrlResource(path.toUri());
if (resource.exists() || resource.isReadable())
{
return resource;
} else
{
throw new RuntimeException("找不到文件或无法读取文件");
}
}
/**
* 批量删除文件
*
* @param fileNameArr 服务端保存的文件的名数组
* @param targetPath 目标路径
*/
public static void deleteFile(String[] fileNameArr, String targetPath)
{
for (String fileName : fileNameArr)
{
String filePath = targetPath + fileName;
File file = new File(filePath);
if (file.exists())
{
try
{
Files.delete(file.toPath());
} catch (IOException e)
{
e.printStackTrace();
LOGGER.warn("文件删除失败", e);
}
} else
{
LOGGER.warn("文件: {} 删除失败,该文件不存在", fileName);
}
}
}
/**
* 根据图片路径获取图片的base64编码
*
* @param filePath 图片路径
* @return base64编码
*/
public static String encodeImageToBase64(String filePath) throws IOException
{
byte[] fileContent;
try (FileInputStream fis = new FileInputStream(new File(filePath)); FileChannel fileChannel = fis.getChannel())
{
// 估计文件大小并分配缓冲区
long size = fileChannel.size();
fileContent = new byte[(int) size];
// 读取文件内容到缓冲区
int bytesRead = fileChannel.read(ByteBuffer.wrap(fileContent));
if (bytesRead == -1)
{
throw new IOException("Could not read file");
}
}
// 使用Java 8的Base64类进行编码
return Base64.getEncoder().encodeToString(fileContent);
}
/**
* 根据文件对象获取字节数组
*
* @param file 文件对象
* @return
*/
public static byte[] getBytesByFile(File file)
{
byte[] bytes = null;
if (file != null && file.exists())
{
FileInputStream fis = null;
ByteArrayOutputStream bos = null;
try
{
fis = new FileInputStream(file);
bos = new ByteArrayOutputStream();
byte[] buffer = new byte[1024];
int len = -1;
while ((len = fis.read(buffer)) != -1)
{
bos.write(buffer, 0, len);
}
bytes = bos.toByteArray();
} catch (Exception e)
{
throw new RuntimeException("文件对象转化字节数组失败!");
} finally
{
IOUtils.closeQuietly(fis);
IOUtils.closeQuietly(bos);
}
}
return bytes;
}
}