mirror of https://gitee.com/topiam/eiam
Merge remote-tracking branch 'origin/master' into feature-app_permission
commit
e014dd5249
|
@ -51,7 +51,11 @@ public enum StorageProvider implements Serializable {
|
|||
/**
|
||||
* minio
|
||||
*/
|
||||
MINIO("minio", "minio", MinIoStorage.class);
|
||||
MINIO("minio", "minio", MinIoStorage.class),
|
||||
/**
|
||||
* S3
|
||||
*/
|
||||
S3("s3", "s3", S3Storage.class);
|
||||
|
||||
/**
|
||||
* code
|
||||
|
|
|
@ -67,8 +67,8 @@ public class MinIoStorage extends AbstractStorage {
|
|||
.credentials(minioConfig.getAccessKey(), minioConfig.getSecretKey()).build();
|
||||
createBucket(this.minioClient, minioConfig);
|
||||
} catch (Exception e) {
|
||||
log.error("Create bucket excception: {}", e.getMessage(), e);
|
||||
throw new StorageProviderException("Create bucket excception", e);
|
||||
log.error("Create bucket exception: {}", e.getMessage(), e);
|
||||
throw new StorageProviderException("Create bucket exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
@ -100,7 +100,7 @@ public class MinIoStorage extends AbstractStorage {
|
|||
+ SEPARATOR
|
||||
+ URLEncoder.encode(key, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
} catch (Exception e) {
|
||||
log.error("minio download exception: {}", e.getMessage(), e);
|
||||
log.error("minio upload exception: {}", e.getMessage(), e);
|
||||
throw new StorageProviderException("minio upload exception", e);
|
||||
}
|
||||
}
|
||||
|
@ -117,7 +117,7 @@ public class MinIoStorage extends AbstractStorage {
|
|||
return downloadUrl.replace(minioConfig.getEndpoint(), minioConfig.getDomain());
|
||||
} catch (Exception e) {
|
||||
log.error("minio download exception: {}", e.getMessage(), e);
|
||||
throw new StorageProviderException("minio upload exception", e);
|
||||
throw new StorageProviderException("minio download exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
|
|
|
@ -0,0 +1,191 @@
|
|||
/*
|
||||
* eiam-common - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
package cn.topiam.employee.common.storage.impl;
|
||||
|
||||
import java.io.InputStream;
|
||||
import java.net.URI;
|
||||
import java.net.URLEncoder;
|
||||
import java.nio.charset.StandardCharsets;
|
||||
import java.time.Duration;
|
||||
|
||||
import org.apache.commons.lang3.StringUtils;
|
||||
import org.hibernate.validator.constraints.URL;
|
||||
import org.jetbrains.annotations.NotNull;
|
||||
|
||||
import cn.topiam.employee.common.jackjson.encrypt.JsonPropertyEncrypt;
|
||||
import cn.topiam.employee.common.storage.AbstractStorage;
|
||||
import cn.topiam.employee.common.storage.StorageConfig;
|
||||
import cn.topiam.employee.common.storage.StorageProviderException;
|
||||
|
||||
import lombok.Data;
|
||||
import lombok.EqualsAndHashCode;
|
||||
import lombok.extern.slf4j.Slf4j;
|
||||
|
||||
import jakarta.validation.constraints.NotEmpty;
|
||||
import software.amazon.awssdk.auth.credentials.AwsBasicCredentials;
|
||||
import software.amazon.awssdk.auth.credentials.StaticCredentialsProvider;
|
||||
import software.amazon.awssdk.core.sync.RequestBody;
|
||||
import software.amazon.awssdk.regions.Region;
|
||||
import software.amazon.awssdk.services.s3.S3Client;
|
||||
import software.amazon.awssdk.services.s3.model.*;
|
||||
import software.amazon.awssdk.services.s3.presigner.S3Presigner;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.GetObjectPresignRequest;
|
||||
import software.amazon.awssdk.services.s3.presigner.model.PresignedGetObjectRequest;
|
||||
import static cn.topiam.employee.common.constant.StorageConstants.URL_REGEXP;
|
||||
|
||||
/**
|
||||
* S3 协议实现
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2023/08/29 22:30
|
||||
*/
|
||||
@Slf4j
|
||||
public class S3Storage extends AbstractStorage {
|
||||
|
||||
private final S3Client s3Client;
|
||||
|
||||
private final S3Presigner s3Presigner;
|
||||
|
||||
private final Config s3Config;
|
||||
|
||||
public S3Storage(StorageConfig config) {
|
||||
super(config);
|
||||
// 获取客户端
|
||||
this.s3Config = (Config) this.config.getConfig();
|
||||
this.s3Client = getS3Client();
|
||||
this.s3Presigner = getS3Presigner();
|
||||
createBucket();
|
||||
}
|
||||
|
||||
private S3Client getS3Client() {
|
||||
return S3Client.builder().serviceConfiguration(b -> b.checksumValidationEnabled(false))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials
|
||||
.create(s3Config.getAccessKeyId(), s3Config.getSecretAccessKey())))
|
||||
.region(getRegion()).endpointOverride(URI.create(s3Config.getEndpoint())).build();
|
||||
}
|
||||
|
||||
private S3Presigner getS3Presigner() {
|
||||
return S3Presigner.builder().region(getRegion())
|
||||
.endpointOverride(URI.create(s3Config.getEndpoint()))
|
||||
.credentialsProvider(StaticCredentialsProvider.create(AwsBasicCredentials
|
||||
.create(s3Config.getAccessKeyId(), s3Config.getSecretAccessKey())))
|
||||
.build();
|
||||
}
|
||||
|
||||
/**
|
||||
* 创建Bucket
|
||||
*/
|
||||
protected void createBucket() {
|
||||
try {
|
||||
// 获取bucket是否存在
|
||||
HeadBucketRequest bucketRequestWait = HeadBucketRequest.builder()
|
||||
.bucket(this.s3Config.getBucket()).build();
|
||||
s3Client.headBucket(bucketRequestWait);
|
||||
} catch (S3Exception se) {
|
||||
if (se.statusCode() == 404) {
|
||||
// 创建bucket
|
||||
CreateBucketRequest bucketRequest = CreateBucketRequest.builder()
|
||||
.bucket(this.s3Config.getBucket()).build();
|
||||
this.s3Client.createBucket(bucketRequest);
|
||||
} else {
|
||||
log.error("查询bucket是否存在异常:[{}]", se.getMessage(), se);
|
||||
throw se;
|
||||
}
|
||||
} catch (Exception e) {
|
||||
log.error("create bucket exception: {}", e.getMessage(), e);
|
||||
throw new StorageProviderException("create bucket exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String upload(@NotNull String fileName,
|
||||
InputStream inputStream) throws StorageProviderException {
|
||||
try {
|
||||
String key = s3Config.getLocation() + SEPARATOR + getFileName(fileName);
|
||||
PutObjectRequest putOb = PutObjectRequest.builder().bucket(s3Config.getBucket())
|
||||
.key(key).build();
|
||||
this.s3Client.putObject(putOb, RequestBody.fromBytes(inputStream.readAllBytes()));
|
||||
return this.s3Config.getDomain() + SEPARATOR
|
||||
+ URLEncoder.encode(key, StandardCharsets.UTF_8).replaceAll("\\+", "%20");
|
||||
} catch (Exception e) {
|
||||
log.error("[{}] upload exception: {}", this.config.getProvider(), e.getMessage(), e);
|
||||
throw new StorageProviderException("upload exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
@Override
|
||||
public String download(String path) throws StorageProviderException {
|
||||
try {
|
||||
GetObjectRequest getObjectRequest = GetObjectRequest.builder()
|
||||
.bucket(this.s3Config.getBucket()).key(path).build();
|
||||
GetObjectPresignRequest getObjectPresignRequest = GetObjectPresignRequest.builder()
|
||||
.signatureDuration(Duration.ofSeconds(EXPIRY_SECONDS))
|
||||
.getObjectRequest(getObjectRequest).build();
|
||||
PresignedGetObjectRequest presignedGetObjectRequest = s3Presigner
|
||||
.presignGetObject(getObjectPresignRequest);
|
||||
String downloadUrl = presignedGetObjectRequest.url().toString();
|
||||
return downloadUrl.replace(this.s3Config.getEndpoint(), this.s3Config.getDomain());
|
||||
} catch (Exception e) {
|
||||
log.error("[{}] download exception: {}", this.config.getProvider(), e.getMessage(), e);
|
||||
throw new StorageProviderException("download exception", e);
|
||||
}
|
||||
}
|
||||
|
||||
private Region getRegion() {
|
||||
if (StringUtils.isNotBlank(s3Config.getRegion())) {
|
||||
return Region.of(s3Config.getRegion());
|
||||
}
|
||||
return Region.AWS_GLOBAL;
|
||||
}
|
||||
|
||||
@EqualsAndHashCode(callSuper = true)
|
||||
@Data
|
||||
public static class Config extends StorageConfig.Config {
|
||||
|
||||
/**
|
||||
* AccessKeyId
|
||||
*/
|
||||
@NotEmpty(message = "AccessKeyId不能为空")
|
||||
private String accessKeyId;
|
||||
|
||||
/**
|
||||
* SecretAccessKey
|
||||
*/
|
||||
@JsonPropertyEncrypt
|
||||
@NotEmpty(message = "SecretAccessKey不能为空")
|
||||
private String secretAccessKey;
|
||||
|
||||
/**
|
||||
* endpoint
|
||||
*/
|
||||
@URL(message = "Endpoint格式不正确", regexp = URL_REGEXP)
|
||||
@NotEmpty(message = "Endpoint不能为空")
|
||||
private String endpoint;
|
||||
|
||||
/**
|
||||
* bucket
|
||||
*/
|
||||
@NotEmpty(message = "Bucket不能为空")
|
||||
private String bucket;
|
||||
|
||||
/**
|
||||
* Region
|
||||
*/
|
||||
private String region;
|
||||
}
|
||||
}
|
|
@ -32,6 +32,7 @@ import AliCloudOss from './components/AliCloud';
|
|||
import MinIO from './components/MinIo';
|
||||
import QiQiuKodo from './components/QiNiu';
|
||||
import TencentCos from './components/Tencent';
|
||||
import S3 from './components/S3';
|
||||
import { Container } from '@/components/Container';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
|
@ -239,12 +240,19 @@ const Storage = () => {
|
|||
id: 'pages.setting.storage_provider.provider.minio',
|
||||
}),
|
||||
},
|
||||
{
|
||||
value: OssProvider.S3,
|
||||
label: intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
{provider === OssProvider.ALIYUN_OSS && <AliCloudOss />}
|
||||
{provider === OssProvider.TENCENT_COS && <TencentCos />}
|
||||
{provider === OssProvider.QINIU_KODO && <QiQiuKodo />}
|
||||
{provider === OssProvider.MINIO && <MinIO />}
|
||||
{provider === OssProvider.S3 && <S3 />}
|
||||
</>
|
||||
)}
|
||||
</ProForm>
|
||||
|
|
|
@ -0,0 +1,120 @@
|
|||
/*
|
||||
* eiam-console - Employee Identity and Access Management
|
||||
* Copyright © 2022-Present Jinan Yuanchuang Network Technology Co., Ltd. (support@topiam.cn)
|
||||
*
|
||||
* This program is free software: you can redistribute it and/or modify
|
||||
* it under the terms of the GNU Affero General Public License as published by
|
||||
* the Free Software Foundation, either version 3 of the License, or
|
||||
* (at your option) any later version.
|
||||
*
|
||||
* This program is distributed in the hope that it will be useful,
|
||||
* but WITHOUT ANY WARRANTY; without even the implied warranty of
|
||||
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
|
||||
* GNU Affero General Public License for more details.
|
||||
*
|
||||
* You should have received a copy of the GNU Affero General Public License
|
||||
* along with this program. If not, see <http://www.gnu.org/licenses/>.
|
||||
*/
|
||||
import { ProFormText } from '@ant-design/pro-components';
|
||||
import { useIntl } from '@umijs/max';
|
||||
|
||||
export default () => {
|
||||
const intl = useIntl();
|
||||
return (
|
||||
<>
|
||||
<ProFormText
|
||||
name={['config', 'domain']}
|
||||
label={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.domain',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.domain.placeholder',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.domain.rule.0.message',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
/>
|
||||
<ProFormText
|
||||
name={['config', 'endpoint']}
|
||||
label={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.endpoint',
|
||||
})}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.endpoint.placeholder',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.endpoint.rule.0.message',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<ProFormText
|
||||
name={['config', 'accessKeyId']}
|
||||
label="AccessKeyId"
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.access_key_id.placeholder',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.access_key_id.rule.0.message',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
fieldProps={{
|
||||
autoComplete: 'new-password',
|
||||
}}
|
||||
/>
|
||||
<ProFormText.Password
|
||||
name={['config', 'secretAccessKey']}
|
||||
label="SecretAccessKey"
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.secret_access_key.placeholder',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.secret_access_key.rule.0.message',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
/>
|
||||
<ProFormText
|
||||
name={['config', 'bucket']}
|
||||
label={'Bucket'}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.bucket.placeholder',
|
||||
})}
|
||||
rules={[
|
||||
{
|
||||
required: true,
|
||||
message: intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.bucket.rule.0.message',
|
||||
}),
|
||||
},
|
||||
]}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
/>
|
||||
<ProFormText
|
||||
name={['config', 'region']}
|
||||
label={'Region'}
|
||||
placeholder={intl.formatMessage({
|
||||
id: 'pages.setting.storage_provider.provider.s3.region.placeholder',
|
||||
})}
|
||||
fieldProps={{ autoComplete: 'off' }}
|
||||
/>
|
||||
</>
|
||||
);
|
||||
};
|
|
@ -26,6 +26,7 @@ export enum OssProvider {
|
|||
QINIU_KODO = 'qiniu_kodo',
|
||||
LOCAL = 'local',
|
||||
MINIO = 'minio',
|
||||
S3 = 's3',
|
||||
}
|
||||
export enum Language {
|
||||
ZH = 'zh',
|
||||
|
|
|
@ -97,4 +97,29 @@ export default {
|
|||
'pages.setting.storage_provider.minio.endpoint.rule.0.message': 'MinIO Endpoint为必填项',
|
||||
'pages.setting.storage_provider.minio.bucket.placeholder': '请输入MinIO Bucket',
|
||||
'pages.setting.storage_provider.minio.bucket.rule.0.message': 'MinIO Bucket为必填项',
|
||||
'pages.setting.storage_provider.provider.s3': 'S3',
|
||||
'pages.setting.storage_provider.provider.s3.endpoint': 'S3 域名',
|
||||
'pages.setting.storage_provider.provider.s3.endpoint.placeholder':
|
||||
'请输入 S3 域名',
|
||||
'pages.setting.storage_provider.provider.qiniu_kodo.endpoint.rule.0.message':
|
||||
'七牛云Kodo S3 域名为必填项',
|
||||
'pages.setting.storage_provider.provider.s3.domain': '外链域名',
|
||||
'pages.setting.storage_provider.provider.s3.domain.placeholder':
|
||||
'请输入S3 外链域名',
|
||||
'pages.setting.storage_provider.provider.s3.domain.rule.0.message':
|
||||
'S3 外链域名为必填项',
|
||||
'pages.setting.storage_provider.provider.s3.access_key_id.placeholder':
|
||||
'请输入S3 AccessKeyId',
|
||||
'pages.setting.storage_provider.provider.s3.access_key_id.rule.0.message':
|
||||
'S3 AccessKeyId为必填项',
|
||||
'pages.setting.storage_provider.provider.s3.secret_access_key.placeholder':
|
||||
'请输入S3 SecretAccessKey',
|
||||
'pages.setting.storage_provider.provider.s3.secret_access_key.rule.0.message':
|
||||
'S3 SecretAccessKey为必填项',
|
||||
'pages.setting.storage_provider.provider.s3.region.placeholder':
|
||||
'请输入S3 Region',
|
||||
'pages.setting.storage_provider.provider.s3.bucket.placeholder':
|
||||
'请输入S3 Bucket',
|
||||
'pages.setting.storage_provider.provider.s3.bucket.rule.0.message':
|
||||
'S3 Bucket为必填项',
|
||||
};
|
||||
|
|
|
@ -33,10 +33,7 @@ import cn.topiam.employee.common.jackjson.encrypt.EncryptionModule;
|
|||
import cn.topiam.employee.common.storage.StorageConfig;
|
||||
import cn.topiam.employee.common.storage.StorageProviderException;
|
||||
import cn.topiam.employee.common.storage.enums.StorageProvider;
|
||||
import cn.topiam.employee.common.storage.impl.AliYunOssStorage;
|
||||
import cn.topiam.employee.common.storage.impl.MinIoStorage;
|
||||
import cn.topiam.employee.common.storage.impl.QiNiuKodoStorage;
|
||||
import cn.topiam.employee.common.storage.impl.TencentCosStorage;
|
||||
import cn.topiam.employee.common.storage.impl.*;
|
||||
import cn.topiam.employee.console.pojo.result.setting.StorageProviderConfigResult;
|
||||
import cn.topiam.employee.console.pojo.save.setting.StorageConfigSaveParam;
|
||||
import cn.topiam.employee.support.validation.ValidationUtils;
|
||||
|
@ -45,7 +42,7 @@ import jakarta.validation.ValidationException;
|
|||
import static cn.topiam.employee.core.setting.constant.StorageProviderSettingConstants.STORAGE_PROVIDER_KEY;
|
||||
|
||||
/**
|
||||
* 消息设置转换器
|
||||
* 对象存储设置转换器
|
||||
*
|
||||
* @author TopIAM
|
||||
* Created by support@topiam.cn on 2021/10/1 23:18
|
||||
|
@ -120,6 +117,21 @@ public interface StorageSettingConverter {
|
|||
unencryptedConfig.setSecretKey(param.getConfig().getString("secretKey"));
|
||||
checkStorage(MinIoStorage::new, unencryptedConfig);
|
||||
}
|
||||
//S3
|
||||
else if (provider.equals(StorageProvider.S3)) {
|
||||
S3Storage.Config config = objectMapper.readValue(param.getConfig().toJSONString(),
|
||||
S3Storage.Config.class);
|
||||
config.setEndpoint(getUrl(config.getEndpoint()));
|
||||
config.setDomain(getUrl(config.getDomain()));
|
||||
builder.config(config);
|
||||
validateEntity(ValidationUtils.validateEntity(config));
|
||||
|
||||
S3Storage.Config unencryptedConfig = new S3Storage.Config();
|
||||
BeanUtils.copyProperties(config, unencryptedConfig);
|
||||
unencryptedConfig
|
||||
.setSecretAccessKey(param.getConfig().getString("secretAccessKey"));
|
||||
checkStorage(S3Storage::new, unencryptedConfig);
|
||||
}
|
||||
entity.setName(STORAGE_PROVIDER_KEY);
|
||||
// 指定序列化输入的类型
|
||||
objectMapper.activateDefaultTyping(objectMapper.getPolymorphicTypeValidator(),
|
||||
|
|
Loading…
Reference in New Issue