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<String> 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<String> 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<String> 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<String> objectNames) {
|
if (objectNames == null || objectNames.isEmpty()) {
|
// 列表为空,直接返回,无需操作
|
return;
|
}
|
|
try {
|
MinioClient minioClient = MinioClient.builder()
|
.endpoint(endpoint)
|
.credentials(accessKey, secretKey)
|
.build();
|
|
List<DeleteObject> 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);
|
}
|
}
|
|
}
|