fix oss文件下载

This commit is contained in:
秋辞未寒
2026-03-26 00:19:44 +08:00
parent e00837a26f
commit f45f58c340
6 changed files with 103 additions and 33 deletions

View File

@@ -10,6 +10,7 @@ import org.dromara.common.oss.io.OutputStreamDownloadSubscriber;
import org.dromara.common.oss.model.GetObjectResult; import org.dromara.common.oss.model.GetObjectResult;
import org.dromara.common.oss.model.HandleAsyncResult; import org.dromara.common.oss.model.HandleAsyncResult;
import org.dromara.common.oss.model.PutObjectResult; 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.AsyncRequestBody;
import software.amazon.awssdk.core.async.AsyncResponseTransformer; import software.amazon.awssdk.core.async.AsyncResponseTransformer;
import software.amazon.awssdk.core.async.ResponsePublisher; 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.Files;
import java.nio.file.Path; import java.nio.file.Path;
import java.time.Duration; import java.time.Duration;
import java.time.LocalDateTime; import java.time.ZoneOffset;
import java.util.Collection; import java.util.Collection;
import java.util.Map; import java.util.Map;
import java.util.Objects; import java.util.Objects;
@@ -309,7 +310,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
try { try {
ResponsePublisher<GetObjectResponse> publisher = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toPublisher(), null); ResponsePublisher<GetObjectResponse> publisher = doCustomDownload(builder -> builder.bucket(bucket).key(key), AsyncResponseTransformer.toPublisher(), null);
GetObjectResult getObjectResult = buildGetObjectResult(key, publisher.response()); GetObjectResult getObjectResult = buildGetObjectResult(key, publisher.response());
publisher.subscribe(downloadSubscriber); publisher.subscribe(downloadSubscriber).join();
return getObjectResult; return getObjectResult;
} catch (Exception e) { } catch (Exception e) {
if (e instanceof S3StorageException ex) { if (e instanceof S3StorageException ex) {
@@ -319,6 +320,21 @@ public abstract class AbstractOssClientImpl implements OssClient {
} }
} }
@Override
public <T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
try {
ResponseInputStream<GetObjectResponse> 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 @Override
public GetObjectResult bucketDownload(String bucket, String key, Path path) { public GetObjectResult bucketDownload(String bucket, String key, Path path) {
try (OutputStream out = Files.newOutputStream(path)) { try (OutputStream out = Files.newOutputStream(path)) {
@@ -362,7 +378,7 @@ public abstract class AbstractOssClientImpl implements OssClient {
return GetObjectResult.form( return GetObjectResult.form(
key, key,
response.eTag(), response.eTag(),
LocalDateTime.from(response.lastModified()), response.lastModified().atOffset(ZoneOffset.UTC).toLocalDateTime(),
response.contentLength(), response.contentLength(),
response.contentType(), response.contentType(),
response.contentDisposition(), response.contentDisposition(),
@@ -446,6 +462,11 @@ public abstract class AbstractOssClientImpl implements OssClient {
return bucketDownload(defaultBucket(), key, downloadSubscriber); return bucketDownload(defaultBucket(), key, downloadSubscriber);
} }
@Override
public <T> T download(String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer) {
return bucketDownload(defaultBucket(), key, downloadTransformer);
}
@Override @Override
public GetObjectResult download(String key, Path path) { public GetObjectResult download(String key, Path path) {
return bucketDownload(defaultBucket(), key, path); return bucketDownload(defaultBucket(), key, path);

View File

@@ -43,12 +43,12 @@ public interface OssClient extends AutoCloseable {
/** /**
* S3 存储客户端ID * S3 存储客户端ID
* * <p>
* 用于标识客户端,初始化后不允许更改 * 用于标识客户端,初始化后不允许更改
* *
* @return S3 存储客户端ID * @return S3 存储客户端ID
*/ */
default String clientId(){ default String clientId() {
return IdUtil.fastSimpleUUID(); return IdUtil.fastSimpleUUID();
} }
@@ -219,6 +219,16 @@ public interface OssClient extends AutoCloseable {
*/ */
GetObjectResult bucketDownload(String bucket, String key, OutputStreamDownloadSubscriber downloadSubscriber); GetObjectResult bucketDownload(String bucket, String key, OutputStreamDownloadSubscriber downloadSubscriber);
/**
* 将指定存储桶中的对象下载到转换器中,由使用者决定返回值。
*
* @param bucket 存储桶名称
* @param key 对象键
* @param downloadTransformer 下载转换器
* @return 下载结果
*/
<T> T bucketDownload(String bucket, String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer);
/** /**
* 将指定存储桶中的对象下载到本地路径。 * 将指定存储桶中的对象下载到本地路径。
* *
@@ -364,6 +374,15 @@ public interface OssClient extends AutoCloseable {
*/ */
GetObjectResult download(String key, OutputStreamDownloadSubscriber downloadSubscriber); GetObjectResult download(String key, OutputStreamDownloadSubscriber downloadSubscriber);
/**
* 将指定存储桶中的对象下载到转换器中,由使用者决定返回值。
*
* @param key 对象键
* @param downloadTransformer 下载转换器
* @return 下载结果
*/
<T> T download(String key, BiFunction<GetObjectResult, InputStream, T> downloadTransformer);
/** /**
* 将默认存储桶中的对象下载到本地路径。 * 将默认存储桶中的对象下载到本地路径。
* *

View File

@@ -3,7 +3,6 @@ package org.dromara.common.oss.io;
import org.dromara.common.oss.exception.S3StorageException; import org.dromara.common.oss.exception.S3StorageException;
import java.io.FileOutputStream; import java.io.FileOutputStream;
import java.io.IOException;
import java.io.OutputStream; import java.io.OutputStream;
import java.nio.ByteBuffer; import java.nio.ByteBuffer;
import java.nio.channels.Channels; import java.nio.channels.Channels;
@@ -19,11 +18,15 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
private final WritableByteChannel channel; private final WritableByteChannel channel;
private OutputStreamDownloadSubscriber(WritableByteChannel channel) { private final boolean allowAutoClose;
private OutputStreamDownloadSubscriber(WritableByteChannel channel, boolean allowAutoClose) {
this.channel = channel; this.channel = channel;
this.allowAutoClose = allowAutoClose;
} }
private OutputStreamDownloadSubscriber(OutputStream out) { private OutputStreamDownloadSubscriber(OutputStream out, boolean allowAutoClose) {
this.allowAutoClose = allowAutoClose;
// 创建可写入的字节通道 // 创建可写入的字节通道
if (out instanceof FileOutputStream outputStream) { if (out instanceof FileOutputStream outputStream) {
// 如果是文件输入流,直接获取文件输出流的 Channel // 如果是文件输入流,直接获取文件输出流的 Channel
@@ -35,18 +38,18 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
@Override @Override
public void accept(ByteBuffer byteBuffer) { public void accept(ByteBuffer byteBuffer) {
try (channel) { try {
while (byteBuffer.hasRemaining()) { while (byteBuffer.hasRemaining()) {
channel.write(byteBuffer); channel.write(byteBuffer);
} }
} catch (IOException e) { } catch (Exception e) {
throw S3StorageException.form(e); throw S3StorageException.form(e);
} }
} }
@Override @Override
public void close() throws Exception { public void close() throws Exception {
if (channel.isOpen()) { if (channel.isOpen() && allowAutoClose) {
channel.close(); channel.close();
} }
} }
@@ -58,7 +61,18 @@ public class OutputStreamDownloadSubscriber implements Consumer<ByteBuffer>, Aut
* @return 输出流下载订阅器 * @return 输出流下载订阅器
*/ */
public static OutputStreamDownloadSubscriber create(OutputStream out) { 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<ByteBuffer>, Aut
* @return 输出流下载订阅器 * @return 输出流下载订阅器
*/ */
public static OutputStreamDownloadSubscriber create(WritableByteChannel channel) { 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);
} }
} }

View File

@@ -2,7 +2,6 @@ package org.dromara.system.controller.system;
import cn.dev33.satoken.annotation.SaCheckPermission; import cn.dev33.satoken.annotation.SaCheckPermission;
import jakarta.servlet.http.HttpServletResponse;
import jakarta.validation.constraints.NotEmpty; import jakarta.validation.constraints.NotEmpty;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.domain.PageResult; 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.domain.vo.SysOssVo;
import org.dromara.system.service.ISysOssService; import org.dromara.system.service.ISysOssService;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.validation.annotation.Validated; import org.springframework.validation.annotation.Validated;
import org.springframework.web.bind.annotation.*; import org.springframework.web.bind.annotation.*;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -83,13 +83,12 @@ public class SysOssController extends BaseController {
* 下载OSS对象 * 下载OSS对象
* *
* @param ossId OSS对象ID * @param ossId OSS对象ID
* @param response HTTP 响应
* @throws IOException IO 异常 * @throws IOException IO 异常
*/ */
@SaCheckPermission("system:oss:download") @SaCheckPermission("system:oss:download")
@GetMapping("/download/{ossId}") @GetMapping("/download/{ossId}")
public void download(@PathVariable Long ossId, HttpServletResponse response) throws IOException { public ResponseEntity<byte[]> download(@PathVariable Long ossId) throws IOException {
ossService.download(ossId, response); return ossService.download(ossId);
} }
/** /**

View File

@@ -1,14 +1,13 @@
package org.dromara.system.service; package org.dromara.system.service;
import jakarta.servlet.http.HttpServletResponse;
import org.dromara.common.core.domain.PageResult; import org.dromara.common.core.domain.PageResult;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.system.domain.bo.SysOssBo; import org.dromara.system.domain.bo.SysOssBo;
import org.dromara.system.domain.vo.SysOssVo; import org.dromara.system.domain.vo.SysOssVo;
import org.springframework.http.ResponseEntity;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
import java.io.File; import java.io.File;
import java.io.IOException;
import java.util.Collection; import java.util.Collection;
import java.util.List; import java.util.List;
@@ -64,9 +63,8 @@ public interface ISysOssService {
* 文件下载方法,支持一次性下载完整文件 * 文件下载方法,支持一次性下载完整文件
* *
* @param ossId OSS对象ID * @param ossId OSS对象ID
* @param response HttpServletResponse对象用于设置响应头和向客户端发送文件内容
*/ */
void download(Long ossId, HttpServletResponse response) throws IOException; ResponseEntity<byte[]> download(Long ossId);
/** /**
* 删除OSS对象存储 * 删除OSS对象存储

View File

@@ -2,11 +2,11 @@ package org.dromara.system.service.impl;
import cn.hutool.core.bean.BeanUtil; import cn.hutool.core.bean.BeanUtil;
import cn.hutool.core.convert.Convert; import cn.hutool.core.convert.Convert;
import cn.hutool.core.io.IoUtil;
import cn.hutool.core.util.ObjectUtil; import cn.hutool.core.util.ObjectUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import jakarta.servlet.http.HttpServletResponse;
import lombok.RequiredArgsConstructor; import lombok.RequiredArgsConstructor;
import org.dromara.common.core.constant.CacheNames; import org.dromara.common.core.constant.CacheNames;
import org.dromara.common.core.domain.PageResult; 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.json.utils.JsonUtils;
import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.mybatis.core.page.PageQuery;
import org.dromara.common.oss.client.OssClient; 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.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.common.oss.util.S3ObjectUtil;
import org.dromara.system.domain.SysOss; import org.dromara.system.domain.SysOss;
import org.dromara.system.domain.SysOssExt; import org.dromara.system.domain.SysOssExt;
@@ -35,6 +34,7 @@ import org.dromara.system.service.ISysOssService;
import org.jetbrains.annotations.NotNull; import org.jetbrains.annotations.NotNull;
import org.springframework.cache.annotation.Cacheable; import org.springframework.cache.annotation.Cacheable;
import org.springframework.http.MediaType; import org.springframework.http.MediaType;
import org.springframework.http.ResponseEntity;
import org.springframework.stereotype.Service; import org.springframework.stereotype.Service;
import org.springframework.web.multipart.MultipartFile; import org.springframework.web.multipart.MultipartFile;
@@ -182,20 +182,28 @@ public class SysOssServiceImpl implements ISysOssService, OssService {
/** /**
* 文件下载方法,支持一次性下载完整文件 * 文件下载方法,支持一次性下载完整文件
* *
* @param ossId OSS对象ID * @param ossId OSS对象ID
* @param response HttpServletResponse对象用于设置响应头和向客户端发送文件内容
*/ */
@Override @Override
public void download(Long ossId, HttpServletResponse response) throws IOException { public ResponseEntity<byte[]> download(Long ossId) {
SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId); SysOssVo sysOss = SpringUtils.getAopProxy(this).getById(ossId);
if (ObjectUtil.isNull(sysOss)) { if (ObjectUtil.isNull(sysOss)) {
throw new ServiceException("文件数据不存在!"); throw new ServiceException("文件数据不存在!");
} }
FileUtils.setAttachmentResponseHeader(response, sysOss.getOriginalName()); String percentEncodedFileName = FileUtils.percentEncode(sysOss.getOriginalName());
response.setContentType(MediaType.APPLICATION_OCTET_STREAM_VALUE + "; charset=UTF-8"); String contentDispositionValue = "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName);
OssClient instance = OssFactory.instance(sysOss.getService()); return OssFactory.instance(sysOss.getService())
GetObjectResult result = instance.download(sysOss.getFileName(), response.getOutputStream()); .download(sysOss.getFileName(), (result, inputStream) -> {
response.setContentLengthLong(result.size()); // 构建响应实体
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));
});
} }
/** /**