FEATURE (s3): Add support of virtual-styled-domains and S3 prefix

This commit is contained in:
Rostislav Dugin
2025-11-16 11:22:03 +03:00
parent 0bc93389cc
commit 408675023a
18 changed files with 321 additions and 101 deletions

File diff suppressed because one or more lines are too long

Before

Width:  |  Height:  |  Size: 22 KiB

After

Width:  |  Height:  |  Size: 34 KiB

View File

@@ -22,6 +22,9 @@ type S3Storage struct {
S3AccessKey string `json:"s3AccessKey" gorm:"not null;type:text;column:s3_access_key"`
S3SecretKey string `json:"s3SecretKey" gorm:"not null;type:text;column:s3_secret_key"`
S3Endpoint string `json:"s3Endpoint" gorm:"type:text;column:s3_endpoint"`
S3Prefix string `json:"s3Prefix" gorm:"type:text;column:s3_prefix"`
S3UseVirtualHostedStyle bool `json:"s3UseVirtualHostedStyle" gorm:"default:false;column:s3_use_virtual_hosted_style"`
}
func (s *S3Storage) TableName() string {
@@ -34,11 +37,13 @@ func (s *S3Storage) SaveFile(logger *slog.Logger, fileID uuid.UUID, file io.Read
return err
}
objectKey := s.buildObjectKey(fileID.String())
// Upload the file using MinIO client with streaming (size = -1 for unknown size)
_, err = client.PutObject(
context.TODO(),
s.S3Bucket,
fileID.String(),
objectKey,
file,
-1,
minio.PutObjectOptions{},
@@ -56,10 +61,12 @@ func (s *S3Storage) GetFile(fileID uuid.UUID) (io.ReadCloser, error) {
return nil, err
}
objectKey := s.buildObjectKey(fileID.String())
object, err := client.GetObject(
context.TODO(),
s.S3Bucket,
fileID.String(),
objectKey,
minio.GetObjectOptions{},
)
if err != nil {
@@ -90,11 +97,13 @@ func (s *S3Storage) DeleteFile(fileID uuid.UUID) error {
return err
}
objectKey := s.buildObjectKey(fileID.String())
// Delete the object using MinIO client
err = client.RemoveObject(
context.TODO(),
s.S3Bucket,
fileID.String(),
objectKey,
minio.RemoveObjectOptions{},
)
if err != nil {
@@ -150,6 +159,7 @@ func (s *S3Storage) TestConnection() error {
// Test write and delete permissions by uploading and removing a small test file
testFileID := uuid.New().String() + "-test"
testObjectKey := s.buildObjectKey(testFileID)
testData := []byte("test connection")
testReader := bytes.NewReader(testData)
@@ -157,7 +167,7 @@ func (s *S3Storage) TestConnection() error {
_, err = client.PutObject(
ctx,
s.S3Bucket,
testFileID,
testObjectKey,
testReader,
int64(len(testData)),
minio.PutObjectOptions{},
@@ -170,7 +180,7 @@ func (s *S3Storage) TestConnection() error {
err = client.RemoveObject(
ctx,
s.S3Bucket,
testFileID,
testObjectKey,
minio.RemoveObjectOptions{},
)
if err != nil {
@@ -189,6 +199,7 @@ func (s *S3Storage) Update(incoming *S3Storage) {
s.S3Bucket = incoming.S3Bucket
s.S3Region = incoming.S3Region
s.S3Endpoint = incoming.S3Endpoint
s.S3UseVirtualHostedStyle = incoming.S3UseVirtualHostedStyle
if incoming.S3AccessKey != "" {
s.S3AccessKey = incoming.S3AccessKey
@@ -197,6 +208,24 @@ func (s *S3Storage) Update(incoming *S3Storage) {
if incoming.S3SecretKey != "" {
s.S3SecretKey = incoming.S3SecretKey
}
// we do not allow to change the prefix after creation,
// otherwise we will have to migrate all the data to the new prefix
}
func (s *S3Storage) buildObjectKey(fileName string) string {
if s.S3Prefix == "" {
return fileName
}
prefix := s.S3Prefix
prefix = strings.TrimPrefix(prefix, "/")
if !strings.HasSuffix(prefix, "/") {
prefix = prefix + "/"
}
return prefix + fileName
}
func (s *S3Storage) getClient() (*minio.Client, error) {
@@ -215,11 +244,18 @@ func (s *S3Storage) getClient() (*minio.Client, error) {
endpoint = fmt.Sprintf("s3.%s.amazonaws.com", s.S3Region)
}
// Configure bucket lookup strategy
bucketLookup := minio.BucketLookupAuto
if s.S3UseVirtualHostedStyle {
bucketLookup = minio.BucketLookupDNS
}
// Initialize the MinIO client
minioClient, err := minio.New(endpoint, &minio.Options{
Creds: credentials.NewStaticV4(s.S3AccessKey, s.S3SecretKey, ""),
Secure: useSSL,
Region: s.S3Region,
Creds: credentials.NewStaticV4(s.S3AccessKey, s.S3SecretKey, ""),
Secure: useSSL,
Region: s.S3Region,
BucketLookup: bucketLookup,
})
if err != nil {
return nil, fmt.Errorf("failed to initialize MinIO client: %w", err)

View File

@@ -0,0 +1,17 @@
-- +goose Up
-- +goose StatementBegin
ALTER TABLE s3_storages
ADD COLUMN s3_prefix TEXT;
ALTER TABLE s3_storages
ADD COLUMN s3_use_virtual_hosted_style BOOLEAN NOT NULL DEFAULT FALSE;
-- +goose StatementEnd
-- +goose Down
-- +goose StatementBegin
ALTER TABLE s3_storages
DROP COLUMN s3_use_virtual_hosted_style;
ALTER TABLE s3_storages
DROP COLUMN s3_prefix;
-- +goose StatementEnd

View File

@@ -4,4 +4,6 @@ export interface S3Storage {
s3AccessKey: string;
s3SecretKey: string;
s3Endpoint?: string;
s3Prefix?: string;
s3UseVirtualHostedStyle?: boolean;
}

View File

@@ -16,7 +16,7 @@ import { EditDatabaseSpecificDataComponent } from './edit/EditDatabaseSpecificDa
interface Props {
workspaceId: string;
onCreated: () => void;
onCreated: (databaseId: string) => void;
onClose: () => void;
}
@@ -58,7 +58,7 @@ export const CreateDatabaseComponent = ({ workspaceId, onCreated, onClose }: Pro
await backupsApi.makeBackup(createdDatabase.id);
}
onCreated();
onCreated(createdDatabase.id);
onClose();
} catch (error) {
alert(error);

View File

@@ -14,6 +14,8 @@ interface Props {
isCanManageDBs: boolean;
}
const SELECTED_DATABASE_STORAGE_KEY = 'selectedDatabaseId';
export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }: Props) => {
const [isLoading, setIsLoading] = useState(true);
const [databases, setDatabases] = useState<Database[]>([]);
@@ -22,7 +24,16 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }:
const [isShowAddDatabase, setIsShowAddDatabase] = useState(false);
const [selectedDatabaseId, setSelectedDatabaseId] = useState<string | undefined>(undefined);
const loadDatabases = (isSilent = false) => {
const updateSelectedDatabaseId = (databaseId: string | undefined) => {
setSelectedDatabaseId(databaseId);
if (databaseId) {
localStorage.setItem(`${SELECTED_DATABASE_STORAGE_KEY}_${workspace.id}`, databaseId);
} else {
localStorage.removeItem(`${SELECTED_DATABASE_STORAGE_KEY}_${workspace.id}`);
}
};
const loadDatabases = (isSilent = false, selectDatabaseId?: string) => {
if (!isSilent) {
setIsLoading(true);
}
@@ -31,8 +42,17 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }:
.getDatabases(workspace.id)
.then((databases) => {
setDatabases(databases);
if (!selectedDatabaseId && !isSilent) {
setSelectedDatabaseId(databases[0]?.id);
if (selectDatabaseId) {
updateSelectedDatabaseId(selectDatabaseId);
} else if (!selectedDatabaseId && !isSilent) {
const savedDatabaseId = localStorage.getItem(
`${SELECTED_DATABASE_STORAGE_KEY}_${workspace.id}`,
);
const databaseToSelect =
savedDatabaseId && databases.some((db) => db.id === savedDatabaseId)
? savedDatabaseId
: databases[0]?.id;
updateSelectedDatabaseId(databaseToSelect);
}
})
.catch((e) => alert(e.message))
@@ -95,7 +115,7 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }:
key={database.id}
database={database}
selectedDatabaseId={selectedDatabaseId}
setSelectedDatabaseId={setSelectedDatabaseId}
setSelectedDatabaseId={updateSelectedDatabaseId}
/>
))
: searchQuery && (
@@ -119,10 +139,11 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }:
loadDatabases();
}}
onDatabaseDeleted={() => {
loadDatabases();
setSelectedDatabaseId(
databases.filter((database) => database.id !== selectedDatabaseId)[0]?.id,
const remainingDatabases = databases.filter(
(database) => database.id !== selectedDatabaseId,
);
updateSelectedDatabaseId(remainingDatabases[0]?.id);
loadDatabases();
}}
isCanManageDBs={isCanManageDBs}
/>
@@ -141,8 +162,8 @@ export const DatabasesComponent = ({ contentHeight, workspace, isCanManageDBs }:
<CreateDatabaseComponent
workspaceId={workspace.id}
onCreated={() => {
loadDatabases();
onCreated={(databaseId) => {
loadDatabases(false, databaseId);
setIsShowAddDatabase(false);
}}
onClose={() => setIsShowAddDatabase(false)}

View File

@@ -255,7 +255,10 @@ export function EditNotifierComponent({
<EditTelegramNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestNotificationSuccess(false);
}}
/>
)}
@@ -263,7 +266,10 @@ export function EditNotifierComponent({
<EditEmailNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestNotificationSuccess(false);
}}
/>
)}
@@ -271,7 +277,10 @@ export function EditNotifierComponent({
<EditWebhookNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestNotificationSuccess(false);
}}
/>
)}
@@ -279,7 +288,10 @@ export function EditNotifierComponent({
<EditSlackNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestNotificationSuccess(false);
}}
/>
)}
@@ -287,14 +299,20 @@ export function EditNotifierComponent({
<EditDiscordNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestNotificationSuccess(false);
}}
/>
)}
{notifier?.notifierType === NotifierType.TEAMS && (
<EditTeamsNotifierComponent
notifier={notifier}
setNotifier={setNotifier}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestNotificationSuccess(false);
}}
/>
)}
</div>

