From f45f58c3400c78f509490ba71bff01dbe63ccb3a Mon Sep 17 00:00:00 2001 From: =?UTF-8?q?=E7=A7=8B=E8=BE=9E=E6=9C=AA=E5=AF=92?= <545073804@qq.com> Date: Thu, 26 Mar 2026 00:19:44 +0800 Subject: [PATCH] =?UTF-8?q?fix=20oss=E6=96=87=E4=BB=B6=E4=B8=8B=E8=BD=BD?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../oss/client/AbstractOssClientImpl.java | 27 ++++++++++-- .../dromara/common/oss/client/OssClient.java | 23 ++++++++++- .../io/OutputStreamDownloadSubscriber.java | 41 +++++++++++++++---- .../controller/system/SysOssController.java | 7 ++-- .../system/service/ISysOssService.java | 6 +-- .../service/impl/SysOssServiceImpl.java | 32 +++++++++------ 6 files changed, 103 insertions(+), 33 deletions(-) diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java index f3dd981f1..249661b8f 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/AbstractOssClientImpl.java @@ -10,6 +10,7 @@ import org.dromara.common.oss.io.OutputStreamDownloadSubscriber; import org.dromara.common.oss.model.GetObjectResult; import org.dromara.common.oss.model.HandleAsyncResult; import org.dromara.common.oss.model.PutObjectResult; +import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.ResponsePublisher; @@ -29,7 +30,7 @@ import java.nio.channels.WritableByteChannel; import java.nio.file.Files; import java.nio.file.Path; import java.time.Duration; -import java.time.LocalDateTime; +import java.time.ZoneOffset; import java.util.Collection; import java.util.Map; import java.util.Objects; @@ -309,7 +310,7 @@ public abstract class AbstractOssClientImpl implements OssClient { try { ResponsePublisher publisher = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toPublisher(), null); GetObjectResult getObjectResult = buildGetObjectResult(key, publisher.response()); - publisher.subscribe(downloadSubscriber); + publisher.subscribe(downloadSubscriber).join(); return getObjectResult; } catch (Exception e) { if (e instanceof S3StorageException ex) { @@ -319,6 +320,21 @@ public abstract class AbstractOssClientImpl implements OssClient { } } + @Override + public T bucketDownload(String bucket, String key, BiFunction downloadTransformer) { + try { + ResponseInputStream responseInputStream = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toBlockingInputStream(), null); + GetObjectResponse response = responseInputStream.response(); + GetObjectResult getObjectResult = buildGetObjectResult(key, response); + return downloadTransformer.apply(getObjectResult, responseInputStream); + } catch (Exception e) { + if (e instanceof S3StorageException ex) { + throw ex; + } + throw S3StorageException.form(e); + } + } + @Override public GetObjectResult bucketDownload(String bucket, String key, Path path) { try (OutputStream out = Files.newOutputStream(path)) { @@ -362,7 +378,7 @@ public abstract class AbstractOssClientImpl implements OssClient { return GetObjectResult.form( key, response.eTag(), - LocalDateTime.from(response.lastModified()), + response.lastModified().atOffset(ZoneOffset.UTC).toLocalDateTime(), response.contentLength(), response.contentType(), response.contentDisposition(), @@ -446,6 +462,11 @@ public abstract class AbstractOssClientImpl implements OssClient { return bucketDownload(defaultBucket(), key, downloadSubscriber); } + @Override + public T download(String key, BiFunction downloadTransformer) { + return bucketDownload(defaultBucket(), key, downloadTransformer); + } + @Override public GetObjectResult download(String key, Path path) { return bucketDownload(defaultBucket(), key, path); diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/OssClient.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/OssClient.java index bb64ca4e0..23b2eee9b 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/OssClient.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/client/OssClient.java @@ -43,12 +43,12 @@ public interface OssClient extends AutoCloseable { /** * S3 存储客户端ID - * + *

* 用于标识客户端,初始化后不允许更改 * * @return S3 存储客户端ID */ - default String clientId(){ + default String clientId() { return IdUtil.fastSimpleUUID(); } @@ -219,6 +219,16 @@ public interface OssClient extends AutoCloseable { */ GetObjectResult bucketDownload(String bucket, String key, OutputStreamDownloadSubscriber downloadSubscriber); + /** + * 将指定存储桶中的对象下载到转换器中,由使用者决定返回值。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param downloadTransformer 下载转换器 + * @return 下载结果 + */ + T bucketDownload(String bucket, String key, BiFunction downloadTransformer); + /** * 将指定存储桶中的对象下载到本地路径。 * @@ -364,6 +374,15 @@ public interface OssClient extends AutoCloseable { */ GetObjectResult download(String key, OutputStreamDownloadSubscriber downloadSubscriber); + /** + * 将指定存储桶中的对象下载到转换器中,由使用者决定返回值。 + * + * @param key 对象键 + * @param downloadTransformer 下载转换器 + * @return 下载结果 + */ + T download(String key, BiFunction downloadTransformer); + /** * 将默认存储桶中的对象下载到本地路径。 * diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/io/OutputStreamDownloadSubscriber.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/io/OutputStreamDownloadSubscriber.java index 229098141..14f96a517 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/io/OutputStreamDownloadSubscriber.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/io/OutputStreamDownloadSubscriber.java @@ -3,7 +3,6 @@ package org.dromara.common.oss.io; import org.dromara.common.oss.exception.S3StorageException; import java.io.FileOutputStream; -import java.io.IOException; import java.io.OutputStream; import java.nio.ByteBuffer; import java.nio.channels.Channels; @@ -19,11 +18,15 @@ public class OutputStreamDownloadSubscriber implements Consumer, Aut private final WritableByteChannel channel; - private OutputStreamDownloadSubscriber(WritableByteChannel channel) { + private final boolean allowAutoClose; + + private OutputStreamDownloadSubscriber(WritableByteChannel channel, boolean allowAutoClose) { this.channel = channel; + this.allowAutoClose = allowAutoClose; } - private OutputStreamDownloadSubscriber(OutputStream out) { + private OutputStreamDownloadSubscriber(OutputStream out, boolean allowAutoClose) { + this.allowAutoClose = allowAutoClose; // 创建可写入的字节通道 if (out instanceof FileOutputStream outputStream) { // 如果是文件输入流,直接获取文件输出流的 Channel @@ -35,18 +38,18 @@ public class OutputStreamDownloadSubscriber implements Consumer, Aut @Override public void accept(ByteBuffer byteBuffer) { - try (channel) { + try { while (byteBuffer.hasRemaining()) { channel.write(byteBuffer); } - } catch (IOException e) { + } catch (Exception e) { throw S3StorageException.form(e); } } @Override public void close() throws Exception { - if (channel.isOpen()) { + if (channel.isOpen() && allowAutoClose) { channel.close(); } } @@ -58,7 +61,18 @@ public class OutputStreamDownloadSubscriber implements Consumer, Aut * @return 输出流下载订阅器 */ public static OutputStreamDownloadSubscriber create(OutputStream out) { - return new OutputStreamDownloadSubscriber(out); + return create(out, false); + } + + /** + * 创建一个输出流下载订阅器 + * + * @param out 输出流 + * @param allowAutoClose 是否允许自动关闭流 + * @return 输出流下载订阅器 + */ + public static OutputStreamDownloadSubscriber create(OutputStream out, boolean allowAutoClose) { + return new OutputStreamDownloadSubscriber(out, allowAutoClose); } /** @@ -68,7 +82,18 @@ public class OutputStreamDownloadSubscriber implements Consumer, Aut * @return 输出流下载订阅器 */ public static OutputStreamDownloadSubscriber create(WritableByteChannel channel) { - return new OutputStreamDownloadSubscriber(channel); + return create(channel, false); + } + + /** + * 创建一个输出流下载订阅器 + * + * @param channel 可写字节通道 + * @param allowAutoClose 是否允许自动关闭流 + * @return 输出流下载订阅器 + */ + public static OutputStreamDownloadSubscriber create(WritableByteChannel channel, boolean allowAutoClose) { + return new OutputStreamDownloadSubscriber(channel, allowAutoClose); } } diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java index ea2c28058..36f520ab0 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/controller/system/SysOssController.java @@ -2,7 +2,6 @@ package org.dromara.system.controller.system; import cn.dev33.satoken.annotation.SaCheckPermission; -import jakarta.servlet.http.HttpServletResponse; import jakarta.validation.constraints.NotEmpty; import lombok.RequiredArgsConstructor; import org.dromara.common.core.domain.PageResult; @@ -16,6 +15,7 @@ import org.dromara.system.domain.bo.SysOssBo; import org.dromara.system.domain.vo.SysOssVo; import org.dromara.system.service.ISysOssService; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.validation.annotation.Validated; import org.springframework.web.bind.annotation.*; import org.springframework.web.multipart.MultipartFile; @@ -83,13 +83,12 @@ public class SysOssController extends BaseController { * 下载OSS对象 * * @param ossId OSS对象ID - * @param response HTTP 响应 * @throws IOException IO 异常 */ @SaCheckPermission("system:oss:download") @GetMapping("/download/{ossId}") - public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException { - ossService.download(ossId, response); + public ResponseEntity download(@PathVariable Long ossId) throws IOException { + return ossService.download(ossId); } /** diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java index da8d5fa0b..52870e874 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/ISysOssService.java @@ -1,14 +1,13 @@ package org.dromara.system.service; -import jakarta.servlet.http.HttpServletResponse; import org.dromara.common.core.domain.PageResult; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.system.domain.bo.SysOssBo; import org.dromara.system.domain.vo.SysOssVo; +import org.springframework.http.ResponseEntity; import org.springframework.web.multipart.MultipartFile; import java.io.File; -import java.io.IOException; import java.util.Collection; import java.util.List; @@ -64,9 +63,8 @@ public interface ISysOssService { * 文件下载方法,支持一次性下载完整文件 * * @param ossId OSS对象ID - * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容 */ - void download(Long ossId, HttpServletResponse response) throws IOException; + ResponseEntity download(Long ossId); /** * 删除OSS对象存储 diff --git a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java index fbed33b1c..9b0da5efe 100644 --- a/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java +++ b/ruoyi-modules/ruoyi-system/src/main/java/org/dromara/system/service/impl/SysOssServiceImpl.java @@ -2,11 +2,11 @@ package org.dromara.system.service.impl; import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.convert.Convert; +import cn.hutool.core.io.IoUtil; import cn.hutool.core.util.ObjectUtil; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; -import jakarta.servlet.http.HttpServletResponse; import lombok.RequiredArgsConstructor; import org.dromara.common.core.constant.CacheNames; import org.dromara.common.core.domain.PageResult; @@ -21,10 +21,9 @@ import org.dromara.common.core.utils.file.FileUtils; import org.dromara.common.json.utils.JsonUtils; import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.oss.client.OssClient; -import org.dromara.common.oss.factory.OssFactory; -import org.dromara.common.oss.model.GetObjectResult; -import org.dromara.common.oss.model.PutObjectResult; import org.dromara.common.oss.enums.AccessPolicy; +import org.dromara.common.oss.factory.OssFactory; +import org.dromara.common.oss.model.PutObjectResult; import org.dromara.common.oss.util.S3ObjectUtil; import org.dromara.system.domain.SysOss; import org.dromara.system.domain.SysOssExt; @@ -35,6 +34,7 @@ import org.dromara.system.service.ISysOssService; import org.jetbrains.annotations.NotNull; import org.springframework.cache.annotation.Cacheable; import org.springframework.http.MediaType; +import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; import org.springframework.web.multipart.MultipartFile; @@ -182,20 +182,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService { /** * 文件下载方法,支持一次性下载完整文件 * - * @param ossId OSS对象ID - * @param response HttpServletResponse对象,用于设置响应头和向客户端发送文件内容 + * @param ossId OSS对象ID */ @Override - public void download(Long ossId, HttpServletResponse response) throws IOException { + public ResponseEntity download(Long ossId) { SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId); if (ObjectUtil.isNull(sysOss)) { throw new ServiceException("文件数据不存在!"); } - FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName()); - response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); - OssClient instance = OssFactory.instance(sysOss.getService()); - GetObjectResult result = instance.download(sysOss.getFileName(), response.getOutputStream()); - response.setContentLengthLong(result.size()); + String percentEncodedFileName = FileUtils.percentEncode(sysOss.getOriginalName()); + String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName); + return OssFactory.instance(sysOss.getService()) + .download(sysOss.getFileName(), (result, inputStream) -> { + // 构建响应实体 + return ResponseEntity.ok() + .header("Access-Control-Expose-Headers", "Content-Disposition,download-filename") + .header("Content-disposition", contentDispositionValue) + .header("download-filename", percentEncodedFileName) + .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentLength(result.size()) + .body(IoUtil.readBytes(inputStream)); + }); + } /**