🐛 修复凭证分片上传 Mixed Content,支持服务端代传兜底
Co-authored-by: Cursor <cursoragent@cursor.com>
This commit is contained in:
@@ -3,6 +3,7 @@ package org.springblade.file.config;
|
||||
import com.aliyun.oss.OSSClientBuilder;
|
||||
import com.obs.services.ObsClient;
|
||||
import io.minio.MinioClient;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.file.service.IFileService;
|
||||
@@ -20,6 +21,7 @@ import org.springframework.core.env.Environment;
|
||||
* @author bfhuange
|
||||
* @date 2024/10/21
|
||||
*/
|
||||
@Slf4j
|
||||
@Configuration
|
||||
public class FileConfig {
|
||||
|
||||
@@ -63,6 +65,15 @@ public class FileConfig {
|
||||
|
||||
private IFileService buildMinioFileService(FileStorageProperties.StoreProperties properties) {
|
||||
validateStoreProperties(STORAGE_MINIO, properties);
|
||||
if (StringUtil.isBlank(properties.getTransformEndpoint())
|
||||
&& StringUtil.isNotBlank(properties.getEndpoint())
|
||||
&& properties.getEndpoint().trim().toLowerCase().startsWith("http://")) {
|
||||
log.warn("MinIO transform-endpoint 未配置,endpoint={}。HTTPS 页面分片直传会被浏览器 Mixed Content 拦截,将回退为服务端代传",
|
||||
properties.getEndpoint());
|
||||
} else {
|
||||
log.info("MinIO 文件存储已启用 endpoint={} transformEndpoint={}",
|
||||
properties.getEndpoint(), properties.getTransformEndpoint());
|
||||
}
|
||||
MinioClient minioClient = MinioClient.builder()
|
||||
.endpoint(properties.getEndpoint())
|
||||
.credentials(properties.getAccessKeyId(), properties.getAccessKeySecret())
|
||||
@@ -95,12 +106,15 @@ public class FileConfig {
|
||||
FileStorageProperties.StoreProperties mergedProperties = new FileStorageProperties.StoreProperties();
|
||||
String fileStoragePrefix = "file.storage." + prefix;
|
||||
mergedProperties.setEndpoint(firstNotBlank(properties.getEndpoint(), environment.getProperty(fileStoragePrefix + ".endpoint"), environment.getProperty(prefix + ".endpoint")));
|
||||
// 资源管理页 /resource/oss 的外网地址写在库表,不进 Spring Environment;凭证分片必须单独配 file.storage.minio.transform-endpoint
|
||||
mergedProperties.setTransformEndpoint(firstNotBlank(
|
||||
properties.getTransformEndpoint(),
|
||||
environment.getProperty(fileStoragePrefix + ".transform-endpoint"),
|
||||
environment.getProperty(fileStoragePrefix + ".transformEndpoint"),
|
||||
environment.getProperty(prefix + ".transform-endpoint"),
|
||||
// 兼容 blade-resource / oss 配置,避免仅改对象存储外网地址时分片上传仍走内网 HTTP
|
||||
environment.getProperty("oss.transform-endpoint")
|
||||
environment.getProperty(prefix + ".transformEndpoint"),
|
||||
environment.getProperty("oss.transform-endpoint"),
|
||||
environment.getProperty("oss.transformEndpoint")
|
||||
));
|
||||
mergedProperties.setAccessKeyId(firstNotBlank(properties.getAccessKeyId(), environment.getProperty(fileStoragePrefix + ".access-key-id"), environment.getProperty(fileStoragePrefix + ".access-key"), environment.getProperty(prefix + ".access-key-id"), environment.getProperty(prefix + ".access-key")));
|
||||
mergedProperties.setAccessKeySecret(firstNotBlank(properties.getAccessKeySecret(), environment.getProperty(fileStoragePrefix + ".access-key-secret"), environment.getProperty(fileStoragePrefix + ".secret-key"), environment.getProperty(prefix + ".access-key-secret"), environment.getProperty(prefix + ".secret-key")));
|
||||
@@ -113,7 +127,7 @@ public class FileConfig {
|
||||
private String firstNotBlank(String... values) {
|
||||
for (String value : values) {
|
||||
if (StringUtil.isNotBlank(value)) {
|
||||
return value;
|
||||
return value.trim();
|
||||
}
|
||||
}
|
||||
return null;
|
||||
|
||||
+22
@@ -111,4 +111,26 @@ public class FileTaskController extends BladeController {
|
||||
String uploadUrl = fileTaskService.getUploadUrl(id, partNumber);
|
||||
return R.data(uploadUrl);
|
||||
}
|
||||
|
||||
@ApiLog("上传任务-服务端代传分片")
|
||||
@PostMapping(value = "/uploadPart", consumes = org.springframework.http.MediaType.MULTIPART_FORM_DATA_VALUE)
|
||||
@Parameters({
|
||||
@Parameter(name = "id", description = "文件任务id", in = ParameterIn.QUERY, schema = @Schema(type = "string")),
|
||||
@Parameter(name = "partNumber", description = "分段编号", in = ParameterIn.QUERY, schema = @Schema(type = "int"))
|
||||
})
|
||||
@Operation(summary = "服务端代传分片", description = "HTTPS 页面无法直连 HTTP MinIO 时,由服务端转发分片到对象存储")
|
||||
public R<String> uploadPart(Long id, Integer partNumber, org.springframework.web.multipart.MultipartFile file) throws java.io.IOException {
|
||||
if (file == null || file.isEmpty()) {
|
||||
return R.fail("分片内容不能为空");
|
||||
}
|
||||
java.io.File tempFile = java.io.File.createTempFile("file-part-", ".bin");
|
||||
try {
|
||||
file.transferTo(tempFile);
|
||||
return R.data(fileTaskService.uploadPart(id, partNumber, tempFile));
|
||||
} finally {
|
||||
if (tempFile.exists() && !tempFile.delete()) {
|
||||
tempFile.deleteOnExit();
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
+10
@@ -74,6 +74,16 @@ public interface IFileTaskService extends IService<FileTask> {
|
||||
*/
|
||||
String getUploadUrl(Long id, Integer partNumber);
|
||||
|
||||
/**
|
||||
* 服务端代传分片(HTTPS 页面无法直连 HTTP MinIO 时使用)
|
||||
*
|
||||
* @param id 文件任务id
|
||||
* @param partNumber 分段编号
|
||||
* @param partFile 分片临时文件
|
||||
* @return ETag
|
||||
*/
|
||||
String uploadPart(Long id, Integer partNumber, java.io.File partFile);
|
||||
|
||||
/**
|
||||
* 取消文件任务
|
||||
* @param taskIds
|
||||
|
||||
+27
@@ -259,6 +259,33 @@ public class FileTaskServiceImpl extends ServiceImpl<FileTaskMapper, FileTask> i
|
||||
return fileService.getUploadPartUrl(fileTask.getObjectKey(), fileTask.getUploadId(), partNumber, null);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String uploadPart(Long id, Integer partNumber, java.io.File partFile) {
|
||||
FileTask fileTask = this.getById(id);
|
||||
Func.requireNotNull(fileTask, "文件任务不存在");
|
||||
if (FileTaskStatus.COMPLETED.getCode().equals(fileTask.getStatus())) {
|
||||
throw new ServiceException("文件任务已完成");
|
||||
}
|
||||
if (FileTaskStatus.PAUSED.getCode().equals(fileTask.getStatus())) {
|
||||
throw new ServiceException("文件任务已暂停");
|
||||
}
|
||||
if (partFile == null || !partFile.exists()) {
|
||||
throw new ServiceException("分片文件不存在");
|
||||
}
|
||||
org.springblade.file.pojo.vo.PartEtagVO partEtag = fileService.uploadPart(
|
||||
fileTask.getObjectKey(),
|
||||
fileTask.getUploadId(),
|
||||
partFile,
|
||||
partNumber,
|
||||
0L,
|
||||
partFile.length()
|
||||
);
|
||||
if (partEtag == null || StringUtil.isBlank(partEtag.getEtag())) {
|
||||
throw new ServiceException("分片上传未返回 ETag");
|
||||
}
|
||||
return partEtag.getEtag().replace("\"", "");
|
||||
}
|
||||
|
||||
@Transactional(rollbackFor = Exception.class)
|
||||
@Override
|
||||
public void cancelBatchByIds(List<Long> taskIds) {
|
||||
|
||||
+33
@@ -30,6 +30,7 @@ import io.minio.ComposeSource;
|
||||
import io.minio.GetObjectArgs;
|
||||
import io.minio.GetPresignedObjectUrlArgs;
|
||||
import io.minio.MinioClient;
|
||||
import io.minio.ObjectWriteResponse;
|
||||
import io.minio.PutObjectArgs;
|
||||
import io.minio.RemoveObjectArgs;
|
||||
import io.minio.UploadObjectArgs;
|
||||
@@ -39,8 +40,10 @@ import lombok.extern.slf4j.Slf4j;
|
||||
import org.springblade.core.log.exception.ServiceException;
|
||||
import org.springblade.core.tool.utils.StringUtil;
|
||||
import org.springblade.file.config.FileStorageProperties;
|
||||
import org.springblade.file.pojo.vo.PartEtagVO;
|
||||
|
||||
import java.io.File;
|
||||
import java.io.FileInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Files;
|
||||
@@ -88,6 +91,36 @@ public class MinioFileServiceImpl extends AbstractFileService {
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public PartEtagVO uploadPart(String objectKey, String uploadId, File file, int partNumber, Long offset, Long partSize) {
|
||||
long start = offset == null ? 0L : offset;
|
||||
long size = partSize == null ? Math.max(0L, file.length() - start) : partSize;
|
||||
try (FileInputStream inputStream = new FileInputStream(file)) {
|
||||
long skipped = inputStream.skip(start);
|
||||
if (skipped < start) {
|
||||
throw new ServiceException("MinIO分段上传读取文件偏移失败");
|
||||
}
|
||||
ObjectWriteResponse response = minioClient.putObject(PutObjectArgs.builder()
|
||||
.bucket(properties.getBucketName())
|
||||
.object(partObjectKey(objectKey, uploadId, partNumber))
|
||||
.stream(inputStream, size, PART_SIZE)
|
||||
.build());
|
||||
String etag = response.etag() == null ? "" : response.etag().replace("\"", "");
|
||||
return new PartEtagVO(etag, partNumber);
|
||||
} catch (ServiceException serviceException) {
|
||||
throw serviceException;
|
||||
} catch (Exception exception) {
|
||||
log.error("MinIO分段上传异常 objectKey:{} partNumber:{}", objectKey, partNumber, exception);
|
||||
throw new ServiceException("MinIO分段上传异常");
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void cancelMultipartUpload(String objectKey, String uploadId) {
|
||||
// MinIO 分片以独立对象模拟,取消时由任务层清理业务状态即可
|
||||
log.debug("MinIO取消分段上传 objectKey:{} uploadId:{}", objectKey, uploadId);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void completeMultipartUpload(String objectKey, String uploadId, List<org.springblade.file.pojo.vo.PartEtagVO> partEtags) {
|
||||
try {
|
||||
|
||||
@@ -120,11 +120,12 @@ baidu:
|
||||
request-timeout: 30s
|
||||
token-refresh-advance: 1m
|
||||
|
||||
# 凭证分片上传等走 blade-file,外网地址需单独配置(与 /resource/oss 的 transformEndpoint 保持一致)
|
||||
# 凭证分片上传走 blade-file,与 /resource/oss 库表配置相互独立;外网地址必须在此配置并发布到 Nacos 后重启 blade-file
|
||||
# 不要使用可被空环境变量覆盖的 ${VAR:default},否则仍会签发 http://内网IP 预签名导致 Mixed Content
|
||||
file:
|
||||
storage:
|
||||
minio:
|
||||
transform-endpoint: ${MINIO_TRANSFORM_ENDPOINT:https://tms-prod.xinghy56.com/oss}
|
||||
transform-endpoint: https://tms-prod.xinghy56.com/oss
|
||||
|
||||
powerjob:
|
||||
worker:
|
||||
|
||||
Reference in New Issue
Block a user