package org.sxkj.resource.util; import io.minio.*; import io.minio.errors.ErrorResponseException; import io.minio.messages.DeleteError; import io.minio.messages.DeleteObject; import lombok.extern.slf4j.Slf4j; import java.io.*; import java.nio.file.Files; import java.nio.file.Path; import java.nio.file.Paths; import java.nio.file.StandardCopyOption; import java.text.SimpleDateFormat; import java.util.Date; import java.util.List; import java.util.stream.Collectors; import java.util.zip.Deflater; import java.util.zip.ZipEntry; import java.util.zip.ZipOutputStream; @Slf4j public class MinioFileDownloader { private final String bucketPath; public MinioFileDownloader(String bucketPath) { this.bucketPath = bucketPath; } /** * 复制文件到指定的文件夹 * * @param imageFilePathsInZip * @param localSaveDir */ public void copyTheFile(List imageFilePathsInZip, String localSaveDir) { try { for (String prefix : imageFilePathsInZip) { // 把文件A写到文件夹B里面去 String localFolderPath = bucketPath + prefix; copyFileToDirectory(localFolderPath, localSaveDir); } } catch (IOException e) { // 删除文件夹 try { Files.deleteIfExists(Paths.get(localSaveDir)); } catch (IOException ex) { throw new RuntimeException(ex); } throw new RuntimeException(e); } } /** * 使用文件流将源文件复制到目标文件夹中 * * @param sourceFilePath 源文件的路径 * @param targetFolderPath 目标文件夹的路径 * @throws IOException 如果复制过程中出现IO异常 */ public void copyFileToDirectory(String sourceFilePath, String targetFolderPath) throws IOException { // 创建源文件对象 File sourceFile = new File(sourceFilePath); // 检查源文件是否存在且是文件 if (!sourceFile.exists() || !sourceFile.isFile()) { throw new FileNotFoundException("源文件不存在或不是一个有效的文件: " + sourceFilePath); } // 创建目标文件夹(如果不存在) File targetFolder = new File(targetFolderPath); if (!targetFolder.exists()) { targetFolder.mkdirs(); } // 构造目标文件路径 File targetFile = new File(targetFolder, sourceFile.getName()); // 使用文件输入输出流进行复制 try (FileInputStream fis = new FileInputStream(sourceFile); FileOutputStream fos = new FileOutputStream(targetFile)) { byte[] buffer = new byte[4096]; // 缓冲区 int bytesRead; while ((bytesRead = fis.read(buffer)) != -1) { fos.write(buffer, 0, bytesRead); } } // System.out.println("文件复制成功: " + targetFile.getAbsolutePath()); } public void downloadAndZipFolders(List prefixes, String localSaveDir, String time) throws Exception { // 创建目标文件夹路径并生成zip文件名 Path localSavePath = Paths.get(localSaveDir); if (!Files.exists(localSavePath)) { Files.createDirectories(localSavePath); } String zipFileName = localSavePath.resolve(time).toString(); // 创建压缩文件输出流 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileName))) { zos.setLevel(Deflater.BEST_COMPRESSION); // 使用最高压缩级别 // 遍历每个前缀 for (String prefix : prefixes) { // 从本地文件系统获取文件 String localFolderPath = bucketPath + prefix; File localFolder = new File(localFolderPath); if (!localFolder.exists() || !localFolder.isDirectory()) { throw new FileNotFoundException("Local folder not found: " + localFolderPath); } // 压缩文件夹 zipFolder(localFolder, prefix, zos); } } } /** * 下载(从本地指定路径读取)多个图片文件,并将它们压缩到一个zip文件中。 * * @param imageFilePathsInZip 一个列表,其中每个字符串代表: * 1. 文件在本地存储中的相对路径 (相对于 bucketPath)。 * 2. 该文件在最终生成的 ZIP 文件中的条目名称 (路径 + 文件名)。 * 例如: 如果 bucketPath 是 "/mnt/data/", imageFilePathsInZip 中的一个元素可以是 "folderA/image1.jpg", * 那么本地文件路径是 "/mnt/data/folderA/image1.jpg", * 在 zip 中的条目名也是 "folderA/image1.jpg". * 如果只是文件名如 "image1.jpg", 则本地文件是 "/mnt/data/image1.jpg", zip中条目名是 "image1.jpg". * @param localSaveDir ZIP 文件保存的目录。 * @param zipFileBaseName ZIP 文件的基础名称 (不含 .zip 后缀,例如用时间戳 "20231027103000")。 * @throws Exception 如果发生错误,如文件未找到、IO错误等。 */ public void downloadAndZipImages(List imageFilePathsInZip, String localSaveDir, String zipFileBaseName) throws Exception { // 1. 创建目标保存目录(如果不存在) Path localSavePath = Paths.get(localSaveDir); if (!Files.exists(localSavePath)) { Files.createDirectories(localSavePath); } // 2. 生成最终的 ZIP 文件名 (完整路径) String zipFileName = localSavePath.resolve(zipFileBaseName).toString(); // 3. 创建压缩文件输出流 try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(zipFileName))) { zos.setLevel(Deflater.BEST_COMPRESSION); // 使用最高压缩级别 byte[] buffer = new byte[4096]; // 缓冲区 // 4. 遍历每个图片文件路径 for (String filePathInZip : imageFilePathsInZip) { // 构造本地文件的完整路径 String localFilePath = bucketPath + filePathInZip; // bucketPath 是本地文件存储的根目录 File localImageFile = new File(localFilePath); if (!localImageFile.exists() || !localImageFile.isFile()) { // 可以选择抛出异常,或者记录日志并跳过此文件 log.info("本地图片文件未找到或不是一个有效的文件: " + localFilePath + " - 跳过该文件。"); continue; // 跳过不存在或不是文件的项 } // 5. 创建 ZIP 条目 // filePathInZip 将作为文件在 ZIP 包内的路径和名称 ZipEntry zipEntry = new ZipEntry(filePathInZip); zos.putNextEntry(zipEntry); // 6. 将文件内容写入 ZIP 输出流 try (FileInputStream fis = new FileInputStream(localImageFile)) { int length; while ((length = fis.read(buffer)) > 0) { zos.write(buffer, 0, length); } } zos.closeEntry(); // 关闭当前 ZIP 条目 log.info("已添加到压缩包: " + filePathInZip + " (源路径: " + localFilePath + ")"); } log.info("ZIP 文件创建成功: " + zipFileName); } catch (IOException e) { // 如果发生IO异常,可以删除可能已创建的不完整zip文件 try { Files.deleteIfExists(Paths.get(zipFileName)); } catch (IOException cleanupEx) { log.error("无法清理部分生成的 ZIP 文件: " + cleanupEx.getMessage()); } throw new Exception("创建 ZIP 文件失败: " + e.getMessage(), e); } } private void zipFolder(File folder, String parentFolder, ZipOutputStream zos) throws IOException { for (File file : folder.listFiles()) { if (file.isDirectory()) { zipFolder(file, parentFolder + "/" + file.getName(), zos); continue; } zos.putNextEntry(new ZipEntry(parentFolder + "/" + file.getName())); try (FileInputStream fis = new FileInputStream(file)) { byte[] buffer = new byte[1024]; int length; while ((length = fis.read(buffer)) >= 0) { zos.write(buffer, 0, length); } } zos.closeEntry(); } } /** * 删除minio文件 * * @param endpoint * @param accessKey * @param secretKey * @param bucketName * @param objectName * @return */ public static boolean deleteFileFromMinio(String endpoint, String accessKey, String secretKey, String bucketName, String objectName) { try { // 创建MinioClient实例 MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); // 检查文件是否存在 try { StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); // 删除文件 minioClient.removeObject(RemoveObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); log.info("成功删除文件:" + objectName); return true; } catch (Exception e) { // 如果文件不存在,则捕获异常 log.error("该文件不存在" + objectName); return false; } } catch (Exception e) { log.error("文件删除失败" + objectName); return false; } } /** * 航线文件上传 * * @param endpoint * @param accessKey * @param secretKey * @param bucketName * @param objectName * @param file * @param waylineType * @return */ public static String checkAndUploadFileToMinio(String endpoint, String accessKey, String secretKey, String bucketName, String objectName, File file, String waylineType) { try { // 将数字类型的 waylineType 转换为对应的字符串前缀 String waylinePrefix; switch (waylineType) { case "0": waylinePrefix = "wayline"; break; case "1": waylinePrefix = "patches"; break; case "2": waylinePrefix = "survey"; break; case "3": waylinePrefix = "point"; break; case "4": waylinePrefix = "zs"; break; default: waylinePrefix = "default"; } // 创建 MinioClient 实例 MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); // 获取当前日期并格式化为 "yyyyMMdd" String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date()); // 添加时间戳到文件名 String timestamp = String.valueOf(System.currentTimeMillis()); String objectPath = String.format("wayline/%s/%s_%s", currentDate, waylinePrefix, timestamp + ".kmz"); // 上传文件 minioClient.uploadObject(UploadObjectArgs.builder() .bucket(bucketName) .object(objectName) .filename(file.getAbsolutePath()) .build()); return "/" + objectName; // 返回上传成功的文件路径 } catch (Exception e) { log.error("航线文件上传失败: " + e.getMessage()); throw new RuntimeException("航线文件上传失败"); } } public static String checkAndUploadFileToMinioByDp(String endpoint, String accessKey, String secretKey, String bucketName, String objectName, File file, String waylineType) { try { // 将数字类型的 waylineType 转换为对应的字符串前缀 String waylinePrefix; switch (waylineType) { case "0": waylinePrefix = "wayline"; break; case "1": waylinePrefix = "patches"; break; case "2": waylinePrefix = "survey"; break; case "3": waylinePrefix = "point"; break; case "4": waylinePrefix = "zs"; break; default: waylinePrefix = "default"; } // 创建 MinioClient 实例 MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); // 获取当前日期并格式化为 "yyyyMMdd" String currentDate = new SimpleDateFormat("yyyyMMdd").format(new Date()); // 添加时间戳到文件名 String timestamp = String.valueOf(System.currentTimeMillis()); String objectPath = String.format("wayline/%s/%s_%s", currentDate, waylinePrefix, timestamp + ".kmz"); //上传文件 minioClient.uploadObject(UploadObjectArgs.builder() .bucket(bucketName) .object(objectPath) .filename(file.getAbsolutePath()) .build() ); return "/" + objectPath; // 返回上传成功的文件路径 } catch (Exception e) { log.error("航线文件上传失败: " + e.getMessage()); throw new RuntimeException("航线文件上传失败"); } } public static InputStream downloadFileFromMinio(String endpoint, String accessKey, String secretKey, String bucketName, String objectName) { try { // 创建 MinioClient 实例 MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); // 检查文件是否存在并获取输入流 StatObjectResponse stat = minioClient.statObject(StatObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); // 返回文件流 return minioClient.getObject(GetObjectArgs.builder() .bucket(bucketName) .object(objectName) .build()); } catch (Exception e) { log.error(objectName + "文件下载失败:" + e); return null; // 返回 null 表示文件不存在或下载失败 } } /** * (新增) 从MinIO批量删除对象。 * * @param endpoint MinIO服务的URL * @param accessKey Access key * @param secretKey Secret key * @param bucketName Bucket名称 * @param objectNames 要删除的对象名称列表 * @return 如果所有文件都成功删除或不存在,则返回true;如果至少有一个文件删除失败,则返回false。 */ public static void deleteObjects(String endpoint, String accessKey, String secretKey, String bucketName, List objectNames) { if (objectNames == null || objectNames.isEmpty()) { // 列表为空,直接返回,无需操作 return; } try { MinioClient minioClient = MinioClient.builder() .endpoint(endpoint) .credentials(accessKey, secretKey) .build(); List objectsToDelete = objectNames.stream() .map(DeleteObject::new) .collect(Collectors.toList()); RemoveObjectsArgs args = RemoveObjectsArgs.builder() .bucket(bucketName) .objects(objectsToDelete) .build(); // 发送删除请求,但不迭代检查结果。 // 惰性迭代:如果不调用 for-each 或 .iterator(),网络请求可能不会立即触发。 // 为了确保请求被发送,我们需要消耗掉这个迭代器。 // 最简单的方式是调用一个不会做任何事的 forEach。 minioClient.removeObjects(args).forEach(result -> { // 这个 lambda 体是空的,我们只是为了消耗迭代器以触发网络请求。 // 也可以在这里处理异常,如果需要的话。 try { result.get(); // 调用 get() 会在有错误时抛出异常 } catch (Exception e) { log.warn("MinIO删除单个对象时出错 (已忽略): {}", e.getMessage()); } }); log.info("已向MinIO发送批量删除 {} 个对象的请求。", objectNames.size()); } catch (Exception e) { // 捕获致命错误,例如网络连接失败、认证错误等 log.error("发送MinIO批量删除请求时发生严重错误: {}", e.getMessage(), e); // 将其包装成RuntimeException抛出,以便上层 @Transactional 能感知到并回滚 throw new RuntimeException("发送MinIO批量删除请求失败。", e); } } }