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 249661b8f..b7c5f7a64 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 @@ -2,14 +2,15 @@ package org.dromara.common.oss.client; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.IdUtil; -import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.utils.StringUtils; import org.dromara.common.oss.config.OssClientConfig; import org.dromara.common.oss.exception.S3StorageException; 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.Options; import org.dromara.common.oss.model.PutObjectResult; +import org.jspecify.annotations.NullMarked; import software.amazon.awssdk.core.ResponseInputStream; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; @@ -24,8 +25,8 @@ import software.amazon.awssdk.transfer.s3.progress.TransferListener; import java.io.*; import java.nio.channels.Channels; -import java.nio.channels.FileChannel; import java.nio.channels.ReadableByteChannel; +import java.nio.channels.SeekableByteChannel; import java.nio.channels.WritableByteChannel; import java.nio.file.Files; import java.nio.file.Path; @@ -46,7 +47,6 @@ import java.util.function.Function; * * @author 秋辞未寒 */ -@Slf4j public abstract class AbstractOssClientImpl implements OssClient { private final AtomicBoolean initialized = new AtomicBoolean(false); @@ -204,20 +204,32 @@ public abstract class AbstractOssClientImpl implements OssClient { } @Override - public PutObjectResult bucketUpload(String bucket, String key, Path path) { + public PutObjectResult bucketUpload(String bucket, String key, Path path, Options options) { AsyncRequestBody body = AsyncRequestBody.fromFile(path); - return bucketUpload(bucket, key, body); + return bucketUpload(bucket, key, body, options); + } + + @Override + public PutObjectResult bucketUpload(String bucket, String key, Path path) { + return bucketUpload(bucket, key, path, Options.builder()); + } + + @Override + public PutObjectResult bucketUpload(String bucket, String key, File file, Options options) { + AsyncRequestBody body = AsyncRequestBody.fromFile(file); + return bucketUpload(bucket, key, body, options); } @Override public PutObjectResult bucketUpload(String bucket, String key, File file) { - AsyncRequestBody body = AsyncRequestBody.fromFile(file); - return bucketUpload(bucket, key, body); + return bucketUpload(bucket, key, file, Options.builder()); } @Override - public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file) { + public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file, Options options) { try { + // 以文件的大小为准 + options.setLength(file.length()); return bucketUpload(bucket, key, file.getChannel(), -1L); } catch (Exception e) { if (e instanceof S3StorageException ex) { @@ -228,13 +240,21 @@ public abstract class AbstractOssClientImpl implements OssClient { } @Override - public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength) { - long size = contentLength; - try (channel; InputStream in = Channels.newInputStream(channel)) { - if (channel instanceof FileChannel fileChannel) { - size = fileChannel.size(); + public PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file) { + return bucketUpload(bucket, key, file, Options.builder()); + } + + @Override + public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength, Options options) { + // 让调用者自行处理通道的关闭 + InputStream in = Channels.newInputStream(channel); + try { + // 如果可以实时获取文件大小,则优先是有实时获取的 + long size = contentLength; + if (channel instanceof SeekableByteChannel byteChannel) { + size = byteChannel.size(); } - return bucketUpload(bucket, key, in, size); + return bucketUpload(bucket, key, in, size, options); } catch (Exception e) { if (e instanceof S3StorageException ex) { throw ex; @@ -243,16 +263,27 @@ public abstract class AbstractOssClientImpl implements OssClient { } } + @Override + public PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength) { + return bucketUpload(bucket, key, channel, contentLength, Options.builder()); + } + + @Override + public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength, Options options) { + options.setLength(contentLength); + AsyncRequestBody body = AsyncRequestBody.fromInputStream(in, contentLength, asyncExecutor); + return bucketUpload(bucket, key, body, options); + } + @Override public PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength) { - AsyncRequestBody body = AsyncRequestBody.fromInputStream(in, contentLength, asyncExecutor); - return bucketUpload(bucket, key, body); + return bucketUpload(bucket, key, in, contentLength, Options.builder()); } @Override - public PutObjectResult bucketUpload(String bucket, String key, byte[] data) { + public PutObjectResult bucketUpload(String bucket, String key, byte[] data, Options options) { try (ByteArrayInputStream in = new ByteArrayInputStream(data)) { - return bucketUpload(bucket, key, in, data.length); + return bucketUpload(bucket, key, in, data.length, options); } catch (Exception e) { if (e instanceof S3StorageException ex) { throw ex; @@ -261,15 +292,28 @@ public abstract class AbstractOssClientImpl implements OssClient { } } + @Override + public PutObjectResult bucketUpload(String bucket, String key, byte[] data) { + return bucketUpload(bucket, key, data, Options.builder()); + } - private PutObjectResult bucketUpload(String bucket, String key, AsyncRequestBody body) { - Long contentLength = body.contentLength().orElse(null); + @NullMarked + private PutObjectResult bucketUpload(String bucket, String key, AsyncRequestBody body, Options options) { + // 优先使用body中的内容大小,如果不存在,再获取可选项中的 + Long contentLength = body.contentLength().orElse(options.getLength()); + // 优先使用body中的内容类型,如果不存在,再获取可选项中的 + String contentType = StringUtils.isBlank(options.getContentType()) ? body.contentType() : options.getContentType(); + String md5Digest = options.getMd5Digest(); + Map metadata = options.getMetadata(); + Collection transferListeners = options.getTransferListeners(); HandleAsyncResult result = doCustomUpload(body, builder -> { builder.bucket(bucket) .key(key) + .contentMD5(md5Digest) + .contentType(contentType) .contentLength(contentLength) - ; - }); + .metadata(metadata); + }, transferListeners); if (result.isFailure()) { throw S3StorageException.form(result.error()); } @@ -278,11 +322,13 @@ public abstract class AbstractOssClientImpl implements OssClient { throw S3StorageException.form("response is empty."); } PutObjectResponse response = opt.get(); - String bucketUrl = config.getBucketUrl(bucket); // 不知道什么原因导致 response.size() 返回了一个 null size ,此处做一个适配... Long size = response.size(); - size = size == null ? contentLength : size; - return PutObjectResult.form("%s/%s".formatted(bucketUrl, key), key, response.eTag(), size == null ? 0 : size); + if (size == null) { + size = contentLength == null ? 0 : contentLength; + } + String bucketUrl = config.getBucketUrl(bucket); + return PutObjectResult.form("%s/%s".formatted(bucketUrl, key), key, response.eTag(), size); } @Override @@ -427,31 +473,61 @@ public abstract class AbstractOssClientImpl implements OssClient { } } + @Override + public PutObjectResult upload(String key, Path path, Options options) { + return bucketUpload(defaultBucket(), key, path, options); + } + @Override public PutObjectResult upload(String key, Path path) { return bucketUpload(defaultBucket(), key, path); } + @Override + public PutObjectResult upload(String key, File file, Options options) { + return bucketUpload(defaultBucket(), key, file, options); + } + @Override public PutObjectResult upload(String key, File file) { return bucketUpload(defaultBucket(), key, file); } + @Override + public PutObjectResult upload(String key, RandomAccessFile file, Options options) { + return bucketUpload(defaultBucket(), key, file, options); + } + @Override public PutObjectResult upload(String key, RandomAccessFile file) { return bucketUpload(defaultBucket(), key, file); } + @Override + public PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength, Options options) { + return bucketUpload(defaultBucket(), key, channel, contentLength, options); + } + @Override public PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength) { return bucketUpload(defaultBucket(), key, channel, contentLength); } + @Override + public PutObjectResult upload(String key, InputStream in, long contentLength, Options options) { + return bucketUpload(defaultBucket(), key, in, contentLength, options); + } + @Override public PutObjectResult upload(String key, InputStream in, long contentLength) { return bucketUpload(defaultBucket(), key, in, contentLength); } + @Override + public PutObjectResult upload(String key, byte[] data, Options options) { + return bucketUpload(defaultBucket(), key, data, options); + } + @Override public PutObjectResult upload(String key, byte[] data) { return bucketUpload(defaultBucket(), key, data); 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 23b2eee9b..4281cfa3b 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 @@ -5,6 +5,7 @@ import org.dromara.common.oss.config.OssClientConfig; 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.Options; import org.dromara.common.oss.model.PutObjectResult; import software.amazon.awssdk.core.async.AsyncRequestBody; import software.amazon.awssdk.core.async.AsyncResponseTransformer; @@ -136,6 +137,17 @@ public interface OssClient extends AutoCloseable { */ HandleAsyncResult doCustomUpload(AsyncRequestBody body, Consumer putObjectRequestBuilderConsumer); + /** + * 将本地路径对应的文件上传到指定存储桶。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param path 文件路径 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult bucketUpload(String bucket, String key, Path path, Options options); + /** * 将本地路径对应的文件上传到指定存储桶。 * @@ -146,6 +158,17 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult bucketUpload(String bucket, String key, Path path); + /** + * 将文件上传到指定存储桶。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param file 文件对象 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult bucketUpload(String bucket, String key, File file, Options options); + /** * 将文件上传到指定存储桶。 * @@ -156,6 +179,17 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult bucketUpload(String bucket, String key, File file); + /** + * 将随机访问文件上传到指定存储桶。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param file 随机访问文件 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file, Options options); + /** * 将随机访问文件上传到指定存储桶。 * @@ -166,6 +200,18 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult bucketUpload(String bucket, String key, RandomAccessFile file); + /** + * 将可读通道中的数据上传到指定存储桶。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param channel 数据通道 + * @param contentLength 内容长度 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength, Options options); + /** * 将可读通道中的数据上传到指定存储桶。 * @@ -177,6 +223,18 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult bucketUpload(String bucket, String key, ReadableByteChannel channel, long contentLength); + /** + * 将可读通道中的数据上传到指定存储桶。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param in 输入流 + * @param contentLength 内容长度 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength, Options options); + /** * 将输入流中的数据上传到指定存储桶。 * @@ -188,6 +246,17 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult bucketUpload(String bucket, String key, InputStream in, long contentLength); + /** + * 将字节数组上传到指定存储桶。 + * + * @param bucket 存储桶名称 + * @param key 对象键 + * @param data 字节数组 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult bucketUpload(String bucket, String key, byte[] data, Options options); + /** * 将字节数组上传到指定存储桶。 * @@ -309,6 +378,16 @@ public interface OssClient extends AutoCloseable { */ String bucketPresignPutUrl(String bucket, String key, Duration expiredTime, Map metadata); + /** + * 将本地路径对应的文件上传到默认存储桶。 + * + * @param key 对象键 + * @param path 文件路径 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult upload(String key, Path path, Options options); + /** * 将本地路径对应的文件上传到默认存储桶。 * @@ -318,6 +397,16 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult upload(String key, Path path); + /** + * 将文件上传到默认存储桶。 + * + * @param key 对象键 + * @param file 文件对象 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult upload(String key, File file, Options options); + /** * 将文件上传到默认存储桶。 * @@ -327,6 +416,16 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult upload(String key, File file); + /** + * 将随机访问文件上传到默认存储桶。 + * + * @param key 对象键 + * @param file 随机访问文件 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult upload(String key, RandomAccessFile file, Options options); + /** * 将随机访问文件上传到默认存储桶。 * @@ -336,6 +435,17 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult upload(String key, RandomAccessFile file); + /** + * 将可读通道中的数据上传到默认存储桶。 + * + * @param key 对象键 + * @param channel 数据通道 + * @param contentLength 内容长度 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength, Options options); + /** * 将可读通道中的数据上传到默认存储桶。 * @@ -346,6 +456,17 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult upload(String key, ReadableByteChannel channel, long contentLength); + /** + * 将输入流中的数据上传到默认存储桶。 + * + * @param key 对象键 + * @param in 输入流 + * @param contentLength 内容长度 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult upload(String key, InputStream in, long contentLength, Options options); + /** * 将输入流中的数据上传到默认存储桶。 * @@ -356,6 +477,16 @@ public interface OssClient extends AutoCloseable { */ PutObjectResult upload(String key, InputStream in, long contentLength); + /** + * 将字节数组上传到默认存储桶。 + * + * @param key 对象键 + * @param data 字节数组 + * @param options 可选项 + * @return 上传结果 + */ + PutObjectResult upload(String key, byte[] data, Options options); + /** * 将字节数组上传到默认存储桶。 * diff --git a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java index c2a85a3b0..45a1bf04a 100644 --- a/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java +++ b/ruoyi-common/ruoyi-common-oss/src/main/java/org/dromara/common/oss/factory/OssFactory.java @@ -66,7 +66,7 @@ public class OssFactory { CLIENT_CACHE.put(configKey, client); return client; } finally { - LOCK.lock(); + LOCK.unlock(); } } 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 9b0da5efe..85ce1ce64 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 @@ -8,6 +8,7 @@ import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper; import com.baomidou.mybatisplus.core.toolkit.Wrappers; import com.baomidou.mybatisplus.extension.plugins.pagination.Page; import lombok.RequiredArgsConstructor; +import lombok.extern.slf4j.Slf4j; import org.dromara.common.core.constant.CacheNames; import org.dromara.common.core.domain.PageResult; import org.dromara.common.core.domain.dto.OssDTO; @@ -23,6 +24,7 @@ import org.dromara.common.mybatis.core.page.PageQuery; import org.dromara.common.oss.client.OssClient; import org.dromara.common.oss.enums.AccessPolicy; import org.dromara.common.oss.factory.OssFactory; +import org.dromara.common.oss.model.Options; import org.dromara.common.oss.model.PutObjectResult; import org.dromara.common.oss.util.S3ObjectUtil; import org.dromara.system.domain.SysOss; @@ -33,6 +35,7 @@ import org.dromara.system.mapper.SysOssMapper; import org.dromara.system.service.ISysOssService; import org.jetbrains.annotations.NotNull; import org.springframework.cache.annotation.Cacheable; +import org.springframework.http.HttpHeaders; import org.springframework.http.MediaType; import org.springframework.http.ResponseEntity; import org.springframework.stereotype.Service; @@ -40,6 +43,7 @@ import org.springframework.web.multipart.MultipartFile; import java.io.File; import java.io.IOException; +import java.io.InputStream; import java.time.Duration; import java.util.ArrayList; import java.util.Collection; @@ -51,6 +55,7 @@ import java.util.Map; * * @author Lion Li */ +@Slf4j @RequiredArgsConstructor @Service public class SysOssServiceImpl implements ISysOssService, OssService { @@ -191,15 +196,21 @@ public class SysOssServiceImpl implements ISysOssService, OssService { throw new ServiceException("文件数据不存在!"); } 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) -> { + // 尝试解析媒体类型,如果解析失败,则使用 application/octet-stream + MediaType mediaType; + try { + mediaType = MediaType.parseMediaType(result.contentType()); + } catch (Exception e) { + mediaType = MediaType.APPLICATION_OCTET_STREAM; + } // 构建响应实体 return ResponseEntity.ok() - .header("Access-Control-Expose-Headers", "Content-Disposition,download-filename") - .header("Content-disposition", contentDispositionValue) + .header(HttpHeaders.ACCESS_CONTROL_EXPOSE_HEADERS, "Content-Disposition,download-filename") + .header(HttpHeaders.CONTENT_DISPOSITION, "attachment; filename=%s;filename*=utf-8''%s".formatted(percentEncodedFileName, percentEncodedFileName)) .header("download-filename", percentEncodedFileName) - .contentType(MediaType.APPLICATION_OCTET_STREAM) + .contentType(mediaType) .contentLength(result.size()) .body(IoUtil.readBytes(inputStream)); }); @@ -223,7 +234,9 @@ public class SysOssServiceImpl implements ISysOssService, OssService { OssClient instance = OssFactory.instance(); try { String pathKey = S3ObjectUtil.buildPathKey(originalfileName); - PutObjectResult result = instance.upload(pathKey, file.getInputStream(), file.getSize()); + InputStream inputStream = file.getInputStream(); + PutObjectResult result = instance.upload(pathKey, inputStream, file.getSize(), Options.builder().setContentType(file.getContentType())); + IoUtil.close(inputStream); SysOssExt ext1 = new SysOssExt(); ext1.setFileSize(file.getSize()); ext1.setContentType(file.getContentType()); @@ -249,7 +262,7 @@ public class SysOssServiceImpl implements ISysOssService, OssService { String suffix = StringUtils.substring(originalfileName, originalfileName.lastIndexOf("."), originalfileName.length()); OssClient instance = OssFactory.instance(); String pathKey = S3ObjectUtil.buildPathKey(originalfileName); - PutObjectResult result = instance.upload(pathKey, file); + PutObjectResult result = instance.upload(pathKey, file, Options.builder().setContentType(FileUtils.getMimeType(file.toPath()))); SysOssExt ext1 = new SysOssExt(); ext1.setFileSize(result.size()); // 保存文件信息