metadataMap = fileInfo.getMetadata();
+ this.sha256 = metadataMap != null
+ ? StrUtil.blankToDefault(metadataMap.get("sha256"), metadataMap.get("etag"))
+ : null;
+ this.metadata = JSONUtil.toJsonStr(metadataMap);
+ String thumbnailPath = fileInfo.getThumbnailPath();
+ if (StrUtil.isNotBlank(thumbnailPath)) {
+ String normalizedThumbnailPath = thumbnailPath.replace("\\", StringConstants.SLASH);
+ if (!normalizedThumbnailPath.startsWith("http://") && !normalizedThumbnailPath.startsWith("https://")) {
+ normalizedThumbnailPath = StrUtil.removePrefix(normalizedThumbnailPath, StringConstants.SLASH);
+ }
+ this.thumbnailName = FileNameUtil.getName(normalizedThumbnailPath);
+ }
+ this.thumbnailSize = fileInfo.getThumbnailSize();
+ this.thumbnailMetadata = null;
+ this.setCreateTime(fileInfo.getUploadTime());
}
/**
@@ -156,27 +173,27 @@ public class FileDO extends BaseDO {
public FileInfo toFileInfo(StorageDO storage) {
FileInfo fileInfo = new FileInfo();
fileInfo.setPlatform(storage.getCode());
- fileInfo.setFilename(this.name);
- fileInfo.setOriginalFilename(this.originalName);
- // 暂不使用,所以保持空
- fileInfo.setBasePath(StringConstants.EMPTY);
+ fileInfo.setBucket(storage.getBucketName());
+ fileInfo.setFileId(this.getId() == null ? null : String.valueOf(this.getId()));
+ fileInfo.setName(this.name);
+ fileInfo.setOriginalFileName(this.originalName);
fileInfo.setSize(this.size);
- fileInfo.setPath(StringConstants.SLASH.equals(this.parentPath)
- ? StringConstants.EMPTY
- : StrUtil.appendIfMissing(StrUtil
- .removePrefix(this.parentPath, StringConstants.SLASH), StringConstants.SLASH));
- fileInfo.setExt(this.extension);
+ String normalizedPath = StrUtil.removePrefix(this.path, StringConstants.SLASH);
+ fileInfo.setPath(normalizedPath);
+ fileInfo.setFullPath(normalizedPath);
fileInfo.setContentType(this.contentType);
if (StrUtil.isNotBlank(this.metadata)) {
fileInfo.setMetadata(JSONUtil.toBean(this.metadata, Map.class));
}
- fileInfo.setUrl(StrUtil.removePrefix(this.path, StringConstants.SLASH));
+ fileInfo.setUrl(URLUtil.normalize(storage.getUrlPrefix() + normalizedPath, false, true));
// 缩略图信息
- fileInfo.setThFilename(this.thumbnailName);
- fileInfo.setThSize(this.thumbnailSize);
- fileInfo.setThUrl(fileInfo.getPath() + fileInfo.getThFilename());
- if (StrUtil.isNotBlank(this.thumbnailMetadata)) {
- fileInfo.setThMetadata(JSONUtil.toBean(this.thumbnailMetadata, Map.class));
+ if (StrUtil.isNotBlank(this.thumbnailName)) {
+ String normalizedParentPath = StringConstants.SLASH.equals(this.parentPath)
+ ? StringConstants.EMPTY
+ : StrUtil.appendIfMissing(StrUtil
+ .removePrefix(this.parentPath, StringConstants.SLASH), StringConstants.SLASH);
+ fileInfo.setThumbnailPath(normalizedParentPath + this.thumbnailName);
+ fileInfo.setThumbnailSize(this.thumbnailSize);
}
return fileInfo;
}
diff --git a/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java b/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java
index ef95098465ac51660de275f4f7f84a1dbc3062da..a8767e6cd900afd10ee241449af78b16f394e447 100644
--- a/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java
+++ b/continew-system/src/main/java/top/continew/admin/system/model/entity/StorageDO.java
@@ -86,6 +86,21 @@ public class StorageDO extends BaseDO {
*/
private String domain;
+ /**
+ * 分片上传阈值(字节)
+ */
+ private Long multipartUploadThreshold;
+
+ /**
+ * 分片上传大小(字节)
+ */
+ private Long multipartUploadPartSize;
+
+ /**
+ * 本地分片临时目录
+ */
+ private String multipartTempDir;
+
/**
* 启用回收站
*/
@@ -139,4 +154,4 @@ public class StorageDO extends BaseDO {
}
return "%s://%s.%s/".formatted(url.getProtocol(), this.bucketName, host);
}
-}
\ No newline at end of file
+}
diff --git a/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java b/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
index 56a4283076a47be9f54bea93dc32a26623384966..8536bf32701bf57c2e61263634f40d414dfd923f 100644
--- a/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
+++ b/continew-system/src/main/java/top/continew/admin/system/model/req/StorageReq.java
@@ -20,6 +20,7 @@ import cn.sticki.spel.validator.constrain.SpelNotBlank;
import cn.sticki.spel.validator.jakarta.SpelValid;
import com.fasterxml.jackson.annotation.JsonIgnore;
import io.swagger.v3.oas.annotations.media.Schema;
+import jakarta.validation.constraints.Min;
import jakarta.validation.constraints.NotBlank;
import jakarta.validation.constraints.NotNull;
import jakarta.validation.constraints.Pattern;
@@ -112,6 +113,27 @@ public class StorageReq implements Serializable {
@NotBlank(message = "访问路径不能为空", groups = ValidationGroup.Storage.Local.class)
private String domain;
+ /**
+ * 分片上传阈值(字节)
+ */
+ @Schema(description = "分片上传阈值(字节)", example = "10485760")
+ @Min(value = 1, message = "分片上传阈值必须大于 0")
+ private Long multipartUploadThreshold;
+
+ /**
+ * 分片上传大小(字节)
+ */
+ @Schema(description = "分片上传大小(字节)", example = "5242880")
+ @Min(value = 1, message = "分片上传大小必须大于 0")
+ private Long multipartUploadPartSize;
+
+ /**
+ * 本地分片临时目录
+ */
+ @Schema(description = "本地分片临时目录", example = "/tmp/continew-multipart")
+ @Length(max = 255, message = "本地分片临时目录长度不能超过 {max} 个字符")
+ private String multipartTempDir;
+
/**
* 启用回收站
*/
@@ -150,4 +172,4 @@ public class StorageReq implements Serializable {
*/
@JsonIgnore
private Boolean isDefault;
-}
\ No newline at end of file
+}
diff --git a/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java b/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java
index 0cfa93a30ea846dffa6278ef20eeca71ffc06241..85c288577d7f5615fb3ea0fe005d3403a96a07a2 100644
--- a/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java
+++ b/continew-system/src/main/java/top/continew/admin/system/model/resp/StorageResp.java
@@ -85,6 +85,24 @@ public class StorageResp extends BaseDetailResp {
@Schema(description = "域名", example = "http://localhost:8000/file")
private String domain;
+ /**
+ * 分片上传阈值(字节)
+ */
+ @Schema(description = "分片上传阈值(字节)", example = "10485760")
+ private Long multipartUploadThreshold;
+
+ /**
+ * 分片上传大小(字节)
+ */
+ @Schema(description = "分片上传大小(字节)", example = "5242880")
+ private Long multipartUploadPartSize;
+
+ /**
+ * 本地分片临时目录
+ */
+ @Schema(description = "本地分片临时目录", example = "/tmp/continew-multipart")
+ private String multipartTempDir;
+
/**
* 启用回收站
*/
@@ -120,4 +138,4 @@ public class StorageResp extends BaseDetailResp {
return this.getIsDefault();
}
-}
\ No newline at end of file
+}
diff --git a/continew-system/src/main/java/top/continew/admin/system/handler/StorageHandler.java b/continew-system/src/main/java/top/continew/admin/system/model/resp/file/FileUploadConfigResp.java
similarity index 30%
rename from continew-system/src/main/java/top/continew/admin/system/handler/StorageHandler.java
rename to continew-system/src/main/java/top/continew/admin/system/model/resp/file/FileUploadConfigResp.java
index 4882de6e7135ebe2f4c2e46e1ffa4e66de6823ce..1b94ae10fec54a3ae4195456558ba1272ef83a2e 100644
--- a/continew-system/src/main/java/top/continew/admin/system/handler/StorageHandler.java
+++ b/continew-system/src/main/java/top/continew/admin/system/model/resp/file/FileUploadConfigResp.java
@@ -14,68 +14,67 @@
* limitations under the License.
*/
-package top.continew.admin.system.handler;
+package top.continew.admin.system.model.resp.file;
-import org.springframework.web.multipart.MultipartFile;
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
import top.continew.admin.system.enums.StorageTypeEnum;
-import top.continew.admin.system.model.entity.StorageDO;
-import top.continew.admin.system.model.req.MultipartUploadInitReq;
-import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
-import top.continew.admin.system.model.resp.file.MultipartUploadResp;
-import java.util.List;
+import java.io.Serial;
+import java.io.Serializable;
/**
- * 存储类型处理器
- *
- * 专注于文件操作,不包含业务逻辑
+ * 默认存储上传配置响应参数
*
- * @author KAI
- * @since 2025/7/30 17:15
+ * @author echo
+ * @since 2026/3/3 12:20
*/
-public interface StorageHandler {
+@Data
+@Schema(description = "默认存储上传配置响应参数")
+public class FileUploadConfigResp implements Serializable {
- MultipartUploadInitResp initMultipartUpload(StorageDO storageDO, MultipartUploadInitReq req);
+ @Serial
+ private static final long serialVersionUID = 1L;
/**
- * 分片上传
- *
- * @param storageDO 存储实体
- * @param path 存储路径
- * @param uploadId 文件名
- * @param file 文件对象
- * @return {@link MultipartUploadResp} 分片上传结果
+ * 默认存储 ID
*/
- MultipartUploadResp uploadPart(StorageDO storageDO,
- String path,
- String uploadId,
- Integer partNumber,
- MultipartFile file);
+ @Schema(description = "默认存储 ID", example = "1")
+ private Long storageId;
/**
- * 合并分片
- *
- * @param storageDO 存储实体
- * @param uploadId 上传Id
+ * 默认存储名称
*/
- void completeMultipartUpload(StorageDO storageDO,
- List parts,
- String path,
- String uploadId,
- boolean needVerify);
+ @Schema(description = "默认存储名称", example = "本地存储")
+ private String storageName;
/**
- * 清楚分片
- *
- * @param storageDO 存储实体
- * @param uploadId 上传Id
+ * 默认存储编码
*/
- void cleanPart(StorageDO storageDO, String uploadId);
+ @Schema(description = "默认存储编码", example = "local")
+ private String storageCode;
/**
- * 获取存储类型
- *
- * @return 存储类型
+ * 存储类型(1: 本地存储,2: 对象存储)
*/
- StorageTypeEnum getType();
+ @Schema(description = "存储类型(1: 本地存储,2: 对象存储)", example = "1")
+ private StorageTypeEnum storageType;
+
+ /**
+ * 分片上传阈值(字节)
+ */
+ @Schema(description = "分片上传阈值(字节)", example = "10485760")
+ private Long multipartUploadThreshold;
+
+ /**
+ * 分片上传大小(字节)
+ */
+ @Schema(description = "分片上传大小(字节)", example = "5242880")
+ private Long multipartUploadPartSize;
+
+ /**
+ * 本地分片临时目录(仅本地存储返回)
+ */
+ @Schema(description = "本地分片临时目录(仅本地存储返回)", example = "/tmp/continew-multipart")
+ private String multipartTempDir;
}
diff --git a/continew-system/src/main/java/top/continew/admin/system/model/resp/file/FileUploadProgressResp.java b/continew-system/src/main/java/top/continew/admin/system/model/resp/file/FileUploadProgressResp.java
new file mode 100644
index 0000000000000000000000000000000000000000..aeb09ed66a2a7b5af0f4402e054fd78442b2ac7d
--- /dev/null
+++ b/continew-system/src/main/java/top/continew/admin/system/model/resp/file/FileUploadProgressResp.java
@@ -0,0 +1,86 @@
+/*
+ * Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
+ *
+ * Licensed under the Apache License, Version 2.0 (the "License");
+ * you may not use this file except in compliance with the License.
+ * You may obtain a copy of the License at
+ *
+ * http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+
+package top.continew.admin.system.model.resp.file;
+
+import io.swagger.v3.oas.annotations.media.Schema;
+import lombok.Data;
+import top.continew.admin.system.enums.FileUploadProgressStatusEnum;
+
+import java.io.Serial;
+import java.io.Serializable;
+
+/**
+ * 文件上传进度响应参数
+ *
+ * @author echo
+ * @since 2026/3/3 12:20
+ */
+@Data
+@Schema(description = "文件上传进度响应参数")
+public class FileUploadProgressResp implements Serializable {
+
+ @Serial
+ private static final long serialVersionUID = 1L;
+
+ /**
+ * 上传任务 ID
+ */
+ @Schema(description = "上传任务 ID", example = "upload-task-1")
+ private String uploadTaskId;
+
+ /**
+ * 任务状态(INIT/UPLOADING/FINALIZING/COMPLETED/FAILED/NOT_FOUND)
+ */
+ @Schema(description = "任务状态(INIT/UPLOADING/FINALIZING/COMPLETED/FAILED/NOT_FOUND)", example = "UPLOADING")
+ private FileUploadProgressStatusEnum status;
+
+ /**
+ * 上传进度百分比
+ */
+ @Schema(description = "上传进度百分比", example = "65")
+ private Integer percentage;
+
+ /**
+ * 已上传字节数
+ */
+ @Schema(description = "已上传字节数", example = "3407872")
+ private Long bytesRead;
+
+ /**
+ * 总字节数
+ */
+ @Schema(description = "总字节数", example = "5242880")
+ private Long totalBytes;
+
+ /**
+ * 文件 ID(完成上传后返回)
+ */
+ @Schema(description = "文件 ID(完成上传后返回)", example = "1897293810343682049")
+ private String fileId;
+
+ /**
+ * 文件 URL(完成上传后返回)
+ */
+ @Schema(description = "文件 URL(完成上传后返回)", example = "http://localhost:8000/file/example.png")
+ private String url;
+
+ /**
+ * 错误信息
+ */
+ @Schema(description = "错误信息")
+ private String message;
+}
diff --git a/continew-system/src/main/java/top/continew/admin/system/service/FileService.java b/continew-system/src/main/java/top/continew/admin/system/service/FileService.java
index ff1bc1eb9f784003dd94b4da31b2d510c9affa41..59700eb2a9b560d63bbe2e75d06596579c93470a 100644
--- a/continew-system/src/main/java/top/continew/admin/system/service/FileService.java
+++ b/continew-system/src/main/java/top/continew/admin/system/service/FileService.java
@@ -17,17 +17,18 @@
package top.continew.admin.system.service;
import cn.hutool.core.util.StrUtil;
-import org.dromara.x.file.storage.core.FileInfo;
import org.springframework.web.multipart.MultipartFile;
import top.continew.admin.common.base.service.BaseService;
import top.continew.admin.system.model.entity.FileDO;
import top.continew.admin.system.model.entity.StorageDO;
import top.continew.admin.system.model.query.FileQuery;
import top.continew.admin.system.model.req.FileReq;
+import top.continew.admin.system.model.resp.file.FileUploadProgressResp;
import top.continew.admin.system.model.resp.file.FileResp;
import top.continew.admin.system.model.resp.file.FileStatisticsResp;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.data.service.IService;
+import top.continew.starter.storage.domain.model.resp.FileInfo;
import java.io.File;
import java.io.IOException;
@@ -76,6 +77,23 @@ public interface FileService extends BaseService> entry : fileListGroup.entrySet()) {
StorageDO storage = storageGroup.get(entry.getKey());
- // 清空回收站
- FileInfo fileInfo = new FileInfo();
- fileInfo.setPlatform(storage.getCode());
- fileInfo.setBasePath(StringConstants.EMPTY);
- fileInfo.setPath(storage.getRecycleBinPath());
- fileStorageService.delete(fileInfo);
+ List deletePaths = entry.getValue()
+ .stream()
+ .filter(file -> !FileTypeEnum.DIR.equals(file.getType()))
+ .map(file -> normalizeStoragePath(storage.getRecycleBinPath() + normalizeStoragePath(file
+ .getPath())))
+ .toList();
+ if (CollUtil.isNotEmpty(deletePaths)) {
+ fileStorageService.batchDelete(storage.getCode(), storage.getBucketName(), deletePaths);
+ }
}
} finally {
InterceptorIgnoreHelper.clearIgnoreStrategy();
}
}
+ private String normalizeStoragePath(String path) {
+ return StrUtil.removePrefix(path.replace("\\", StringConstants.SLASH)
+ .replaceAll("/+", StringConstants.SLASH), StringConstants.SLASH);
+ }
+
/**
* 根据 ID 查询
*
diff --git a/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java b/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java
index e1504007bc2c098c447e64ba3579afb715f34593..ef84ddd0dc2af621137366b90867a8841c75f007 100644
--- a/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java
+++ b/continew-system/src/main/java/top/continew/admin/system/service/impl/FileServiceImpl.java
@@ -18,16 +18,13 @@ package top.continew.admin.system.service.impl;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.io.file.FileNameUtil;
-import cn.hutool.core.util.ClassUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.core.util.URLUtil;
+import cn.hutool.crypto.digest.DigestUtil;
+import cn.hutool.json.JSONUtil;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
-import org.dromara.x.file.storage.core.FileInfo;
-import org.dromara.x.file.storage.core.FileStorageService;
-import org.dromara.x.file.storage.core.ProgressListener;
-import org.dromara.x.file.storage.core.upload.UploadPretreatment;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
@@ -41,19 +38,28 @@ import top.continew.admin.system.model.entity.FileDO;
import top.continew.admin.system.model.entity.StorageDO;
import top.continew.admin.system.model.query.FileQuery;
import top.continew.admin.system.model.req.FileReq;
+import top.continew.admin.system.model.resp.file.FileUploadProgressResp;
import top.continew.admin.system.model.resp.file.FileResp;
import top.continew.admin.system.model.resp.file.FileStatisticsResp;
+import top.continew.admin.system.enums.FileUploadProgressStatusEnum;
import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.StorageService;
import top.continew.admin.system.util.FileNameGenerator;
import top.continew.starter.cache.redisson.util.RedisLockUtils;
+import top.continew.starter.cache.redisson.util.RedisUtils;
import top.continew.starter.core.constant.StringConstants;
import top.continew.starter.core.util.CollUtils;
import top.continew.starter.core.util.StrUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
+import top.continew.starter.storage.core.FileStorageService;
+import top.continew.starter.storage.core.UploadPretreatment;
+import top.continew.starter.storage.domain.model.resp.FileInfo;
import java.io.File;
+import java.io.IOException;
+import java.time.Duration;
+import java.util.HashMap;
import java.util.List;
import java.util.Map;
import java.util.function.Function;
@@ -70,6 +76,9 @@ import java.util.stream.Collectors;
@RequiredArgsConstructor
public class FileServiceImpl extends BaseServiceImpl implements FileService {
+ private static final String FILE_UPLOAD_PROGRESS_PREFIX = "file:upload:progress:";
+ private static final Duration FILE_UPLOAD_PROGRESS_EXPIRE = Duration.ofHours(1);
+
private final FileStorageService fileStorageService;
@Lazy
@Resource
@@ -106,12 +115,18 @@ public class FileServiceImpl extends BaseServiceImpl storageIds) {
if (CollUtil.isEmpty(storageIds)) {
@@ -216,13 +252,14 @@ public class FileServiceImpl extends BaseServiceImpl allExtensions = FileTypeEnum.getAllExtensions();
CheckUtils.throwIf(!allExtensions.contains(extName), "不支持的文件类型,仅支持 {} 格式的文件", String
.join(StringConstants.COMMA, allExtensions));
@@ -235,38 +272,54 @@ public class FileServiceImpl extends BaseServiceImpl img.size(100, 100));
+ uploadPretreatment.thumbnail(100, 100);
}
- uploadPretreatment.setProgressMonitor(new ProgressListener() {
- @Override
- public void start() {
- log.info("开始上传文件: {}", uniqueFileName);
+ uploadPretreatment.onProgress((progressSize, allSize, percentage) -> {
+ log.info("文件 [{}] 已上传 [{}],总大小 [{}],进度 [{}%]", uniqueFileName, progressSize, allSize, percentage);
+ if (StrUtil.isNotBlank(normalizedUploadTaskId)) {
+ FileUploadProgressStatusEnum status = (allSize > 0 && progressSize >= allSize) || percentage >= 100
+ ? FileUploadProgressStatusEnum.FINALIZING
+ : FileUploadProgressStatusEnum.UPLOADING;
+ this.saveUploadProgress(normalizedUploadTaskId, status, progressSize, allSize, percentage, null, null, null);
}
-
- @Override
- public void progress(long progressSize, Long allSize) {
- log.info("文件 [{}] 已上传 [{}],总大小 [{}]", uniqueFileName, progressSize, allSize);
+ });
+ try {
+ // 上传
+ log.info("开始上传文件: {}", uniqueFileName);
+ FileInfo fileInfo = uploadPretreatment.upload();
+ log.info("文件 [{}] 上传完成", uniqueFileName);
+ FileInfo result = this.postProcessUploadResult(fileInfo, storage);
+ if (StrUtil.isNotBlank(normalizedUploadTaskId)) {
+ long completedSize = result.getSize() == null ? totalBytes : result.getSize();
+ this.saveUploadProgress(normalizedUploadTaskId, FileUploadProgressStatusEnum.COMPLETED, completedSize, totalBytes, 100, null, result
+ .getFileId(), result.getUrl());
}
-
- @Override
- public void finish() {
- log.info("文件 [{}] 上传完成", uniqueFileName);
+ return result;
+ } catch (RuntimeException e) {
+ if (StrUtil.isNotBlank(normalizedUploadTaskId)) {
+ this.saveUploadProgress(normalizedUploadTaskId, FileUploadProgressStatusEnum.FAILED, 0L, totalBytes, 0, e
+ .getMessage(), null, null);
}
- });
- // 上传
- return uploadPretreatment.upload();
+ throw e;
+ }
}
/**
@@ -288,7 +341,7 @@ public class FileServiceImpl extends BaseServiceImpl
- * 1.如果 path 为 {@code /},则设置为空
+ * 1.如果 path 为 {@code /},则保持为 {@code /}(避免触发自动日期目录)
* 2.如果 path 不以 {@code /} 结尾,则添加后缀 {@code /}
* 3.如果 path 以 {@code /} 开头,则移除前缀 {@code /}
* 示例:yyyy/MM/dd/
@@ -299,7 +352,8 @@ public class FileServiceImpl extends BaseServiceImpl());
+ }
+ fileInfo.setUrl(URLUtil.normalize(storage.getUrlPrefix() + normalizeStoragePath(fileInfo
+ .getPath()), false, true));
+ if (StrUtil.isNotBlank(fileInfo.getThumbnailPath()) && !StrUtil.startWithAny(fileInfo
+ .getThumbnailPath(), "http://", "https://")) {
+ fileInfo.setThumbnailPath(URLUtil.normalize(storage.getUrlPrefix() + normalizeStoragePath(fileInfo
+ .getThumbnailPath()), false, true));
+ }
+ return fileInfo;
+ }
+
+ private String normalizeStoragePath(String path) {
+ return StrUtil.removePrefix(path.replace("\\", StringConstants.SLASH)
+ .replaceAll("/+", StringConstants.SLASH), StringConstants.SLASH);
+ }
+
+ private long getFileSize(Object file) {
+ if (file instanceof MultipartFile multipartFile) {
+ return multipartFile.getSize();
+ }
+ if (file instanceof File ioFile) {
+ return ioFile.length();
+ }
+ return 0L;
+ }
+
+ private void saveUploadProgress(String uploadTaskId,
+ FileUploadProgressStatusEnum status,
+ long bytesRead,
+ long totalBytes,
+ int percentage,
+ String message,
+ String fileId,
+ String url) {
+ String key = FILE_UPLOAD_PROGRESS_PREFIX + uploadTaskId;
+ FileUploadProgressResp current = null;
+ Object value = RedisUtils.get(key);
+ if (value != null) {
+ current = JSONUtil.toBean(value.toString(), FileUploadProgressResp.class);
+ }
+ if (current != null && isFinalStatus(current.getStatus()) && !isFinalStatus(status)) {
+ return;
+ }
+ if (!isFinalStatus(status) && current != null) {
+ bytesRead = Math.max(bytesRead, current.getBytesRead() == null ? 0L : current.getBytesRead());
+ percentage = Math.max(percentage, current.getPercentage() == null ? 0 : current.getPercentage());
+ }
+
+ FileUploadProgressResp resp = new FileUploadProgressResp();
+ resp.setUploadTaskId(uploadTaskId);
+ resp.setStatus(status);
+ resp.setBytesRead(bytesRead);
+ resp.setTotalBytes(totalBytes);
+ resp.setPercentage(percentage);
+ resp.setMessage(StrUtil.blankToDefault(message, current == null ? null : current.getMessage()));
+ resp.setFileId(StrUtil.blankToDefault(fileId, current == null ? null : current.getFileId()));
+ resp.setUrl(StrUtil.blankToDefault(url, current == null ? null : current.getUrl()));
+ RedisUtils.set(key, JSONUtil.toJsonStr(resp), FILE_UPLOAD_PROGRESS_EXPIRE);
+ }
+
+ private boolean isFinalStatus(String status) {
+ return StrUtil.equalsAnyIgnoreCase(status, FileUploadProgressStatusEnum.COMPLETED
+ .getValue(), FileUploadProgressStatusEnum.FAILED.getValue());
+ }
+
+ private boolean isFinalStatus(FileUploadProgressStatusEnum status) {
+ return status == FileUploadProgressStatusEnum.COMPLETED || status == FileUploadProgressStatusEnum.FAILED;
+ }
+}
diff --git a/continew-system/src/main/java/top/continew/admin/system/service/impl/MultipartUploadServiceImpl.java b/continew-system/src/main/java/top/continew/admin/system/service/impl/MultipartUploadServiceImpl.java
index ef6bb67a3a2a6b90a4ff1ea21eadee05f8ee36fb..6c83ca09b37cf99e1fe389044708fad12a07a490 100644
--- a/continew-system/src/main/java/top/continew/admin/system/service/impl/MultipartUploadServiceImpl.java
+++ b/continew-system/src/main/java/top/continew/admin/system/service/impl/MultipartUploadServiceImpl.java
@@ -21,16 +21,10 @@ import cn.hutool.core.util.StrUtil;
import lombok.RequiredArgsConstructor;
import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile;
-import top.continew.admin.system.constant.MultipartUploadConstants;
-import top.continew.admin.system.dao.MultipartUploadDao;
import top.continew.admin.system.enums.FileTypeEnum;
-import top.continew.admin.system.factory.StorageHandlerFactory;
-import top.continew.admin.system.handler.StorageHandler;
-import top.continew.admin.system.handler.impl.LocalStorageHandler;
import top.continew.admin.system.model.entity.FileDO;
import top.continew.admin.system.model.entity.StorageDO;
import top.continew.admin.system.model.req.MultipartUploadInitReq;
-import top.continew.admin.system.model.resp.file.FilePartInfo;
import top.continew.admin.system.model.resp.file.MultipartUploadInitResp;
import top.continew.admin.system.model.resp.file.MultipartUploadResp;
import top.continew.admin.system.mapper.FileMapper;
@@ -39,12 +33,12 @@ import top.continew.admin.system.service.MultipartUploadService;
import top.continew.admin.system.service.StorageService;
import top.continew.admin.system.util.FileNameGenerator;
import top.continew.starter.core.exception.BaseException;
+import top.continew.starter.core.constant.StringConstants;
+import top.continew.starter.storage.core.FileStorageService;
+import top.continew.starter.storage.domain.model.resp.FileInfo;
+import top.continew.starter.storage.domain.model.resp.MultipartInitResp;
-import java.time.LocalDateTime;
-import java.util.List;
-import java.util.Map;
-import java.util.Set;
-import java.util.stream.Collectors;
+import java.io.IOException;
/**
* 分片上传业务实现
@@ -57,39 +51,13 @@ import java.util.stream.Collectors;
public class MultipartUploadServiceImpl implements MultipartUploadService {
private final StorageService storageService;
-
- private final StorageHandlerFactory storageHandlerFactory;
-
- private final MultipartUploadDao multipartUploadDao;
-
+ private final FileStorageService fileStorageService;
private final FileService fileService;
-
private final FileMapper fileMapper;
@Override
public MultipartUploadInitResp initMultipartUpload(MultipartUploadInitReq multiPartUploadInitReq) {
- // 后续可以增加storageCode参数 指定某个存储平台 当前设计是默认存储平台
StorageDO storageDO = storageService.getByCode(null);
- // 根据文件Md5查询当前存储平台是否初始化过分片
- String uploadId = multipartUploadDao.getUploadIdByMd5(multiPartUploadInitReq.getFileMd5());
- if (StrUtil.isNotBlank(uploadId)) {
- MultipartUploadInitResp multipartUpload = multipartUploadDao.getMultipartUpload(uploadId);
- //对比存储平台和分片大小是否一致 一致则返回结果
- if (multipartUpload != null && multipartUpload.getPartSize()
- .equals(MultipartUploadConstants.MULTIPART_UPLOAD_PART_SIZE) && multipartUpload.getPlatform()
- .equals(storageDO.getCode())) {
- // 获取已上传分片信息
- List fileParts = multipartUploadDao.getFileParts(uploadId);
- Set partNumbers = fileParts.stream()
- .map(FilePartInfo::getPartNumber)
- .collect(Collectors.toSet());
- multipartUpload.setUploadedPartNumbers(partNumbers);
- return multipartUpload;
- }
- //todo else 待定 更换存储平台 或分片大小有变更 是否需要删除原先分片
-
- }
-
// 检测文件名是否已存在(同一目录下文件名不能重复)
String originalFileName = multiPartUploadInitReq.getFileName();
String parentPath = multiPartUploadInitReq.getParentPath();
@@ -104,124 +72,143 @@ public class MultipartUploadServiceImpl implements MultipartUploadService {
}
// 生成唯一文件名(处理重名情况)
- String uniqueFileName = FileNameGenerator.generateUniqueName(originalFileName, parentPath, storageDO.getId(), fileMapper);
+ String uniqueFileName = FileNameGenerator.generateUniqueName(originalFileName, parentPath, storageDO
+ .getId(), fileMapper);
multiPartUploadInitReq.setFileName(uniqueFileName);
-
- StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
- //文件元信息
- Map metaData = multiPartUploadInitReq.getMetaData();
- MultipartUploadInitResp multipartUploadInitResp = storageHandler
- .initMultipartUpload(storageDO, multiPartUploadInitReq);
- // 缓存文件信息,md5和uploadId映射
- multipartUploadDao.setMultipartUpload(multipartUploadInitResp.getUploadId(), multipartUploadInitResp, metaData);
- multipartUploadDao.setMd5Mapping(multiPartUploadInitReq.getFileMd5(), multipartUploadInitResp.getUploadId());
- return multipartUploadInitResp;
+ fileService.createParentDir(StrUtil.blankToDefault(parentPath, StringConstants.SLASH), storageDO);
+
+ top.continew.starter.storage.domain.model.req.MultipartUploadInitReq storageReq = new top.continew.starter.storage.domain.model.req.MultipartUploadInitReq();
+ storageReq.setPlatform(storageDO.getCode());
+ storageReq.setBucket(storageDO.getBucketName());
+ storageReq.setFileName(uniqueFileName);
+ storageReq.setFileSize(multiPartUploadInitReq.getFileSize());
+ storageReq.setFileMd5(multiPartUploadInitReq.getFileMd5());
+ storageReq.setContentType(multiPartUploadInitReq.getContentType());
+ storageReq.setParentPath(StrUtil.blankToDefault(parentPath, StringConstants.SLASH));
+ storageReq.setMetadata(multiPartUploadInitReq.getMetaData());
+ MultipartInitResp initResp = fileStorageService.initMultipartUpload(storageReq);
+ return this.toAdminInitResp(initResp);
}
@Override
public MultipartUploadResp uploadPart(MultipartFile file, String uploadId, Integer partNumber, String path) {
- StorageDO storageDO = storageService.getByCode(null);
- StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
- MultipartUploadResp resp = storageHandler.uploadPart(storageDO, path, uploadId, partNumber, file);
- FilePartInfo partInfo = new FilePartInfo();
- partInfo.setUploadId(uploadId);
- partInfo.setBucket(storageDO.getBucketName());
- partInfo.setPath(path);
- partInfo.setPartNumber(partNumber);
- partInfo.setPartETag(resp.getPartETag());
- partInfo.setPartSize(resp.getPartSize());
- partInfo.setStatus("SUCCESS");
- partInfo.setUploadTime(LocalDateTime.now());
- multipartUploadDao.setFilePart(uploadId, partInfo);
- return resp;
+ MultipartInitResp session = fileStorageService.getMultipartSession(uploadId);
+ if (session == null) {
+ throw new BaseException("无效的 uploadId: " + uploadId);
+ }
+ validatePartSize(file, session, partNumber);
+ String targetPath = StrUtil.blankToDefault(session.getPath(), path);
+ try {
+ top.continew.starter.storage.domain.model.resp.MultipartUploadResp resp = fileStorageService
+ .uploadPart(session.getPlatform(), session
+ .getBucket(), normalizeStoragePath(targetPath), uploadId, partNumber, file.getInputStream());
+ return this.toAdminUploadResp(resp);
+ } catch (IOException e) {
+ throw new BaseException("上传分片失败: " + e.getMessage(), e);
+ }
}
@Override
public FileDO completeMultipartUpload(String uploadId) {
- StorageDO storageDO = storageService.getByCode(null);
- // 从 FileRecorder 获取所有分片信息
- List recordedParts = multipartUploadDao.getFileParts(uploadId);
- MultipartUploadInitResp initResp = multipartUploadDao.getMultipartUpload(uploadId);
- // 转换为 MultipartUploadResp
- List parts = recordedParts.stream().map(partInfo -> {
- MultipartUploadResp resp = new MultipartUploadResp();
- resp.setPartNumber(partInfo.getPartNumber());
- resp.setPartETag(partInfo.getPartETag());
- resp.setPartSize(partInfo.getPartSize());
- resp.setSuccess("SUCCESS".equals(partInfo.getStatus()));
- return resp;
- }).collect(Collectors.toList());
-
- // 如果没有记录,使用客户端传入的分片信息
- if (parts.isEmpty()) {
- throw new BaseException("没有找到任何分片信息");
+ MultipartInitResp session = fileStorageService.getMultipartSession(uploadId);
+ if (session == null) {
+ throw new BaseException("无效的 uploadId: " + uploadId);
}
-
- // 验证分片完整性
- validatePartsCompleteness(parts);
-
- // 获取策略,判断是否需要验证
- boolean needVerify = true;
- StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
- if (storageHandler instanceof LocalStorageHandler) {
- needVerify = false;
+ FileInfo fileInfo = fileStorageService.completeMultipartUpload(uploadId, null);
+ StorageDO storageDO = storageService.getByCode(session.getPlatform());
+ FileDO file = fileMapper.lambdaQuery()
+ .eq(FileDO::getStorageId, storageDO.getId())
+ .eq(FileDO::getPath, StringConstants.SLASH + normalizeStoragePath(session.getPath()))
+ .one();
+ if (file != null) {
+ return file;
}
-
- // 完成上传
- storageHandler.completeMultipartUpload(storageDO, parts, initResp.getPath(), uploadId, needVerify);
- // 文件名已在初始化阶段处理为唯一文件名
- String uniqueFileName = initResp.getFileName().replaceFirst("^[/\\\\]+", "");
- FileDO file = new FileDO();
- file.setName(uniqueFileName);
- file.setOriginalName(uniqueFileName);
- file.setPath(initResp.getPath());
- file.setParentPath(initResp.getParentPath());
- file.setSize(initResp.getFileSize());
- file.setSha256(initResp.getFileMd5());
- file.setExtension(initResp.getExtension());
- file.setContentType(initResp.getContentType());
- file.setType(FileTypeEnum.getByExtension(FileUtil.extName(uniqueFileName)));
- file.setStorageId(storageDO.getId());
- fileService.save(file);
- multipartUploadDao.deleteMultipartUpload(uploadId);
- return file;
+ // 兼容兜底:记录器未完成落库时补录
+ FileDO fallback = new FileDO(fileInfo);
+ fallback.setStorageId(storageDO.getId());
+ fallback.setType(FileTypeEnum.getByExtension(FileUtil.extName(fallback.getName())));
+ fileService.save(fallback);
+ return fallback;
}
@Override
public void cancelMultipartUpload(String uploadId) {
- StorageDO storageDO = storageService.getByCode(null);
- multipartUploadDao.deleteMultipartUploadAll(uploadId);
- StorageHandler storageHandler = storageHandlerFactory.createHandler(storageDO.getType());
- storageHandler.cleanPart(storageDO, uploadId);
+ fileStorageService.abortMultipartUpload(uploadId);
+ }
+
+ private MultipartUploadInitResp toAdminInitResp(MultipartInitResp source) {
+ MultipartUploadInitResp target = new MultipartUploadInitResp();
+ target.setFileId(source.getFileId());
+ target.setUploadId(source.getUploadId());
+ target.setBucket(source.getBucket());
+ target.setPlatform(source.getPlatform());
+ target.setFileName(source.getFileName());
+ target.setFileMd5(source.getFileMd5());
+ target.setFileSize(source.getFileSize());
+ target.setExtension(source.getExtension());
+ target.setContentType(source.getContentType());
+ target.setParentPath(source.getParentPath());
+ target.setPath(StringConstants.SLASH + normalizeStoragePath(source.getPath()));
+ target.setPartSize(source.getPartSize());
+ target.setUploadedPartNumbers(source.getUploadedPartNumbers());
+ return target;
+ }
+
+ private MultipartUploadResp toAdminUploadResp(top.continew.starter.storage.domain.model.resp.MultipartUploadResp source) {
+ MultipartUploadResp target = new MultipartUploadResp();
+ target.setPartNumber(source.getPartNumber());
+ target.setPartETag(source.getPartETag());
+ target.setPartSize(source.getPartSize());
+ target.setSuccess(source.isSuccess());
+ target.setErrorMessage(source.getErrorMessage());
+ return target;
}
/**
- * 验证分片完整性
+ * 基于分片会话固化参数校验当前分片大小一致性。
+ *
+ *
+ * 规则:
+ *
+ *
+ * 1. 分片序号必须在合法区间(1..totalParts)
+ *
+ *
+ * 2. 非最后一片大小必须等于会话 partSize
+ *
+ *
+ * 3. 最后一片大小必须等于剩余字节数(支持整除时等于 partSize)
+ *
*
- * @param parts 分片信息
+ * @param file 当前上传分片
+ * @param session 分片上传会话
+ * @param partNumber 分片序号
*/
- private void validatePartsCompleteness(List parts) {
- if (parts.isEmpty()) {
- throw new BaseException("没有找到任何分片信息");
+ private void validatePartSize(MultipartFile file, MultipartInitResp session, Integer partNumber) {
+ if (partNumber == null || partNumber < 1) {
+ throw new BaseException("分片序号不合法: " + partNumber);
}
-
- // 检查分片编号连续性
- List partNumbers = parts.stream().map(MultipartUploadResp::getPartNumber).sorted().toList();
-
- for (int i = 0; i < partNumbers.size(); i++) {
- if (partNumbers.get(i) != i + 1) {
- throw new BaseException("分片编号不连续,缺失分片: " + (i + 1));
- }
+ Long sessionPartSize = session.getPartSize();
+ Long sessionFileSize = session.getFileSize();
+ if (sessionPartSize == null || sessionPartSize <= 0 || sessionFileSize == null || sessionFileSize <= 0) {
+ return;
}
-
- // 检查是否所有分片都成功
- List failedParts = parts.stream()
- .filter(part -> !part.isSuccess())
- .map(MultipartUploadResp::getPartNumber)
- .toList();
-
- if (!failedParts.isEmpty()) {
- throw new BaseException("存在失败的分片: " + failedParts);
+ long partSize = sessionPartSize;
+ long fileSize = sessionFileSize;
+ long totalParts = (fileSize + partSize - 1) / partSize;
+ if (partNumber > totalParts) {
+ throw new BaseException("分片序号超出范围: " + partNumber);
}
+ long expectedPartSize = partNumber < totalParts ? partSize : fileSize - (totalParts - 1) * partSize;
+ long actualPartSize = file.getSize();
+ if (actualPartSize != expectedPartSize) {
+ throw new BaseException("分片大小不匹配: partNumber=%s, expected=%s, actual=%s"
+ .formatted(partNumber, expectedPartSize, actualPartSize));
+ }
+ }
+
+ private String normalizeStoragePath(String path) {
+ return StrUtil.removePrefix(path.replace("\\", StringConstants.SLASH)
+ .replaceAll("/+", StringConstants.SLASH), StringConstants.SLASH);
}
}
diff --git a/continew-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java b/continew-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java
index d900bfeac18551f5f3bba6009439f8865fe2301b..114a0a5a9045b26aa4021080691c3f1614ea3fef 100644
--- a/continew-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java
+++ b/continew-system/src/main/java/top/continew/admin/system/service/impl/StorageServiceImpl.java
@@ -17,15 +17,9 @@
package top.continew.admin.system.service.impl;
import cn.hutool.core.bean.BeanUtil;
-import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.StrUtil;
-import cn.hutool.core.util.URLUtil;
import jakarta.annotation.Resource;
import lombok.RequiredArgsConstructor;
-import org.dromara.x.file.storage.core.FileStorageProperties;
-import org.dromara.x.file.storage.core.FileStorageService;
-import org.dromara.x.file.storage.core.FileStorageServiceBuilder;
-import org.dromara.x.file.storage.core.platform.FileStorage;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import top.continew.admin.common.base.service.BaseServiceImpl;
@@ -38,16 +32,21 @@ import top.continew.admin.system.model.entity.StorageDO;
import top.continew.admin.system.model.query.StorageQuery;
import top.continew.admin.system.model.req.StorageReq;
import top.continew.admin.system.model.resp.StorageResp;
+import top.continew.admin.system.model.resp.file.FileUploadConfigResp;
import top.continew.admin.system.service.FileService;
import top.continew.admin.system.service.StorageService;
import top.continew.starter.core.util.ExceptionUtils;
-import top.continew.starter.core.util.SpringWebUtils;
import top.continew.starter.core.util.validation.CheckUtils;
import top.continew.starter.core.util.validation.ValidationUtils;
+import top.continew.starter.storage.autoconfigure.properties.LocalStorageConfig;
+import top.continew.starter.storage.autoconfigure.properties.OssStorageConfig;
+import top.continew.starter.storage.autoconfigure.properties.StorageProperties;
+import top.continew.starter.storage.common.constant.StorageConstant;
+import top.continew.starter.storage.core.FileStorageService;
+import top.continew.starter.storage.strategy.impl.LocalStorageStrategy;
+import top.continew.starter.storage.strategy.impl.OssStorageStrategy;
-import java.util.Collections;
import java.util.List;
-import java.util.concurrent.CopyOnWriteArrayList;
/**
* 存储业务实现
@@ -60,6 +59,7 @@ import java.util.concurrent.CopyOnWriteArrayList;
public class StorageServiceImpl extends BaseServiceImpl implements StorageService {
private final FileStorageService fileStorageService;
+ private final StorageProperties storageProperties;
@Resource
private FileService fileService;
@@ -159,6 +159,7 @@ public class StorageServiceImpl extends BaseServiceImpl fileStorageList = fileStorageService.getFileStorageList();
+ if (fileStorageService.exists(storage.getCode()) && fileStorageService.isDynamic(storage.getCode())) {
+ fileStorageService.unload(storage.getCode());
+ }
switch (storage.getType()) {
case LOCAL -> {
- FileStorageProperties.LocalPlusConfig config = new FileStorageProperties.LocalPlusConfig();
+ LocalStorageConfig config = new LocalStorageConfig();
+ config.setEnabled(true);
config.setPlatform(storage.getCode());
- config.setStoragePath(storage.getBucketName());
- fileStorageList.addAll(FileStorageServiceBuilder.buildLocalPlusFileStorage(Collections
- .singletonList(config)));
- // 注册资源映射
- SpringWebUtils.registerResourceHandler(MapUtil.of(URLUtil.url(storage.getDomain()).getPath(), storage
- .getBucketName()));
+ config.setBucketName(storage.getBucketName());
+ config.setEndpoint(storage.getDomain());
+ config.setMultipartUploadThreshold(resolveMultipartUploadThreshold(storage));
+ config.setMultipartUploadPartSize(resolveMultipartUploadPartSize(storage));
+ config.setMultipartTempDir(resolveLocalMultipartTempDir(storage));
+ fileStorageService.register(new LocalStorageStrategy(config));
}
case OSS -> {
- FileStorageProperties.AmazonS3Config config = new FileStorageProperties.AmazonS3Config();
+ OssStorageConfig config = new OssStorageConfig();
+ config.setEnabled(true);
config.setPlatform(storage.getCode());
config.setAccessKey(storage.getAccessKey());
config.setSecretKey(storage.getSecretKey());
- config.setEndPoint(storage.getEndpoint());
+ config.setEndpoint(storage.getEndpoint());
config.setBucketName(storage.getBucketName());
- fileStorageList.addAll(FileStorageServiceBuilder.buildAmazonS3FileStorage(Collections
- .singletonList(config), null));
+ config.setDomain(storage.getDomain());
+ config.setMultipartUploadThreshold(resolveMultipartUploadThreshold(storage));
+ config.setMultipartUploadPartSize(resolveMultipartUploadPartSize(storage));
+ config.setPathStyleAccessEnabled(true);
+ fileStorageService.register(new OssStorageStrategy(config));
}
default -> throw new IllegalArgumentException("不支持的存储类型:%s".formatted(storage.getType()));
}
+ if (Boolean.TRUE.equals(storage.getIsDefault())) {
+ fileStorageService.defaultStorage(storage.getCode());
+ }
}
@Override
public void unload(StorageDO storage) {
- FileStorage fileStorage = fileStorageService.getFileStorage(storage.getCode());
- if (fileStorage == null) {
+ if (!fileStorageService.exists(storage.getCode()) || !fileStorageService.isDynamic(storage.getCode())) {
return;
}
- CopyOnWriteArrayList fileStorageList = fileStorageService.getFileStorageList();
- fileStorageList.remove(fileStorage);
- fileStorage.close();
- // 本地存储引擎需要移除资源映射
- if (StorageTypeEnum.LOCAL.equals(storage.getType())) {
- SpringWebUtils.deRegisterResourceHandler(MapUtil.of(URLUtil.url(storage.getDomain()).getPath(), storage
- .getBucketName()));
- }
+ fileStorageService.unload(storage.getCode());
+ }
+
+ /**
+ * 解析分片上传阈值(字节)
+ *
+ *
+ * 当上传文件大小超过该阈值时,存储策略将走分片上传流程。
+ * 取值优先级:
+ *
+ *
+ * 1. 全局配置 {@code continew-starter.storage.multipart-upload-threshold}
+ *
+ *
+ * 2. 框架默认值 {@link StorageConstant#DEFAULT_MULTIPART_UPLOAD_THRESHOLD}
+ *
+ *
+ * @return 最终生效的分片上传阈值(字节)
+ */
+ private long resolveMultipartUploadThreshold() {
+ long threshold = storageProperties.getMultipartUploadThreshold();
+ return threshold > 0 ? threshold : StorageConstant.DEFAULT_MULTIPART_UPLOAD_THRESHOLD;
+ }
+
+ /**
+ * 解析存储实例分片上传阈值(字节)
+ *
+ *
+ * 取值优先级:
+ *
+ *
+ * 1. 存储实例配置({@link StorageDO#getMultipartUploadThreshold()})
+ *
+ *
+ * 2. 全局配置(见 {@link #resolveMultipartUploadThreshold()})
+ *
+ *
+ * @param storage 存储配置实体
+ * @return 最终生效的分片上传阈值(字节)
+ */
+ private long resolveMultipartUploadThreshold(StorageDO storage) {
+ Long threshold = storage.getMultipartUploadThreshold();
+ return (threshold != null && threshold > 0) ? threshold : resolveMultipartUploadThreshold();
+ }
+
+ /**
+ * 解析全局分片大小(字节)
+ *
+ *
+ * 用于在存储实例未配置分片大小时作为回退值。
+ * 取值优先级:
+ *
+ *
+ * 1. 全局配置 {@code continew-starter.storage.multipart-upload-part-size}
+ *
+ *
+ * 2. 框架默认值 {@link StorageConstant#DEFAULT_MULTIPART_UPLOAD_PART_SIZE}
+ *
+ *
+ * @return 最终生效的全局分片大小(字节)
+ */
+ private long resolveMultipartUploadPartSize() {
+ long partSize = storageProperties.getMultipartUploadPartSize();
+ return partSize > 0 ? partSize : StorageConstant.DEFAULT_MULTIPART_UPLOAD_PART_SIZE;
+ }
+
+ /**
+ * 解析存储实例分片大小(字节)
+ *
+ *
+ * 取值优先级:
+ *
+ *
+ * 1. 存储实例配置({@link StorageDO#getMultipartUploadPartSize()})
+ *
+ *
+ * 2. 全局配置(见 {@link #resolveMultipartUploadPartSize()})
+ *
+ *
+ * @param storage 存储配置实体
+ * @return 最终生效的分片大小(字节)
+ */
+ private long resolveMultipartUploadPartSize(StorageDO storage) {
+ Long partSize = storage.getMultipartUploadPartSize();
+ return (partSize != null && partSize > 0) ? partSize : resolveMultipartUploadPartSize();
+ }
+
+ /**
+ * 解析全局本地分片临时目录
+ *
+ *
+ * 用于本地存储实例未配置临时目录时的回退值。
+ * 取值优先级:
+ *
+ *
+ * 1. 全局配置 {@code continew-starter.storage.local-multipart-temp-dir}
+ *
+ *
+ * 2. 框架默认值 {@link StorageConstant#DEFAULT_LOCAL_MULTIPART_TEMP_DIR}
+ *
+ *
+ * @return 最终生效的全局本地分片临时目录
+ */
+ private String resolveLocalMultipartTempDir() {
+ return StrUtil.isBlank(storageProperties.getLocalMultipartTempDir())
+ ? StorageConstant.DEFAULT_LOCAL_MULTIPART_TEMP_DIR
+ : storageProperties.getLocalMultipartTempDir();
+ }
+
+ /**
+ * 解析存储实例本地分片临时目录
+ *
+ *
+ * 仅本地存储策略使用该值。
+ * 取值优先级:
+ *
+ *
+ * 1. 存储实例配置({@link StorageDO#getMultipartTempDir()},并进行 trim)
+ *
+ *
+ * 2. 全局配置(见 {@link #resolveLocalMultipartTempDir()})
+ *
+ *
+ * @param storage 存储配置实体
+ * @return 最终生效的本地分片临时目录
+ */
+ private String resolveLocalMultipartTempDir(StorageDO storage) {
+ return StrUtil.isBlank(storage.getMultipartTempDir())
+ ? resolveLocalMultipartTempDir()
+ : storage.getMultipartTempDir().trim();
}
/**
@@ -253,4 +401,4 @@ public class StorageServiceImpl extends BaseServiceImpl
* 当目标目录存在同名文件时,自动添加序号后缀:
*
- * - file.txt → file(1).txt → file(2).txt → ...
- * - 无扩展名:README → README(1) → README(2) → ...
- * - 隐藏文件:.gitignore → .gitignore(1) → .gitignore(2) → ...
+ * - file.txt → file(1).txt → file(2).txt → ...
+ * - 无扩展名:README → README(1) → README(2) → ...
+ * - 隐藏文件:.gitignore → .gitignore(1) → .gitignore(2) → ...
*
*
*
@@ -90,7 +90,9 @@ public class FileNameGenerator {
// 安全限制,防止无限循环
if (counter > 9999) {
log.warn("文件名重命名超过最大限制,使用当前时间戳: {}", fileName);
- return baseName + "_" + System.currentTimeMillis() + (StrUtil.isNotBlank(extension) ? "." + extension : "");
+ return baseName + "_" + System.currentTimeMillis() + (StrUtil.isNotBlank(extension)
+ ? "." + extension
+ : "");
}
}
}
@@ -102,10 +104,10 @@ public class FileNameGenerator {
* 示例:
*
*
- * - "document.pdf" → ["document", "pdf"]
- * - "README" → ["README", ""]
- * - ".gitignore" → [".gitignore", ""]
- * - "archive.tar.gz" → ["archive.tar", "gz"]
+ * - "document.pdf" → ["document", "pdf"]
+ * - "README" → ["README", ""]
+ * - ".gitignore" → [".gitignore", ""]
+ * - "archive.tar.gz" → ["archive.tar", "gz"]
*
*
* @param fileName 文件名
@@ -113,7 +115,7 @@ public class FileNameGenerator {
*/
public static String[] parseFileName(String fileName) {
if (StrUtil.isBlank(fileName)) {
- return new String[]{"", ""};
+ return new String[] {"", ""};
}
// 处理隐藏文件(以.开头)
@@ -122,7 +124,7 @@ public class FileNameGenerator {
// 处理空文件名(如只有"."的情况)
if (nameWithoutDot.isEmpty()) {
- return new String[]{fileName, ""};
+ return new String[] {fileName, ""};
}
// 查找最后一个点号位置
@@ -130,18 +132,20 @@ public class FileNameGenerator {
// 点号不存在或在开头(如 ".bashrc"),视为无扩展名
if (lastDotIndex <= 0) {
- return new String[]{fileName, ""};
+ return new String[] {fileName, ""};
}
- String baseName = isHidden ? "." + nameWithoutDot.substring(0, lastDotIndex) : nameWithoutDot.substring(0, lastDotIndex);
+ String baseName = isHidden
+ ? "." + nameWithoutDot.substring(0, lastDotIndex)
+ : nameWithoutDot.substring(0, lastDotIndex);
String extension = nameWithoutDot.substring(lastDotIndex + 1);
// 扩展名不应包含路径分隔符(安全检查)
if (extension.contains("/") || extension.contains("\\")) {
- return new String[]{fileName, ""};
+ return new String[] {fileName, ""};
}
- return new String[]{baseName, extension};
+ return new String[] {baseName, extension};
}
/**
@@ -194,7 +198,10 @@ public class FileNameGenerator {
* @param fileMapper 文件Mapper
* @return 文件名列表
*/
- private static List selectNamesByParentPath(String parentPath, Long storageId, String namePrefix, FileMapper fileMapper) {
+ private static List selectNamesByParentPath(String parentPath,
+ Long storageId,
+ String namePrefix,
+ FileMapper fileMapper) {
var wrapper = fileMapper.lambdaQuery()
.eq(FileDO::getParentPath, parentPath)
.eq(FileDO::getStorageId, storageId)
diff --git a/pom.xml b/pom.xml
index 6e721cacd75626e0bd0cd5379f6a93fd5dd541ff..8c4df8c8c7a51480944b1f35d2ebbf58868bf5df 100644
--- a/pom.xml
+++ b/pom.xml
@@ -13,7 +13,7 @@
top.continew.starter
continew-starter
- 2.15.0
+ 2.16.0-SNAPSHOT
top.continew.admin