完成对象存储删除接口,优化部分代码

This commit is contained in:
luke 2025-06-20 11:15:05 +08:00
parent 59d1a9de3a
commit c1376a2e68
16 changed files with 465 additions and 19 deletions

View File

@ -9,6 +9,7 @@
| 1.0.3 | 2025-06-16 | Luke.Ye | 新增文档上传至对象存储接口 |
| 1.0.4 | 2025-06-17 | Luke.Ye | 上传文档记录入库 |
| 1.0.5 | 2025-06-19 | Luke.Ye | 文档导入ES逻辑优化 |
| 1.0.6 | 2025-06-20 | Luke.Ye | 完成对象存储删除接口,优化部分代码 |

View File

@ -3,6 +3,8 @@ package com.knowledge.base.application.service;
import com.knowledge.base.domain.common.model.PageResult;
import com.knowledge.base.infrastructure.north.dto.doc.OSRecordDTO;
import java.util.List;
/**
* @author Luke.ye
* @date 2025/6/16 19:13
@ -14,6 +16,27 @@ public interface DocAppService {
*/
PageResult<OSRecordDTO> pageQueryOSRecord(int page, int size, String uploader);
/**
* 批量获取上传记录
* @param recordIds
* @return
*/
List<OSRecordDTO> batchQueryOSRecord(List<Long> recordIds);
/**
* 批量删除上传记录
*
* @param clearAll 是否清空所有
* true: 清空ES/缓存/MySQL/对象存储/本地存储中所有相关的数据
* false仅清除 MySQL+对象存储 中的数据
* @param recordIds 本地存储路径
* @return
*/
boolean batchRemoveOSFiles(boolean clearAll, List<Long> recordIds);
/**
* 保存上传记录
*/

View File

