| ¶Ô±ÈÐÂÎļþ |
| | |
| | | package com.ruoyi.basic.task; |
| | | |
| | | import com.ruoyi.basic.mapper.StorageBlobMapper; |
| | | import com.ruoyi.basic.pojo.StorageBlob; |
| | | import com.ruoyi.common.config.FileProperties; |
| | | import lombok.RequiredArgsConstructor; |
| | | import lombok.extern.slf4j.Slf4j; |
| | | import org.springframework.scheduling.annotation.Scheduled; |
| | | import org.springframework.stereotype.Component; |
| | | import org.springframework.util.CollectionUtils; |
| | | import org.springframework.util.StringUtils; |
| | | |
| | | import java.io.File; |
| | | import java.util.ArrayDeque; |
| | | import java.util.ArrayList; |
| | | import java.util.Deque; |
| | | import java.util.HashSet; |
| | | import java.util.List; |
| | | import java.util.Set; |
| | | import java.util.concurrent.atomic.AtomicBoolean; |
| | | |
| | | /** |
| | | * æ¸
çæ ææä»¶å®æ¶ä»»å¡ã |
| | | */ |
| | | @Slf4j |
| | | @Component |
| | | @RequiredArgsConstructor |
| | | public class StorageBlobCleanupTask { |
| | | |
| | | private static final int DB_BATCH_SIZE = 500; |
| | | private static final int FILE_NAME_BATCH_SIZE = 1000; |
| | | |
| | | private final StorageBlobMapper storageBlobMapper; |
| | | private final FileProperties fileProperties; |
| | | |
| | | private final AtomicBoolean running = new AtomicBoolean(false); |
| | | |
| | | /** |
| | | * æ¯æ 1 å·åæ¨ 2 ç¹æ§è¡ä¸æ¬¡ï¼ |
| | | * 1. å é¤ storage_blob 䏿ªè¢« storage_attachment å
³èçè®°å½åå
¶æä»¶ |
| | | * 2. å é¤ç£çä¸ä¸åå¨äº storage_blob.uid_filename çæä»¶ |
| | | */ |
| | | @Scheduled(cron = "0 0 2 1 * ?") |
| | | public void cleanupUnusedStorageFiles() { |
| | | if (!running.compareAndSet(false, true)) { |
| | | log.warn("æä»¶æ¸
ç任塿£å¨æ§è¡ï¼æ¬æ¬¡è·³è¿"); |
| | | return; |
| | | } |
| | | |
| | | long start = System.currentTimeMillis(); |
| | | log.info("æä»¶æ¸
çä»»å¡å¼å§æ§è¡ï¼æ ¹ç®å½ï¼{}", fileProperties.getPath()); |
| | | try { |
| | | int removedBlobCount = cleanupOrphanStorageBlobs(); |
| | | int removedDiskFileCount = cleanupOrphanDiskFiles(); |
| | | long cost = System.currentTimeMillis() - start; |
| | | log.info("æä»¶æ¸
ç任塿§è¡å®æï¼å é¤å¤å¿ blob è®°å½ï¼{}ï¼å é¤ç£çæ ææä»¶ï¼{}ï¼èæ¶ï¼{} ms", |
| | | removedBlobCount, removedDiskFileCount, cost); |
| | | } catch (Exception e) { |
| | | log.error("æä»¶æ¸
ç任塿§è¡å¤±è´¥", e); |
| | | } finally { |
| | | running.set(false); |
| | | } |
| | | } |
| | | |
| | | private int cleanupOrphanStorageBlobs() { |
| | | long lastId = 0L; |
| | | int removedCount = 0; |
| | | |
| | | while (true) { |
| | | List<StorageBlob> orphanBlobs = storageBlobMapper.selectOrphanBlobsByIdRange(lastId, DB_BATCH_SIZE); |
| | | if (CollectionUtils.isEmpty(orphanBlobs)) { |
| | | break; |
| | | } |
| | | |
| | | List<Long> ids = new ArrayList<>(orphanBlobs.size()); |
| | | for (StorageBlob storageBlob : orphanBlobs) { |
| | | ids.add(storageBlob.getId()); |
| | | deleteBlobFiles(storageBlob); |
| | | } |
| | | storageBlobMapper.deleteByIdList(ids); |
| | | removedCount += ids.size(); |
| | | lastId = orphanBlobs.get(orphanBlobs.size() - 1).getId(); |
| | | |
| | | log.info("å·²å é¤ä¸æ¹å¤å¿ blobï¼batchSize={}ï¼lastId={}", ids.size(), lastId); |
| | | } |
| | | |
| | | return removedCount; |
| | | } |
| | | |
| | | private int cleanupOrphanDiskFiles() { |
| | | File rootDirectory = new File(fileProperties.getPath()); |
| | | if (!rootDirectory.exists() || !rootDirectory.isDirectory()) { |
| | | log.warn("æä»¶æ ¹ç®å½ä¸å卿䏿¯ç®å½ï¼è·³è¿ç£çæ¸
çï¼{}", fileProperties.getPath()); |
| | | return 0; |
| | | } |
| | | |
| | | int deletedCount = 0; |
| | | Deque<File> directories = new ArrayDeque<>(); |
| | | directories.push(rootDirectory); |
| | | |
| | | while (!directories.isEmpty()) { |
| | | File currentDirectory = directories.pop(); |
| | | File[] children = currentDirectory.listFiles(); |
| | | if (children == null || children.length == 0) { |
| | | continue; |
| | | } |
| | | |
| | | List<File> filesInDirectory = new ArrayList<>(); |
| | | for (File child : children) { |
| | | if (child.isDirectory()) { |
| | | directories.push(child); |
| | | } else if (child.isFile()) { |
| | | filesInDirectory.add(child); |
| | | } |
| | | } |
| | | |
| | | deletedCount += cleanupFilesInDirectory(filesInDirectory); |
| | | } |
| | | |
| | | return deletedCount; |
| | | } |
| | | |
| | | private int cleanupFilesInDirectory(List<File> filesInDirectory) { |
| | | if (CollectionUtils.isEmpty(filesInDirectory)) { |
| | | return 0; |
| | | } |
| | | |
| | | int deletedCount = 0; |
| | | for (int start = 0; start < filesInDirectory.size(); start += FILE_NAME_BATCH_SIZE) { |
| | | int end = Math.min(start + FILE_NAME_BATCH_SIZE, filesInDirectory.size()); |
| | | List<File> batchFiles = filesInDirectory.subList(start, end); |
| | | List<String> fileNames = new ArrayList<>(batchFiles.size()); |
| | | for (File file : batchFiles) { |
| | | fileNames.add(file.getName()); |
| | | } |
| | | |
| | | Set<String> existingFileNames = new HashSet<>(storageBlobMapper.selectExistingUidFilenames(fileNames)); |
| | | for (File file : batchFiles) { |
| | | if (!existingFileNames.contains(file.getName()) && safeDelete(file)) { |
| | | deletedCount++; |
| | | } |
| | | } |
| | | } |
| | | return deletedCount; |
| | | } |
| | | |
| | | private void deleteBlobFiles(StorageBlob storageBlob) { |
| | | File originalFile = resolveBlobFile(storageBlob); |
| | | safeDelete(originalFile); |
| | | |
| | | File compressedFile = resolveCompressedFile(originalFile); |
| | | safeDelete(compressedFile); |
| | | } |
| | | |
| | | private File resolveBlobFile(StorageBlob storageBlob) { |
| | | String basePath = fileProperties.getPath(); |
| | | if (!StringUtils.hasText(storageBlob.getPath())) { |
| | | return new File(basePath, storageBlob.getUidFilename()); |
| | | } |
| | | return new File(new File(basePath, storageBlob.getPath()), storageBlob.getUidFilename()); |
| | | } |
| | | |
| | | private File resolveCompressedFile(File originalFile) { |
| | | if (originalFile == null) { |
| | | return null; |
| | | } |
| | | File parent = originalFile.getParentFile(); |
| | | if (parent == null) { |
| | | return null; |
| | | } |
| | | return new File(parent, "thumb_" + originalFile.getName()); |
| | | } |
| | | |
| | | private boolean safeDelete(File file) { |
| | | if (file == null || !file.exists() || !file.isFile()) { |
| | | return false; |
| | | } |
| | | if (file.delete()) { |
| | | return true; |
| | | } |
| | | log.warn("å é¤æä»¶å¤±è´¥ï¼{}", file.getAbsolutePath()); |
| | | return false; |
| | | } |
| | | } |