兼容+功能
This commit is contained in:
@@ -4,6 +4,7 @@ import com.senmesh.common.util.UserContextHolder;
|
||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.UserFileDTO;
|
||||
import com.senmesh.sfile.service.NormalUserFileService;
|
||||
import com.senmesh.sfile.service.UserFileService;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
@@ -21,7 +22,7 @@ public class NormalUserFileServiceImpl implements NormalUserFileService {
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long create(UserFileCreateDTO createDTO) {
|
||||
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
||||
createDTO.setUserId(UserContextHolder.getExistLoginUserId());
|
||||
// 设置根类型为普通
|
||||
createDTO.setRootType(RootTypeEnum.NORMAL.getType());
|
||||
|
||||
@@ -1,5 +1,6 @@
|
||||
package com.senmesh.sfile.service.impl;
|
||||
|
||||
import com.senmesh.common.util.RandomUtils;
|
||||
import com.senmesh.common.util.UserContextHolder;
|
||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||
@@ -36,15 +37,19 @@ public class ShareUserFileServiceImpl implements ShareUserFileService {
|
||||
calendar.add(Calendar.DAY_OF_MONTH, validDays);
|
||||
Date endTime = calendar.getTime();
|
||||
|
||||
// 生成随机6位数字访问码
|
||||
String accessCode = RandomUtils.nextSixVerificationCode();
|
||||
|
||||
// 调用UserFileService的重载create方法,根据普通文件创建分享文件
|
||||
ExpandUserFileCreateDTO expandCreateDTO = ExpandUserFileCreateDTO.builder()
|
||||
.userFileId(userFileId)
|
||||
.userId(userId)
|
||||
.rootType(RootTypeEnum.SHARE.getType())
|
||||
.fileStatus(FileStatusEnum.NORMAL.getStatus())
|
||||
.accessCode(accessCode)
|
||||
.endTime(endTime)
|
||||
.build();
|
||||
return userFileService.create(expandCreateDTO);
|
||||
return userFileService.create(expandCreateDTO).getFolder().getId();
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,455 @@
|
||||
package com.senmesh.sfile.service.impl;
|
||||
|
||||
import com.senmesh.common.exception.util.Assert;
|
||||
import com.senmesh.common.util.JsonUtils;
|
||||
import com.senmesh.component.file.config.FileProperties;
|
||||
import com.senmesh.component.file.service.FileService;
|
||||
import com.senmesh.sfile.constant.FileLocationEnum;
|
||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||
import com.senmesh.sfile.constant.FileTypeEnum;
|
||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||
import com.senmesh.sfile.dto.ExpandUserFileDTO;
|
||||
import com.senmesh.sfile.dto.TransferMetadataDTO;
|
||||
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.UserFileDTO;
|
||||
import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
||||
import com.senmesh.sfile.service.TransferFileMonitorService;
|
||||
import com.senmesh.sfile.service.UserFileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.nio.file.attribute.FileTime;
|
||||
import java.util.Comparator;
|
||||
import java.util.Enumeration;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.stream.Collectors;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipFile;
|
||||
import java.util.zip.ZipInputStream;
|
||||
|
||||
/**
|
||||
* 传输文件监听服务实现
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class TransferFileMonitorServiceImpl implements TransferFileMonitorService {
|
||||
|
||||
@Autowired
|
||||
private FileProperties fileProperties;
|
||||
|
||||
@Autowired
|
||||
private UserFileService userFileService;
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
public void scanAndProcess() {
|
||||
String transferReceiveDir = fileProperties.getTransferReceiveDir();
|
||||
Path receiveDir = Paths.get(transferReceiveDir);
|
||||
|
||||
try {
|
||||
if (!Files.exists(receiveDir)) {
|
||||
Files.createDirectories(receiveDir);
|
||||
log.info("创建接收目录: {}", receiveDir);
|
||||
return;
|
||||
}
|
||||
|
||||
// 1. 扫描 .zip 文件(解包逻辑)
|
||||
List<Path> zipFiles = Files.list(receiveDir)
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".zip"))
|
||||
.filter(p -> !p.toString().endsWith(".tmp"))
|
||||
.sorted(Comparator.comparing(p -> {
|
||||
try {
|
||||
return Files.getLastModifiedTime(p);
|
||||
} catch (IOException e) {
|
||||
return FileTime.fromMillis(0);
|
||||
}
|
||||
}))
|
||||
.limit(10)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!zipFiles.isEmpty()) {
|
||||
log.info("发现 {} 个待处理的传输文件", zipFiles.size());
|
||||
for (Path zipFile : zipFiles) {
|
||||
processZipFile(zipFile);
|
||||
}
|
||||
}
|
||||
|
||||
// 2. 扫描 .finish 文件(完成标记逻辑)
|
||||
List<Path> finishFiles = Files.list(receiveDir)
|
||||
.filter(Files::isRegularFile)
|
||||
.filter(p -> p.toString().endsWith(".finish"))
|
||||
.sorted(Comparator.comparing(p -> {
|
||||
try {
|
||||
return Files.getLastModifiedTime(p);
|
||||
} catch (IOException e) {
|
||||
return FileTime.fromMillis(0);
|
||||
}
|
||||
}))
|
||||
.limit(10)
|
||||
.collect(Collectors.toList());
|
||||
|
||||
if (!finishFiles.isEmpty()) {
|
||||
log.info("发现 {} 个完成标记文件", finishFiles.size());
|
||||
for (Path finishFile : finishFiles) {
|
||||
processFinishFile(finishFile);
|
||||
}
|
||||
}
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("扫描传输目录失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理单个 ZIP 文件(解包逻辑)
|
||||
*/
|
||||
private void processZipFile(Path zipFile) {
|
||||
String fileName = zipFile.getFileName().toString();
|
||||
|
||||
try {
|
||||
// 1. 校验 ZIP 包内文件大小是否匹配,并返回元数据
|
||||
TransferMetadataDTO metadata = verifyFileMD5(zipFile);
|
||||
if (metadata == null) {
|
||||
// 校验不通过,跳过
|
||||
return;
|
||||
}
|
||||
|
||||
log.info("文件大小校验通过,开始解包: {}", fileName);
|
||||
|
||||
// 2. 解包并创建用户文件(传入已解析的元数据,直接创建为正常状态)
|
||||
ExpandUserFileDTO result = unpackTransferFileInternal(zipFile, metadata);
|
||||
|
||||
// 3. 创建 .finish 文件到发送目录,告诉发送方处理完成
|
||||
createFinishFile(fileName);
|
||||
|
||||
// 4. 删除已处理的 ZIP 文件
|
||||
Files.delete(zipFile);
|
||||
log.info("处理完成,已删除 ZIP 文件: {}", fileName);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理文件失败,跳过: {}", fileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
|
||||
/**
|
||||
* 校验文件大小(对比元数据中的大小)
|
||||
*
|
||||
* @param zipFile ZIP 文件路径
|
||||
* @return 校验通过返回 TransferMetadataDTO,否则返回 null
|
||||
*/
|
||||
private TransferMetadataDTO verifyFileMD5(Path zipFile) {
|
||||
String fileName = zipFile.getFileName().toString();
|
||||
|
||||
try (ZipFile zf = new ZipFile(zipFile.toFile())) {
|
||||
// 1. 读取元数据
|
||||
ZipEntry metadataEntry = zf.getEntry("transferMetadata.json");
|
||||
if (metadataEntry == null) {
|
||||
log.debug("ZIP 文件缺少 transferMetadata.json,跳过(可能还在传输中): {}", fileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
TransferMetadataDTO metadata;
|
||||
try (InputStream is = zf.getInputStream(metadataEntry)) {
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[1024];
|
||||
int len;
|
||||
while ((len = is.read(buffer)) > 0) {
|
||||
baos.write(buffer, 0, len);
|
||||
}
|
||||
String metadataJson = new String(baos.toByteArray(), StandardCharsets.UTF_8);
|
||||
metadata = JsonUtils.toObject(metadataJson, TransferMetadataDTO.class);
|
||||
}
|
||||
|
||||
// 2. 检查是否包含 ExpandUserFileDTO
|
||||
ExpandUserFileDTO expandUserFile = metadata.getExpandUserFile();
|
||||
if (expandUserFile == null || expandUserFile.getFiles() == null) {
|
||||
log.debug("元数据中不包含文件信息,跳过(可能还在传输中): {}", fileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
// 3. 创建 userFileId -> fileSize 的映射
|
||||
Map<Long, Long> expectedSizeMap = new HashMap<>();
|
||||
for (UserFileDTO file : expandUserFile.getFiles()) {
|
||||
if (file.getFileSize() != null) {
|
||||
expectedSizeMap.put(file.getId(), file.getFileSize());
|
||||
}
|
||||
}
|
||||
|
||||
if (expectedSizeMap.isEmpty()) {
|
||||
log.debug("元数据中不包含文件大小信息,跳过(可能还在传输中): {}", fileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("开始校验文件大小: {}", fileName);
|
||||
|
||||
// 4. 遍历 ZIP 中的文件,对比大小
|
||||
Enumeration<? extends ZipEntry> entries = zf.entries();
|
||||
int fileCount = 0;
|
||||
int successCount = 0;
|
||||
|
||||
while (entries.hasMoreElements()) {
|
||||
ZipEntry entry = entries.nextElement();
|
||||
|
||||
// 跳过目录和元数据文件
|
||||
if (entry.isDirectory() || "transferMetadata.json".equals(entry.getName())) {
|
||||
continue;
|
||||
}
|
||||
|
||||
fileCount++;
|
||||
|
||||
// 提取 userFileId(去掉路径)
|
||||
String entryName = entry.getName();
|
||||
String userFileIdStr = entryName.contains("/")
|
||||
? entryName.substring(entryName.lastIndexOf("/") + 1)
|
||||
: entryName;
|
||||
|
||||
// 解析 userFileId
|
||||
Long userFileId;
|
||||
try {
|
||||
userFileId = Long.parseLong(userFileIdStr);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法解析 userFileId: {}", userFileIdStr);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取期望的文件大小
|
||||
Long expectedSize = expectedSizeMap.get(userFileId);
|
||||
if (expectedSize == null) {
|
||||
log.warn("元数据中未找到文件大小信息: userFileId={}", userFileId);
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取实际文件大小
|
||||
long actualSize = entry.getSize();
|
||||
|
||||
// 对比大小
|
||||
if (actualSize != expectedSize) {
|
||||
log.error("文件大小不匹配: userFileId={}, 期望={} bytes, 实际={} bytes",
|
||||
userFileId, expectedSize, actualSize);
|
||||
return null;
|
||||
}
|
||||
|
||||
successCount++;
|
||||
log.debug("文件大小校验通过: userFileId={} ({} bytes)", userFileId, actualSize);
|
||||
}
|
||||
|
||||
// 5. 检查文件数量是否匹配
|
||||
if (fileCount == 0 || successCount != expectedSizeMap.size()) {
|
||||
log.debug("文件数量不匹配: 期望={}, 实际={}, 跳过(可能还在传输中): {}",
|
||||
expectedSizeMap.size(), successCount, fileName);
|
||||
return null;
|
||||
}
|
||||
|
||||
log.info("文件大小校验通过: {} (共 {} 个文件)", fileName, successCount);
|
||||
return metadata;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("文件大小校验失败: {}", fileName, e);
|
||||
return null;
|
||||
}
|
||||
}
|
||||
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public void unpackTransferFile(Path zipFilePath) {
|
||||
// 先校验并获取元数据
|
||||
TransferMetadataDTO metadata = verifyFileMD5(zipFilePath);
|
||||
if (metadata == null) {
|
||||
throw new RuntimeException("ZIP 文件校验失败或格式不正确");
|
||||
}
|
||||
// 调用内部方法
|
||||
unpackTransferFileInternal(zipFilePath, metadata);
|
||||
}
|
||||
|
||||
/**
|
||||
* 解包传输文件(内部方法,接收已解析的元数据)
|
||||
*
|
||||
* @return 解包后创建的文件夹和文件信息
|
||||
*/
|
||||
private ExpandUserFileDTO unpackTransferFileInternal(Path zipFilePath, TransferMetadataDTO metadata) {
|
||||
String fileName = zipFilePath.getFileName().toString();
|
||||
log.info("开始解包文件: {}", fileName);
|
||||
|
||||
try {
|
||||
// 1. 从元数据中获取文件夹和文件信息
|
||||
ExpandUserFileDTO expandUserFile = metadata.getExpandUserFile();
|
||||
Assert.notNull(expandUserFile, "metadata 缺少 expandUserFile");
|
||||
|
||||
UserFileDTO folder = expandUserFile.getFolder();
|
||||
List<UserFileDTO> files = expandUserFile.getFiles();
|
||||
Assert.notNull(folder, "expandUserFile 缺少 folder");
|
||||
Assert.notEmpty(files, "expandUserFile 缺少 files");
|
||||
|
||||
// 2. 确定 rootType 并设置到 folder 中
|
||||
String location = fileProperties.getLocation();
|
||||
String rootType;
|
||||
if (FileLocationEnum.INTERNAL.getLocation().equals(location)) {
|
||||
rootType = RootTypeEnum.EXTERNAL_TO_INTERNAL.getType();
|
||||
} else {
|
||||
rootType = RootTypeEnum.INTERNAL_TO_EXTERNAL.getType();
|
||||
}
|
||||
folder.setRootType(rootType);
|
||||
|
||||
// 3. 创建 userFileId -> UserFileDTO 的映射
|
||||
Map<Long, UserFileDTO> fileMap = new HashMap<>();
|
||||
for (UserFileDTO file : files) {
|
||||
fileMap.put(file.getId(), file);
|
||||
}
|
||||
|
||||
// 4. 解析 ZIP 文件,上传文件并设置 fileCode
|
||||
try (ZipInputStream zis = new ZipInputStream(new FileInputStream(zipFilePath.toFile()))) {
|
||||
ZipEntry entry;
|
||||
while ((entry = zis.getNextEntry()) != null) {
|
||||
// 跳过元数据文件和目录
|
||||
if ("transferMetadata.json".equals(entry.getName()) || entry.isDirectory()) {
|
||||
zis.closeEntry();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 提取 userFileId(去掉路径)
|
||||
String entryName = entry.getName();
|
||||
String userFileIdStr = entryName.contains("/")
|
||||
? entryName.substring(entryName.lastIndexOf("/") + 1)
|
||||
: entryName;
|
||||
|
||||
// 解析 userFileId
|
||||
Long userFileId;
|
||||
try {
|
||||
userFileId = Long.parseLong(userFileIdStr);
|
||||
} catch (NumberFormatException e) {
|
||||
log.warn("无法解析 userFileId,跳过: {}", userFileIdStr);
|
||||
zis.closeEntry();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 获取文件信息
|
||||
UserFileDTO fileInfo = fileMap.get(userFileId);
|
||||
if (fileInfo == null) {
|
||||
log.warn("元数据中未找到文件信息,跳过: userFileId={}", userFileId);
|
||||
zis.closeEntry();
|
||||
continue;
|
||||
}
|
||||
|
||||
// 读取文件流到内存
|
||||
ByteArrayOutputStream baos = new ByteArrayOutputStream();
|
||||
byte[] buffer = new byte[8192];
|
||||
int len;
|
||||
while ((len = zis.read(buffer)) > 0) {
|
||||
baos.write(buffer, 0, len);
|
||||
}
|
||||
|
||||
// 上传文件获取 fileCode
|
||||
String originalFileName = fileInfo.getFileName();
|
||||
log.debug("开始上传文件: userFileId={}, 文件名={}", userFileId, originalFileName);
|
||||
ByteArrayInputStream fileStream = new ByteArrayInputStream(baos.toByteArray());
|
||||
String fileCode = fileService.upload(originalFileName, fileStream).getFileCode();
|
||||
log.debug("文件上传成功: userFileId={}, fileCode={}", userFileId, fileCode);
|
||||
|
||||
// 设置 fileCode 到文件信息中
|
||||
fileInfo.setFileCode(fileCode);
|
||||
|
||||
zis.closeEntry();
|
||||
}
|
||||
}
|
||||
|
||||
// 5. 调用 UserFileService 的重载方法创建文件夹和文件
|
||||
ExpandUserFileDTO result = userFileService.create(folder, files);
|
||||
|
||||
log.info("文件解包成功: {}, 创建文件夹ID: {}", fileName, result.getFolder().getId());
|
||||
|
||||
return result;
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("解包文件失败: {}", fileName, e);
|
||||
throw new RuntimeException("解包文件失败: " + e.getMessage(), e);
|
||||
}
|
||||
}
|
||||
/**
|
||||
* 创建 .finish 文件到发送目录,告诉对方处理完成
|
||||
*
|
||||
* @param zipFileName ZIP 文件名(例如:123_456.zip)
|
||||
*/
|
||||
private void createFinishFile(String zipFileName) {
|
||||
try {
|
||||
// 获取发送目录
|
||||
String transferSendDir = fileProperties.getTransferSendDir();
|
||||
Path sendPath = Paths.get(transferSendDir);
|
||||
|
||||
// 确保发送目录存在
|
||||
if (!Files.exists(sendPath)) {
|
||||
Files.createDirectories(sendPath);
|
||||
}
|
||||
|
||||
// 创建 .finish 文件
|
||||
Path finishFile = sendPath.resolve(zipFileName + ".finish");
|
||||
Files.createFile(finishFile);
|
||||
|
||||
log.info("创建完成标记文件到发送目录: {}", finishFile);
|
||||
} catch (Exception e) {
|
||||
log.error("创建完成标记文件失败: {}", zipFileName, e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 处理 .finish 文件(发送方接收到对方的完成标记)
|
||||
*
|
||||
* @param finishFile .finish 文件路径(例如:123_456.zip.finish)
|
||||
*/
|
||||
private void processFinishFile(Path finishFile) {
|
||||
String fileName = finishFile.getFileName().toString();
|
||||
log.info("处理完成标记文件: {}", fileName);
|
||||
|
||||
try {
|
||||
// 1. 解析文件名,获取 userFileId
|
||||
// 文件名格式:123_456.zip.finish
|
||||
String zipFileName = fileName.replace(".finish", "");
|
||||
String[] parts = zipFileName.replace(".zip", "").split("_");
|
||||
|
||||
if (parts.length != 2) {
|
||||
log.warn("无法解析完成标记文件名,跳过: {}", fileName);
|
||||
return;
|
||||
}
|
||||
|
||||
Long folderId = Long.parseLong(parts[0]);
|
||||
Long userId = Long.parseLong(parts[1]);
|
||||
|
||||
log.info("解析完成标记文件: folderId={}, userId={}", folderId, userId);
|
||||
|
||||
// 2. 更新文件夹及其所有子文件状态为正常(modifyStatus 会自动更新所有子文件)
|
||||
UserFileModifyStatusDTO statusDTO = UserFileModifyStatusDTO.builder()
|
||||
.id(folderId)
|
||||
.oldStatus(FileStatusEnum.TRANSFERRING.getStatus())
|
||||
.newStatus(FileStatusEnum.NORMAL.getStatus())
|
||||
.build();
|
||||
userFileService.modifyStatus(statusDTO);
|
||||
log.info("更新文件夹及所有子文件状态为正常: folderId={}", folderId);
|
||||
|
||||
// 3. 删除 .finish 文件
|
||||
Files.delete(finishFile);
|
||||
log.info("处理完成,已删除完成标记文件: {}", fileName);
|
||||
|
||||
} catch (Exception e) {
|
||||
log.error("处理完成标记文件失败: {}", fileName, e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -0,0 +1,151 @@
|
||||
package com.senmesh.sfile.service.impl;
|
||||
|
||||
import com.senmesh.common.util.JsonUtils;
|
||||
import com.senmesh.common.util.UserContextHolder;
|
||||
import com.senmesh.component.file.config.FileProperties;
|
||||
import com.senmesh.component.file.service.FileService;
|
||||
import com.senmesh.sfile.constant.FileLocationEnum;
|
||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||
import com.senmesh.sfile.dto.ExpandUserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.ExpandUserFileDTO;
|
||||
import com.senmesh.sfile.dto.TransferMetadataDTO;
|
||||
import com.senmesh.sfile.dto.TransferUserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.UserFileDTO;
|
||||
import com.senmesh.sfile.service.TransferUserFileService;
|
||||
import com.senmesh.sfile.service.UserFileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.FileOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.util.HashMap;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
import java.util.zip.ZipEntry;
|
||||
import java.util.zip.ZipOutputStream;
|
||||
|
||||
/**
|
||||
* 传输用户文件ServiceImpl
|
||||
*/
|
||||
@Service
|
||||
@Slf4j
|
||||
public class TransferUserFileServiceImpl implements TransferUserFileService {
|
||||
|
||||
@Autowired
|
||||
private UserFileService userFileService;
|
||||
|
||||
@Autowired
|
||||
private FileProperties fileProperties;
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long create(TransferUserFileCreateDTO createDTO) {
|
||||
Long userId = UserContextHolder.getExistLoginUserId();
|
||||
|
||||
// 根据 FileProperties#location 的类型决定是内穿外还是外传内
|
||||
String location = fileProperties.getLocation();
|
||||
String rootType;
|
||||
if (FileLocationEnum.INTERNAL.getLocation().equals(location)) {
|
||||
// 内网 -> 内穿外
|
||||
rootType = RootTypeEnum.INTERNAL_TO_EXTERNAL.getType();
|
||||
} else {
|
||||
// 外网 -> 外传内
|
||||
rootType = RootTypeEnum.EXTERNAL_TO_INTERNAL.getType();
|
||||
}
|
||||
|
||||
// TODO 扫描文件审核
|
||||
// 调用UserFileService的重载create方法
|
||||
ExpandUserFileCreateDTO expandCreateDTO = ExpandUserFileCreateDTO.builder()
|
||||
.userFileId(createDTO.getUserFileId())
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.fileStatus(FileStatusEnum.TRANSFERRING.getStatus())
|
||||
.build();
|
||||
ExpandUserFileDTO expandUserFileDTO = userFileService.create(expandCreateDTO);
|
||||
|
||||
// 打包传输文件
|
||||
try {
|
||||
packTransferFiles(expandUserFileDTO);
|
||||
} catch (Exception e) {
|
||||
log.error("打包传输文件失败", e);
|
||||
throw new RuntimeException("打包传输文件失败: " + e.getMessage());
|
||||
}
|
||||
|
||||
return expandUserFileDTO.getFolder().getId();
|
||||
}
|
||||
|
||||
/**
|
||||
* 打包传输文件
|
||||
* 将文件夹和文件打包成压缩包,并添加transferMetadata.json文件信息
|
||||
*
|
||||
* @param expandUserFileDTO 扩展用户文件DTO
|
||||
*/
|
||||
private void packTransferFiles(ExpandUserFileDTO expandUserFileDTO) throws IOException {
|
||||
UserFileDTO folder = expandUserFileDTO.getFolder();
|
||||
List<UserFileDTO> files = expandUserFileDTO.getFiles();
|
||||
|
||||
// 1. 创建传输发送目录
|
||||
String transferSendDir = fileProperties.getTransferSendDir();
|
||||
Path transferPath = Paths.get(transferSendDir);
|
||||
if (!Files.exists(transferPath)) {
|
||||
Files.createDirectories(transferPath);
|
||||
}
|
||||
|
||||
// 2. 创建最终传输包
|
||||
String finalFileName = folder.getUserId() + "_" + folder.getId() + ".zip";
|
||||
Path finalFilePath = transferPath.resolve(finalFileName);
|
||||
|
||||
try (ZipOutputStream zos = new ZipOutputStream(new FileOutputStream(finalFilePath.toFile()))) {
|
||||
// 2.1 添加文件夹中的文件
|
||||
String folderName = folder.getFileName();
|
||||
for (UserFileDTO file : files) {
|
||||
String fileCode = file.getFileCode();
|
||||
Long userFileId = file.getId();
|
||||
|
||||
// 创建 ZipEntry(使用 userFileId 命名)
|
||||
String entryName = folderName + "/" + userFileId;
|
||||
ZipEntry zipEntry = new ZipEntry(entryName);
|
||||
zos.putNextEntry(zipEntry);
|
||||
|
||||
// 边读边写
|
||||
try (InputStream inputStream = fileService.getFile(fileCode)) {
|
||||
byte[] buffer = new byte[8192];
|
||||
int len;
|
||||
while ((len = inputStream.read(buffer)) > 0) {
|
||||
zos.write(buffer, 0, len);
|
||||
}
|
||||
}
|
||||
zos.closeEntry();
|
||||
|
||||
log.debug("添加文件: userFileId={}, 原文件名={}, 大小={} bytes",
|
||||
userFileId, file.getFileName(), file.getFileSize());
|
||||
}
|
||||
|
||||
// 2.2 添加元数据文件
|
||||
TransferMetadataDTO metadata = TransferMetadataDTO.builder()
|
||||
.expandUserFile(expandUserFileDTO)
|
||||
.build();
|
||||
|
||||
String metadataJson = JsonUtils.toJsonString(metadata);
|
||||
ZipEntry metadataEntry = new ZipEntry("transferMetadata.json");
|
||||
zos.putNextEntry(metadataEntry);
|
||||
zos.write(metadataJson.getBytes("UTF-8"));
|
||||
zos.closeEntry();
|
||||
}
|
||||
|
||||
log.info("传输文件打包完成: {}", finalFilePath);
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
||||
@@ -4,11 +4,14 @@ import com.senmesh.common.constant.CommonConstants;
|
||||
import com.senmesh.common.exception.util.Assert;
|
||||
import com.senmesh.common.util.ListUtil;
|
||||
import com.senmesh.common.util.UserContextHolder;
|
||||
import com.senmesh.component.file.service.FileService;
|
||||
import com.senmesh.sfile.constant.FileStatusEnum;
|
||||
import com.senmesh.sfile.constant.FileTypeEnum;
|
||||
import com.senmesh.sfile.constant.RootTypeEnum;
|
||||
import com.senmesh.sfile.convert.UserFileConvert;
|
||||
import com.senmesh.sfile.dao.UserFileMapper;
|
||||
import com.senmesh.sfile.dto.ExpandUserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.ExpandUserFileDTO;
|
||||
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.UserFileDTO;
|
||||
import com.senmesh.sfile.dto.UserFileModifyDTO;
|
||||
@@ -16,73 +19,84 @@ import com.senmesh.sfile.dto.UserFileModifyStatusDTO;
|
||||
import com.senmesh.sfile.dto.UserFileQueryDTO;
|
||||
import com.senmesh.sfile.entity.UserFile;
|
||||
import com.senmesh.sfile.service.UserFileService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.stereotype.Service;
|
||||
import org.springframework.transaction.annotation.Transactional;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.text.SimpleDateFormat;
|
||||
import java.util.ArrayList;
|
||||
import java.util.Date;
|
||||
import java.util.List;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 用户文件ServiceImpl
|
||||
*/
|
||||
@Slf4j
|
||||
@Service
|
||||
public class UserFileServiceImpl implements UserFileService {
|
||||
|
||||
@Autowired
|
||||
private UserFileMapper userFileMapper;
|
||||
|
||||
@Autowired
|
||||
private FileService fileService;
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long create(UserFileCreateDTO createDTO) {
|
||||
public UserFileDTO create(UserFileCreateDTO createDTO) {
|
||||
Long parentId = createDTO.getParentId();
|
||||
String parentPath = createDTO.getParentPath();
|
||||
Long userId = createDTO.getUserId();
|
||||
String rootType = createDTO.getRootType();
|
||||
|
||||
// 1. 校验文件类型时fileCode不能为空
|
||||
// 1. 校验 userId 和 rootType 不能为空
|
||||
Assert.notNull(userId, "用户ID不能为空");
|
||||
Assert.notBlank(rootType, "根类型不能为空");
|
||||
|
||||
// 2. 校验文件类型时fileCode不能为空
|
||||
if (FileTypeEnum.isFile(createDTO.getFileType())) {
|
||||
Assert.notBlank(createDTO.getFileCode(), "文件编码不能为空");
|
||||
}
|
||||
|
||||
// 2. 校验父节点(如果不是根目录)
|
||||
if (parentId != null && parentId != 0L) {
|
||||
// 如果没有提供parentPath,需要查询父节点
|
||||
if (StringUtils.isBlank(parentPath)) {
|
||||
UserFileDTO parentFile = this.findOne(UserFileQueryDTO.builder()
|
||||
.id(parentId)
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.build());
|
||||
Assert.isTrue(FileTypeEnum.isFolder(parentFile.getFileType()), "父节点必须是文件夹");
|
||||
// 获取父路径用于后续拼接
|
||||
parentPath = parentFile.getPath();
|
||||
}
|
||||
// 如果提供了parentPath,信任调用方,不做校验
|
||||
// 3. 处理 parentPath(性能优化:如果提供了 parentPath,直接使用,避免查询)
|
||||
if (StringUtils.isBlank(parentPath) && parentId != null && parentId != 0L) {
|
||||
// 如果没有提供 parentPath,但提供了 parentId,需要查询父节点获取 parentPath
|
||||
UserFileDTO parentFile = this.findOne(UserFileQueryDTO.builder()
|
||||
.id(parentId)
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.build());
|
||||
Assert.isTrue(FileTypeEnum.isFolder(parentFile.getFileType()), "父节点必须是文件夹");
|
||||
// 获取父路径用于后续拼接
|
||||
parentPath = parentFile.getPath();
|
||||
}
|
||||
// 如果提供了 parentPath,直接信任调用方,不做校验
|
||||
|
||||
// 3. 插入数据获取ID
|
||||
// 4. 插入数据获取ID
|
||||
UserFile userFile = UserFileConvert.INSTANCE.toEntity(createDTO);
|
||||
userFileMapper.saveEntity(userFile);
|
||||
Long newId = userFile.getId();
|
||||
|
||||
// 4. 构建path
|
||||
// 5. 构建path
|
||||
String path = StringUtils.isNotBlank(parentPath)
|
||||
? parentPath + CommonConstants.COMMA + newId
|
||||
: String.valueOf(newId);
|
||||
|
||||
// 5. 更新path
|
||||
// 6. 更新path
|
||||
userFile.setPath(path);
|
||||
userFileMapper.updateEntitySelective(userFile);
|
||||
|
||||
return newId;
|
||||
// 7. 转换为DTO返回
|
||||
return UserFileConvert.INSTANCE.toDto(userFile);
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public Long create(ExpandUserFileCreateDTO expandCreateDTO) {
|
||||
public ExpandUserFileDTO create(ExpandUserFileCreateDTO expandCreateDTO) {
|
||||
Long userFileId = expandCreateDTO.getUserFileId();
|
||||
Long userId = expandCreateDTO.getUserId();
|
||||
String rootType = expandCreateDTO.getRootType();
|
||||
@@ -94,26 +108,7 @@ public class UserFileServiceImpl implements UserFileService {
|
||||
.rootType(RootTypeEnum.NORMAL.getType())
|
||||
.build());
|
||||
|
||||
// 2. 创建文件夹,文件名 = 原文件名 + 当前时间
|
||||
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String folderName = normalFile.getFileName() + "_" + timestamp;
|
||||
|
||||
UserFileCreateDTO folderCreateDTO = UserFileCreateDTO.builder()
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.fileType(FileTypeEnum.FOLDER.getType())
|
||||
.fileName(folderName)
|
||||
.parentId(0L)
|
||||
.endTime(expandCreateDTO.getEndTime())
|
||||
.accessCode(expandCreateDTO.getAccessCode())
|
||||
.build();
|
||||
Long folderId = this.create(folderCreateDTO);
|
||||
String folderPath = String.valueOf(folderId);
|
||||
|
||||
// 3. 查询需要复制的文件列表
|
||||
// 查询条件:path LIKE 'xxx%' AND fileType = '1'
|
||||
// 如果是文件:会查到自己
|
||||
// 如果是文件夹:会查到其下所有文件类型的子文件
|
||||
UserFileQueryDTO queryDTO = UserFileQueryDTO.builder()
|
||||
.userId(userId)
|
||||
.rootType(RootTypeEnum.NORMAL.getType())
|
||||
@@ -122,23 +117,98 @@ public class UserFileServiceImpl implements UserFileService {
|
||||
.build();
|
||||
queryDTO.disablePage();
|
||||
List<UserFileDTO> filesToCopy = this.list(queryDTO);
|
||||
Assert.notEmpty(filesToCopy, "未查询到需要处理的文件");
|
||||
|
||||
// 2. 创建文件夹,文件名 = 原文件名 + 当前时间
|
||||
String timestamp = new SimpleDateFormat("yyyyMMddHHmmss").format(new Date());
|
||||
String folderName = normalFile.getFileName() + "_" + timestamp;
|
||||
|
||||
UserFileCreateDTO folderCreateDTO = UserFileCreateDTO.builder()
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.fileStatus(expandCreateDTO.getFileStatus())
|
||||
.fileType(FileTypeEnum.FOLDER.getType())
|
||||
.fileName(folderName)
|
||||
.parentId(0L)
|
||||
.endTime(expandCreateDTO.getEndTime())
|
||||
.accessCode(expandCreateDTO.getAccessCode())
|
||||
.build();
|
||||
UserFileDTO folderDTO = this.create(folderCreateDTO);
|
||||
Long folderId = folderDTO.getId();
|
||||
String folderPath = String.valueOf(folderId);
|
||||
|
||||
|
||||
// 4. 在文件夹下创建文件对象
|
||||
List<UserFileDTO> createdFiles = new java.util.ArrayList<>();
|
||||
for (UserFileDTO fileDTO : filesToCopy) {
|
||||
UserFileCreateDTO fileCreateDTO = UserFileCreateDTO.builder()
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.fileStatus(expandCreateDTO.getFileStatus())
|
||||
.fileType(FileTypeEnum.FILE.getType())
|
||||
.fileName(fileDTO.getFileName())
|
||||
.fileCode(fileDTO.getFileCode())
|
||||
.parentId(folderId)
|
||||
.parentPath(folderPath)
|
||||
.build();
|
||||
this.create(fileCreateDTO);
|
||||
UserFileDTO createdFile = this.create(fileCreateDTO);
|
||||
createdFiles.add(createdFile);
|
||||
}
|
||||
|
||||
return folderId;
|
||||
// 6. 返回结果
|
||||
return ExpandUserFileDTO.builder()
|
||||
.folder(folderDTO)
|
||||
.files(createdFiles)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
public ExpandUserFileDTO create(UserFileDTO folder, List<UserFileDTO> files) {
|
||||
Long userId = folder.getUserId();
|
||||
String rootType = folder.getRootType();
|
||||
|
||||
// 3. 创建文件夹
|
||||
UserFileCreateDTO folderCreateDTO = UserFileCreateDTO.builder()
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.fileType(FileTypeEnum.FOLDER.getType())
|
||||
.fileName(folder.getFileName())
|
||||
.parentId(0L)
|
||||
.fileStatus(FileStatusEnum.NORMAL.getStatus())
|
||||
.build();
|
||||
UserFileDTO folderDTO = this.create(folderCreateDTO);
|
||||
Long folderId = folderDTO.getId();
|
||||
String folderPath = String.valueOf(folderId);
|
||||
|
||||
log.info("创建文件夹成功: folderId={}, 文件名={}", folderId, folder.getFileName());
|
||||
|
||||
// 4. 创建文件列表
|
||||
List<UserFileDTO> createdFiles = new ArrayList<>();
|
||||
for (UserFileDTO fileInfo : files) {
|
||||
|
||||
// 创建文件记录
|
||||
UserFileCreateDTO fileCreateDTO = UserFileCreateDTO.builder()
|
||||
.userId(userId)
|
||||
.rootType(rootType)
|
||||
.fileType(FileTypeEnum.FILE.getType())
|
||||
.fileName(fileInfo.getFileName())
|
||||
.fileCode(fileInfo.getFileCode())
|
||||
.parentId(folderId)
|
||||
.parentPath(folderPath)
|
||||
.fileStatus(FileStatusEnum.NORMAL.getStatus())
|
||||
.build();
|
||||
|
||||
UserFileDTO createdFile = this.create(fileCreateDTO);
|
||||
createdFiles.add(createdFile);
|
||||
}
|
||||
|
||||
// 5. 返回结果
|
||||
return ExpandUserFileDTO.builder()
|
||||
.folder(folderDTO)
|
||||
.files(createdFiles)
|
||||
.build();
|
||||
}
|
||||
|
||||
@Override
|
||||
@Transactional
|
||||
|
||||
Reference in New Issue
Block a user