兼容+功能
This commit is contained in:
@@ -0,0 +1,59 @@
|
||||
package com.senmesh.sfile.constant;
|
||||
|
||||
/**
|
||||
* 文件位置枚举
|
||||
*/
|
||||
public enum FileLocationEnum {
|
||||
|
||||
/**
|
||||
* 内网
|
||||
*/
|
||||
INTERNAL("1", "内网"),
|
||||
|
||||
/**
|
||||
* 外网
|
||||
*/
|
||||
EXTERNAL("2", "外网");
|
||||
|
||||
private final String location;
|
||||
private final String desc;
|
||||
|
||||
FileLocationEnum(String location, String desc) {
|
||||
this.location = location;
|
||||
this.desc = desc;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public String getDesc() {
|
||||
return desc;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据位置获取枚举
|
||||
*
|
||||
* @param location 位置
|
||||
* @return FileLocationEnum
|
||||
*/
|
||||
public static FileLocationEnum getByLocation(String location) {
|
||||
for (FileLocationEnum fileLocationEnum : FileLocationEnum.values()) {
|
||||
if (fileLocationEnum.getLocation().equals(location)) {
|
||||
return fileLocationEnum;
|
||||
}
|
||||
}
|
||||
return null;
|
||||
}
|
||||
|
||||
/**
|
||||
* 判断是否为内网
|
||||
*
|
||||
* @param location 位置
|
||||
* @return true-内网,false-外网
|
||||
*/
|
||||
public static boolean isInternal(String location) {
|
||||
return INTERNAL.getLocation().equals(location);
|
||||
}
|
||||
}
|
||||
|
||||
@@ -43,7 +43,7 @@ public class NormalUserFileController {
|
||||
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建")
|
||||
public AlitaResult<Long> create(@Validated @RequestBody UserFileCreateDTO createDTO) {
|
||||
public AlitaResult<UserFileDTO> create(@Validated @RequestBody UserFileCreateDTO createDTO) {
|
||||
return AlitaResult.success(normalUserFileService.create(createDTO));
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.senmesh.sfile.controller;
|
||||
|
||||
import com.senmesh.common.AlitaResult;
|
||||
import com.senmesh.sfile.dto.TransferUserFileCreateDTO;
|
||||
import com.senmesh.sfile.service.TransferUserFileService;
|
||||
import io.swagger.annotations.Api;
|
||||
import io.swagger.annotations.ApiOperation;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.validation.annotation.Validated;
|
||||
import org.springframework.web.bind.annotation.PostMapping;
|
||||
import org.springframework.web.bind.annotation.RequestBody;
|
||||
import org.springframework.web.bind.annotation.RequestMapping;
|
||||
import org.springframework.web.bind.annotation.RestController;
|
||||
|
||||
/**
|
||||
* 传输用户文件Controller
|
||||
*/
|
||||
@RestController
|
||||
@RequestMapping("transfer_user_file")
|
||||
@Api(tags = "传输用户文件")
|
||||
public class TransferUserFileController {
|
||||
|
||||
@Autowired
|
||||
private TransferUserFileService transferUserFileService;
|
||||
|
||||
@PostMapping("/create")
|
||||
@ApiOperation("创建")
|
||||
public AlitaResult<Long> create(@Validated @RequestBody TransferUserFileCreateDTO createDTO) {
|
||||
return AlitaResult.success(transferUserFileService.create(createDTO));
|
||||
}
|
||||
}
|
||||
|
||||
@@ -29,11 +29,11 @@ public class UserFileController {
|
||||
@Autowired
|
||||
private UserFileService userFileService;
|
||||
|
||||
@PostMapping("/create")
|
||||
/* @PostMapping("/create")
|
||||
@ApiOperation("创建")
|
||||
public AlitaResult<Long> create(@Validated @RequestBody UserFileCreateDTO createDTO) {
|
||||
return AlitaResult.success(userFileService.create(createDTO));
|
||||
}
|
||||
}*/
|
||||
|
||||
@PostMapping("/modify")
|
||||
@ApiOperation("修改")
|
||||
|
||||
32
src/main/java/com/senmesh/sfile/dto/ExpandUserFileDTO.java
Normal file
32
src/main/java/com/senmesh/sfile/dto/ExpandUserFileDTO.java
Normal file
@@ -0,0 +1,32 @@
|
||||
package com.senmesh.sfile.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 扩展用户文件DTO
|
||||
*/
|
||||
@ApiModel("扩展用户文件DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class ExpandUserFileDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("文件夹信息")
|
||||
private UserFileDTO folder;
|
||||
|
||||
@ApiModelProperty("文件列表")
|
||||
private List<UserFileDTO> files;
|
||||
|
||||
}
|
||||
|
||||
@@ -15,7 +15,7 @@ import lombok.experimental.SuperBuilder;
|
||||
*/
|
||||
@ApiModel("文件种类查询DTO")
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
|
||||
28
src/main/java/com/senmesh/sfile/dto/TransferMetadataDTO.java
Normal file
28
src/main/java/com/senmesh/sfile/dto/TransferMetadataDTO.java
Normal file
@@ -0,0 +1,28 @@
|
||||
package com.senmesh.sfile.dto;
|
||||
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import java.io.Serializable;
|
||||
|
||||
/**
|
||||
* 传输元数据DTO
|
||||
*/
|
||||
@ApiModel("传输元数据DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
public class TransferMetadataDTO implements Serializable {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("扩展用户文件DTO")
|
||||
private ExpandUserFileDTO expandUserFile;
|
||||
|
||||
}
|
||||
|
||||
@@ -0,0 +1,32 @@
|
||||
package com.senmesh.sfile.dto;
|
||||
|
||||
import com.senmesh.common.dto.BaseOperateDTO;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
|
||||
import javax.validation.constraints.NotNull;
|
||||
|
||||
/**
|
||||
* 传输用户文件创建DTO
|
||||
*/
|
||||
@ApiModel("传输用户文件创建DTO")
|
||||
@Data
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
public class TransferUserFileCreateDTO extends BaseOperateDTO {
|
||||
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("用户文件ID")
|
||||
@NotNull(message = "用户文件ID不能为空")
|
||||
private Long userFileId;
|
||||
|
||||
}
|
||||
|
||||
@@ -30,7 +30,7 @@ public class UserFileCreateDTO extends BaseOperateDTO {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
@ApiModelProperty("用户id")
|
||||
@NotNull(message = "用户id不能为空")
|
||||
// @NotNull(message = "用户id不能为空")
|
||||
private Long userId;
|
||||
|
||||
@ApiModelProperty("根类型:1普通/2内穿外/3外传内/4我的分享")
|
||||
|
||||
@@ -44,6 +44,9 @@ public class UserFileDTO extends BaseDTO {
|
||||
@ApiModelProperty("文件编码")
|
||||
private String fileCode;
|
||||
|
||||
@ApiModelProperty("文件大小")
|
||||
private Long fileSize;
|
||||
|
||||
@ApiModelProperty("文件状态:1正常2失效11解析中12待审核13传输中14失败")
|
||||
private String fileStatus;
|
||||
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.senmesh.common.dto.PageQueryDTO;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -14,7 +15,7 @@ import lombok.experimental.SuperBuilder;
|
||||
*/
|
||||
@ApiModel("用户文件查询DTO")
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
|
||||
@@ -4,6 +4,7 @@ import com.senmesh.common.dto.PageQueryDTO;
|
||||
import io.swagger.annotations.ApiModel;
|
||||
import io.swagger.annotations.ApiModelProperty;
|
||||
import lombok.AllArgsConstructor;
|
||||
import lombok.Builder;
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.NoArgsConstructor;
|
||||
@@ -14,7 +15,7 @@ import lombok.experimental.SuperBuilder;
|
||||
*/
|
||||
@ApiModel("用户空间查询DTO")
|
||||
@Data
|
||||
@SuperBuilder
|
||||
@Builder
|
||||
@NoArgsConstructor
|
||||
@AllArgsConstructor
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
|
||||
@@ -0,0 +1,39 @@
|
||||
package com.senmesh.sfile.job;
|
||||
|
||||
import com.senmesh.common.annotation.AlitaStopWatch;
|
||||
import com.senmesh.component.file.config.FileProperties;
|
||||
import com.senmesh.sfile.service.TransferFileMonitorService;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.scheduling.annotation.Scheduled;
|
||||
import org.springframework.stereotype.Component;
|
||||
|
||||
/**
|
||||
* 传输文件监听定时任务
|
||||
*/
|
||||
@Component
|
||||
@Slf4j
|
||||
public class TransferFileMonitorJob {
|
||||
|
||||
@Autowired
|
||||
private TransferFileMonitorService transferFileMonitorService;
|
||||
|
||||
@Autowired
|
||||
private FileProperties fileProperties;
|
||||
|
||||
/**
|
||||
* 定时扫描传输目录
|
||||
* 默认每分钟执行一次
|
||||
*/
|
||||
@Scheduled(cron = "${file.transfer.monitor.cron:0 */1 * * * ?}")
|
||||
@AlitaStopWatch
|
||||
public void execute() {
|
||||
try {
|
||||
log.debug("开始扫描传输目录");
|
||||
transferFileMonitorService.scanAndProcess();
|
||||
} catch (Exception e) {
|
||||
log.error("传输文件监听处理失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.senmesh.sfile.dao.FileCategoryMapper">
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<sql id="queryFromSql">
|
||||
FROM x_file_category t
|
||||
WHERE t.deleted = 0
|
||||
WHERE t.deleted = FALSE
|
||||
<if test='param.id != null'>
|
||||
AND t.id = #{param.id}
|
||||
</if>
|
||||
|
||||
@@ -1,17 +1,18 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.senmesh.sfile.dao.UserFileMapper">
|
||||
|
||||
<select id="list" resultType="com.senmesh.sfile.dto.UserFileDTO">
|
||||
SELECT t.*
|
||||
SELECT t.*, fr.file_size
|
||||
<include refid="queryFromSql"></include>
|
||||
ORDER BY t.id DESC
|
||||
</select>
|
||||
|
||||
<sql id="queryFromSql">
|
||||
FROM x_user_file t
|
||||
WHERE t.deleted = 0
|
||||
LEFT JOIN xs_file_record fr ON t.file_code = fr.file_code AND fr.deleted = FALSE
|
||||
WHERE t.deleted = FALSE
|
||||
<if test='param.id != null'>
|
||||
AND t.id = #{param.id}
|
||||
</if>
|
||||
@@ -48,12 +49,10 @@
|
||||
|
||||
<update id="updatePathByParentIdChange">
|
||||
UPDATE x_user_file child
|
||||
INNER JOIN x_user_file current ON current.id = #{fileId} AND current.deleted = 0
|
||||
LEFT JOIN x_user_file parent ON parent.id = #{newParentId} AND parent.deleted = 0
|
||||
SET child.path = CASE
|
||||
SET path = CASE
|
||||
WHEN child.id = #{fileId} THEN
|
||||
CASE
|
||||
WHEN #{newParentId} = 0 THEN CAST(#{fileId} AS CHAR)
|
||||
WHEN #{newParentId} = 0 THEN CAST(#{fileId} AS VARCHAR)
|
||||
ELSE CONCAT(parent.path, ',', #{fileId})
|
||||
END
|
||||
ELSE
|
||||
@@ -63,35 +62,44 @@
|
||||
WHEN #{newParentId} = 0 THEN CONCAT(#{fileId}, ',')
|
||||
ELSE CONCAT(parent.path, ',', #{fileId}, ',')
|
||||
END)
|
||||
END
|
||||
WHERE (child.id = #{fileId} OR child.path LIKE CONCAT(current.path, ',%'))
|
||||
AND child.deleted = 0
|
||||
END,
|
||||
update_by = #{operateUserId},
|
||||
update_time = NOW()
|
||||
FROM x_user_file current
|
||||
LEFT JOIN x_user_file parent ON parent.id = #{newParentId} AND parent.deleted = FALSE
|
||||
WHERE current.id = #{fileId}
|
||||
AND current.deleted = FALSE
|
||||
AND (child.id = #{fileId} OR child.path LIKE CONCAT(current.path, ',%'))
|
||||
AND child.deleted = FALSE
|
||||
AND child.user_id = current.user_id
|
||||
AND child.root_type = current.root_type
|
||||
</update>
|
||||
|
||||
<update id="deleteFileWithChildren">
|
||||
UPDATE x_user_file child
|
||||
INNER JOIN x_user_file current ON current.id = #{fileId} AND current.deleted = 0
|
||||
SET child.deleted = 1,
|
||||
child.update_by = #{operateUserId},
|
||||
child.update_time = NOW()
|
||||
WHERE (child.id = #{fileId} OR child.path LIKE CONCAT(current.path, ',%'))
|
||||
AND child.deleted = 0
|
||||
SET deleted = TRUE,
|
||||
update_by = #{operateUserId},
|
||||
update_time = NOW()
|
||||
FROM x_user_file current
|
||||
WHERE current.id = #{fileId}
|
||||
AND current.deleted = FALSE
|
||||
AND (child.id = #{fileId} OR child.path LIKE CONCAT(current.path, ',%'))
|
||||
AND child.deleted = FALSE
|
||||
AND child.user_id = current.user_id
|
||||
AND child.root_type = current.root_type
|
||||
</update>
|
||||
|
||||
<update id="updateStatusWithChildren">
|
||||
UPDATE x_user_file child
|
||||
INNER JOIN x_user_file current ON current.id = #{fileId}
|
||||
AND current.deleted = 0
|
||||
SET file_status = #{newStatus},
|
||||
update_by = #{operateUserId},
|
||||
update_time = NOW()
|
||||
FROM x_user_file current
|
||||
WHERE current.id = #{fileId}
|
||||
AND current.deleted = FALSE
|
||||
AND current.file_status = #{oldStatus}
|
||||
SET child.file_status = #{newStatus},
|
||||
child.update_by = #{operateUserId},
|
||||
child.update_time = NOW()
|
||||
WHERE (child.id = #{fileId} OR child.path LIKE CONCAT(current.path, ',%'))
|
||||
AND child.deleted = 0
|
||||
AND (child.id = #{fileId} OR child.path LIKE CONCAT(current.path, ',%'))
|
||||
AND child.deleted = FALSE
|
||||
AND child.user_id = current.user_id
|
||||
AND child.root_type = current.root_type
|
||||
</update>
|
||||
|
||||
@@ -1,4 +1,4 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
|
||||
|
||||
<mapper namespace="com.senmesh.sfile.dao.UserSpaceMapper">
|
||||
@@ -11,7 +11,7 @@
|
||||
|
||||
<sql id="queryFromSql">
|
||||
FROM x_user_space t
|
||||
WHERE t.deleted = 0
|
||||
WHERE t.deleted = FALSE
|
||||
<if test='param.id != null'>
|
||||
AND t.id = #{param.id}
|
||||
</if>
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.senmesh.sfile.service;
|
||||
|
||||
import com.senmesh.sfile.dto.UserFileCreateDTO;
|
||||
import com.senmesh.sfile.dto.UserFileDTO;
|
||||
|
||||
/**
|
||||
* 普通用户文件Service
|
||||
@@ -11,8 +12,8 @@ public interface NormalUserFileService {
|
||||
* 创建普通用户文件
|
||||
*
|
||||
* @param createDTO 创建DTO
|
||||
* @return 主键ID
|
||||
* @return UserFileDTO
|
||||
*/
|
||||
Long create(UserFileCreateDTO createDTO);
|
||||
UserFileDTO create(UserFileCreateDTO createDTO);
|
||||
}
|
||||
|
||||
|
||||
@@ -0,0 +1,22 @@
|
||||
package com.senmesh.sfile.service;
|
||||
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* 传输文件监听服务
|
||||
*/
|
||||
public interface TransferFileMonitorService {
|
||||
|
||||
/**
|
||||
* 扫描并处理传输目录中的文件
|
||||
*/
|
||||
void scanAndProcess();
|
||||
|
||||
/**
|
||||
* 解包单个传输文件
|
||||
*
|
||||
* @param zipFilePath zip文件路径
|
||||
*/
|
||||
void unpackTransferFile(Path zipFilePath);
|
||||
}
|
||||
|
||||
@@ -0,0 +1,19 @@
|
||||
package com.senmesh.sfile.service;
|
||||
|
||||
import com.senmesh.sfile.dto.TransferUserFileCreateDTO;
|
||||
|
||||
/**
|
||||
* 传输用户文件Service
|
||||
*/
|
||||
public interface TransferUserFileService {
|
||||
|
||||
/**
|
||||
* 创建传输用户文件
|
||||
* 根据 FileProperties#location 的类型决定是内穿外还是外传内
|
||||
*
|
||||
* @param createDTO 创建DTO
|
||||
* @return 主键ID
|
||||
*/
|
||||
Long create(TransferUserFileCreateDTO createDTO);
|
||||
}
|
||||
|
||||
@@ -1,6 +1,7 @@
|
||||
package com.senmesh.sfile.service;
|
||||
|
||||
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;
|
||||
@@ -18,18 +19,30 @@ public interface UserFileService {
|
||||
* 创建用户文件
|
||||
*
|
||||
* @param createDTO 创建DTO
|
||||
* @return 主键ID
|
||||
* @return UserFileDTO
|
||||
*/
|
||||
Long create(UserFileCreateDTO createDTO);
|
||||
UserFileDTO create(UserFileCreateDTO createDTO);
|
||||
|
||||
/**
|
||||
* 根据普通文件创建新的文件(复制)
|
||||
* 创建一个文件夹,文件名为原文件名+时间戳,然后复制原文件下所有文件类型的子文件
|
||||
*
|
||||
* @param expandCreateDTO 扩展创建DTO
|
||||
* @return 新创建的文件夹ID
|
||||
* @return 扩展用户文件DTO(包含文件夹信息和文件列表)
|
||||
*/
|
||||
Long create(ExpandUserFileCreateDTO expandCreateDTO);
|
||||
ExpandUserFileDTO create(ExpandUserFileCreateDTO expandCreateDTO);
|
||||
|
||||
/**
|
||||
* 根据文件夹和文件列表创建文件夹和文件(用于传输文件解包)
|
||||
* 1. 从 folder 中获取 userId 和 rootType
|
||||
* 2. 创建文件夹
|
||||
* 3. 遍历文件列表,通过 fileCode 获取文件流,上传文件获取新的 fileCode,然后创建用户文件记录
|
||||
*
|
||||
* @param folder 文件夹信息(包含 userId、rootType、fileName 等)
|
||||
* @param files 文件列表(每个文件包含 fileCode、fileName 等)
|
||||
* @return 扩展用户文件DTO(包含创建后的文件夹信息和文件列表)
|
||||
*/
|
||||
ExpandUserFileDTO create(UserFileDTO folder, List<UserFileDTO> files);
|
||||
|
||||
/**
|
||||
* 修改用户文件
|
||||
|
||||
@@ -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