refactor(storage): 新增存储模块 - 本地和 S3 两种存储模式

This commit is contained in:
吴泽威
2024-12-30 21:01:26 +08:00
parent eb2cac54f7
commit bf2e30e560
29 changed files with 2437 additions and 196 deletions

View File

@@ -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>

View File

@@ -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";
}

View File

@@ -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);
}

View File

@@ -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) {
}
}

View File

@@ -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);
}
}

View File

@@ -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();
}
}

View File

@@ -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));
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}

View File

@@ -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);
}
}

View File

@@ -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));
}
}

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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);
}
}
}

View File

@@ -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.");
}
}

View File

@@ -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;
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}

View File

@@ -1 +1 @@
top.continew.starter.storage.local.autoconfigure.LocalStorageAutoConfiguration
top.continew.starter.storage.autoconfigure.LocalStorageAutoconfigure

View File

@@ -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>

View File

@@ -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();
}
}

View File

@@ -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;
}
}

View File

@@ -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);
}
}
}

View File

@@ -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;
}
}
}

View File

@@ -0,0 +1 @@
top.continew.starter.storage.autoconfigure.OssStorageAutoconfigure

View File

@@ -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>