Skip to content
Open
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension


Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -27,7 +27,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;

/**
* 任务调度服务端启动程序
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -31,7 +31,7 @@
import org.springframework.boot.SpringApplication;
import org.springframework.boot.SpringBootVersion;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.web.ServerProperties;
import org.springframework.boot.web.server.autoconfigure.ServerProperties;
import org.springframework.cloud.openfeign.EnableFeignClients;
import org.springframework.scheduling.annotation.EnableAsync;
import org.springframework.scheduling.annotation.EnableScheduling;
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -136,6 +136,11 @@ public boolean update(FileInfo fileInfo) {
fileInfo.setFileId(String.valueOf(file.getId()));
return true;
} else {
// 完成分片时原始文件名已由 starter 从会话回填;为空时沿用已有展示名,避免用存储名覆盖
if (StrUtil.isBlank(fileInfo.getOriginalFileName()) && StrUtil.isNotBlank(existFile
.getOriginalName())) {
file.setOriginalName(existFile.getOriginalName());
}
file.setId(existFile.getId());
boolean updated = fileMapper.updateById(file) > 0;
fileInfo.setFileId(String.valueOf(existFile.getId()));
Expand Down Expand Up @@ -225,20 +230,7 @@ public void deleteMd5Mapping(String md5) {
@Override
public void saveMultipartSession(String uploadId, MultipartInitResp initResp,
Map<String, String> metadata) {
MultipartUploadCreateResp target = new MultipartUploadCreateResp();
target.setFileId(initResp.getFileId());
target.setUploadId(initResp.getUploadId());
target.setBucket(initResp.getBucket());
target.setPlatform(initResp.getPlatform());
target.setFileName(initResp.getFileName());
target.setFileMd5(initResp.getFileMd5());
target.setFileSize(ObjectUtil.defaultIfNull(initResp.getFileSize(), 0L));
target.setExtension(initResp.getExtension());
target.setContentType(initResp.getContentType());
target.setParentPath(initResp.getParentPath());
target.setPath(initResp.getPath());
target.setPartSize(initResp.getPartSize());
target.setUploadedPartNumbers(initResp.getUploadedPartNumbers());
MultipartUploadCreateResp target = toCreateResp(initResp);
String key = MultipartUploadConstants.MULTIPART_UPLOAD_PREFIX + uploadId;
String metadataKey = MultipartUploadConstants.MULTIPART_METADATA_PREFIX + uploadId;
try {
Expand Down Expand Up @@ -275,12 +267,48 @@ public MultipartInitResp getMultipartSession(String uploadId) {
if (source == null) {
return null;
}
return toInitResp(source);
}

/**
* 会话转换为 Redis 存储对象
*
* @param source 分片上传会话
* @return Redis 存储对象
*/
static MultipartUploadCreateResp toCreateResp(MultipartInitResp source) {
MultipartUploadCreateResp target = new MultipartUploadCreateResp();
target.setFileId(source.getFileId());
target.setUploadId(source.getUploadId());
target.setBucket(source.getBucket());
target.setPlatform(source.getPlatform());
target.setFileName(source.getFileName());
target.setOriginalFileName(source.getOriginalFileName());
target.setFileMd5(source.getFileMd5());
target.setFileSize(ObjectUtil.defaultIfNull(source.getFileSize(), 0L));
target.setExtension(source.getExtension());
target.setContentType(source.getContentType());
target.setParentPath(source.getParentPath());
target.setPath(source.getPath());
target.setPartSize(source.getPartSize());
target.setUploadedPartNumbers(source.getUploadedPartNumbers());
return target;
}

