springboot 大文件分片上传

时间:2025-02-16 09:28:12
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; } }