Merge remote-tracking branch 'origin/master' into feature-app_permission

pull/42/head
smallbun 2023-09-05 19:56:23 +08:00
commit e014dd5249
8 changed files with 371 additions and 10 deletions

View File

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

View File

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

View File

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

View File

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

View File

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

View File

@ -26,6 +26,7 @@ export enum OssProvider {
QINIU_KODO = 'qiniu_kodo',
LOCAL = 'local',
MINIO = 'minio',
S3 = 's3',
}
export enum Language {
ZH = 'zh',

View File

@ -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为必填项',
};

View File

@ -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(),