mirror of
https://github.com/DrizzleTime/Foxel.git
synced 2026-07-05 06:11:48 +08:00
feat: add S3 mapping configuration and API endpoints
This commit is contained in:
@@ -287,6 +287,14 @@ export const en = {
|
||||
'Email Settings': 'Email Settings',
|
||||
'AI Settings': 'AI Settings',
|
||||
'Protocol Mappings': 'Protocol Mappings',
|
||||
'S3 Mapping': 'S3 Mapping',
|
||||
'S3 Endpoint': 'S3 Endpoint',
|
||||
'Bucket Name': 'Bucket Name',
|
||||
'Bucket API Path': 'Bucket API Path',
|
||||
'Region': 'Region',
|
||||
'Base Path': 'Base Path',
|
||||
'Access Key': 'Access Key',
|
||||
'Secret Key': 'Secret Key',
|
||||
'Vision Model': 'Vision Model',
|
||||
'Embedding Model': 'Embedding Model',
|
||||
'Embedding Dimension': 'Embedding Dimension',
|
||||
@@ -333,6 +341,14 @@ export const en = {
|
||||
'Favicon URL': 'Favicon URL',
|
||||
'App Domain': 'App Domain',
|
||||
'File Domain': 'File Domain',
|
||||
'Configure Access Key and Secret to enable S3 mapping.': 'Configure Access Key and Secret to enable S3 mapping.',
|
||||
'Mount point inside the virtual file system (e.g. / or /workspace).': 'Mount point inside the virtual file system (e.g. / or /workspace).',
|
||||
'Please input bucket name': 'Please input bucket name',
|
||||
'Please input region': 'Please input region',
|
||||
'Please input access key': 'Please input access key',
|
||||
'Please input secret key': 'Please input secret key',
|
||||
'Save S3 Settings': 'Save S3 Settings',
|
||||
'Example CLI command': 'Example CLI command',
|
||||
'WebDAV Mapping': 'WebDAV Mapping',
|
||||
'WebDAV Endpoint': 'WebDAV Endpoint',
|
||||
'Basic (system account password)': 'Basic (system account password)',
|
||||
@@ -340,7 +356,6 @@ export const en = {
|
||||
'Client Compatibility': 'Client Compatibility',
|
||||
'Supports Finder, Windows network drive, rclone, and other WebDAV clients.': 'Supports Finder, Windows network drive, rclone, and other WebDAV clients.',
|
||||
'Toggle the switch to expose the virtual file system via WebDAV.': 'Toggle the switch to expose the virtual file system via WebDAV.',
|
||||
'S3 Mapping': 'S3 Mapping',
|
||||
'SMTP Settings': 'SMTP Settings',
|
||||
'SMTP Host': 'SMTP Host',
|
||||
'Please input SMTP host': 'Please input SMTP host',
|
||||
|
||||
@@ -308,6 +308,14 @@ export const zh = {
|
||||
'Email Settings': '邮箱设置',
|
||||
'AI Settings': 'AI设置',
|
||||
'Protocol Mappings': '映射协议',
|
||||
'S3 Mapping': 'S3 映射',
|
||||
'S3 Endpoint': 'S3 访问地址',
|
||||
'Bucket Name': 'Bucket 名称',
|
||||
'Bucket API Path': 'Bucket API 路径',
|
||||
'Region': '区域',
|
||||
'Base Path': '基础路径',
|
||||
'Access Key': 'Access Key',
|
||||
'Secret Key': 'Secret Key',
|
||||
'Choose Template': '选择模板',
|
||||
'Configure Provider': '配置提供商',
|
||||
'Back to Templates': '返回选择',
|
||||
@@ -358,6 +366,14 @@ export const zh = {
|
||||
'Favicon URL': 'Favicon 地址',
|
||||
'App Domain': '应用域名',
|
||||
'File Domain': '文件域名',
|
||||
'Configure Access Key and Secret to enable S3 mapping.': '配置 Access Key 与 Secret 后才能启用 S3 映射。',
|
||||
'Mount point inside the virtual file system (e.g. / or /workspace).': '虚拟文件系统中的挂载路径,例如 / 或 /workspace。',
|
||||
'Please input bucket name': '请输入 Bucket 名',
|
||||
'Please input region': '请输入 Region',
|
||||
'Please input access key': '请输入 Access Key',
|
||||
'Please input secret key': '请输入 Secret Key',
|
||||
'Save S3 Settings': '保存 S3 配置',
|
||||
'Example CLI command': '示例 CLI 命令',
|
||||
'WebDAV Mapping': 'WebDAV 映射',
|
||||
'WebDAV Endpoint': 'WebDAV 访问地址',
|
||||
'Basic (system account password)': 'Basic(系统账号密码)',
|
||||
@@ -365,7 +381,6 @@ export const zh = {
|
||||
'Client Compatibility': '客户端兼容性',
|
||||
'Supports Finder, Windows network drive, rclone, and other WebDAV clients.': '兼容 Finder、Windows 网络驱动器、rclone 等 WebDAV 客户端。',
|
||||
'Toggle the switch to expose the virtual file system via WebDAV.': '通过开关控制是否对外暴露虚拟文件系统的 WebDAV 协议。',
|
||||
'S3 Mapping': 'S3 映射',
|
||||
'SMTP Settings': 'SMTP 配置',
|
||||
'SMTP Host': 'SMTP 服务器',
|
||||
'Please input SMTP host': '请输入 SMTP 服务器',
|
||||
|
||||
@@ -1,5 +1,5 @@
|
||||
import { useEffect, useMemo, useState } from 'react';
|
||||
import { Card, Descriptions, Space, Switch, Typography } from 'antd';
|
||||
import { Alert, Button, Card, Descriptions, Form, Input, Space, Switch, Typography } from 'antd';
|
||||
import { useI18n } from '../../../i18n';
|
||||
|
||||
interface ProtocolMappingsTabProps {
|
||||
@@ -9,6 +9,14 @@ interface ProtocolMappingsTabProps {
|
||||
}
|
||||
|
||||
const WEBDAV_KEY = 'WEBDAV_MAPPING_ENABLED';
|
||||
const S3_KEYS = {
|
||||
ENABLED: 'S3_MAPPING_ENABLED',
|
||||
BUCKET: 'S3_MAPPING_BUCKET',
|
||||
REGION: 'S3_MAPPING_REGION',
|
||||
BASE_PATH: 'S3_MAPPING_BASE_PATH',
|
||||
ACCESS_KEY: 'S3_MAPPING_ACCESS_KEY',
|
||||
SECRET_KEY: 'S3_MAPPING_SECRET_KEY',
|
||||
};
|
||||
|
||||
const truthy = new Set(['1', 'true', 'yes', 'on']);
|
||||
|
||||
@@ -16,10 +24,27 @@ export default function ProtocolMappingsTab({ config, loading, onSave }: Protoco
|
||||
const { t } = useI18n();
|
||||
const [webdavEnabled, setWebdavEnabled] = useState(() => truthy.has((config[WEBDAV_KEY] ?? '1').toLowerCase()));
|
||||
const [webdavSaving, setWebdavSaving] = useState(false);
|
||||
const [s3Enabled, setS3Enabled] = useState(() => truthy.has((config[S3_KEYS.ENABLED] ?? '1').toLowerCase()));
|
||||
const [s3ToggleSaving, setS3ToggleSaving] = useState(false);
|
||||
const [s3FormSaving, setS3FormSaving] = useState(false);
|
||||
const [s3Form] = Form.useForm();
|
||||
const watchBucket = Form.useWatch('bucket', s3Form);
|
||||
const watchRegion = Form.useWatch('region', s3Form);
|
||||
const watchBasePath = Form.useWatch('basePath', s3Form);
|
||||
const watchAccessKey = Form.useWatch('accessKey', s3Form);
|
||||
const watchSecretKey = Form.useWatch('secretKey', s3Form);
|
||||
|
||||
useEffect(() => {
|
||||
setWebdavEnabled(truthy.has((config[WEBDAV_KEY] ?? '1').toLowerCase()));
|
||||
}, [config]);
|
||||
setS3Enabled(truthy.has((config[S3_KEYS.ENABLED] ?? '1').toLowerCase()));
|
||||
s3Form.setFieldsValue({
|
||||
bucket: config[S3_KEYS.BUCKET] ?? 'foxel',
|
||||
region: config[S3_KEYS.REGION] ?? 'us-east-1',
|
||||
basePath: config[S3_KEYS.BASE_PATH] ?? '/',
|
||||
accessKey: config[S3_KEYS.ACCESS_KEY] ?? '',
|
||||
secretKey: config[S3_KEYS.SECRET_KEY] ?? '',
|
||||
});
|
||||
}, [config, s3Form]);
|
||||
|
||||
const webdavEndpoint = useMemo(() => {
|
||||
const configured = (config.APP_DOMAIN ?? '').trim();
|
||||
@@ -34,6 +59,67 @@ export default function ProtocolMappingsTab({ config, loading, onSave }: Protoco
|
||||
return '/webdav';
|
||||
}, [config.APP_DOMAIN]);
|
||||
|
||||
const baseOrigin = useMemo(() => {
|
||||
const configured = (config.APP_DOMAIN ?? '').trim();
|
||||
if (configured) {
|
||||
const hasProtocol = configured.startsWith('http://') || configured.startsWith('https://');
|
||||
return (hasProtocol ? configured : `https://${configured}`).replace(/\/$/, '');
|
||||
}
|
||||
if (typeof window !== 'undefined') {
|
||||
return window.location.origin.replace(/\/$/, '');
|
||||
}
|
||||
return '';
|
||||
}, [config.APP_DOMAIN]);
|
||||
|
||||
const bucketValue = (watchBucket ?? config[S3_KEYS.BUCKET] ?? 'foxel').trim() || 'foxel';
|
||||
const s3Endpoint = useMemo(() => {
|
||||
if (!baseOrigin) return '/s3';
|
||||
return `${baseOrigin.replace(/\/$/, '')}/s3`;
|
||||
}, [baseOrigin]);
|
||||
const bucketApiPath = useMemo(() => `${s3Endpoint.replace(/\/$/, '')}/${encodeURIComponent(bucketValue)}`, [s3Endpoint, bucketValue]);
|
||||
|
||||
const handleToggleS3 = async (checked: boolean) => {
|
||||
setS3ToggleSaving(true);
|
||||
try {
|
||||
await onSave({ [S3_KEYS.ENABLED]: checked ? '1' : '0' });
|
||||
setS3Enabled(checked);
|
||||
} finally {
|
||||
setS3ToggleSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const normalizeBasePath = (value?: string) => {
|
||||
const trimmed = (value ?? '/').trim();
|
||||
if (!trimmed) return '/';
|
||||
if (!trimmed.startsWith('/')) {
|
||||
return `/${trimmed}`;
|
||||
}
|
||||
return trimmed.replace(/\/+$/, '') || '/';
|
||||
};
|
||||
|
||||
const regionValue = (watchRegion ?? config[S3_KEYS.REGION] ?? 'us-east-1').trim() || 'us-east-1';
|
||||
const basePathValue = normalizeBasePath(watchBasePath ?? config[S3_KEYS.BASE_PATH] ?? '/');
|
||||
const accessKeyValue = (watchAccessKey ?? config[S3_KEYS.ACCESS_KEY] ?? '').trim();
|
||||
const secretValue = (watchSecretKey ?? config[S3_KEYS.SECRET_KEY] ?? '').trim();
|
||||
const exampleCommand = `aws --endpoint-url ${s3Endpoint} s3 ls s3://${bucketValue}/`;
|
||||
|
||||
const handleSaveS3 = async (values: Record<string, string>) => {
|
||||
setS3FormSaving(true);
|
||||
try {
|
||||
await onSave({
|
||||
[S3_KEYS.BUCKET]: values.bucket?.trim() || 'foxel',
|
||||
[S3_KEYS.REGION]: values.region?.trim() || 'us-east-1',
|
||||
[S3_KEYS.BASE_PATH]: normalizeBasePath(values.basePath),
|
||||
[S3_KEYS.ACCESS_KEY]: values.accessKey?.trim() || '',
|
||||
[S3_KEYS.SECRET_KEY]: values.secretKey?.trim() || '',
|
||||
});
|
||||
} finally {
|
||||
setS3FormSaving(false);
|
||||
}
|
||||
};
|
||||
|
||||
const hasS3Credentials = Boolean(accessKeyValue && secretValue);
|
||||
|
||||
const handleToggleWebdav = async (checked: boolean) => {
|
||||
setWebdavSaving(true);
|
||||
try {
|
||||
@@ -94,8 +180,126 @@ export default function ProtocolMappingsTab({ config, loading, onSave }: Protoco
|
||||
</Typography.Text>
|
||||
</Card>
|
||||
|
||||
<Card title={t('S3 Mapping')}>
|
||||
<Typography.Text type="secondary">{t('Coming soon')}</Typography.Text>
|
||||
<Card
|
||||
title={t('S3 Mapping')}
|
||||
extra={(
|
||||
<Switch
|
||||
checked={s3Enabled}
|
||||
loading={s3ToggleSaving}
|
||||
disabled={loading}
|
||||
onChange={handleToggleS3}
|
||||
/>
|
||||
)}
|
||||
>
|
||||
<Space direction="vertical" size={16} style={{ width: '100%' }}>
|
||||
{!hasS3Credentials && (
|
||||
<Alert
|
||||
type="warning"
|
||||
message={t('Configure Access Key and Secret to enable S3 mapping.')}
|
||||
showIcon
|
||||
/>
|
||||
)}
|
||||
<Descriptions
|
||||
column={1}
|
||||
size="small"
|
||||
items={[
|
||||
{
|
||||
key: 'endpoint',
|
||||
label: t('S3 Endpoint'),
|
||||
children: (
|
||||
<Typography.Text copyable={{ text: s3Endpoint }}>
|
||||
<code>{s3Endpoint}</code>
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'bucket',
|
||||
label: t('Bucket Name'),
|
||||
children: bucketValue,
|
||||
},
|
||||
{
|
||||
key: 'bucket-path',
|
||||
label: t('Bucket API Path'),
|
||||
children: (
|
||||
<Typography.Text copyable={{ text: bucketApiPath }}>
|
||||
<code>{bucketApiPath}</code>
|
||||
</Typography.Text>
|
||||
),
|
||||
},
|
||||
{
|
||||
key: 'region',
|
||||
label: t('Region'),
|
||||
children: regionValue,
|
||||
},
|
||||
{
|
||||
key: 'base-path',
|
||||
label: t('Base Path'),
|
||||
children: basePathValue,
|
||||
},
|
||||
{
|
||||
key: 'access',
|
||||
label: t('Access Key'),
|
||||
children: accessKeyValue ? (
|
||||
<Typography.Text copyable={{ text: accessKeyValue }}>{accessKeyValue}</Typography.Text>
|
||||
) : t('Not set'),
|
||||
},
|
||||
]}
|
||||
/>
|
||||
<Form
|
||||
form={s3Form}
|
||||
layout="vertical"
|
||||
onFinish={handleSaveS3}
|
||||
disabled={!s3Enabled || loading}
|
||||
style={{ width: '100%' }}
|
||||
>
|
||||
<Form.Item
|
||||
name="bucket"
|
||||
label={t('Bucket Name')}
|
||||
rules={[{ required: true, message: t('Please input bucket name') }]}
|
||||
>
|
||||
<Input disabled={!s3Enabled || loading} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="region"
|
||||
label={t('Region')}
|
||||
rules={[{ required: true, message: t('Please input region') }]}
|
||||
>
|
||||
<Input disabled={!s3Enabled || loading} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="basePath"
|
||||
label={t('Base Path')}
|
||||
tooltip={t('Mount point inside the virtual file system (e.g. / or /workspace).')}
|
||||
>
|
||||
<Input disabled={!s3Enabled || loading} placeholder="/" />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="accessKey"
|
||||
label={t('Access Key')}
|
||||
rules={[{ required: true, message: t('Please input access key') }]}
|
||||
>
|
||||
<Input disabled={!s3Enabled || loading} />
|
||||
</Form.Item>
|
||||
<Form.Item
|
||||
name="secretKey"
|
||||
label={t('Secret Key')}
|
||||
rules={[{ required: true, message: t('Please input secret key') }]}
|
||||
>
|
||||
<Input.Password disabled={!s3Enabled || loading} />
|
||||
</Form.Item>
|
||||
<Form.Item>
|
||||
<Button type="primary" htmlType="submit" loading={s3FormSaving} disabled={!s3Enabled} block>
|
||||
{t('Save S3 Settings')}
|
||||
</Button>
|
||||
</Form.Item>
|
||||
</Form>
|
||||
<Typography.Paragraph type="secondary">
|
||||
{t('Example CLI command')}
|
||||
<Typography.Text code style={{ display: 'block', marginTop: 8 }} copyable={{ text: exampleCommand }}>
|
||||
{exampleCommand}
|
||||
</Typography.Text>
|
||||
</Typography.Paragraph>
|
||||
</Space>
|
||||
</Card>
|
||||
</Space>
|
||||
);
|
||||
|
||||
Reference in New Issue
Block a user