/**
* Redis 存储对象转换为会话
*
* @param source Redis 存储对象
* @return 分片上传会话
*/
static MultipartInitResp toInitResp(MultipartUploadCreateResp source) {
MultipartInitResp target = new MultipartInitResp();
target.setFileId(source.getFileId());
target.setUploadId(source.getUploadId());
target.setBucket(source.getBucket());
target.setPlatform(source.getPlatform());
target.setFileName(source.getFileName());
target.setOriginalFileName(source.getOriginalFileName());
target.setFileMd5(source.getFileMd5());
target.setFileSize(source.getFileSize());
target.setExtension(source.getExtension());
Expand All @@ -298,14 +326,10 @@ public void deleteMultipartSession(String uploadId) {
String key = MultipartUploadConstants.MULTIPART_UPLOAD_PREFIX + uploadId;
String metadataKey = MultipartUploadConstants.MULTIPART_METADATA_PREFIX + uploadId;
String expireKey = MultipartUploadConstants.MULTIPART_EXPIRE_PREFIX + uploadId;
MultipartInitResp initResp = getMultipartSession(uploadId);
String fileMd5 = initResp != null ? initResp.getFileMd5() : null;
if (StrUtil.isNotBlank(fileMd5)) {
deleteMd5Mapping(fileMd5);
}
RedisUtils.delete(key);
RedisUtils.delete(metadataKey);
RedisUtils.delete(expireKey);
// 会话键映射由 starter 在 cleanup 中调用 deleteSessionKeyMapping 条件删除
log.debug("删除分片上传信息: uploadId={}", uploadId);
} catch (Exception e) {
log.error("删除分片上传信息失败: uploadId={}", uploadId, e);
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -48,11 +48,19 @@ public class FileDO extends BaseDO {

/**
* 名称
*
* <p>
* 存储中的实际文件名:文件为「唯一 ID + 原扩展名」(见 {@code FileNameGenerator}),文件夹仍为可读名称。
* </p>
*/
private String name;

/**
* 原始名称
*
* <p>
* 用户上传时的原始文件名,也是列表、详情等界面展示的名称。
* </p>
*/
private String originalName;

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -40,8 +40,12 @@ public class FileResp extends BaseDetailResp {

/**
* 名称
*
* <p>
* 存储中的实际文件名,文件为「唯一 ID + 原扩展名」,展示请使用 {@link #originalName}。
* </p>
*/
@Schema(description = "名称", example = "6824afe8408da079832dcfb6.jpg")
@Schema(description = "名称(存储名)", example = "3f2a1c9e5b7d4a6f8c0e1d2b3a4c5d6e.jpg")
private String name;

/**
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -57,11 +57,17 @@ public class MultipartUploadCreateResp implements Serializable {
private String platform;

/**
* 文件名称
* 存储文件名
*/
@Schema(description = "文件名称")
@Schema(description = "存储文件名(唯一 ID + 原扩展名)", example = "3f2a1c9e5b7d4a6f8c0e1d2b3a4c5d6e.zip")
private String fileName;

/**
* 原始文件名
*/
@Schema(description = "原始文件名", example = "年度报告.zip")
private String originalFileName;

/**
* 文件MD5
*/
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -184,6 +184,20 @@ public void beforeUpdate(FileReq req, Long id) {
// 需与新建入口做同样的路径穿越校验,避免通过修改接口改写出存储根目录之外的路径
StoragePathValidator.validate(req.getParentPath());
StoragePathValidator.validateName(req.getOriginalName());
// 修改上级目录只会改写记录中的路径,并不会搬运存储中的实体文件,改完即失效,故拒绝实际变更;
// null 表示未提交该字段(如仅重命名),空白值则视为要改到根目录,同样需要参与比较
if (req.getParentPath() != null) {
String targetParentPath = StoragePathValidator.normalizeParentPath(req.getParentPath());
// TODO 此处与基类 update 各查一次 getById,如后续基类支持回传实体可合并为一次
FileDO existFile = this.getById(id);
if (existFile != null) {
CheckUtils
.throwIf(!StoragePathValidator.isSameParentPath(targetParentPath, existFile
.getParentPath()), "暂不支持移动文件或文件夹");
}
// 统一根目录表示,避免空白值被写入记录后又按另一种形式拼出路径
req.setParentPath(targetParentPath);
}
}

@Override
Expand Down Expand Up @@ -308,10 +322,8 @@ private FileInfo doUpload(Object file, String parentPath, String storageCode, St
// 创建父级目录
this.createParentDir(parentPath, storage);

// 生成唯一文件名(处理重名情况)
String uniqueFileName =
FileNameGenerator.generateUniqueName(originalFileName, parentPath, storage
.getId(), baseMapper);
// 落盘文件名使用唯一 ID(扩展名不变),原始文件名仅作为展示名,避免同名文件互相覆盖
String storedFileName = FileNameGenerator.generateStoredName(originalFileName);
String sha256 = calculateSha256(file);
String normalizedUploadTaskId = StrUtil.emptyToNull(StrUtil.trim(uploadTaskId));
long totalBytes = this.getFileSize(file);
Expand All @@ -323,7 +335,7 @@ private FileInfo doUpload(Object file, String parentPath, String storageCode, St
UploadPretreatment uploadPretreatment = fileStorageService.of(normalizeUploadSource(file))
.platform(storage.getCode())
.path(this.pretreatmentPath(parentPath))
.fileName(uniqueFileName)
.fileName(storedFileName)
.metadata("sha256", sha256)
.metadata("storageCode", storage.getCode())
.metadata("storageId", String.valueOf(storage.getId()));
Expand All @@ -332,8 +344,8 @@ private FileInfo doUpload(Object file, String parentPath, String storageCode, St
uploadPretreatment.thumbnail(100, 100);
}
uploadPretreatment.onProgress((progressSize, allSize, percentage) -> {
log.info("文件 [{}] 已上传 [{}],总大小 [{}],进度 [{}%]", uniqueFileName, progressSize, allSize,
percentage);
log.info("文件 [{}] 已上传 [{}],总大小 [{}],进度 [{}%]", originalFileName, progressSize,
allSize, percentage);
if (StrUtil.isNotBlank(normalizedUploadTaskId)) {
FileUploadProgressStatusEnum status =
(allSize > 0 && progressSize >= allSize) || percentage >= 100
Expand All @@ -345,9 +357,9 @@ private FileInfo doUpload(Object file, String parentPath, String storageCode, St
});
try {
// 上传
log.info("开始上传文件: {}", uniqueFileName);
log.info("开始上传文件: {}", originalFileName);
FileInfo fileInfo = uploadPretreatment.upload();
log.info("文件 [{}] 上传完成", uniqueFileName);
log.info("文件 [{}] 上传完成", originalFileName);
FileInfo result = this.postProcessUploadResult(fileInfo, storage);
if (StrUtil.isNotBlank(normalizedUploadTaskId)) {
long completedSize = result.getSize() == null ? totalBytes : result.getSize();
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -64,39 +64,26 @@ public class MultipartUploadServiceImpl implements MultipartUploadService {
public MultipartUploadCreateResp initMultipartUpload(
MultipartUploadCreateReq multiPartUploadInitReq) {
// 校验文件扩展名是否在白名单内,防止任意类型文件上传(如 .jsp/.html 等)
String extName = FileUtil.extName(multiPartUploadInitReq.getFileName()).toLowerCase();
String originalFileName = multiPartUploadInitReq.getFileName();
FileTypeEnum fileType = FileTypeEnum.getByExtension(FileUtil.extName(originalFileName));
List<String> allExtensions = FileTypeEnum.getAllExtensions();
CheckUtils.throwIf(!allExtensions.contains(extName), "不支持的文件类型,仅支持 {} 格式的文件", String
CheckUtils.throwIf(fileType == FileTypeEnum.UNKNOWN, "不支持的文件类型,仅支持 {} 格式的文件", String
.join(StringConstants.COMMA, allExtensions));
// 校验上级目录路径与文件名,防止通过 ../ 等路径穿越将文件写入存储根目录之外
StoragePathValidator.validate(multiPartUploadInitReq.getParentPath());
StoragePathValidator.validateName(multiPartUploadInitReq.getFileName());
StoragePathValidator.validateName(originalFileName);
StorageDO storageDO = storageService.getByCode(null);
// 检测文件名是否已存在(同一目录下文件名不能重复)
String originalFileName = multiPartUploadInitReq.getFileName();
String parentPath = multiPartUploadInitReq.getParentPath();
boolean exists = fileMapper.lambdaQuery()
.eq(FileDO::getParentPath, parentPath)
.eq(FileDO::getStorageId, storageDO.getId())
.eq(FileDO::getName, originalFileName)
.ne(FileDO::getType, FileTypeEnum.DIR)
.exists();
if (exists) {
throw new BaseException("文件名已存在:" + originalFileName);
}

// 生成唯一文件名(处理重名情况)
String uniqueFileName =
FileNameGenerator.generateUniqueName(originalFileName, parentPath, storageDO
.getId(), fileMapper);
multiPartUploadInitReq.setFileName(uniqueFileName);
fileService.createParentDir(StrUtil.blankToDefault(parentPath, StringConstants.SLASH),
storageDO);

// 落盘文件名使用唯一 ID(扩展名不变),原始文件名交给 starter 会话携带,完成上传时自动回填
String storedFileName = FileNameGenerator.generateStoredName(originalFileName);
MultipartUploadInitReq storageReq = new MultipartUploadInitReq();
storageReq.setPlatform(storageDO.getCode());
storageReq.setBucket(storageDO.getBucketName());
storageReq.setFileName(uniqueFileName);
storageReq.setFileName(storedFileName);
storageReq.setOriginalFileName(originalFileName);
storageReq.setFileSize(multiPartUploadInitReq.getFileSize());
storageReq.setFileMd5(multiPartUploadInitReq.getFileMd5());
storageReq.setContentType(multiPartUploadInitReq.getContentType());
Expand Down Expand Up @@ -148,7 +135,7 @@ public FileDO completeMultipartUpload(String uploadId) {
if (file != null) {
return file;
}
// 兼容兜底:记录器未完成落库时补录
// 兼容兜底:记录器未完成落库时补录,原始文件名已由 starter 从会话回填到 fileInfo
FileDO fallback = new FileDO(fileInfo);
fallback.setStorageId(storageDO.getId());
fallback.setType(FileTypeEnum.getByExtension(FileUtil.extName(fallback.getName())));
Expand All @@ -168,6 +155,7 @@ private MultipartUploadCreateResp toAdminInitResp(MultipartInitResp source) {
target.setBucket(source.getBucket());
target.setPlatform(source.getPlatform());
target.setFileName(source.getFileName());
target.setOriginalFileName(source.getOriginalFileName());
target.setFileMd5(source.getFileMd5());
target.setFileSize(source.getFileSize());
target.setExtension(source.getExtension());
Expand Down
Loading
Loading