feat: 修改S3配置的类型,并实现系统管理页面中S3相关的账号和cos配置

This commit is contained in:
Pan Qiancheng 2025-10-16 14:44:21 +08:00
parent 6ce0cf80aa
commit 88482b96c1
5 changed files with 1157 additions and 590 deletions

View File

@ -11,6 +11,140 @@ import {
LocalCloudConfig, LocalCloudConfig,
} from '../../../../types/Config'; } from '../../../../types/Config';
function S3Account(props: {
accounts: LocalCloudConfig[];
setValue: (path: string, value: any) => void;
removeItem: (path: string, index: number) => void;
addItem: (path: string, index: number) => void;
}) {
const { accounts, setValue, removeItem, addItem } = props;
return (
<Col flex="auto">
<Divider orientation="left" className={Styles.title}>
S3配置
</Divider>
<Tabs
tabPosition={'top'}
size={'middle'}
type="editable-card"
hideAdd={!(accounts.length > 0)}
onEdit={(targetKey: any, action: 'add' | 'remove') => {
if (action === 'add') {
addItem('', accounts.length);
} else {
removeItem('', parseInt(targetKey, 10));
}
}}
items={
accounts.length > 0
? accounts.map((ele, idx) => ({
key: `${idx}`,
label: `帐号${idx + 1}`,
children: (
<Form
colon={false}
labelAlign="left"
layout="vertical"
style={{ marginTop: 10 }}
>
<Form.Item
label="accessKey"
//name="accessKey"
>
<>
<Input
placeholder="请输入accessKey"
type="text"
value={ele.accessKey}
onChange={(e) =>
setValue(
`${idx}.accessKey`,
e.target.value
)
}
/>
</>
</Form.Item>
<Form.Item
label="secretKey"
//name="secretKey"
>
<>
<Input
placeholder="请输入secretKey"
type="text"
value={ele.secretKey}
onChange={(e) =>
setValue(
`${idx}.secretKey`,
e.target.value
)
}
/>
</>
</Form.Item>
</Form>
),
}))
: [
{
label: '新建帐号',
key: '0',
children: (
<Form
colon={true}
labelAlign="left"
layout="vertical"
style={{ marginTop: 10 }}
>
<Form.Item
label="accessKey"
// name="accessKey"
>
<>
<Input
placeholder="请输入accessKey"
type="text"
value=""
onChange={(e) =>
setValue(
`0.accessKey`,
e.target.value
)
}
/>
</>
</Form.Item>
<Form.Item
label="secretKey"
// name="secretKey"
>
<>
<Input
placeholder="请输入secretKey"
type="text"
value=""
onChange={(e) =>
setValue(
`0.secretKey`,
e.target.value
)
}
/>
</>
</Form.Item>
</Form>
),
},
]
}
></Tabs>
</Col>
);
}
function LocalAccount(props: { function LocalAccount(props: {
accounts: LocalCloudConfig[]; accounts: LocalCloudConfig[];
setValue: (path: string, value: any) => void; setValue: (path: string, value: any) => void;
@ -1035,7 +1169,7 @@ export default function Account(props: {
removeItem: (path: string, index: number) => void; removeItem: (path: string, index: number) => void;
}) { }) {
const { account, setValue, removeItem } = props; const { account, setValue, removeItem } = props;
const { tencent, qiniu, ali, amap, ctyun, local } = account; const { tencent, qiniu, ali, amap, ctyun, local, s3 } = account;
return ( return (
<Space direction="vertical" size="middle" style={{ display: 'flex' }}> <Space direction="vertical" size="middle" style={{ display: 'flex' }}>
<Row> <Row>
@ -1073,6 +1207,12 @@ export default function Account(props: {
removeItem={(path, index) => removeItem(`local`, index)} removeItem={(path, index) => removeItem(`local`, index)}
addItem={(path, index) => setValue(`local.${index}`, {})} addItem={(path, index) => setValue(`local.${index}`, {})}
/> />
<S3Account
accounts={s3 || []}
setValue={(path, value) => setValue(`s3.${path}`, value)}
removeItem={(path, index) => removeItem(`s3`, index)}
addItem={(path, index) => setValue(`s3.${index}`, {})}
/>
<AmapAccount <AmapAccount
accounts={amap || []} accounts={amap || []}
setValue={(path, value) => setValue(`amap.${path}`, value)} setValue={(path, value) => setValue(`amap.${path}`, value)}

View File

@ -12,7 +12,7 @@ import {
} from 'antd'; } from 'antd';
import Styles from './web.module.less'; import Styles from './web.module.less';
import { QiniuZone, CTYunZone, ALiYunZone, TencentYunZone } from 'oak-external-sdk'; import { QiniuZone, CTYunZone, ALiYunZone, TencentYunZone } from 'oak-external-sdk';
import { Config, QiniuCosConfig, CTYunCosConfig, ALiYunCosConfig, TencentYunCosConfig, LocalCosConfig, Protocol } from '../../../../types/Config'; import { Config, QiniuCosConfig, CTYunCosConfig, ALiYunCosConfig, TencentYunCosConfig, LocalCosConfig, Protocol, S3CosConfig } from '../../../../types/Config';
// https://developer.qiniu.com/kodo/1671/region-endpoint-fq // https://developer.qiniu.com/kodo/1671/region-endpoint-fq
const QiniuZoneArray: Array<{ label: string; value: QiniuZone }> = [ const QiniuZoneArray: Array<{ label: string; value: QiniuZone }> = [
@ -386,8 +386,7 @@ function CTYunCos(props: {
) => { ) => {
if (action === 'add') { if (action === 'add') {
setValue( setValue(
`buckets.${ `buckets.${cos?.buckets?.length || 0
cos?.buckets?.length || 0
}`, }`,
{} {}
); );
@ -707,8 +706,7 @@ function ALiYunCos(props: {
) => { ) => {
if (action === 'add') { if (action === 'add') {
setValue( setValue(
`buckets.${ `buckets.${cos?.buckets?.length || 0
cos?.buckets?.length || 0
}`, }`,
{} {}
); );
@ -1008,8 +1006,7 @@ function TencentYunCos(props: {
) => { ) => {
if (action === 'add') { if (action === 'add') {
setValue( setValue(
`buckets.${ `buckets.${cos?.buckets?.length || 0
cos?.buckets?.length || 0
}`, }`,
{} {}
); );
@ -1249,8 +1246,7 @@ function LocalCos(props: {
) => { ) => {
if (action === 'add') { if (action === 'add') {
setValue( setValue(
`buckets.${ `buckets.${cos?.buckets?.length || 0
cos?.buckets?.length || 0
}`, }`,
{} {}
); );
@ -1482,13 +1478,425 @@ function LocalCos(props: {
); );
} }
const AWSS3RegionArray: Array<{ label: string; value: string }> = [
{
label: "使用自定义endpoint",
value: "custom",
},
// 美国
{
label: '美国东部(弗吉尼亚北部)',
value: 'us-east-1',
},
{
label: '美国东部(俄亥俄)',
value: 'us-east-2',
},
{
label: '美国西部(加利福尼亚北部)',
value: 'us-west-1',
},
{
label: '美国西部(俄勒冈)',
value: 'us-west-2',
},
// 亚太地区
{
label: '亚太地区(孟买)',
value: 'ap-south-1',
},
{
label: '亚太地区(海得拉巴)',
value: 'ap-south-2',
},
{
label: '亚太地区(东京)',
value: 'ap-northeast-1',
},
{
label: '亚太地区(首尔)',
value: 'ap-northeast-2',
},
{
label: '亚太地区(大阪)',
value: 'ap-northeast-3',
},
{
label: '亚太地区(新加坡)',
value: 'ap-southeast-1',
},
{
label: '亚太地区(悉尼)',
value: 'ap-southeast-2',
},
{
label: '亚太地区(雅加达)',
value: 'ap-southeast-3',
},
{
label: '亚太地区(墨尔本)',
value: 'ap-southeast-4',
},
{
label: '亚太地区(中国香港)',
value: 'ap-east-1',
},
// 欧洲
{
label: '欧洲(爱尔兰)',
value: 'eu-west-1',
},
{
label: '欧洲(伦敦)',
value: 'eu-west-2',
},
{
label: '欧洲(巴黎)',
value: 'eu-west-3',
},
{
label: '欧洲(法兰克福)',
value: 'eu-central-1',
},
{
label: '欧洲(苏黎世)',
value: 'eu-central-2',
},
{
label: '欧洲(斯德哥尔摩)',
value: 'eu-north-1',
},
{
label: '欧洲(米兰)',
value: 'eu-south-1',
},
{
label: '欧洲(西班牙)',
value: 'eu-south-2',
},
// 加拿大
{
label: '加拿大(中部)',
value: 'ca-central-1',
},
{
label: '加拿大西部(卡尔加里)',
value: 'ca-west-1',
},
// 南美洲
{
label: '南美洲(圣保罗)',
value: 'sa-east-1',
},
// 中东
{
label: '中东(巴林)',
value: 'me-south-1',
},
{
label: '中东(阿联酋)',
value: 'me-central-1',
},
// 非洲
{
label: '非洲(开普敦)',
value: 'af-south-1',
},
// 以色列
{
label: '以色列(特拉维夫)',
value: 'il-central-1',
},
];
function S3Cos(props: {
cos: S3CosConfig;
setValue: (path: string, value: any) => void;
removeItem: (path: string, index: number) => void;
}) {
const { cos, setValue, removeItem } = props;
return (
<Col flex="auto">
<Divider orientation="left" className={Styles.title}>
S3对象存储配置
</Divider>
<Tabs
tabPosition={'top'}
size={'middle'}
type="card"
items={[
{
key: '0',
label: '配置项',
children: (
<Form
colon={true}
labelAlign="left"
layout="vertical"
style={{ marginTop: 10 }}
>
<Form.Item
label="accessKey"
//name="accessKey"
>
<>
<Input
placeholder="请输入accessKey"
type="text"
value={cos?.accessKey}
onChange={(e) =>
setValue(
`accessKey`,
e.target.value
)
}
/>
</>
</Form.Item>
<Form.Item
label="endpoint"
//name="accessKey"
rules={[
{
pattern: /^(([0-9]{1,3}\.){3}[0-9]{1,3}|[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?(\.[a-zA-Z0-9]([a-zA-Z0-9-]{0,61}[a-zA-Z0-9])?)*)(:\d{1,5})?$/,
message: '格式必须为: 域名:端口 或 IP:端口不可包含http://、https://前缀或/后缀'
}
]}
>
<>
<Input
placeholder="请输入endpoint"
type="text"
value={cos?.endpoint}
onChange={(e) =>
setValue(
`endpoint`,
e.target.value
)
}
/>
</>
</Form.Item>
{/* <Form.Item
label="bucket"
//name="bucket"
>
<>
<Input
placeholder="请输入bucket"
type="text"
value={cos?.bucket}
onChange={(e) =>
setValue(
`bucket`,
e.target.value
)
}
/>
</>
</Form.Item> */}
<Divider
orientation="left"
className={Styles.title}
>
bucket配置
</Divider>
<Tabs
tabPosition={'top'}
size={'middle'}
type="editable-card"
// hideAdd={!(sms.length > 0)}
onEdit={(
targetKey: any,
action: 'add' | 'remove'
) => {
if (action === 'add') {
setValue(
`buckets.${cos?.buckets?.length || 0
}`,
{}
);
} else {
removeItem(
'buckets',
parseInt(targetKey, 10)
);
}
}}
items={
cos?.buckets?.length > 0
? cos.buckets.map((ele, idx) => ({
key: `${idx}`,
label: `bucket:${idx + 1}`,
children: (
<Form
colon={false}
labelAlign="left"
layout="vertical"
style={{
marginTop: 10,
}}
>
<Form.Item
label="name"
// name="name"
>
<>
<Input
placeholder="请输入name"
type="text"
value={
ele.name
}
onChange={(
e
) =>
setValue(
`buckets.${idx}.name`,
e
.target
.value
)
}
/>
</>
</Form.Item>
<Form.Item
label="zone"
//name="uploadHost"
>
<>
<Select
style={{
width: '100%',
}}
placeholder="请选择存储区域"
value={
ele.zone
}
onChange={(
value: string
) => {
setValue(
`buckets.${idx}.zone`,
value
);
}}
options={
AWSS3RegionArray
}
/>
</>
</Form.Item>
<Form.Item
label="domain"
// name="domain"
>
<>
<Input
placeholder="请输入domain"
type="text"
value={
ele.domain
}
onChange={(
e
) =>
setValue(
`buckets.${idx}.domain`,
e
.target
.value
)
}
/>
</>
</Form.Item>
<Form.Item
label="protocol"
// name="protocol"
>
<>
<Select
mode="multiple"
allowClear
style={{
width: '100%',
}}
placeholder="请选择协议"
value={
ele?.protocol as Protocol[]
}
onChange={(
value: Protocol[]
) => {
setValue(
`buckets.${idx}.protocol`,
value
);
}}
options={[
{
label: 'http:',
value: 'http:',
},
{
label: 'https:',
value: 'https:',
},
]}
/>
</>
</Form.Item>
</Form>
),
}))
: []
}
/>
{cos?.buckets?.length > 0 && (
<Form.Item
label="defaultBucket"
//name="uploadHost"
>
<>
<Select
allowClear
style={{ width: '100%' }}
placeholder="请选择默认bucket"
value={cos.defaultBucket}
onChange={(value: string) => {
setValue(
`defaultBucket`,
value
);
}}
options={cos.buckets.map(
(ele) => ({
label: ele.name,
value: ele.name,
})
)}
/>
</>
</Form.Item>
)}
</Form>
),
},
]}
></Tabs>
</Col>
);
}
export default function Cos(props: { export default function Cos(props: {
cos: Required<Config>['Cos']; cos: Required<Config>['Cos'];
setValue: (path: string, value: any) => void; setValue: (path: string, value: any) => void;
removeItem: (path: string, index: number) => void; removeItem: (path: string, index: number) => void;
}) { }) {
const { cos, setValue, removeItem } = props; const { cos, setValue, removeItem } = props;
const { qiniu, ctyun, aliyun, tencent, local } = cos; const { qiniu, ctyun, aliyun, tencent, local, s3 } = cos;
return ( return (
<Space direction="vertical" size="middle" style={{ display: 'flex' }}> <Space direction="vertical" size="middle" style={{ display: 'flex' }}>
<Row> <Row>
@ -1520,6 +1928,11 @@ export default function Cos(props: {
removeItem(`tencent.${path}`, index) removeItem(`tencent.${path}`, index)
} }
/> />
<S3Cos
cos={s3!}
setValue={(path, value) => setValue(`s3.${path}`, value)}
removeItem={(path, index) => removeItem(`s3.${path}`, index)}
/>
<LocalCos <LocalCos
cos={local!} cos={local!}
setValue={(path, value) => setValue(`local.${path}`, value)} setValue={(path, value) => setValue(`local.${path}`, value)}

View File

@ -94,13 +94,12 @@ export type S3Zone =
// S3/Minio 配置类型 // S3/Minio 配置类型
export type S3CosConfig = { export type S3CosConfig = {
accessKey: string; accessKey: string;
secretKey: string; // S3 需要 secretKey endpoint?: string; // 自定义端点Minio 必需AWS S3 可选)
buckets: { buckets: {
zone?: S3Zone; // S3 区域 (某些 S3 兼容服务可能不需要) zone?: S3Zone; // S3 区域 (某些 S3 兼容服务可能不需要)
name: string; // 桶名称 name: string; // 桶名称
domain: string; // 访问域名 domain: string; // 访问域名
protocol: Protocol | Protocol[]; protocol: Protocol | Protocol[];
endpoint?: string; // 自定义端点Minio 必需AWS S3 可选)
pathStyle?: boolean; // 是否使用路径风格 URLMinio 常用) pathStyle?: boolean; // 是否使用路径风格 URLMinio 常用)
}[]; }[];
defaultBucket: string; // 默认上传桶 defaultBucket: string; // 默认上传桶

View File

@ -4,24 +4,36 @@ import { CosBackend } from '../../types/Cos';
import S3 from './s3'; import S3 from './s3';
import { OpSchema } from '../../oak-app-domain/ExtraFile/Schema'; import { OpSchema } from '../../oak-app-domain/ExtraFile/Schema';
import { S3CosConfig } from '../../types/Config'; import { S3CosConfig, S3Zone } from '../../types/Config';
import { S3Instance, S3SDK } from 'oak-external-sdk'; import { S3Instance, S3SDK } from 'oak-external-sdk';
import { OakExternalException } from 'oak-domain/lib/types/Exception'; import { OakExternalException } from 'oak-domain/lib/types/Exception';
export default class S3Backend extends S3 implements CosBackend<EntityDict> { export default class S3Backend extends S3 implements CosBackend<EntityDict> {
private getConfigAndInstance( private getConfigAndInstance(
application: EntityDict['application']['Schema'] application: EntityDict['application']['Schema'],
bucket: string | null | undefined
) { ) {
const { config, account } = this.getConfig(application); const { config, account, endpoint, defaultBucket } = this.getConfig(application);
const realBucket = bucket || defaultBucket;
assert(realBucket, '没有指定上传桶,且配置中也没有默认上传桶');
// 在配置中找名称匹配的桶
const { buckets } = config as S3CosConfig;
let bucketConfig = bucket
? buckets.find((ele) => ele.name === realBucket)
: buckets[0];
const instance = S3SDK.getInstance( const instance = S3SDK.getInstance(
account.accessKey, account.accessKey,
account.secretKey, account.secretKey,
config.buckets[0]?.endpoint, // 使用第一个 bucket 的 endpoint 作为默认 endpoint,
config.buckets[0]?.zone bucketConfig?.zone || 'us-east-1' as S3Zone
); );
return { return {
config, config,
instance, instance,
endpoint,
}; };
} }
@ -31,8 +43,7 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
) { ) {
const { bucket } = extraFile; const { bucket } = extraFile;
const key = this.formKey(extraFile); const key = this.formKey(extraFile);
const { instance, config: s3CosConfig } = const { instance, config: s3CosConfig, endpoint } = this.getConfigAndInstance(application, bucket);
this.getConfigAndInstance(application);
const { buckets } = s3CosConfig as S3CosConfig; const { buckets } = s3CosConfig as S3CosConfig;
let bucket2 = bucket; let bucket2 = bucket;
@ -48,7 +59,7 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
const uploadInfo = await (instance as S3Instance).getUploadInfo( const uploadInfo = await (instance as S3Instance).getUploadInfo(
bucket2, bucket2,
key, key,
b.endpoint, endpoint,
b.pathStyle b.pathStyle
); );
@ -65,9 +76,10 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
application: EntityDict['application']['Schema'], application: EntityDict['application']['Schema'],
extraFile: OpSchema extraFile: OpSchema
) { ) {
const { bucket } = extraFile;
const key = this.formKey(extraFile); const key = this.formKey(extraFile);
const { instance, config: s3CosConfig } = const { instance, config: s3CosConfig, endpoint } =
this.getConfigAndInstance(application); this.getConfigAndInstance(application, bucket);
const b = (s3CosConfig as S3CosConfig).buckets.find( const b = (s3CosConfig as S3CosConfig).buckets.find(
(ele) => ele.name === extraFile.bucket (ele) => ele.name === extraFile.bucket
@ -81,7 +93,7 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
const result = await (instance as S3Instance).isExistObject( const result = await (instance as S3Instance).isExistObject(
extraFile.bucket!, extraFile.bucket!,
key, key,
b.endpoint, endpoint,
b.pathStyle b.pathStyle
); );
@ -95,9 +107,10 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
application: EntityDict['application']['Schema'], application: EntityDict['application']['Schema'],
extraFile: OpSchema extraFile: OpSchema
) { ) {
const { bucket } = extraFile
const key = this.formKey(extraFile); const key = this.formKey(extraFile);
const { instance, config: s3CosConfig } = const { instance, config: s3CosConfig, endpoint } =
this.getConfigAndInstance(application); this.getConfigAndInstance(application, bucket);
const b = (s3CosConfig as S3CosConfig).buckets.find( const b = (s3CosConfig as S3CosConfig).buckets.find(
(ele) => ele.name === extraFile.bucket (ele) => ele.name === extraFile.bucket
@ -111,7 +124,7 @@ export default class S3Backend extends S3 implements CosBackend<EntityDict> {
await (instance as S3Instance).removeFile( await (instance as S3Instance).removeFile(
extraFile.bucket!, extraFile.bucket!,
key, key,
b.endpoint, endpoint,
b.pathStyle b.pathStyle
); );
} catch (err: any) { } catch (err: any) {

View File

@ -20,7 +20,7 @@ export default class S3 implements Cos<EntityDict> {
const { config } = system!; const { config } = system!;
const s3Config = config.Cos?.s3; const s3Config = config.Cos?.s3;
assert(s3Config); assert(s3Config);
const { accessKey } = s3Config; const { accessKey, endpoint, defaultBucket } = s3Config;
const account = config.Account?.s3?.find( const account = config.Account?.s3?.find(
(ele) => ele.accessKey === accessKey (ele) => ele.accessKey === accessKey
); );
@ -28,6 +28,8 @@ export default class S3 implements Cos<EntityDict> {
return { return {
config: s3Config, config: s3Config,
account, account,
endpoint,
defaultBucket,
}; };
} }
@ -90,7 +92,7 @@ export default class S3 implements Cos<EntityDict> {
extraFile: Partial<EntityDict['extraFile']['OpSchema']>, extraFile: Partial<EntityDict['extraFile']['OpSchema']>,
style?: string style?: string
) { ) {
const { config: s3CosConfig } = this.getConfig(application); const { config: s3CosConfig, endpoint } = this.getConfig(application);
if (s3CosConfig) { if (s3CosConfig) {
let bucket = ( let bucket = (
@ -98,7 +100,7 @@ export default class S3 implements Cos<EntityDict> {
).find((ele) => ele.name === extraFile.bucket!); ).find((ele) => ele.name === extraFile.bucket!);
if (bucket) { if (bucket) {
const { domain, protocol, endpoint, pathStyle } = bucket; const { domain, protocol, pathStyle } = bucket;
let protocol2 = protocol; let protocol2 = protocol;
if (protocol instanceof Array) { if (protocol instanceof Array) {
const index = (protocol as Protocol[]).includes('https:') const index = (protocol as Protocol[]).includes('https:')