吉安感知网项目-后端
xiebin
2026-01-06 d207a86cdf1ab52ef8cb7cd83bad8fceab8038cf
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
118
119
120
121
122
123
124
125
126
127
128
129
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
161
162
163
164
165
166
167
168
169
170
171
172
173
174
175
176
177
178
179
180
181
182
183
184
185
186
187
188
189
190
191
192
193
194
195
196
197
198
199
200
201
202
203
204
205
206
207
208
209
210
211
212
213
214
215
216
217
218
219
220
221
222
223
224
225
226
227
228
229
230
231
232
233
234
235
236
237
238
239
240
241
242
243
244
245
246
247
248
249
250
251
252
253
254
255
256
257
258
259
260
261
262
263
264
265
266
267
268
269
270
271
272
273
274
275
276
277
278
279
280
281
282
283
284
285
286
287
288
289
290
291
292
293
294
295
296
297
298
299
300
301
302
303
304
305
306
307
308
309
310
311
312
313
314
315
316
317
318
319
320
321
322
323
324
325
326
327
328
329
330
331
332
333
334
335
336
337
338
339
340
341
342
343
344
345
346
347
348
349
350
351
352
353
354
355
356
357
358
359
360
361
362
363
364
365
366
367
368
369
370
371
372
373
374
375
376
377
378
379
380
381
382
383
384
385
386
387
388
389
390
391
392
393
394
395
396
397
398
399
400
401
402
403
404
405
406
407
408
409
410
411
412
413
414
415
416
417
418
419
420
421
422
423
424
425
426
427
428
429
430
431
432
433
434
435
436
437
438
439
440
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);
        }
    }
 
}