@ -1,22 +1,42 @@
package com.knowledge.base.application.service;
import cn.hutool.core.collection.CollectionUtil;
import com.google.common.collect.Lists;
import com.knowledge.base.application.exceptions.AppException;
import com.knowledge.base.domain.common.model.PageResult;
import com.knowledge.base.domain.doc.model.OSRecordDO;
import com.knowledge.base.domain.doc.service.iface.FileDomainService;
import com.knowledge.base.infrastructure.cache.iface.FileCacheService;
import com.knowledge.base.infrastructure.config.ObjectStorageProperties;
import com.knowledge.base.infrastructure.converter.FileDtoConverter;
import com.knowledge.base.infrastructure.north.dto.doc.OSRecordDTO;
import com.knowledge.base.infrastructure.south.es.FileElasticsearchGateway;
import com.knowledge.base.infrastructure.south.minio.ObjectStorageGateway;
import com.knowledge.base.infrastructure.util.LocalFileUtil;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import java.util.List;
import java.util.Map;
import java.util.Objects;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@RequiredArgsConstructor
@Slf4j
public class DocAppServiceImpl implements DocAppService{
private final FileDomainService fileDomainService;
private final ObjectStorageProperties osProperties;
private final FileElasticsearchGateway esGateway;
private final FileCacheService fileCacheService;
private final ObjectStorageGateway osGateway;
@Override
public PageResult<OSRecordDTO> pageQueryOSRecord(int page, int size, String uploader) {
if(page < 1 || size < 1) {
@ -35,6 +55,62 @@ public class DocAppServiceImpl implements DocAppService{
return new PageResult<>(dtoList, osRecordDOPageResult.getTotal());
}
@Override
public List<OSRecordDTO> batchQueryOSRecord(List<Long> recordIds) {
if(CollectionUtil.isEmpty(recordIds)) {
throw new AppException("参数recordIds不能为空");
}
List<OSRecordDO> osRecordDOList = null;
try {
osRecordDOList = fileDomainService.batchQueryOSRecord(recordIds);
} catch (Exception e) {
throw new AppException("批量获取对象存储记录失败请检查recordIds", e);
}
return FileDtoConverter.toDTOs(osRecordDOList);
}
@Override
public boolean batchRemoveOSFiles(boolean clearAll, List<Long> recordIds) {
if(CollectionUtil.isEmpty(recordIds)) {
throw new AppException("参数recordIds不能为空");
}
try {
List<OSRecordDTO> osRecordDTOS = Optional.ofNullable(batchQueryOSRecord(recordIds)).orElse(Lists.newArrayList());
List<String> filesLocalRelaPath = osRecordDTOS.stream().filter(Objects::nonNull)
.map(OSRecordDTO::getLocalRelaFilePath).collect(Collectors.toList());
Map<String, List<String>> bucketToObjectsMap = osRecordDTOS.stream()
.collect(Collectors.groupingBy(
OSRecordDTO::getBucketName,
Collectors.mapping(OSRecordDTO::getObjectPath, Collectors.toList())
));
// 删除关联数据库记录
fileDomainService.batchRemoveOSFiles(recordIds);
// 删除对象存储中的文件
bucketToObjectsMap.forEach((bucket, objPaths) -> {
try {
osGateway.deleteObjects(bucket, objPaths);
} catch (Exception e) {
log.error("删除对象存储中的文件失败. bucket: {}", bucket, e);
log.error("待删除路径如下: {}", objPaths);
}
});
if(clearAll) {
// 删除本地文件
LocalFileUtil.deleteFilesByRelativePath(osProperties.getLocalSearchablePathPrefix(), filesLocalRelaPath);
// 删除ES
esGateway.deleteByFilepaths(filesLocalRelaPath);
// 删除缓存
fileCacheService.removeMetaCacheBatch(filesLocalRelaPath);
}
return true;
} catch (Exception e) {
throw new AppException("批量删除对象存储记录失败请检查recordIds", e);
}
}
@Override
public void saveOSUplodRecord(OSRecordDTO dto) {
try {

View File

@ -3,6 +3,7 @@ package com.knowledge.base.domain.doc.repository.iface;
import com.knowledge.base.domain.common.model.PageResult;
import com.knowledge.base.domain.doc.repository.po.OSRecord;
import java.util.List;
import java.util.Optional;
public interface OSRecordRepository {
@ -23,4 +24,17 @@ public interface OSRecordRepository {
* @return
*/
Optional<OSRecord> getLatestRecordByRelaPath(String localRelaFilePath);
/**
* 批量获取
* @param recordIds
* @return
*/
List<OSRecord> batchQueryOSRecord(List<Long> recordIds);
/**
* 批量删除
* @param recordIds
*/
boolean batchRemoveOSFiles(List<Long> recordIds);
}

View File

@ -31,4 +31,18 @@ public interface FileDomainService {
* @return
*/
Optional<OSRecordDO> getLatestRecordByRelaPath(String localRelaFilePath);
/**
* 批量获取上传记录
* @param recordIds
* @return
*/
List<OSRecordDO> batchQueryOSRecord(List<Long> recordIds);
/**
* 批量删除上传记录
* @param recordIds
* @return
*/
boolean batchRemoveOSFiles(List<Long> recordIds);
}

View File

@ -13,23 +13,16 @@ import com.knowledge.base.domain.doc.service.iface.FileDomainService;
import com.knowledge.base.infrastructure.cache.iface.FileCacheService;
import com.knowledge.base.infrastructure.config.ConstantConfig;
import com.knowledge.base.infrastructure.config.ThreadPoolConfig;
import com.knowledge.base.infrastructure.south.es.FileElasticsearchGateway;
import com.knowledge.base.infrastructure.util.*;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import java.io.IOException;
import java.nio.file.*;
import java.util.HashMap;
import java.util.Map;
import java.util.Optional;
import java.util.Set;
import java.util.*;
public abstract class AbstractBaseFileImporter implements DocumentImporter {
@ -37,7 +30,7 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
private static final String INDEX_NAME = "documents";
@Autowired
protected RestHighLevelClient esClient;
private FileElasticsearchGateway esGateway;
@Autowired
private FileCacheService fileCacheService;
@ -129,9 +122,8 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
// === Step 4: 检查 ES 是否已有未变动版本 ===
String docId = SafeIdUtil.encode(localRelaFilePath);
GetRequest getRequest = new GetRequest(INDEX_NAME, docId);
if (esClient.exists(getRequest, RequestOptions.DEFAULT)) {
GetResponse existing = esClient.get(getRequest, RequestOptions.DEFAULT);
GetResponse existing = esGateway.getIfExists(INDEX_NAME, docId);
if(Objects.nonNull(existing)) {
Map<String, Object> existingSource = existing.getSourceAsMap();
Object esMtime = existingSource.get("mtime");
if (esMtime != null && Long.parseLong(esMtime.toString()) == localMTime) {
@ -139,7 +131,7 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
fileCacheService.cacheMeta(localRelaFilePath, transToMetaJson(existingSource), ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES);
return true;
}
esClient.delete(new DeleteRequest(INDEX_NAME, docId), RequestOptions.DEFAULT);
esGateway.deleteDoc(INDEX_NAME, docId);
logger.info("[ES] 已删除旧版本文件: {}", localRelaFilePath);
}
@ -149,8 +141,7 @@ public abstract class AbstractBaseFileImporter implements DocumentImporter {
Map<String, Object> doc = buildDocument(fileNameWithSuffix, localRelaFilePath, content, localMTime, extInfo);
// === Step 6: 写入 Elasticsearch 并更新缓存 ===
IndexRequest request = new IndexRequest(INDEX_NAME).id(docId).source(doc);
esClient.index(request, RequestOptions.DEFAULT);
esGateway.saveDoc(INDEX_NAME, docId, doc);
logger.info("导入成功: {}", localRelaFilePath);
fileCacheService.cacheMeta(localRelaFilePath, transToMetaJson(doc), ConstantConfig.FILE_META_CACHE_EXPIRED_MINUTES);

View File

@ -77,4 +77,15 @@ public class FileDomainServiceImpl implements FileDomainService {
}
return Optional.of(BeanConvertUtil.convert(osRecordOpt.get(), OSRecordDO.class));
}
@Override
public List<OSRecordDO> batchQueryOSRecord(List<Long> recordIds) {
return BeanConvertUtil.convertList(osRecordRepository.batchQueryOSRecord(recordIds), OSRecordDO.class);
}
@Override
public boolean batchRemoveOSFiles(List<Long> recordIds) {
osRecordRepository.batchRemoveOSFiles(recordIds);
return true;
}
}

View File

@ -1,5 +1,6 @@
package com.knowledge.base.infrastructure.cache.iface;
import java.util.Collection;
import java.util.Optional;
/**
@ -31,6 +32,8 @@ public interface FileCacheService {
*/
default void removeMetaCache(String fnWithRelativePath) {};
default public void removeMetaCacheBatch(Collection<String> fileRelativePaths) {}
/**
* 清空所有缓存注意某些实现可能未实现
*/

View File

@ -1,5 +1,6 @@
package com.knowledge.base.infrastructure.cache.impl;
import cn.hutool.core.collection.CollectionUtil;
import com.knowledge.base.infrastructure.cache.iface.FileCacheService;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
@ -7,6 +8,7 @@ import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.Collection;
import java.util.HashSet;
import java.util.Optional;
import java.util.Set;
@ -48,6 +50,21 @@ public class RedisFileCacheServiceImpl implements FileCacheService {
redisTemplate.delete(key("meta", fnWithRelativePath));
}
@Override
public void removeMetaCacheBatch(Collection<String> fileRelativePaths) {
if (CollectionUtil.isEmpty(fileRelativePaths)) {
return;
}
Set<String> redisKeys = new HashSet<>();
for (String path : fileRelativePaths) {
redisKeys.add(key("meta", path));
}
redisTemplate.delete(redisKeys);
log.info("批量删除 Redis 文件缓存,数量: {}", redisKeys.size());
}
@Override
public void clearAll() {
log.warn("正在清空 Redis 文件缓存,前缀: {}", FILE_CACHE_PREFIX);

View File

@ -1,5 +1,7 @@
package com.knowledge.base.infrastructure.converter;
import cn.hutool.core.collection.CollectionUtil;
import com.google.common.collect.Lists;
import com.knowledge.base.domain.doc.model.FileDO;
import com.knowledge.base.domain.doc.model.OSRecordDO;
import com.knowledge.base.infrastructure.north.dto.doc.FileDTO;
@ -8,6 +10,7 @@ import org.springframework.beans.BeanUtils;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.List;
public class FileDtoConverter {
private static final DateTimeFormatter dtf = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
@ -66,4 +69,12 @@ public class FileDtoConverter {
return dto;
}
public static List<OSRecordDTO> toDTOs(List<OSRecordDO> doObjList) {
if(CollectionUtil.isEmpty(doObjList)) {
return Lists.newArrayList();
}
List<OSRecordDTO> dtoList = Lists.newArrayList();
doObjList.forEach(doObj -> dtoList.add(toDTO(doObj)));
return dtoList;
}
}

View File

@ -14,7 +14,7 @@ import com.knowledge.base.infrastructure.config.DynamicConfig;
import com.knowledge.base.infrastructure.config.ObjectStorageProperties;
import com.knowledge.base.infrastructure.north.dto.doc.OSRecordDTO;
import com.knowledge.base.infrastructure.north.dto.user.UserDTO;
import com.knowledge.base.infrastructure.south.ObjectStorageGateway;
import com.knowledge.base.infrastructure.south.minio.ObjectStorageGateway;
import com.knowledge.base.infrastructure.util.DateUtil;
import com.knowledge.base.infrastructure.util.ThreadPoolUtil;
import lombok.RequiredArgsConstructor;
@ -166,6 +166,26 @@ public class FileWriteController {
}
}
@PostMapping("/batch-delete-os-upload")
public ResponseEntity<?> batchDeleteOSUpload(@CookieValue(value = ConstantConfig.COOKIE_KEY, required = false) String cookieToken,
@RequestHeader(value = "Authorization", required = false) String token,
@RequestBody Map<String, Object> body) {
// TODO 越权校验
Object idsObj = body.get("recordIds");
if (!(idsObj instanceof List)) {
return ResponseEntity.badRequest().body("recordIds 必须为数组");
}
@SuppressWarnings("unchecked")
List<Object> idObjects = (List<Object>) idsObj;
List<Long> recordIds = idObjects.stream()
.filter(Objects::nonNull)
.map(Object::toString)
.map(Long::parseLong)
.collect(Collectors.toList());
boolean removeRes = docAppService.batchRemoveOSFiles(true, recordIds);
return ResponseEntity.ok(Map.of("result", removeRes));
}
private void handleAsyncRecord(String token, String bucket, String originFileNameWithSuffix, String suffix,
String s3FullPath, String url, byte[] fileBytes, boolean searchable) {
try {

View File

@ -1,9 +1,11 @@
package com.knowledge.base.infrastructure.repository.persistence.doc;
import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.util.StrUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.google.common.collect.Lists;
import com.knowledge.base.domain.common.model.PageResult;
import com.knowledge.base.domain.doc.repository.iface.OSRecordRepository;
import com.knowledge.base.domain.doc.repository.po.OSRecord;
@ -11,6 +13,7 @@ import com.knowledge.base.infrastructure.repository.mapper.doc.OSRecordMapper;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Repository;
import java.util.List;
import java.util.Optional;
@Repository
@ -46,4 +49,20 @@ public class OSRecordRepositoryImpl implements OSRecordRepository {
.orderByDesc("upload_time"));
return Optional.ofNullable(fileLatestRecord);
}
@Override
public List<OSRecord> batchQueryOSRecord(List<Long> recordIds) {
if(CollectionUtil.isEmpty(recordIds)) {
return Lists.newArrayList();
}
return osRecordMapper.selectBatchIds(recordIds);
}
@Override
public boolean batchRemoveOSFiles(List<Long> recordIds) {
if(CollectionUtil.isEmpty(recordIds)) {
return true;
}
return osRecordMapper.deleteBatchIds(recordIds) > 0;
}
}

View File

@ -0,0 +1,134 @@
package com.knowledge.base.infrastructure.south.es;
import com.knowledge.base.infrastructure.util.SafeIdUtil;
import lombok.extern.slf4j.Slf4j;
import org.elasticsearch.action.bulk.BulkRequest;
import org.elasticsearch.action.bulk.BulkResponse;
import org.elasticsearch.action.delete.DeleteRequest;
import org.elasticsearch.action.get.GetRequest;
import org.elasticsearch.action.get.GetResponse;
import org.elasticsearch.action.index.IndexRequest;
import org.elasticsearch.client.RequestOptions;
import org.elasticsearch.client.RestHighLevelClient;
import org.elasticsearch.index.query.TermQueryBuilder;
import org.elasticsearch.search.SearchHit;
import org.elasticsearch.search.builder.SearchSourceBuilder;
import org.elasticsearch.action.search.SearchRequest;
import org.elasticsearch.action.search.SearchResponse;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.List;
import java.util.Map;
/**
* @author Luke.ye
* @date 2025/6/20 10:20
*/
@Slf4j
@Component
public class FileElasticsearchGateway {
private static final String INDEX_NAME = "documents";
@Autowired
private RestHighLevelClient esClient;
/**
* 根据 filepath 批量删除文档
*
* @param filepaths 相对路径列表
*/
public void deleteByFilepaths(List<String> filepaths) {
if (filepaths == null || filepaths.isEmpty()) {
return;
}
try {
BulkRequest bulkDelete = new BulkRequest();
for (String filepath : filepaths) {
String docId = SafeIdUtil.encode(filepath); // 和导入逻辑保持一致
bulkDelete.add(new DeleteRequest(INDEX_NAME, docId));
}
BulkResponse response = esClient.bulk(bulkDelete, RequestOptions.DEFAULT);
if (response.hasFailures()) {
log.warn("部分删除失败: {}", response.buildFailureMessage());
} else {
log.info("已成功删除 {} 个文档", filepaths.size());
}
} catch (IOException e) {
log.error("批量删除文件失败", e);
}
}
/**
* 根据字段删除例如filename/url
* 可扩展调用者传字段名与值
*/
public void deleteByField(String field, String value) {
try {
SearchSourceBuilder builder = new SearchSourceBuilder();
builder.query(new TermQueryBuilder(field, value)).size(1000); // 单次最多匹配1000条
SearchRequest searchRequest = new SearchRequest(INDEX_NAME).source(builder);
SearchResponse response = esClient.search(searchRequest, RequestOptions.DEFAULT);
BulkRequest bulkRequest = new BulkRequest();
for (SearchHit hit : response.getHits().getHits()) {
bulkRequest.add(new DeleteRequest(INDEX_NAME, hit.getId()));
}
if (bulkRequest.numberOfActions() > 0) {
BulkResponse result = esClient.bulk(bulkRequest, RequestOptions.DEFAULT);
if (result.hasFailures()) {
log.warn("字段 {} 删除失败: {}", field, result.buildFailureMessage());
}
}
} catch (IOException e) {
log.error("deleteByField 出错", e);
}
}
/**
* 判断文档是否存在并获取内容
*/
public GetResponse getIfExists(String index, String docId) {
try {
GetRequest getRequest = new GetRequest(index, docId);
if (!esClient.exists(getRequest, RequestOptions.DEFAULT)) {
return null;
}
return esClient.get(getRequest, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("ES 获取文档失败: index={}, docId={}", index, docId, e);
return null;
}
}
/**
* 删除文档
*/
public void deleteDoc(String index, String docId) {
try {
esClient.delete(new DeleteRequest(index, docId), RequestOptions.DEFAULT);
} catch (Exception e) {
log.warn("ES 删除文档失败: index={}, docId={}", index, docId, e);
}
}
/**
* 写入/更新文档
*/
public void saveDoc(String index, String docId, Map<String, Object> source) {
try {
IndexRequest request = new IndexRequest(index).id(docId).source(source);
esClient.index(request, RequestOptions.DEFAULT);
} catch (Exception e) {
log.error("ES 写入文档失败: index={}, docId={}", index, docId, e);
}
}
}

View File

@ -1,8 +1,10 @@
package com.knowledge.base.infrastructure.south;
package com.knowledge.base.infrastructure.south.minio;
import com.knowledge.base.infrastructure.config.ObjectStorageProperties;
import io.minio.*;
import io.minio.http.Method;
import io.minio.messages.DeleteError;
import io.minio.messages.DeleteObject;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;
@ -10,6 +12,7 @@ import org.springframework.web.multipart.MultipartFile;
import java.util.List;
import java.util.Optional;
import java.util.stream.Collectors;
@Component
@Slf4j
@ -76,6 +79,37 @@ public class MinioOSGatewayImpl implements ObjectStorageGateway {
}
}
@Override
public void deleteObjects(String bucket, List<String> objectPaths) {
if (objectPaths == null || objectPaths.isEmpty()) {
log.info("无需删除objectPath 列表为空");
return;
}
try {
Iterable<Result<DeleteError>> results = minioClient.removeObjects(
RemoveObjectsArgs.builder()
.bucket(bucket)
.objects(objectPaths.stream()
.map(e -> new DeleteObject(e))
.collect(Collectors.toList()))
.build()
);
for (Result<DeleteError> result : results) {
DeleteError error = result.get();
log.warn("删除失败: object={}, message={}", error.objectName(), error.message());
}
log.info("MinIO 批量删除完成,共 {} 个对象", objectPaths.size());
} catch (Exception e) {
log.error("MinIO 批量删除出错", e);
throw new RuntimeException("MinIO 批量删除失败", e);
}
}
/**
* 判断 bucket 是否公开默认策略通过配置控制
*/

View File

@ -1,7 +1,9 @@
package com.knowledge.base.infrastructure.south;
package com.knowledge.base.infrastructure.south.minio;
import org.springframework.web.multipart.MultipartFile;
import java.util.List;
public interface ObjectStorageGateway {
/**
@ -21,4 +23,5 @@ public interface ObjectStorageGateway {
*/
String generateUrl(String bucket, String objectPath) throws Exception;
void deleteObjects(String bucket, List<String> objectPaths);
}

View File

@ -0,0 +1,75 @@
package com.knowledge.base.infrastructure.util;
import lombok.extern.slf4j.Slf4j;
import java.io.IOException;
import java.nio.file.*;
import java.util.List;
/**
* @author Luke.ye
* @date 2025/6/20 10:45
*/
@Slf4j
public class LocalFileUtil {
/**
* 批量删除文件根据 basePath + 相对路径 拼接
*
* @param localPathPrefix 本地搜索路径前缀
* @param relativePaths 文件相对路径列表
* @return 删除成功数量
*/
public static int deleteFilesByRelativePath(String localPathPrefix, List<String> relativePaths) {
if (relativePaths == null || relativePaths.isEmpty()) {
log.info("无需删除文件,相对路径为空");
return 0;
}
int successCount = 0;
for (String relativePath : relativePaths) {
Path path = Paths.get(localPathPrefix, relativePath).toAbsolutePath().normalize();
try {
if (Files.exists(path) && Files.isRegularFile(path)) {
Files.delete(path);
log.info("已删除本地文件: {}", path);
successCount++;
} else {
log.warn("文件不存在或不是常规文件,跳过: {}", path);
}
} catch (IOException e) {
log.error("删除本地文件失败: {}", path, e);
}
}
log.info("批量删除本地文件完成,共 {} 个请求,成功删除 {} 个", relativePaths.size(), successCount);
return successCount;
}
/**
* 写入文件到指定目录下自动创建目录
*
* @param localPathPrefix 本地存储根路径 /data/searchable/yyyy-MM-dd
* @param relativeDir 目标子目录路径 "a/b"
* @param filename 文件名 "xx.pdf"
* @param content 文件内容字节
* @return 最终写入的绝对路径
* @throws IOException 写入失败时抛出
*/
public static Path writeFile(String localPathPrefix, String relativeDir, String filename, byte[] content) throws IOException {
Path dirPath = Paths.get(localPathPrefix, relativeDir).toAbsolutePath().normalize();
if (!Files.exists(dirPath)) {
Files.createDirectories(dirPath);
log.info("创建本地目录: {}", dirPath);
}
Path targetFilePath = dirPath.resolve(filename);
Files.write(targetFilePath, content);
log.info("写入本地文件成功: {}", targetFilePath);
return targetFilePath;
}
}