View File

@@ -5,10 +5,10 @@ import type { Notifier } from '../../../../../entity/notifiers';
interface Props {
notifier: Notifier;
setNotifier: (notifier: Notifier) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditDiscordNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) {
export function EditDiscordNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
return (
<>
<div className="flex">
@@ -26,7 +26,7 @@ export function EditDiscordNotifierComponent({ notifier, setNotifier, setIsUnsav
channelWebhookUrl: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"

View File

@@ -6,10 +6,10 @@ import type { Notifier } from '../../../../../entity/notifiers';
interface Props {
notifier: Notifier;
setNotifier: (notifier: Notifier) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) {
export function EditEmailNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
return (
<>
<div className="mb-1 flex items-center">
@@ -26,7 +26,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
targetEmail: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -52,7 +52,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
smtpHost: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -75,7 +75,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
smtpPort: Number(e.target.value),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -97,7 +97,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
smtpUser: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -120,7 +120,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
smtpPassword: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -142,7 +142,7 @@ export function EditEmailNotifierComponent({ notifier, setNotifier, setIsUnsaved
from: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"

View File

@@ -5,10 +5,10 @@ import type { Notifier } from '../../../../../entity/notifiers';
interface Props {
notifier: Notifier;
setNotifier: (notifier: Notifier) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) {
export function EditSlackNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
return (
<>
<div className="mb-1 ml-[130px] max-w-[200px]" style={{ lineHeight: 1 }}>
@@ -38,7 +38,7 @@ export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved
botToken: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"
@@ -63,7 +63,7 @@ export function EditSlackNotifierComponent({ notifier, setNotifier, setIsUnsaved
targetChatId: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"

View File

@@ -7,10 +7,10 @@ import type { Notifier } from '../../../../../entity/notifiers';
interface Props {
notifier: Notifier;
setNotifier: (notifier: Notifier) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) {
export function EditTeamsNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
const value = notifier?.teamsNotifier?.powerAutomateUrl || '';
const onChange = (e: React.ChangeEvent<HTMLInputElement>) => {
@@ -22,7 +22,7 @@ export function EditTeamsNotifierComponent({ notifier, setNotifier, setIsUnsaved
powerAutomateUrl,
},
});
setIsUnsaved(true);
setUnsaved();
};
return (

View File

@@ -7,10 +7,10 @@ import type { Notifier } from '../../../../../entity/notifiers';
interface Props {
notifier: Notifier;
setNotifier: (notifier: Notifier) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) {
export function EditTelegramNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
const [isShowHowToGetChatId, setIsShowHowToGetChatId] = useState(false);
useEffect(() => {
@@ -42,7 +42,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa
botToken: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"
@@ -78,7 +78,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa
targetChatId: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"
@@ -137,7 +137,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa
threadId: checked ? notifier.telegramNotifier.threadId : undefined,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
/>
@@ -171,7 +171,7 @@ export function EditTelegramNotifierComponent({ notifier, setNotifier, setIsUnsa
threadId: !isNaN(threadId!) ? threadId : undefined,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"

View File

@@ -7,10 +7,10 @@ import { WebhookMethod } from '../../../../../entity/notifiers/models/webhook/We
interface Props {
notifier: Notifier;
setNotifier: (notifier: Notifier) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsaved }: Props) {
export function EditWebhookNotifierComponent({ notifier, setNotifier, setUnsaved }: Props) {
return (
<>
<div className="flex items-center">
@@ -27,7 +27,7 @@ export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsav
webhookUrl: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"
@@ -50,7 +50,7 @@ export function EditWebhookNotifierComponent({ notifier, setNotifier, setIsUnsav
webhookMethod: value,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full"

View File

@@ -250,7 +250,10 @@ export function EditStorageComponent({
<EditS3StorageComponent
storage={storage}
setStorage={setStorage}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestConnectionSuccess(false);
}}
/>
)}
@@ -258,7 +261,10 @@ export function EditStorageComponent({
<EditGoogleDriveStorageComponent
storage={storage}
setStorage={setStorage}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestConnectionSuccess(false);
}}
/>
)}
@@ -266,7 +272,10 @@ export function EditStorageComponent({
<EditNASStorageComponent
storage={storage}
setStorage={setStorage}
setIsUnsaved={setIsUnsaved}
setUnsaved={() => {
setIsUnsaved(true);
setIsTestConnectionSuccess(false);
}}
/>
)}
</div>

View File

@@ -7,10 +7,10 @@ import type { StorageOauthDto } from '../../../../../entity/storages/models/Stor
interface Props {
storage: Storage;
setStorage: (storage: Storage) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsaved }: Props) {
export function EditGoogleDriveStorageComponent({ storage, setStorage, setUnsaved }: Props) {
const goToAuthUrl = () => {
if (!storage?.googleDriveStorage?.clientId || !storage?.googleDriveStorage?.clientSecret) {
return;
@@ -60,7 +60,7 @@ export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsa
clientId: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -83,7 +83,7 @@ export function EditGoogleDriveStorageComponent({ storage, setStorage, setIsUnsa
clientSecret: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"

View File

@@ -6,10 +6,10 @@ import type { Storage } from '../../../../../entity/storages';
interface Props {
storage: Storage;
setStorage: (storage: Storage) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: Props) {
export function EditNASStorageComponent({ storage, setStorage, setUnsaved }: Props) {
return (
<>
<div className="mb-1 flex items-center">
@@ -26,7 +26,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
host: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -48,7 +48,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
port: value,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -72,7 +72,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
share: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -94,7 +94,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
username: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -116,7 +116,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
password: e.target.value,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -138,7 +138,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
useSsl: checked,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
/>
@@ -162,7 +162,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
domain: e.target.value.trim() || undefined,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -197,7 +197,7 @@ export function EditNASStorageComponent({ storage, setStorage, setIsUnsaved }: P
path: pathValue || undefined,
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"

View File

@@ -1,15 +1,20 @@
import { InfoCircleOutlined } from '@ant-design/icons';
import { Input, Tooltip } from 'antd';
import { DownOutlined, InfoCircleOutlined, UpOutlined } from '@ant-design/icons';
import { Checkbox, Input, Tooltip } from 'antd';
import { useState } from 'react';
import type { Storage } from '../../../../../entity/storages';
interface Props {
storage: Storage;
setStorage: (storage: Storage) => void;
setIsUnsaved: (isUnsaved: boolean) => void;
setUnsaved: () => void;
}
export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Props) {
export function EditS3StorageComponent({ storage, setStorage, setUnsaved }: Props) {
const hasAdvancedValues =
!!storage?.s3Storage?.s3Prefix || !!storage?.s3Storage?.s3UseVirtualHostedStyle;
const [showAdvanced, setShowAdvanced] = useState(hasAdvancedValues);
return (
<>
<div className="mb-2 flex items-center">
@@ -36,7 +41,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
s3Bucket: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -58,7 +63,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
s3Region: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -67,7 +72,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
</div>
<div className="mb-1 flex items-center">
<div className="min-w-[110px]">Access Key</div>
<div className="min-w-[110px]">Access key</div>
<Input.Password
value={storage?.s3Storage?.s3AccessKey || ''}
onChange={(e) => {
@@ -80,7 +85,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
s3AccessKey: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -89,7 +94,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
</div>
<div className="mb-1 flex items-center">
<div className="min-w-[110px]">Secret Key</div>
<div className="min-w-[110px]">Secret key</div>
<Input.Password
value={storage?.s3Storage?.s3SecretKey || ''}
onChange={(e) => {
@@ -102,7 +107,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
s3SecretKey: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -124,7 +129,7 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
s3Endpoint: e.target.value.trim(),
},
});
setIsUnsaved(true);
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
@@ -138,6 +143,85 @@ export function EditS3StorageComponent({ storage, setStorage, setIsUnsaved }: Pr
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
</Tooltip>
</div>
<div className="mt-4 mb-3 flex items-center">
<div
className="flex cursor-pointer items-center text-sm text-blue-600 hover:text-blue-800"
onClick={() => setShowAdvanced(!showAdvanced)}
>
<span className="mr-2">Advanced settings</span>
{showAdvanced ? (
<UpOutlined style={{ fontSize: '12px' }} />
) : (
<DownOutlined style={{ fontSize: '12px' }} />
)}
</div>
</div>
{showAdvanced && (
<>
<div className="mb-1 flex items-center">
<div className="min-w-[110px]">Folder prefix</div>
<Input
value={storage?.s3Storage?.s3Prefix || ''}
onChange={(e) => {
if (!storage?.s3Storage) return;
setStorage({
...storage,
s3Storage: {
...storage.s3Storage,
s3Prefix: e.target.value.trim(),
},
});
setUnsaved();
}}
size="small"
className="w-full max-w-[250px]"
placeholder="my-prefix/ (optional)"
/>
<Tooltip
className="cursor-pointer"
title="Optional prefix for all object keys (e.g., 'backups/' or 'my_team/'). May not work with some S3-compatible storages."
>
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
</Tooltip>
</div>
<div className="mb-1 flex items-center">
<div className="min-w-[110px]">Virtual host</div>
<Checkbox
checked={storage?.s3Storage?.s3UseVirtualHostedStyle || false}
onChange={(e) => {
if (!storage?.s3Storage) return;
setStorage({
...storage,
s3Storage: {
...storage.s3Storage,
s3UseVirtualHostedStyle: e.target.checked,
},
});
setUnsaved();
}}
>
Use virtual-styled domains
</Checkbox>
<Tooltip
className="cursor-pointer"
title="Use virtual-hosted-style URLs (bucket.s3.region.amazonaws.com) instead of path-style (s3.region.amazonaws.com/bucket). May be required if you see COS errors."
>
<InfoCircleOutlined className="ml-2" style={{ color: 'gray' }} />
</Tooltip>
</div>
</>
)}
<div className="mb-5" />
</>
);
}

View File

@@ -31,6 +31,20 @@ export function ShowS3StorageComponent({ storage }: Props) {
<div className="min-w-[110px]">Endpoint</div>
{storage?.s3Storage?.s3Endpoint || '-'}
</div>
{storage?.s3Storage?.s3Prefix && (
<div className="mb-1 flex items-center">
<div className="min-w-[110px]">Prefix</div>
{storage.s3Storage.s3Prefix}
</div>
)}
{storage?.s3Storage?.s3UseVirtualHostedStyle && (
<div className="mb-1 flex items-center">
<div className="min-w-[110px]">Virtual Host</div>
Enabled
</div>
)}
</>
);
}