mirror of
https://github.com/continew-org/continew-starter.git
synced 2025-09-08 16:57:09 +08:00
feat(storage): 新增 S3 存储模块,重构本地存储
This commit is contained in:
@@ -70,6 +70,11 @@
|
||||
<ttl.version>2.14.5</ttl.version>
|
||||
<ip2region.version>3.2.12</ip2region.version>
|
||||
<hutool.version>5.8.34</hutool.version>
|
||||
<!--对象存储版本-->
|
||||
<s3.version>2.29.23</s3.version>
|
||||
<s3-crt.version>0.33.5</s3-crt.version>
|
||||
<!--缩略图处理版本-->
|
||||
<thumbnails.version>0.4.20</thumbnails.version>
|
||||
<!-- Maven Plugin Versions -->
|
||||
<flatten.version>1.6.0</flatten.version>
|
||||
<spotless.version>2.43.0</spotless.version>
|
||||
@@ -253,18 +258,32 @@
|
||||
<version>${easy-excel.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- X File Storage(一行代码将文件存储到本地、FTP、SFTP、WebDAV、阿里云 OSS、华为云 OBS...等其它兼容 S3 协议的存储平台) -->
|
||||
<!-- S3 for Java 2.x -->
|
||||
<dependency>
|
||||
<groupId>org.dromara.x-file-storage</groupId>
|
||||
<artifactId>x-file-storage-spring</artifactId>
|
||||
<version>${x-file-storage.version}</version>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<version>${s3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Amazon S3(Amazon Simple Storage Service,亚马逊简单存储服务,通用存储协议 S3,兼容主流云厂商对象存储) -->
|
||||
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
|
||||
<dependency>
|
||||
<groupId>com.amazonaws</groupId>
|
||||
<artifactId>aws-java-sdk-s3</artifactId>
|
||||
<version>${aws-s3.version}</version>
|
||||
<groupId>software.amazon.awssdk.crt</groupId>
|
||||
<artifactId>aws-crt</artifactId>
|
||||
<version>${s3-crt.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3-transfer-manager</artifactId>
|
||||
<version>${s3.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!--图片处理工具-主要用做图片缩略处理-->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
<version>${thumbnails.version}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- Graceful Response(一个Spring Boot技术栈下的优雅响应处理组件,可以帮助开发者完成响应数据封装、异常处理、错误码填充等过程,提高开发效率,提高代码质量) -->
|
||||
@@ -476,6 +495,13 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 存储模块 - 核心模块 -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-storage-core</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 存储模块 - 本地存储 -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
@@ -483,6 +509,13 @@
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 存储模块 - 对象存储 -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-storage-oss</artifactId>
|
||||
<version>${revision}</version>
|
||||
</dependency>
|
||||
|
||||
<!-- 日志模块 - 基于拦截器实现(Spring Boot Actuator HttpTrace 增强版) -->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
|
@@ -0,0 +1,30 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-storage</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-starter-storage-core</artifactId>
|
||||
<description>ContiNew Starter 存储模块 - 核心模块</description>
|
||||
|
||||
<dependencies>
|
||||
|
||||
<!--redisson 缓存模块-->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-cache-redisson</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--图片处理工具-主要用做图片缩略处理-->
|
||||
<dependency>
|
||||
<groupId>net.coobird</groupId>
|
||||
<artifactId>thumbnailator</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
|
||||
|
||||
</project>
|
@@ -0,0 +1,42 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.constant;
|
||||
|
||||
/**
|
||||
* 存储 常量
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16 19:09
|
||||
*/
|
||||
public class StorageConstant {
|
||||
|
||||
/**
|
||||
* 默认存储 Key
|
||||
*/
|
||||
public static final String DEFAULT_KEY = "storage:default_config";
|
||||
|
||||
/**
|
||||
* 云服务商 域名前缀
|
||||
* <p>目前只支持 阿里云-oss 华为云-obs 腾讯云-cos</p>
|
||||
*/
|
||||
public static final String[] CLOUD_SERVICE_PREFIX = new String[] {"oss", "cos", "obs"};
|
||||
|
||||
/**
|
||||
* 缩略图后缀
|
||||
*/
|
||||
public static final String SMALL_SUFFIX = "small";
|
||||
}
|
@@ -0,0 +1,35 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.dao;
|
||||
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
|
||||
/**
|
||||
* 存储记录持久层接口
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/17 16:49
|
||||
*/
|
||||
public interface StorageDao {
|
||||
|
||||
/**
|
||||
* 记录上传信息
|
||||
*
|
||||
* @param uploadResp 上传信息
|
||||
*/
|
||||
void add(UploadResp uploadResp);
|
||||
}
|
@@ -0,0 +1,33 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.dao.impl;
|
||||
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
|
||||
/**
|
||||
* 默认记录实现,此类并不能真正保存记录,只是用来脱离数据库运行,保证文件上传功能可以正常使用
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/18 08:48
|
||||
**/
|
||||
public class StorageDaoDefaultImpl implements StorageDao {
|
||||
@Override
|
||||
public void add(UploadResp uploadResp) {
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,106 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.decorator;
|
||||
|
||||
import top.continew.starter.storage.model.resp.ThumbnailResp;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
import top.continew.starter.storage.strategy.StorageStrategy;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 装饰器基类 - 用于重写
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/30 19:33
|
||||
*/
|
||||
public abstract class AbstractStorageDecorator<C> implements StorageStrategy<C> {
|
||||
|
||||
protected StorageStrategy<C> delegate;
|
||||
|
||||
protected AbstractStorageDecorator(StorageStrategy<C> delegate) {
|
||||
this.delegate = delegate;
|
||||
}
|
||||
|
||||
@Override
|
||||
public C getClient() {
|
||||
return delegate.getClient();
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean bucketExists(String bucketName) {
|
||||
return delegate.bucketExists(bucketName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createBucket(String bucketName) {
|
||||
delegate.createBucket(bucketName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String fileName, InputStream inputStream, String fileType) {
|
||||
return delegate.upload(fileName, inputStream, fileType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail) {
|
||||
return delegate.upload(fileName, path, inputStream, fileType, isThumbnail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail) {
|
||||
return delegate.upload(bucketName, fileName, path, inputStream, fileType, isThumbnail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType) {
|
||||
delegate.upload(bucketName, fileName, path, inputStream, fileType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThumbnailResp uploadThumbnail(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType) {
|
||||
return delegate.uploadThumbnail(bucketName, fileName, path, inputStream, fileType);
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream download(String bucketName, String fileName) {
|
||||
return delegate.download(bucketName, fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String bucketName, String fileName) {
|
||||
delegate.delete(bucketName, fileName);
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageBase64(String bucketName, String fileName) {
|
||||
return delegate.getImageBase64(bucketName, fileName);
|
||||
}
|
||||
}
|
@@ -0,0 +1,102 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.enums;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import top.continew.starter.core.enums.BaseEnum;
|
||||
|
||||
import java.util.Arrays;
|
||||
import java.util.Collections;
|
||||
import java.util.List;
|
||||
|
||||
/**
|
||||
* 文件类型枚举
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 2023/12/23 13:38
|
||||
*/
|
||||
public enum FileTypeEnum implements BaseEnum<Integer> {
|
||||
|
||||
/**
|
||||
* 其他
|
||||
*/
|
||||
UNKNOWN(1, "其他", Collections.emptyList()),
|
||||
|
||||
/**
|
||||
* 图片
|
||||
*/
|
||||
IMAGE(2, "图片", List
|
||||
.of("jpg", "jpeg", "png", "gif", "bmp", "webp", "ico", "psd", "tiff", "dwg", "jxr", "apng", "xcf")),
|
||||
|
||||
/**
|
||||
* 文档
|
||||
*/
|
||||
DOC(3, "文档", List.of("txt", "pdf", "doc", "xls", "ppt", "docx", "xlsx", "pptx")),
|
||||
|
||||
/**
|
||||
* 视频
|
||||
*/
|
||||
VIDEO(4, "视频", List.of("mp4", "avi", "mkv", "flv", "webm", "wmv", "m4v", "mov", "mpg", "rmvb", "3gp")),
|
||||
|
||||
/**
|
||||
* 音频
|
||||
*/
|
||||
AUDIO(5, "音频", List.of("mp3", "flac", "wav", "ogg", "midi", "m4a", "aac", "amr", "ac3", "aiff")),;
|
||||
|
||||
private final Integer value;
|
||||
private final String description;
|
||||
private final List<String> extensions;
|
||||
|
||||
/**
|
||||
* 根据扩展名查询
|
||||
*
|
||||
* @param extension 扩展名
|
||||
* @return 文件类型
|
||||
*/
|
||||
public static FileTypeEnum getByExtension(String extension) {
|
||||
return Arrays.stream(FileTypeEnum.values())
|
||||
.filter(t -> t.getExtensions().contains(StrUtil.emptyIfNull(extension).toLowerCase()))
|
||||
.findFirst()
|
||||
.orElse(FileTypeEnum.UNKNOWN);
|
||||
}
|
||||
|
||||
FileTypeEnum(Integer value, String description, List<String> extensions) {
|
||||
this.value = value;
|
||||
this.description = description;
|
||||
this.extensions = extensions;
|
||||
}
|
||||
|
||||
public List<String> getExtensions() {
|
||||
return this.extensions;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Integer getValue() {
|
||||
return this.value;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getDescription() {
|
||||
return this.description;
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getColor() {
|
||||
return BaseEnum.super.getColor();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,80 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.manger;
|
||||
|
||||
import top.continew.starter.cache.redisson.util.RedisUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
import top.continew.starter.storage.constant.StorageConstant;
|
||||
import top.continew.starter.storage.strategy.StorageStrategy;
|
||||
|
||||
import java.util.Map;
|
||||
import java.util.concurrent.ConcurrentHashMap;
|
||||
|
||||
/**
|
||||
* 存储策略管理器
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16
|
||||
*/
|
||||
public class StorageManager {
|
||||
|
||||
/**
|
||||
* 存储策略连接信息
|
||||
*/
|
||||
private static final Map<String, StorageStrategy<?>> STORAGE_STRATEGY = new ConcurrentHashMap<>();
|
||||
|
||||
/**
|
||||
* 加载存储策略
|
||||
*
|
||||
* @param code 存储码
|
||||
* @param strategy 对应存储策略
|
||||
*/
|
||||
public static void load(String code, StorageStrategy<?> strategy) {
|
||||
STORAGE_STRATEGY.put(code, strategy);
|
||||
}
|
||||
|
||||
/**
|
||||
* 卸载存储策略
|
||||
*
|
||||
* @param code 存储码
|
||||
*/
|
||||
public static void unload(String code) {
|
||||
STORAGE_STRATEGY.remove(code);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 存储 code 获取对应存储策略
|
||||
*
|
||||
* @param code 代码
|
||||
* @return {@link StorageStrategy }
|
||||
*/
|
||||
public static StorageStrategy<?> instance(String code) {
|
||||
StorageStrategy<?> strategy = STORAGE_STRATEGY.get(code);
|
||||
ValidationUtils.throwIfEmpty(strategy, "未找到存储配置:" + code);
|
||||
return strategy;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取默认存储策略
|
||||
*
|
||||
* @return {@link StorageStrategy }
|
||||
*/
|
||||
public static StorageStrategy<?> instance() {
|
||||
return instance(RedisUtils.get(StorageConstant.DEFAULT_KEY));
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,152 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.model.req;
|
||||
|
||||
/**
|
||||
* 存储配置信息
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/11/04 15:13
|
||||
**/
|
||||
public class StorageProperties {
|
||||
|
||||
/**
|
||||
* 编码
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 访问密钥
|
||||
*/
|
||||
private String accessKey;
|
||||
|
||||
/**
|
||||
* 私有密钥
|
||||
*/
|
||||
private String secretKey;
|
||||
|
||||
/**
|
||||
* 终端节点
|
||||
*/
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* 桶名称
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 域名
|
||||
*/
|
||||
private String domain;
|
||||
|
||||
/**
|
||||
* 作用域
|
||||
*/
|
||||
private String region;
|
||||
|
||||
/**
|
||||
* 是否是默认存储
|
||||
*/
|
||||
private Boolean isDefault;
|
||||
|
||||
public StorageProperties() {
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getAccessKey() {
|
||||
return accessKey;
|
||||
}
|
||||
|
||||
public void setAccessKey(String accessKey) {
|
||||
this.accessKey = accessKey;
|
||||
}
|
||||
|
||||
public String getSecretKey() {
|
||||
return secretKey;
|
||||
}
|
||||
|
||||
public void setSecretKey(String secretKey) {
|
||||
this.secretKey = secretKey;
|
||||
}
|
||||
|
||||
public String getEndpoint() {
|
||||
return endpoint;
|
||||
}
|
||||
|
||||
public void setEndpoint(String endpoint) {
|
||||
this.endpoint = endpoint;
|
||||
}
|
||||
|
||||
public String getBucketName() {
|
||||
return bucketName;
|
||||
}
|
||||
|
||||
public void setBucketName(String bucketName) {
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
public String getDomain() {
|
||||
return domain;
|
||||
}
|
||||
|
||||
public void setDomain(String domain) {
|
||||
this.domain = domain;
|
||||
}
|
||||
|
||||
public String getRegion() {
|
||||
return region;
|
||||
}
|
||||
|
||||
public void setRegion(String region) {
|
||||
this.region = region;
|
||||
}
|
||||
|
||||
public Boolean getIsDefault() {
|
||||
return isDefault;
|
||||
}
|
||||
|
||||
public void setIsDefault(Boolean isDefault) {
|
||||
this.isDefault = isDefault;
|
||||
}
|
||||
|
||||
public StorageProperties(String code,
|
||||
String accessKey,
|
||||
String secretKey,
|
||||
String endpoint,
|
||||
String bucketName,
|
||||
String domain,
|
||||
String region,
|
||||
Boolean isDefault) {
|
||||
this.code = code;
|
||||
this.accessKey = accessKey;
|
||||
this.secretKey = secretKey;
|
||||
this.endpoint = endpoint;
|
||||
this.bucketName = bucketName;
|
||||
this.domain = domain;
|
||||
this.region = region;
|
||||
this.isDefault = isDefault;
|
||||
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.model.resp;
|
||||
|
||||
/**
|
||||
* 缩略图
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/20 17:00
|
||||
*/
|
||||
public class ThumbnailResp {
|
||||
|
||||
/**
|
||||
* 缩略图大小(字节)
|
||||
*/
|
||||
private Long thumbnailSize;
|
||||
|
||||
/**
|
||||
* 缩略图地址 格式 xxx/xxx/xxx.small.jpg
|
||||
*/
|
||||
private String thumbnailPath;
|
||||
|
||||
public ThumbnailResp() {
|
||||
}
|
||||
|
||||
public ThumbnailResp(Long thumbnailSize, String thumbnailPath) {
|
||||
this.thumbnailSize = thumbnailSize;
|
||||
this.thumbnailPath = thumbnailPath;
|
||||
}
|
||||
|
||||
public Long getThumbnailSize() {
|
||||
return thumbnailSize;
|
||||
}
|
||||
|
||||
public void setThumbnailSize(Long thumbnailSize) {
|
||||
this.thumbnailSize = thumbnailSize;
|
||||
}
|
||||
|
||||
public String getThumbnailPath() {
|
||||
return thumbnailPath;
|
||||
}
|
||||
|
||||
public void setThumbnailPath(String thumbnailPath) {
|
||||
this.thumbnailPath = thumbnailPath;
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,215 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.model.resp;
|
||||
|
||||
import java.time.LocalDateTime;
|
||||
|
||||
/**
|
||||
* 上传结果
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/10
|
||||
*/
|
||||
public class UploadResp {
|
||||
|
||||
/**
|
||||
* 存储 code
|
||||
*/
|
||||
private String code;
|
||||
|
||||
/**
|
||||
* 访问地址
|
||||
* <p>如果桶为私有,则提供临时链接,时间默认为 12 小时</p>
|
||||
*/
|
||||
private String url;
|
||||
|
||||
/**
|
||||
* 文件基础路径
|
||||
*/
|
||||
private String basePath;
|
||||
|
||||
/**
|
||||
* 原始 文件名
|
||||
*/
|
||||
private String originalFilename;
|
||||
|
||||
/**
|
||||
* 扩展名
|
||||
*/
|
||||
private String ext;
|
||||
|
||||
/**
|
||||
* 文件大小(字节)
|
||||
*/
|
||||
private long size;
|
||||
|
||||
/**
|
||||
* 已上传对象的实体标记(用来校验文件)-S3
|
||||
*/
|
||||
private String eTag;
|
||||
|
||||
/**
|
||||
* 存储路径
|
||||
* <p></p> 格式 桶/文件名 continew/2024/12/24/1234.jpg
|
||||
*/
|
||||
private String path;
|
||||
|
||||
/**
|
||||
* 存储桶
|
||||
*/
|
||||
private String bucketName;
|
||||
|
||||
/**
|
||||
* 缩略图大小(字节)
|
||||
*/
|
||||
private Long thumbnailSize;
|
||||
|
||||
/**
|
||||
* 缩略图URL
|
||||
*/
|
||||
private String thumbnailUrl;
|
||||
|
||||
/**
|
||||
* 上传时间
|
||||
*/
|
||||
private LocalDateTime createTime;
|
||||
|
||||
public UploadResp() {
|
||||
}
|
||||
|
||||
public UploadResp(String code,
|
||||
String url,
|
||||
String basePath,
|
||||
String originalFilename,
|
||||
String ext,
|
||||
long size,
|
||||
String eTag,
|
||||
String path,
|
||||
String bucketName,
|
||||
Long thumbnailSize,
|
||||
String thumbnailUrl,
|
||||
LocalDateTime createTime) {
|
||||
this.code = code;
|
||||
this.url = url;
|
||||
this.basePath = basePath;
|
||||
this.originalFilename = originalFilename;
|
||||
this.ext = ext;
|
||||
this.size = size;
|
||||
this.eTag = eTag;
|
||||
this.path = path;
|
||||
this.bucketName = bucketName;
|
||||
this.thumbnailSize = thumbnailSize;
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
this.createTime = createTime;
|
||||
}
|
||||
|
||||
public String getCode() {
|
||||
return code;
|
||||
}
|
||||
|
||||
public void setCode(String code) {
|
||||
this.code = code;
|
||||
}
|
||||
|
||||
public String getUrl() {
|
||||
return url;
|
||||
}
|
||||
|
||||
public void setUrl(String url) {
|
||||
this.url = url;
|
||||
}
|
||||
|
||||
public String getBasePath() {
|
||||
return basePath;
|
||||
}
|
||||
|
||||
public void setBasePath(String basePath) {
|
||||
this.basePath = basePath;
|
||||
}
|
||||
|
||||
public String getOriginalFilename() {
|
||||
return originalFilename;
|
||||
}
|
||||
|
||||
public void setOriginalFilename(String originalFilename) {
|
||||
this.originalFilename = originalFilename;
|
||||
}
|
||||
|
||||
public String getExt() {
|
||||
return ext;
|
||||
}
|
||||
|
||||
public void setExt(String ext) {
|
||||
this.ext = ext;
|
||||
}
|
||||
|
||||
public long getSize() {
|
||||
return size;
|
||||
}
|
||||
|
||||
public void setSize(long size) {
|
||||
this.size = size;
|
||||
}
|
||||
|
||||
public String geteTag() {
|
||||
return eTag;
|
||||
}
|
||||
|
||||
public void seteTag(String eTag) {
|
||||
this.eTag = eTag;
|
||||
}
|
||||
|
||||
public String getPath() {
|
||||
return path;
|
||||
}
|
||||
|
||||
public void setPath(String path) {
|
||||
this.path = path;
|
||||
}
|
||||
|
||||
public String getBucketName() {
|
||||
return bucketName;
|
||||
}
|
||||
|
||||
public void setBucketName(String bucketName) {
|
||||
this.bucketName = bucketName;
|
||||
}
|
||||
|
||||
public Long getThumbnailSize() {
|
||||
return thumbnailSize;
|
||||
}
|
||||
|
||||
public void setThumbnailSize(Long thumbnailSize) {
|
||||
this.thumbnailSize = thumbnailSize;
|
||||
}
|
||||
|
||||
public String getThumbnailUrl() {
|
||||
return thumbnailUrl;
|
||||
}
|
||||
|
||||
public void setThumbnailUrl(String thumbnailUrl) {
|
||||
this.thumbnailUrl = thumbnailUrl;
|
||||
}
|
||||
|
||||
public LocalDateTime getCreateTime() {
|
||||
return createTime;
|
||||
}
|
||||
|
||||
public void setCreateTime(LocalDateTime createTime) {
|
||||
this.createTime = createTime;
|
||||
}
|
||||
}
|
@@ -0,0 +1,151 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.strategy;
|
||||
|
||||
import top.continew.starter.storage.model.resp.ThumbnailResp;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
|
||||
import java.io.InputStream;
|
||||
|
||||
/**
|
||||
* 存储策略接口
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16 11:19
|
||||
*/
|
||||
public interface StorageStrategy<C> {
|
||||
|
||||
/**
|
||||
* 获得客户端 - 用于重写时 获取对应存储 code 客户端
|
||||
*
|
||||
* @return {@link Object }
|
||||
*/
|
||||
C getClient();
|
||||
|
||||
/**
|
||||
* 检查桶是否存在
|
||||
* <p> S3: 检查桶是否存在 </p>
|
||||
* <p>local: 检查 默认路径 是否存在 </p>
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @return true 存在 false 不存在
|
||||
*/
|
||||
boolean bucketExists(String bucketName);
|
||||
|
||||
/**
|
||||
* 创建桶
|
||||
* <p> S3: 创建桶 </p>
|
||||
* <p> local: 创建 默认路径下 指定文件夹 </p>
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
void createBucket(String bucketName);
|
||||
|
||||
/**
|
||||
* 上传文件 - 默认桶
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @param inputStream 输入流
|
||||
* @param fileType 文件类型
|
||||
* @return 上传响应
|
||||
*/
|
||||
UploadResp upload(String fileName, InputStream inputStream, String fileType);
|
||||
|
||||
/**
|
||||
* 上传文件 - 默认桶
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @param path 路径
|
||||
* @param inputStream 输入流
|
||||
* @param fileType 文件类型
|
||||
* @param isThumbnail 是缩略图
|
||||
* @return {@link UploadResp }
|
||||
*/
|
||||
UploadResp upload(String fileName, String path, InputStream inputStream, String fileType, boolean isThumbnail);
|
||||
|
||||
/**
|
||||
* 上传文件
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
* @param path 路径
|
||||
* @param inputStream 输入流
|
||||
* @param fileType 文件类型
|
||||
* @param isThumbnail 是缩略图
|
||||
* @return 上传响应
|
||||
*/
|
||||
UploadResp upload(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail);
|
||||
|
||||
/**
|
||||
* 文件上传-基础上传
|
||||
*
|
||||
* @param bucketName 桶名称 - 基础上传不做处理
|
||||
* @param fileName 文件名 - 基础上传不做处理
|
||||
* @param path 路径 - 基础上传不做处理
|
||||
* @param inputStream 输入流
|
||||
* @param fileType 文件类型
|
||||
* @return {@link UploadResp }
|
||||
*/
|
||||
void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType);
|
||||
|
||||
/**
|
||||
* 上传缩略图
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
* @param inputStream 输入流
|
||||
* @param fileType 文件类型
|
||||
* @return {@link UploadResp }
|
||||
*/
|
||||
ThumbnailResp uploadThumbnail(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType);
|
||||
|
||||
/**
|
||||
* 下载文件
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
* @return 文件输入流
|
||||
*/
|
||||
InputStream download(String bucketName, String fileName);
|
||||
|
||||
/**
|
||||
* 删除文件
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
*/
|
||||
void delete(String bucketName, String fileName);
|
||||
|
||||
/**
|
||||
* 获取图像Base64
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
* @return Base64编码的图像
|
||||
*/
|
||||
String getImageBase64(String bucketName, String fileName);
|
||||
|
||||
}
|
@@ -0,0 +1,81 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.util;
|
||||
|
||||
import javax.imageio.ImageIO;
|
||||
import java.awt.*;
|
||||
import java.awt.image.BufferedImage;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.io.OutputStream;
|
||||
|
||||
/**
|
||||
* 图像缩略图工具
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/20 16:49
|
||||
*/
|
||||
public class ImageThumbnailUtils {
|
||||
|
||||
// 默认缩略图尺寸:100x100
|
||||
private static final int DEFAULT_WIDTH = 100;
|
||||
private static final int DEFAULT_HEIGHT = 100;
|
||||
|
||||
/**
|
||||
* 根据输入流生成默认大小(100x100)的缩略图并写入输出流
|
||||
*
|
||||
* @param inputStream 原始图片的输入流
|
||||
* @param outputStream 缩略图输出流
|
||||
* @param suffix 后缀
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public static void generateThumbnail(InputStream inputStream,
|
||||
OutputStream outputStream,
|
||||
String suffix) throws IOException {
|
||||
generateThumbnail(inputStream, outputStream, DEFAULT_WIDTH, DEFAULT_HEIGHT, suffix);
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据输入流和自定义尺寸生成缩略图并写入输出流
|
||||
*
|
||||
* @param inputStream 原始图片的输入流
|
||||
* @param outputStream 缩略图输出流
|
||||
* @param width 缩略图宽度
|
||||
* @param height 缩略图高度
|
||||
* @param suffix 后缀
|
||||
* @throws IOException IOException
|
||||
*/
|
||||
public static void generateThumbnail(InputStream inputStream,
|
||||
OutputStream outputStream,
|
||||
int width,
|
||||
int height,
|
||||
String suffix) throws IOException {
|
||||
// 读取原始图片
|
||||
BufferedImage originalImage = ImageIO.read(inputStream);
|
||||
|
||||
// 调整图片大小
|
||||
Image tmp = originalImage.getScaledInstance(width, height, Image.SCALE_SMOOTH);
|
||||
BufferedImage thumbnail = new BufferedImage(width, height, BufferedImage.TYPE_INT_RGB);
|
||||
|
||||
// 画出缩略图
|
||||
Graphics2D g2d = thumbnail.createGraphics();
|
||||
g2d.drawImage(tmp, 0, 0, null);
|
||||
g2d.dispose();
|
||||
// 写入输出流
|
||||
ImageIO.write(thumbnail, suffix, outputStream);
|
||||
}
|
||||
}
|
@@ -0,0 +1,128 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.util;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.LocalDate;
|
||||
import java.time.LocalDateTime;
|
||||
import java.time.format.DateTimeFormatter;
|
||||
|
||||
/**
|
||||
* 储存工具
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16 19:55
|
||||
*/
|
||||
public class StorageUtils {
|
||||
public StorageUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式文件名
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return {@link String }
|
||||
*/
|
||||
public static String formatFileName(String fileName) {
|
||||
// 获取文件后缀名
|
||||
String suffix = FileUtil.extName(fileName);
|
||||
// 获取当前时间的年月日时分秒格式
|
||||
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyyMMddHHmmss");
|
||||
String datetime = LocalDateTime.now().format(formatter);
|
||||
// 获取当前时间戳
|
||||
String timestamp = String.valueOf(System.currentTimeMillis());
|
||||
// 生成新的文件名
|
||||
return datetime + timestamp + "." + suffix;
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认文件目录
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @return {@link String }
|
||||
*/
|
||||
public static String defaultFileDir(String fileName) {
|
||||
LocalDate today = LocalDate.now();
|
||||
return Paths.get(String.valueOf(today.getYear()), String.valueOf(today.getMonthValue()), String.valueOf(today
|
||||
.getDayOfMonth()), fileName).toString();
|
||||
}
|
||||
|
||||
/**
|
||||
* 默认路径地址 格式 2024/03/10/
|
||||
*
|
||||
* @return {@link String }
|
||||
*/
|
||||
public static String defaultPath() {
|
||||
LocalDate today = LocalDate.now();
|
||||
return Paths.get(String.valueOf(today.getYear()), String.valueOf(today.getMonthValue()), String.valueOf(today
|
||||
.getDayOfMonth())) + StringConstants.SLASH;
|
||||
}
|
||||
|
||||
/**
|
||||
* 根据 endpoint 判断是否带有 http 或 https,如果没有则加上 http 前缀。
|
||||
*
|
||||
* @param endpoint 输入的 endpoint 字符串
|
||||
* @return URI 对象
|
||||
*/
|
||||
public static URI createUriWithProtocol(String endpoint) {
|
||||
// 判断 endpoint 是否包含 http:// 或 https:// 前缀
|
||||
if (!endpoint.startsWith("http://") && !endpoint.startsWith("https://")) {
|
||||
// 如果没有协议前缀,则加上 http://
|
||||
endpoint = "http://" + endpoint;
|
||||
}
|
||||
// 返回 URI 对象
|
||||
return URI.create(endpoint);
|
||||
}
|
||||
|
||||
/**
|
||||
* 生成缩略图文件名
|
||||
*
|
||||
* @param fileName 文件名
|
||||
* @param suffix 后缀
|
||||
* @return {@link String }
|
||||
*/
|
||||
public static String buildThumbnailFileName(String fileName, String suffix) {
|
||||
// 获取文件的扩展名
|
||||
String extName = FileNameUtil.extName(fileName);
|
||||
// 去掉扩展名
|
||||
String baseName = StrUtil.subBefore(fileName, StringConstants.DOT, true);
|
||||
// 拼接新的路径:原始路径 + .缩略图后缀 + .扩展名
|
||||
return baseName + "." + suffix + "." + extName;
|
||||
}
|
||||
|
||||
/**
|
||||
* 可重复读流
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @return {@link InputStream }
|
||||
*/
|
||||
public static InputStream ensureByteArrayStream(InputStream inputStream) {
|
||||
return (inputStream instanceof ByteArrayInputStream)
|
||||
? inputStream
|
||||
: new ByteArrayInputStream(IoUtil.readBytes(inputStream));
|
||||
}
|
||||
|
||||
}
|
@@ -13,10 +13,10 @@
|
||||
<description>ContiNew Starter 存储模块 - 本地存储</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- Spring Web MVC 模块 -->
|
||||
<!--存储 - 核心模块-->
|
||||
<dependency>
|
||||
<groupId>org.springframework</groupId>
|
||||
<artifactId>spring-webmvc</artifactId>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-storage-core</artifactId>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
@@ -0,0 +1,39 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.dao.impl.StorageDaoDefaultImpl;
|
||||
|
||||
/**
|
||||
* 本地存储 - 存储自动配置
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/17 20:23
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class LocalStorageAutoconfigure {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public StorageDao storageDao() {
|
||||
return new StorageDaoDefaultImpl();
|
||||
}
|
||||
}
|
@@ -0,0 +1,85 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.client;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import top.continew.starter.storage.model.req.StorageProperties;
|
||||
|
||||
import java.io.IOException;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
|
||||
/**
|
||||
* 本地客户端
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16 19:37
|
||||
*/
|
||||
public class LocalClient {
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalClient.class);
|
||||
|
||||
/**
|
||||
* 配置属性
|
||||
*/
|
||||
private final StorageProperties properties;
|
||||
|
||||
/**
|
||||
* 构造函数
|
||||
*
|
||||
* @param properties 配置属性
|
||||
*/
|
||||
public LocalClient(StorageProperties properties) {
|
||||
this.properties = properties;
|
||||
// 判断是否是默认存储,若不存在桶目录,则创建
|
||||
if (Boolean.TRUE.equals(properties.getIsDefault())) {
|
||||
String bucketName = properties.getBucketName();
|
||||
if (bucketName != null && !bucketName.isEmpty()) {
|
||||
createBucketDirectory(bucketName);
|
||||
} else {
|
||||
log.info("默认存储-存储桶已存在 => {}", bucketName);
|
||||
}
|
||||
}
|
||||
log.info("加载 Local 存储 => {}", properties.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取属性
|
||||
*
|
||||
* @return {@link StorageProperties }
|
||||
*/
|
||||
public StorageProperties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建桶目录
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
*/
|
||||
private void createBucketDirectory(String bucketName) {
|
||||
Path bucketPath = Path.of(bucketName);
|
||||
try {
|
||||
if (Files.notExists(bucketPath)) {
|
||||
Files.createDirectories(bucketPath);
|
||||
log.info("默认存储-存储桶创建成功 : {}", bucketPath.toAbsolutePath());
|
||||
}
|
||||
} catch (IOException e) {
|
||||
log.error("创建默认存储-存储桶失败 => 路径: {}", bucketPath.toAbsolutePath(), e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -1,79 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.local.autoconfigure;
|
||||
|
||||
import cn.hutool.core.text.CharSequenceUtil;
|
||||
import jakarta.annotation.PostConstruct;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty;
|
||||
import org.springframework.boot.context.properties.EnableConfigurationProperties;
|
||||
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
|
||||
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
|
||||
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 本地文件自动配置
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@EnableWebMvc
|
||||
@AutoConfiguration
|
||||
@EnableConfigurationProperties(LocalStorageProperties.class)
|
||||
@ConditionalOnProperty(prefix = PropertiesConstants.STORAGE_LOCAL, name = PropertiesConstants.ENABLED, havingValue = "true", matchIfMissing = true)
|
||||
public class LocalStorageAutoConfiguration implements WebMvcConfigurer {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(LocalStorageAutoConfiguration.class);
|
||||
private final LocalStorageProperties properties;
|
||||
|
||||
public LocalStorageAutoConfiguration(LocalStorageProperties properties) {
|
||||
this.properties = properties;
|
||||
}
|
||||
|
||||
@Override
|
||||
public void addResourceHandlers(ResourceHandlerRegistry registry) {
|
||||
Map<String, LocalStorageProperties.LocalStorageMapping> mappingMap = properties.getMapping();
|
||||
for (Map.Entry<String, LocalStorageProperties.LocalStorageMapping> mappingEntry : mappingMap.entrySet()) {
|
||||
LocalStorageProperties.LocalStorageMapping mapping = mappingEntry.getValue();
|
||||
String pathPattern = mapping.getPathPattern();
|
||||
String location = mapping.getLocation();
|
||||
if (CharSequenceUtil.isBlank(location)) {
|
||||
throw new IllegalArgumentException("Path pattern [%s] location is null.".formatted(pathPattern));
|
||||
}
|
||||
registry.addResourceHandler(CharSequenceUtil.appendIfMissing(pathPattern, StringConstants.PATH_PATTERN))
|
||||
.addResourceLocations(!location.startsWith("file:")
|
||||
? "file:%s".formatted(this.format(location))
|
||||
: this.format(location))
|
||||
.setCachePeriod(0);
|
||||
}
|
||||
}
|
||||
|
||||
private String format(String location) {
|
||||
return location.replace(StringConstants.BACKSLASH, StringConstants.SLASH);
|
||||
}
|
||||
|
||||
@PostConstruct
|
||||
public void postConstruct() {
|
||||
log.debug("[ContiNew Starter] - Auto Configuration 'Storage-Local' completed initialization.");
|
||||
}
|
||||
}
|
@@ -1,105 +0,0 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.local.autoconfigure;
|
||||
|
||||
import org.springframework.boot.context.properties.ConfigurationProperties;
|
||||
import org.springframework.util.unit.DataSize;
|
||||
import top.continew.starter.core.constant.PropertiesConstants;
|
||||
|
||||
import java.util.HashMap;
|
||||
import java.util.Map;
|
||||
|
||||
/**
|
||||
* 本地存储配置属性
|
||||
*
|
||||
* @author Charles7c
|
||||
* @since 1.1.0
|
||||
*/
|
||||
@ConfigurationProperties(PropertiesConstants.STORAGE_LOCAL)
|
||||
public class LocalStorageProperties {
|
||||
|
||||
/**
|
||||
* 是否启用本地存储
|
||||
*/
|
||||
private boolean enabled = true;
|
||||
|
||||
/**
|
||||
* 存储映射
|
||||
*/
|
||||
private Map<String, LocalStorageMapping> mapping = new HashMap<>();
|
||||
|
||||
/**
|
||||
* 本地存储映射
|
||||
*/
|
||||
public static class LocalStorageMapping {
|
||||
|
||||
/**
|
||||
* 路径模式
|
||||
*/
|
||||
private String pathPattern;
|
||||
|
||||
/**
|
||||
* 资源路径
|
||||
*/
|
||||
private String location;
|
||||
|
||||
/**
|
||||
* 单文件上传大小限制
|
||||
*/
|
||||
private DataSize maxFileSize = DataSize.ofMegabytes(1);
|
||||
|
||||
public String getPathPattern() {
|
||||
return pathPattern;
|
||||
}
|
||||
|
||||
public void setPathPattern(String pathPattern) {
|
||||
this.pathPattern = pathPattern;
|
||||
}
|
||||
|
||||
public String getLocation() {
|
||||
return location;
|
||||
}
|
||||
|
||||
public void setLocation(String location) {
|
||||
this.location = location;
|
||||
}
|
||||
|
||||
public DataSize getMaxFileSize() {
|
||||
return maxFileSize;
|
||||
}
|
||||
|
||||
public void setMaxFileSize(DataSize maxFileSize) {
|
||||
this.maxFileSize = maxFileSize;
|
||||
}
|
||||
}
|
||||
|
||||
public boolean isEnabled() {
|
||||
return enabled;
|
||||
}
|
||||
|
||||
public void setEnabled(boolean enabled) {
|
||||
this.enabled = enabled;
|
||||
}
|
||||
|
||||
public Map<String, LocalStorageMapping> getMapping() {
|
||||
return mapping;
|
||||
}
|
||||
|
||||
public void setMapping(Map<String, LocalStorageMapping> mapping) {
|
||||
this.mapping = mapping;
|
||||
}
|
||||
}
|
@@ -0,0 +1,275 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.strategy;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
import top.continew.starter.core.validation.CheckUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
import top.continew.starter.storage.client.LocalClient;
|
||||
import top.continew.starter.storage.constant.StorageConstant;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.enums.FileTypeEnum;
|
||||
import top.continew.starter.storage.model.req.StorageProperties;
|
||||
import top.continew.starter.storage.model.resp.ThumbnailResp;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
import top.continew.starter.storage.util.ImageThumbnailUtils;
|
||||
import top.continew.starter.storage.util.LocalUtils;
|
||||
import top.continew.starter.storage.util.StorageUtils;
|
||||
|
||||
import java.io.*;
|
||||
import java.nio.file.Files;
|
||||
import java.nio.file.Path;
|
||||
import java.nio.file.Paths;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
|
||||
/**
|
||||
* 本地存储策略
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16 19:48
|
||||
*/
|
||||
public class LocalStorageStrategy implements StorageStrategy<LocalClient> {
|
||||
|
||||
private final LocalClient client;
|
||||
private final StorageDao storageDao;
|
||||
|
||||
public LocalStorageStrategy(LocalClient client, StorageDao storageDao) {
|
||||
this.client = client;
|
||||
this.storageDao = storageDao;
|
||||
}
|
||||
|
||||
private StorageProperties getStorageProperties() {
|
||||
return client.getProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public LocalClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean bucketExists(String bucketName) {
|
||||
try {
|
||||
return Files.exists(Path.of(bucketName));
|
||||
} catch (RuntimeException e) {
|
||||
throw new BusinessException("local存储 查询桶 失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createBucket(String bucketName) {
|
||||
if (!bucketExists(bucketName)) {
|
||||
try {
|
||||
Files.createDirectories(Path.of(bucketName));
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("local存储 创建桶 失败", e);
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String fileName, InputStream inputStream, String fileType) {
|
||||
String bucketName = getStorageProperties().getBucketName();
|
||||
return this.upload(bucketName, fileName, null, inputStream, fileType, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail) {
|
||||
String bucketName = getStorageProperties().getBucketName();
|
||||
return this.upload(bucketName, fileName, path, inputStream, fileType, isThumbnail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail) {
|
||||
try {
|
||||
// 可重复读流
|
||||
inputStream = StorageUtils.ensureByteArrayStream(inputStream);
|
||||
// 获取流大小
|
||||
byte[] originalBytes = IoUtil.readBytes(inputStream);
|
||||
ValidationUtils.throwIf(originalBytes.length == 0, "输入流内容长度不可用或无效");
|
||||
|
||||
// 获取文件扩展名
|
||||
String fileExtension = FileNameUtil.extName(fileName);
|
||||
// 格式化文件名 防止上传后重复
|
||||
String formatFileName = StorageUtils.formatFileName(fileName);
|
||||
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
path = StorageUtils.defaultPath();
|
||||
}
|
||||
// 判断文件夹是否存在 不存在则创建
|
||||
Path folderPath = Paths.get(bucketName, path);
|
||||
if (!Files.exists(folderPath)) {
|
||||
Files.createDirectories(folderPath);
|
||||
}
|
||||
ThumbnailResp thumbnailResp = null;
|
||||
//判断是否需要上传缩略图 前置条件 文件必须为图片
|
||||
boolean contains = FileTypeEnum.IMAGE.getExtensions().contains(fileExtension);
|
||||
if (contains && isThumbnail) {
|
||||
try (InputStream thumbnailStream = new ByteArrayInputStream(originalBytes)) {
|
||||
thumbnailResp = this.uploadThumbnail(bucketName, formatFileName, path, thumbnailStream, fileType);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
try (InputStream uploadStream = new ByteArrayInputStream(originalBytes)) {
|
||||
this.upload(bucketName, formatFileName, path, uploadStream, fileType);
|
||||
}
|
||||
|
||||
// 构建文件 md5
|
||||
String eTag = LocalUtils.calculateMD5(inputStream);
|
||||
// 构建 上传后的文件路径地址 格式 xxx/xxx/xxx.jpg
|
||||
String filePath = Paths.get(path, formatFileName).toString();
|
||||
// 构建 文件上传记录 并返回
|
||||
return buildStorageRecord(bucketName, fileName, filePath, eTag, originalBytes.length, thumbnailResp);
|
||||
} catch (NoSuchAlgorithmException | IOException e) {
|
||||
throw new BusinessException("文件上传异常", e);
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType) {
|
||||
byte[] fileBytes = IoUtil.readBytes(inputStream);
|
||||
// 拼接完整地址
|
||||
String filePath = Paths.get(bucketName, path, fileName).toString();
|
||||
try {
|
||||
//上传文件
|
||||
File targetFile = new File(filePath);
|
||||
try (FileOutputStream fos = new FileOutputStream(targetFile)) {
|
||||
fos.write(fileBytes);
|
||||
}
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("文件上传异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThumbnailResp uploadThumbnail(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType) {
|
||||
// 获取文件扩展名
|
||||
String fileExtension = FileNameUtil.extName(fileName);
|
||||
// 生成缩略图文件名
|
||||
String thumbnailFileName = StorageUtils.buildThumbnailFileName(fileName, StorageConstant.SMALL_SUFFIX);
|
||||
// 处理文件为缩略图
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
ImageThumbnailUtils.generateThumbnail(inputStream, outputStream, fileExtension);
|
||||
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
// 上传文件
|
||||
this.upload(bucketName, thumbnailFileName, path, inputStream, null);
|
||||
|
||||
return new ThumbnailResp((long)outputStream.size(), Paths.get(path, thumbnailFileName).toString());
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("缩略图处理异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream download(String bucketName, String fileName) {
|
||||
String fullPath = Paths.get(bucketName, fileName).toString();
|
||||
File file = new File(fullPath);
|
||||
try {
|
||||
return new FileInputStream(file);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("下载文件异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String bucketName, String fileName) {
|
||||
try {
|
||||
String fullPath = Paths.get(bucketName, fileName).toString();
|
||||
Files.delete(Paths.get(fullPath));
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("删除文件异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageBase64(String bucketName, String fileName) {
|
||||
try (InputStream inputStream = download(bucketName, fileName)) {
|
||||
if (ObjectUtil.isEmpty(inputStream)) {
|
||||
return null;
|
||||
}
|
||||
String extName = FileUtil.extName(fileName);
|
||||
CheckUtils.throwIf(!FileTypeEnum.IMAGE.getExtensions().contains(extName), "{} 不是图像格式", extName);
|
||||
return Base64.getEncoder().encodeToString(inputStream.readAllBytes());
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("无法查看图片", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建存储记录
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 原始文件名
|
||||
* @param filePath 文件路径 xx/xx/xxx.jpg
|
||||
* @param eTag 标签 - md5
|
||||
* @param size 文件大小
|
||||
* @param thumbnailResp 缩略图信息
|
||||
* @return {@link UploadResp }
|
||||
*/
|
||||
private UploadResp buildStorageRecord(String bucketName,
|
||||
String fileName,
|
||||
String filePath,
|
||||
String eTag,
|
||||
long size,
|
||||
ThumbnailResp thumbnailResp) {
|
||||
// 获取当前存储 code
|
||||
String code = client.getProperties().getCode();
|
||||
// 构建访问地址前缀
|
||||
String baseUrl = "http://" + getStorageProperties().getEndpoint() + StringConstants.SLASH;
|
||||
|
||||
UploadResp resp = new UploadResp();
|
||||
resp.setCode(code);
|
||||
resp.setUrl(baseUrl + filePath);
|
||||
resp.setBasePath(filePath);
|
||||
resp.setOriginalFilename(fileName);
|
||||
resp.setExt(FileNameUtil.extName(fileName));
|
||||
resp.setSize(size);
|
||||
resp.seteTag(eTag);
|
||||
resp.setPath(filePath);
|
||||
resp.setBucketName(bucketName);
|
||||
resp.setCreateTime(LocalDateTime.now());
|
||||
if (ObjectUtil.isNotEmpty(thumbnailResp)) {
|
||||
resp.setThumbnailUrl(baseUrl + thumbnailResp.getThumbnailPath());
|
||||
resp.setThumbnailSize(thumbnailResp.getThumbnailSize());
|
||||
}
|
||||
storageDao.add(resp);
|
||||
return resp;
|
||||
}
|
||||
}
|
@@ -0,0 +1,46 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.util;
|
||||
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import net.dreamlu.mica.core.utils.DigestUtil;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.security.NoSuchAlgorithmException;
|
||||
|
||||
/**
|
||||
* 本地存储工具
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/27 11:58
|
||||
*/
|
||||
public class LocalUtils {
|
||||
public LocalUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 计算MD5
|
||||
*
|
||||
* @param inputStream 输入流
|
||||
* @return {@link String }
|
||||
* @throws NoSuchAlgorithmException 没有这样算法例外
|
||||
*/
|
||||
public static String calculateMD5(InputStream inputStream) throws NoSuchAlgorithmException {
|
||||
byte[] fileBytes = IoUtil.readBytes(inputStream);
|
||||
return DigestUtil.md5Hex(fileBytes);
|
||||
}
|
||||
}
|
@@ -1 +1 @@
|
||||
top.continew.starter.storage.local.autoconfigure.LocalStorageAutoConfiguration
|
||||
top.continew.starter.storage.autoconfigure.LocalStorageAutoconfigure
|
@@ -0,0 +1,63 @@
|
||||
<?xml version="1.0" encoding="UTF-8"?>
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
|
||||
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
<parent>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-storage</artifactId>
|
||||
<version>${revision}</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>continew-starter-storage-oss</artifactId>
|
||||
<description>ContiNew Starter 存储模块 - 对象存储</description>
|
||||
|
||||
<dependencies>
|
||||
<!-- S3 SDK for Java 2.x -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3</artifactId>
|
||||
<exclusions>
|
||||
<!-- 基于 Netty 的 HTTP 客户端移除 -->
|
||||
<exclusion>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>netty-nio-client</artifactId>
|
||||
</exclusion>
|
||||
<!-- 基于 CRT 的 HTTP 客户端移除 -->
|
||||
<exclusion>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>aws-crt-client</artifactId>
|
||||
</exclusion>
|
||||
<!-- 基于 Apache 的 HTTP 客户端移除 -->
|
||||
<exclusion>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>apache-client</artifactId>
|
||||
</exclusion>
|
||||
<!-- 配置基于 URL 连接的 HTTP 客户端移除 -->
|
||||
<exclusion>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>url-connection-client</artifactId>
|
||||
</exclusion>
|
||||
</exclusions>
|
||||
</dependency>
|
||||
|
||||
<!-- 使用AWS基于 CRT 的 S3 客户端 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk.crt</groupId>
|
||||
<artifactId>aws-crt</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!-- 基于 AWS CRT 的 S3 客户端的性能增强的 S3 传输管理器 -->
|
||||
<dependency>
|
||||
<groupId>software.amazon.awssdk</groupId>
|
||||
<artifactId>s3-transfer-manager</artifactId>
|
||||
</dependency>
|
||||
|
||||
<!--存储 - 核心模块-->
|
||||
<dependency>
|
||||
<groupId>top.continew</groupId>
|
||||
<artifactId>continew-starter-storage-core</artifactId>
|
||||
</dependency>
|
||||
|
||||
</dependencies>
|
||||
|
||||
</project>
|
@@ -0,0 +1,40 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.autoconfigure;
|
||||
|
||||
import org.springframework.boot.autoconfigure.AutoConfiguration;
|
||||
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.dao.impl.StorageDaoDefaultImpl;
|
||||
|
||||
/**
|
||||
* 对象存储 - 存储自动配置
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/17 20:23
|
||||
*/
|
||||
@AutoConfiguration
|
||||
public class OssStorageAutoconfigure {
|
||||
|
||||
@Bean
|
||||
@ConditionalOnMissingBean
|
||||
public StorageDao storageDao() {
|
||||
return new StorageDaoDefaultImpl();
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1,163 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.client;
|
||||
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.services.s3.S3AsyncClient;
|
||||
import software.amazon.awssdk.services.s3.crt.S3CrtHttpConfiguration;
|
||||
import software.amazon.awssdk.services.s3.model.CreateBucketRequest;
|
||||
import software.amazon.awssdk.services.s3.model.HeadBucketRequest;
|
||||
import software.amazon.awssdk.services.s3.model.NoSuchBucketException;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
import software.amazon.awssdk.transfer.s3.S3TransferManager;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
import top.continew.starter.storage.model.req.StorageProperties;
|
||||
import top.continew.starter.storage.util.OssUtils;
|
||||
import top.continew.starter.storage.util.StorageUtils;
|
||||
|
||||
import java.net.URI;
|
||||
import java.time.Duration;
|
||||
|
||||
/**
|
||||
* S3客户端
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16
|
||||
*/
|
||||
public class OssClient {
|
||||
|
||||
private static final Logger log = LoggerFactory.getLogger(OssClient.class);
|
||||
|
||||
/**
|
||||
* 配置属性
|
||||
*/
|
||||
private final StorageProperties properties;
|
||||
|
||||
/**
|
||||
* s3 异步客户端
|
||||
*/
|
||||
private final S3AsyncClient client;
|
||||
|
||||
/**
|
||||
* S3 数据传输的高级工具
|
||||
*/
|
||||
private final S3TransferManager transferManager;
|
||||
|
||||
/**
|
||||
* S3 预签名
|
||||
*/
|
||||
private final S3Presigner presigner;
|
||||
|
||||
/**
|
||||
* 获取属性
|
||||
*
|
||||
* @return {@link StorageProperties }
|
||||
*/
|
||||
public StorageProperties getProperties() {
|
||||
return properties;
|
||||
}
|
||||
|
||||
/**
|
||||
* 构造方法
|
||||
*
|
||||
* @param s3PropertiesReq 微型性能要求
|
||||
*/
|
||||
public OssClient(StorageProperties s3PropertiesReq) {
|
||||
this.properties = s3PropertiesReq;
|
||||
|
||||
// 创建认证信息
|
||||
StaticCredentialsProvider auth = StaticCredentialsProvider.create(AwsBasicCredentials.create(properties
|
||||
.getAccessKey(), properties.getSecretKey()));
|
||||
|
||||
URI uriWithProtocol = StorageUtils.createUriWithProtocol(properties.getEndpoint());
|
||||
|
||||
// 创建 客户端连接
|
||||
client = S3AsyncClient.crtBuilder()
|
||||
.credentialsProvider(auth) // 认证信息
|
||||
.endpointOverride(uriWithProtocol) // 连接端点
|
||||
.region(OssUtils.getRegion(properties.getRegion()))
|
||||
.targetThroughputInGbps(20.0) //吞吐量
|
||||
.minimumPartSizeInBytes(10 * 1025 * 1024L)
|
||||
.checksumValidationEnabled(false)
|
||||
.httpConfiguration(S3CrtHttpConfiguration.builder()
|
||||
.connectionTimeout(Duration.ofSeconds(60)) // 设置连接超时
|
||||
.build())
|
||||
.build();
|
||||
|
||||
// 基于 CRT 创建 S3 Transfer Manager 的实例
|
||||
this.transferManager = S3TransferManager.builder().s3Client(this.client).build();
|
||||
|
||||
this.presigner = S3Presigner.builder()
|
||||
.region(OssUtils.getRegion(properties.getRegion()))
|
||||
.credentialsProvider(auth)
|
||||
.endpointOverride(uriWithProtocol)
|
||||
.build();
|
||||
|
||||
// 只创建 默认存储的的桶
|
||||
if (s3PropertiesReq.getIsDefault()) {
|
||||
try {
|
||||
// 检查存储桶是否存在
|
||||
client.headBucket(HeadBucketRequest.builder().bucket(properties.getBucketName()).build());
|
||||
log.info("默认存储-存储桶 {} 已存在", properties.getBucketName());
|
||||
} catch (NoSuchBucketException e) {
|
||||
log.info("默认存储桶 {} 不存在,尝试创建...", properties.getBucketName());
|
||||
try {
|
||||
// 创建存储桶
|
||||
client.createBucket(CreateBucketRequest.builder().bucket(properties.getBucketName()).build());
|
||||
log.info("默认存储-存储桶 {} 创建成功", properties.getBucketName());
|
||||
} catch (Exception createException) {
|
||||
log.error("创建默认存储-存储桶 {} 失败", properties.getBucketName(), createException);
|
||||
throw new BusinessException("创建默认存储-桶出错", createException);
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("检查默认存储-存储桶 {} 时出错", properties.getBucketName(), e);
|
||||
throw new BusinessException("检查默认存储-桶时出错", e);
|
||||
}
|
||||
}
|
||||
log.info("加载 S3 存储 => {}", properties.getCode());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得客户端
|
||||
*
|
||||
* @return {@link S3TransferManager }
|
||||
*/
|
||||
public S3AsyncClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得 高效连接客户端 主要用于 上传下载 复制 删除
|
||||
*
|
||||
* @return {@link S3TransferManager }
|
||||
*/
|
||||
public S3TransferManager getTransferManager() {
|
||||
return transferManager;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得 S3 预签名
|
||||
*
|
||||
* @return {@link S3Presigner }
|
||||
*/
|
||||
public S3Presigner getPresigner() {
|
||||
return presigner;
|
||||
}
|
||||
}
|
@@ -0,0 +1,401 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.strategy;
|
||||
|
||||
import cn.hutool.core.io.FileUtil;
|
||||
import cn.hutool.core.io.IoUtil;
|
||||
import cn.hutool.core.io.file.FileNameUtil;
|
||||
import cn.hutool.core.util.ObjectUtil;
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import cn.hutool.json.JSONObject;
|
||||
import org.slf4j.Logger;
|
||||
import org.slf4j.LoggerFactory;
|
||||
import software.amazon.awssdk.core.ResponseInputStream;
|
||||
import software.amazon.awssdk.core.async.AsyncResponseTransformer;
|
||||
import software.amazon.awssdk.core.async.BlockingInputStreamAsyncRequestBody;
|
||||
import software.amazon.awssdk.services.s3.model.*;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||
import software.amazon.awssdk.transfer.s3.model.CompletedUpload;
|
||||
import software.amazon.awssdk.transfer.s3.model.Download;
|
||||
import software.amazon.awssdk.transfer.s3.model.DownloadRequest;
|
||||
import software.amazon.awssdk.transfer.s3.model.Upload;
|
||||
import software.amazon.awssdk.transfer.s3.progress.LoggingTransferListener;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.core.exception.BusinessException;
|
||||
import top.continew.starter.core.validation.CheckUtils;
|
||||
import top.continew.starter.core.validation.ValidationUtils;
|
||||
import top.continew.starter.storage.client.OssClient;
|
||||
import top.continew.starter.storage.constant.StorageConstant;
|
||||
import top.continew.starter.storage.dao.StorageDao;
|
||||
import top.continew.starter.storage.enums.FileTypeEnum;
|
||||
import top.continew.starter.storage.model.req.StorageProperties;
|
||||
import top.continew.starter.storage.model.resp.ThumbnailResp;
|
||||
import top.continew.starter.storage.model.resp.UploadResp;
|
||||
import top.continew.starter.storage.util.ImageThumbnailUtils;
|
||||
import top.continew.starter.storage.util.OssUtils;
|
||||
import top.continew.starter.storage.util.StorageUtils;
|
||||
|
||||
import java.io.ByteArrayInputStream;
|
||||
import java.io.ByteArrayOutputStream;
|
||||
import java.io.IOException;
|
||||
import java.io.InputStream;
|
||||
import java.net.HttpURLConnection;
|
||||
import java.nio.file.Paths;
|
||||
import java.time.Duration;
|
||||
import java.time.LocalDateTime;
|
||||
import java.util.Base64;
|
||||
import java.util.List;
|
||||
import java.util.concurrent.CompletionException;
|
||||
|
||||
/**
|
||||
* OSS存储策略
|
||||
* <p><a href="https://docs.aws.amazon.com/zh_cn/sdk-for-java/latest/developer-guide/home.html">...</a></p>
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/16 20:29
|
||||
*/
|
||||
public class OssStorageStrategy implements StorageStrategy<OssClient> {
|
||||
private final static Logger log = LoggerFactory.getLogger(OssStorageStrategy.class);
|
||||
|
||||
private final OssClient client;
|
||||
private final StorageDao storageDao;
|
||||
private String etag;
|
||||
|
||||
public OssStorageStrategy(OssClient ossClient, StorageDao storageDao) {
|
||||
this.client = ossClient;
|
||||
this.storageDao = storageDao;
|
||||
}
|
||||
|
||||
private StorageProperties getStorageProperties() {
|
||||
return client.getProperties();
|
||||
}
|
||||
|
||||
@Override
|
||||
public OssClient getClient() {
|
||||
return client;
|
||||
}
|
||||
|
||||
@Override
|
||||
public boolean bucketExists(String bucketName) {
|
||||
try {
|
||||
// 调用 headBucket 请求,检查桶是否存在
|
||||
client.getClient().headBucket(HeadBucketRequest.builder().bucket(bucketName).build()).join();
|
||||
return true; // 桶存在
|
||||
} catch (Exception e) {
|
||||
// 捕获异常,详细判断具体原因
|
||||
if (e.getCause() instanceof NoSuchBucketException) {
|
||||
// 桶不存在
|
||||
return false;
|
||||
} else if (e.getCause() instanceof S3Exception s3Exception) {
|
||||
// 检查是否是其他人创建的桶(403 Forbidden 错误)
|
||||
if (s3Exception.statusCode() == HttpURLConnection.HTTP_FORBIDDEN) {
|
||||
throw new BusinessException("全局重复:存储桶名称已被他人创建:" + bucketName);
|
||||
}
|
||||
}
|
||||
// 捕获其他所有异常,并抛出
|
||||
throw new BusinessException("S3 存储桶查询失败,存储桶名称:" + bucketName, e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void createBucket(String bucketName) {
|
||||
try {
|
||||
if (!this.bucketExists(bucketName)) {
|
||||
client.getClient().createBucket(CreateBucketRequest.builder().bucket(bucketName).build()).join();
|
||||
}
|
||||
} catch (S3Exception e) {
|
||||
throw new BusinessException("S3 存储桶,创建失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String fileName, InputStream inputStream, String fileType) {
|
||||
String bucketName = getStorageProperties().getBucketName();
|
||||
return this.upload(bucketName, fileName, null, inputStream, fileType, false);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail) {
|
||||
String bucketName = getStorageProperties().getBucketName();
|
||||
return this.upload(bucketName, fileName, path, inputStream, fileType, isThumbnail);
|
||||
}
|
||||
|
||||
@Override
|
||||
public UploadResp upload(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType,
|
||||
boolean isThumbnail) {
|
||||
try {
|
||||
|
||||
// 可重复读流
|
||||
inputStream = StorageUtils.ensureByteArrayStream(inputStream);
|
||||
byte[] fileBytes = IoUtil.readBytes(inputStream);
|
||||
ValidationUtils.throwIf(fileBytes.length == 0, "输入流内容长度不可用或无效");
|
||||
// 获取文件扩展名
|
||||
String fileExtension = FileNameUtil.extName(fileName);
|
||||
// 格式化文件名 防止上传后重复
|
||||
String formatFileName = StorageUtils.formatFileName(fileName);
|
||||
// 判断文件路径是否为空 为空给默认路径 格式 2024/12/30/
|
||||
if (StrUtil.isEmpty(path)) {
|
||||
path = StorageUtils.defaultPath();
|
||||
}
|
||||
ThumbnailResp thumbnailResp = null;
|
||||
//判断是否需要上传缩略图 前置条件 文件必须为图片
|
||||
boolean contains = FileTypeEnum.IMAGE.getExtensions().contains(fileExtension);
|
||||
if (contains && isThumbnail) {
|
||||
try (InputStream thumbnailStream = new ByteArrayInputStream(fileBytes)) {
|
||||
thumbnailResp = this.uploadThumbnail(bucketName, formatFileName, path, thumbnailStream, fileType);
|
||||
}
|
||||
}
|
||||
|
||||
// 上传文件
|
||||
try (InputStream uploadStream = new ByteArrayInputStream(fileBytes)) {
|
||||
this.upload(bucketName, formatFileName, path, uploadStream, fileType);
|
||||
}
|
||||
String eTag = etag;
|
||||
// 构建 上传后的文件路径地址 格式 xxx/xxx/xxx.jpg
|
||||
String filePath = Paths.get(path, formatFileName).toString();
|
||||
// 构建 文件上传记录 并返回
|
||||
return buildStorageRecord(bucketName, fileName, filePath, eTag, fileBytes.length, thumbnailResp);
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("文件上传异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void upload(String bucketName, String fileName, String path, InputStream inputStream, String fileType) {
|
||||
// 构建 S3 存储 文件路径
|
||||
String filePath = Paths.get(path, fileName).toString();
|
||||
try {
|
||||
long available = inputStream.available();
|
||||
// 构建异步请求体,指定内容长度
|
||||
BlockingInputStreamAsyncRequestBody requestBody = BlockingInputStreamAsyncRequestBody.builder()
|
||||
.contentLength(available)
|
||||
.subscribeTimeout(Duration.ofSeconds(30))
|
||||
.build();
|
||||
|
||||
// 初始化上传任务
|
||||
Upload upload = client.getTransferManager()
|
||||
.upload(u -> u.requestBody(requestBody)
|
||||
.putObjectRequest(b -> b.bucket(bucketName).key(filePath).contentType(fileType).build())
|
||||
.build());
|
||||
|
||||
// 写入输入流内容到请求体
|
||||
requestBody.writeInputStream(inputStream);
|
||||
CompletedUpload uploadResult = upload.completionFuture().join();
|
||||
etag = uploadResult.response().eTag();
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("文件上传异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public ThumbnailResp uploadThumbnail(String bucketName,
|
||||
String fileName,
|
||||
String path,
|
||||
InputStream inputStream,
|
||||
String fileType) {
|
||||
// 获取文件扩展名
|
||||
String fileExtension = FileNameUtil.extName(fileName);
|
||||
// 生成缩略图文件名
|
||||
String thumbnailFileName = StorageUtils.buildThumbnailFileName(fileName, StorageConstant.SMALL_SUFFIX);
|
||||
// 处理文件为缩略图
|
||||
try (ByteArrayOutputStream outputStream = new ByteArrayOutputStream()) {
|
||||
ImageThumbnailUtils.generateThumbnail(inputStream, outputStream, fileExtension);
|
||||
inputStream = new ByteArrayInputStream(outputStream.toByteArray());
|
||||
// 上传文件
|
||||
this.upload(bucketName, thumbnailFileName, path, inputStream, fileType);
|
||||
return new ThumbnailResp((long)outputStream.size(), Paths.get(path, thumbnailFileName).toString());
|
||||
} catch (IOException e) {
|
||||
throw new BusinessException("缩略图处理异常", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public InputStream download(String bucketName, String fileName) {
|
||||
try {
|
||||
// 构建下载请求
|
||||
DownloadRequest<ResponseInputStream<GetObjectResponse>> downloadRequest = DownloadRequest.builder()
|
||||
.getObjectRequest(req -> req.bucket(bucketName).key(fileName).build()) // 设置桶名和对象名
|
||||
.addTransferListener(LoggingTransferListener.create()) // 添加传输监听器
|
||||
.responseTransformer(AsyncResponseTransformer.toBlockingInputStream()) // 转换为阻塞输入流
|
||||
.build();
|
||||
// 执行下载操作
|
||||
Download<ResponseInputStream<GetObjectResponse>> download = client.getTransferManager()
|
||||
.download(downloadRequest);
|
||||
// 直接等待下载完成并返回 InputStream
|
||||
// 返回输入流
|
||||
return download.completionFuture().join().result();
|
||||
} catch (CompletionException e) {
|
||||
// 处理异步执行中的异常
|
||||
throw new BusinessException("文件下载失败,错误信息: " + e.getCause().getMessage(), e.getCause());
|
||||
} catch (Exception e) {
|
||||
// 捕获其他异常
|
||||
throw new BusinessException("文件下载失败,发生未知错误", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public void delete(String bucketName, String fileName) {
|
||||
try {
|
||||
client.getClient().deleteObject(DeleteObjectRequest.builder().bucket(bucketName).key(fileName).build());
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("S3 文件删除失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String getImageBase64(String bucketName, String fileName) {
|
||||
try (InputStream inputStream = download(bucketName, fileName)) {
|
||||
if (ObjectUtil.isEmpty(inputStream)) {
|
||||
return null;
|
||||
}
|
||||
String extName = FileUtil.extName(fileName);
|
||||
boolean contains = FileTypeEnum.IMAGE.getExtensions().contains(extName);
|
||||
CheckUtils.throwIf(!contains, "{}非图片格式,无法获取", extName);
|
||||
return Base64.getEncoder().encodeToString(inputStream.readAllBytes());
|
||||
} catch (Exception e) {
|
||||
throw new BusinessException("图片查看失败", e);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建储存记录
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
* @param filePath 文件路径
|
||||
* @param eTag e 标记
|
||||
* @param contentLength 内容长度
|
||||
* @param thumbnailResp 相应缩略图
|
||||
* @return {@link UploadResp }
|
||||
*/
|
||||
private UploadResp buildStorageRecord(String bucketName,
|
||||
String fileName,
|
||||
String filePath,
|
||||
String eTag,
|
||||
long contentLength,
|
||||
ThumbnailResp thumbnailResp) {
|
||||
// 获取终端地址
|
||||
String endpoint = client.getProperties().getEndpoint();
|
||||
// 判断桶策略
|
||||
boolean isPrivateBucket = this.isPrivate(bucketName);
|
||||
// 如果是私有桶 则生成私有URL链接 默认 访问时间为 12 小时
|
||||
String url = isPrivateBucket
|
||||
? this.getPrivateUrl(bucketName, filePath, 12)
|
||||
: OssUtils.getUrl(endpoint, bucketName) + StringConstants.SLASH + filePath;
|
||||
|
||||
String thumbnailUrl = "";
|
||||
long thumbnailSize = 0;
|
||||
// 判断缩略图响应是否为空
|
||||
if (ObjectUtil.isNotEmpty(thumbnailResp)) {
|
||||
// 同理按照 访问桶策略构建 缩略图访问地址
|
||||
thumbnailUrl = isPrivateBucket
|
||||
? this.getPrivateUrl(bucketName, thumbnailResp.getThumbnailPath(), 12)
|
||||
: OssUtils.getUrl(endpoint, bucketName) + StringConstants.SLASH + thumbnailResp.getThumbnailPath();
|
||||
thumbnailSize = thumbnailResp.getThumbnailSize();
|
||||
}
|
||||
|
||||
UploadResp uploadResp = new UploadResp();
|
||||
uploadResp.setCode(client.getProperties().getCode());
|
||||
uploadResp.setUrl(url);
|
||||
uploadResp.setBasePath(filePath);
|
||||
uploadResp.setOriginalFilename(fileName);
|
||||
uploadResp.setExt(FileNameUtil.extName(fileName));
|
||||
uploadResp.setSize(contentLength);
|
||||
uploadResp.setThumbnailUrl(thumbnailUrl);
|
||||
uploadResp.setThumbnailSize(thumbnailSize);
|
||||
uploadResp.seteTag(eTag);
|
||||
uploadResp.setPath(Paths.get(bucketName, filePath).toString());
|
||||
uploadResp.setBucketName(bucketName);
|
||||
uploadResp.setCreateTime(LocalDateTime.now());
|
||||
storageDao.add(uploadResp);
|
||||
return uploadResp;
|
||||
}
|
||||
|
||||
/**
|
||||
* 是否为私有桶
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @return boolean T 是 F 不是
|
||||
*/
|
||||
private boolean isPrivate(String bucketName) {
|
||||
try {
|
||||
// 尝试获取桶的策略
|
||||
GetBucketPolicyResponse policyResponse = client.getClient()
|
||||
.getBucketPolicy(GetBucketPolicyRequest.builder().bucket(bucketName).build())
|
||||
.join();
|
||||
//转成 json
|
||||
String policy = policyResponse.policy();
|
||||
JSONObject json = new JSONObject(policy);
|
||||
// 为空则是私有
|
||||
return ObjectUtil.isEmpty(json.get("Statement"));
|
||||
} catch (Exception e) {
|
||||
// 如果 getBucketPolicy 抛出异常,说明不是 MinIO 或不支持策略
|
||||
log.warn("获取桶策略失败,可能是 MinIO,异常信息: {}", e.getMessage());
|
||||
}
|
||||
|
||||
try {
|
||||
// 获取桶的 ACL 信息
|
||||
GetBucketAclResponse aclResponse = client.getClient()
|
||||
.getBucketAcl(GetBucketAclRequest.builder().bucket(bucketName).build())
|
||||
.join();
|
||||
List<Grant> grants = aclResponse.grants();
|
||||
// 只存在 FULL_CONTROL 权限并且只有一个 Grant,则认为是私有桶
|
||||
if (grants.size() == 1 && grants.stream()
|
||||
.anyMatch(grant -> grant.permission().equals(Permission.FULL_CONTROL))) {
|
||||
return true;
|
||||
}
|
||||
// 如果存在其他权限 (READ 或 WRITE),认为是公开桶
|
||||
return grants.stream()
|
||||
.noneMatch(grant -> grant.permission().equals(Permission.READ) || grant.permission()
|
||||
.equals(Permission.WRITE));
|
||||
} catch (Exception e) {
|
||||
// 如果 getBucketAcl 失败,可能是权限或连接问题
|
||||
log.error("获取桶 ACL 失败: {}", e.getMessage());
|
||||
return true; // 出现错误时,默认认为桶是私有的
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取私有URL链接
|
||||
*
|
||||
* @param bucketName 桶名称
|
||||
* @param fileName 文件名
|
||||
* @param second 授权时间
|
||||
* @return {@link String }
|
||||
*/
|
||||
private String getPrivateUrl(String bucketName, String fileName, Integer second) {
|
||||
try {
|
||||
return client.getPresigner()
|
||||
.presignGetObject(GetObjectPresignRequest.builder()
|
||||
.signatureDuration(Duration.ofHours(second))
|
||||
.getObjectRequest(GetObjectRequest.builder().bucket(bucketName).key(fileName).build())
|
||||
.build())
|
||||
.url()
|
||||
.toString();
|
||||
} catch (RuntimeException e) {
|
||||
throw new BusinessException("获取私有链接异常", e);
|
||||
}
|
||||
}
|
||||
}
|
@@ -0,0 +1,61 @@
|
||||
/*
|
||||
* Copyright (c) 2022-present Charles7c Authors. All Rights Reserved.
|
||||
* <p>
|
||||
* Licensed under the GNU LESSER GENERAL PUBLIC LICENSE 3.0;
|
||||
* you may not use this file except in compliance with the License.
|
||||
* You may obtain a copy of the License at
|
||||
* <p>
|
||||
* http://www.gnu.org/licenses/lgpl.html
|
||||
* <p>
|
||||
* 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.starter.storage.util;
|
||||
|
||||
import cn.hutool.core.util.StrUtil;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import top.continew.starter.core.constant.StringConstants;
|
||||
import top.continew.starter.storage.constant.StorageConstant;
|
||||
|
||||
/**
|
||||
* OSS 工具
|
||||
*
|
||||
* @author echo
|
||||
* @date 2024/12/17 13:48
|
||||
*/
|
||||
public class OssUtils {
|
||||
public OssUtils() {
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取作用域
|
||||
* <p>如果 region 参数非空,使用 Region.of 方法创建对应的 S3 区域对象,否则返回默认区域</p>
|
||||
*
|
||||
* @param region 区域
|
||||
* @return {@link Region }
|
||||
*/
|
||||
public static Region getRegion(String region) {
|
||||
return StrUtil.isEmpty(region) ? Region.US_EAST_1 : Region.of(region);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取url
|
||||
*
|
||||
* @param endpoint 端点
|
||||
* @param bucketName 桶名称
|
||||
* @return {@link String }
|
||||
*/
|
||||
public static String getUrl(String endpoint, String bucketName) {
|
||||
// 如果是云服务商,直接返回域名或终端点
|
||||
if (StrUtil.containsAny(endpoint, StorageConstant.CLOUD_SERVICE_PREFIX)) {
|
||||
return "http://" + bucketName + StringConstants.DOT + endpoint;
|
||||
} else {
|
||||
return "http://" + endpoint + StringConstants.SLASH + bucketName;
|
||||
}
|
||||
}
|
||||
|
||||
}
|
@@ -0,0 +1 @@
|
||||
top.continew.starter.storage.autoconfigure.OssStorageAutoconfigure
|
@@ -14,7 +14,9 @@
|
||||
<description>ContiNew Starter 存储模块</description>
|
||||
|
||||
<modules>
|
||||
<module>continew-starter-storage-core</module>
|
||||
<module>continew-starter-storage-local</module>
|
||||
<module>continew-starter-storage-oss</module>
|
||||
</modules>
|
||||
|
||||
<dependencies>
|
||||
|
Reference in New Issue
Block a user