feat: update storage schema (#1142)

This commit is contained in:
boojack 2023-02-24 00:02:51 +08:00 committed by GitHub
parent 84fb8b2288
commit 9c5b44d070
No known key found for this signature in database
GPG Key ID: 4AEE18F83AFDEB23
9 changed files with 271 additions and 229 deletions

View File

@ -1,8 +1,16 @@
package api package api
type Storage struct { type StorageType string
ID int `json:"id"`
Name string `json:"name"` const (
StorageS3 StorageType = "S3"
)
type StorageConfig struct {
S3Config *StorageS3Config `json:"s3Config"`
}
type StorageS3Config struct {
EndPoint string `json:"endPoint"` EndPoint string `json:"endPoint"`
Region string `json:"region"` Region string `json:"region"`
AccessKey string `json:"accessKey"` AccessKey string `json:"accessKey"`
@ -11,30 +19,28 @@ type Storage struct {
URLPrefix string `json:"urlPrefix"` URLPrefix string `json:"urlPrefix"`
} }
type Storage struct {
ID int `json:"id"`
Name string `json:"name"`
Type StorageType `json:"type"`
Config *StorageConfig `json:"config"`
}
type StorageCreate struct { type StorageCreate struct {
Name string `json:"name"` Name string `json:"name"`
EndPoint string `json:"endPoint"` Type StorageType `json:"type"`
Region string `json:"region"` Config *StorageConfig `json:"config"`
AccessKey string `json:"accessKey"`
SecretKey string `json:"secretKey"`
Bucket string `json:"bucket"`
URLPrefix string `json:"urlPrefix"`
} }
type StoragePatch struct { type StoragePatch struct {
ID int `json:"id"` ID int `json:"id"`
Name *string `json:"name"` Type StorageType `json:"type"`
EndPoint *string `json:"endPoint"` Name *string `json:"name"`
Region *string `json:"region"` Config *StorageConfig `json:"config"`
AccessKey *string `json:"accessKey"`
SecretKey *string `json:"secretKey"`
Bucket *string `json:"bucket"`
URLPrefix *string `json:"urlPrefix"`
} }
type StorageFind struct { type StorageFind struct {
ID *int `json:"id"` ID *int `json:"id"`
Name *string `json:"name"`
} }
type StorageDelete struct { type StorageDelete struct {

View File

@ -6,31 +6,38 @@ import (
"io" "io"
"github.com/aws/aws-sdk-go-v2/aws" "github.com/aws/aws-sdk-go-v2/aws"
"github.com/aws/aws-sdk-go-v2/config" s3config "github.com/aws/aws-sdk-go-v2/config"
"github.com/aws/aws-sdk-go-v2/credentials" "github.com/aws/aws-sdk-go-v2/credentials"
"github.com/aws/aws-sdk-go-v2/feature/s3/manager" "github.com/aws/aws-sdk-go-v2/feature/s3/manager"
awss3 "github.com/aws/aws-sdk-go-v2/service/s3" awss3 "github.com/aws/aws-sdk-go-v2/service/s3"
"github.com/aws/aws-sdk-go-v2/service/s3/types" "github.com/aws/aws-sdk-go-v2/service/s3/types"
"github.com/usememos/memos/api"
) )
type Client struct { type Config struct {
Client *awss3.Client AccessKey string
BucketName string SecretKey string
URLPrefix string Bucket string
EndPoint string
Region string
URLPrefix string
} }
func NewClient(ctx context.Context, storage *api.Storage) (*Client, error) { type Client struct {
Client *awss3.Client
Config *Config
}
func NewClient(ctx context.Context, config *Config) (*Client, error) {
resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) { resolver := aws.EndpointResolverWithOptionsFunc(func(service, region string, options ...interface{}) (aws.Endpoint, error) {
return aws.Endpoint{ return aws.Endpoint{
URL: storage.EndPoint, URL: config.EndPoint,
SigningRegion: storage.Region, SigningRegion: config.Region,
}, nil }, nil
}) })
cfg, err := config.LoadDefaultConfig(ctx, cfg, err := s3config.LoadDefaultConfig(ctx,
config.WithEndpointResolverWithOptions(resolver), s3config.WithEndpointResolverWithOptions(resolver),
config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(storage.AccessKey, storage.SecretKey, "")), s3config.WithCredentialsProvider(credentials.NewStaticCredentialsProvider(config.AccessKey, config.SecretKey, "")),
) )
if err != nil { if err != nil {
return nil, err return nil, err
@ -39,16 +46,15 @@ func NewClient(ctx context.Context, storage *api.Storage) (*Client, error) {
client := awss3.NewFromConfig(cfg) client := awss3.NewFromConfig(cfg)
return &Client{ return &Client{
Client: client, Client: client,
BucketName: storage.Bucket, Config: config,
URLPrefix: storage.URLPrefix,
}, nil }, nil
} }
func (client *Client) UploadFile(ctx context.Context, filename string, fileType string, src io.Reader, storage *api.Storage) (string, error) { func (client *Client) UploadFile(ctx context.Context, filename string, fileType string, src io.Reader) (string, error) {
uploader := manager.NewUploader(client.Client) uploader := manager.NewUploader(client.Client)
resp, err := uploader.Upload(ctx, &awss3.PutObjectInput{ resp, err := uploader.Upload(ctx, &awss3.PutObjectInput{
Bucket: aws.String(client.BucketName), Bucket: aws.String(client.Config.Bucket),
Key: aws.String(filename), Key: aws.String(filename),
Body: src, Body: src,
ContentType: aws.String(fileType), ContentType: aws.String(fileType),
@ -58,10 +64,10 @@ func (client *Client) UploadFile(ctx context.Context, filename string, fileType
return "", err return "", err
} }
var link string var link string
if storage.URLPrefix == "" { if client.Config.URLPrefix == "" {
link = resp.Location link = resp.Location
} else { } else {
link = fmt.Sprintf("%s/%s", storage.URLPrefix, filename) link = fmt.Sprintf("%s/%s", client.Config.URLPrefix, filename)
} }
return link, nil return link, nil
} }

View File

@ -111,20 +111,32 @@ func (s *Server) registerResourceRoutes(g *echo.Group) {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to find storage").SetInternal(err)
} }
s3client, err := s3.NewClient(ctx, storage) if storage.Type == api.StorageS3 {
if err != nil { s3Config := storage.Config.S3Config
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to new s3 client").SetInternal(err) s3client, err := s3.NewClient(ctx, &s3.Config{
} AccessKey: s3Config.AccessKey,
SecretKey: s3Config.SecretKey,
EndPoint: s3Config.EndPoint,
Region: s3Config.Region,
Bucket: s3Config.Bucket,
URLPrefix: s3Config.URLPrefix,
})
if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to new s3 client").SetInternal(err)
}
link, err := s3client.UploadFile(ctx, filename, filetype, src, storage) link, err := s3client.UploadFile(ctx, filename, filetype, src)
if err != nil { if err != nil {
return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload via s3 client").SetInternal(err) return echo.NewHTTPError(http.StatusInternalServerError, "Failed to upload via s3 client").SetInternal(err)
} }
resourceCreate = &api.ResourceCreate{ resourceCreate = &api.ResourceCreate{
CreatorID: userID, CreatorID: userID,
Filename: filename, Filename: filename,
Type: filetype, Type: filetype,
ExternalLink: link, ExternalLink: link,
}
} else {
return echo.NewHTTPError(http.StatusInternalServerError, "Unsupported storage type")
} }
} }

View File

@ -108,19 +108,15 @@ CREATE TABLE activity (
-- storage -- storage
CREATE TABLE storage ( CREATE TABLE storage (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '' UNIQUE, name TEXT NOT NULL,
end_point TEXT NOT NULL DEFAULT '', type TEXT NOT NULL,
region TEXT NOT NULL DEFAULT '', config TEXT NOT NULL DEFAULT '{}'
access_key TEXT NOT NULL DEFAULT '',
secret_key TEXT NOT NULL DEFAULT '',
bucket TEXT NOT NULL DEFAULT '',
url_prefix TEXT NOT NULL DEFAULT ''
); );
-- idp -- idp
CREATE TABLE idp ( CREATE TABLE idp (
id INTEGER PRIMARY KEY AUTOINCREMENT, id INTEGER PRIMARY KEY AUTOINCREMENT,
name TEXT NOT NULL DEFAULT '', name TEXT NOT NULL,
type TEXT NOT NULL, type TEXT NOT NULL,
identifier_filter TEXT NOT NULL DEFAULT '', identifier_filter TEXT NOT NULL DEFAULT '',
config TEXT NOT NULL DEFAULT '{}' config TEXT NOT NULL DEFAULT '{}'

View File

@ -3,6 +3,7 @@ package store
import ( import (
"context" "context"
"database/sql" "database/sql"
"encoding/json"
"fmt" "fmt"
"strings" "strings"
@ -11,26 +12,18 @@ import (
) )
type storageRaw struct { type storageRaw struct {
ID int ID int
Name string Name string
EndPoint string Type api.StorageType
Region string Config *api.StorageConfig
AccessKey string
SecretKey string
Bucket string
URLPrefix string
} }
func (raw *storageRaw) toStorage() *api.Storage { func (raw *storageRaw) toStorage() *api.Storage {
return &api.Storage{ return &api.Storage{
ID: raw.ID, ID: raw.ID,
Name: raw.Name, Name: raw.Name,
EndPoint: raw.EndPoint, Type: raw.Type,
Region: raw.Region, Config: raw.Config,
AccessKey: raw.AccessKey,
SecretKey: raw.SecretKey,
Bucket: raw.Bucket,
URLPrefix: raw.URLPrefix,
} }
} }
@ -131,27 +124,36 @@ func (s *Store) DeleteStorage(ctx context.Context, delete *api.StorageDelete) er
} }
func createStorageRaw(ctx context.Context, tx *sql.Tx, create *api.StorageCreate) (*storageRaw, error) { func createStorageRaw(ctx context.Context, tx *sql.Tx, create *api.StorageCreate) (*storageRaw, error) {
set := []string{"name", "end_point", "region", "access_key", "secret_key", "bucket", "url_prefix"} set := []string{"name", "type", "config"}
args := []interface{}{create.Name, create.EndPoint, create.Region, create.AccessKey, create.SecretKey, create.Bucket, create.URLPrefix} args := []interface{}{create.Name, create.Type}
placeholder := []string{"?", "?", "?", "?", "?", "?", "?"} placeholder := []string{"?", "?", "?"}
var configBytes []byte
var err error
if create.Type == api.StorageS3 {
configBytes, err = json.Marshal(create.Config.S3Config)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("unsupported storage type %s", string(create.Type))
}
args = append(args, string(configBytes))
query := ` query := `
INSERT INTO storage ( INSERT INTO storage (
` + strings.Join(set, ", ") + ` ` + strings.Join(set, ", ") + `
) )
VALUES (` + strings.Join(placeholder, ",") + `) VALUES (` + strings.Join(placeholder, ",") + `)
RETURNING id, name, end_point, region, access_key, secret_key, bucket, url_prefix RETURNING id
` `
var storageRaw storageRaw storageRaw := storageRaw{
Name: create.Name,
Type: create.Type,
Config: create.Config,
}
if err := tx.QueryRowContext(ctx, query, args...).Scan( if err := tx.QueryRowContext(ctx, query, args...).Scan(
&storageRaw.ID, &storageRaw.ID,
&storageRaw.Name,
&storageRaw.EndPoint,
&storageRaw.Region,
&storageRaw.AccessKey,
&storageRaw.SecretKey,
&storageRaw.Bucket,
&storageRaw.URLPrefix,
); err != nil { ); err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
@ -164,47 +166,48 @@ func patchStorageRaw(ctx context.Context, tx *sql.Tx, patch *api.StoragePatch) (
if v := patch.Name; v != nil { if v := patch.Name; v != nil {
set, args = append(set, "name = ?"), append(args, *v) set, args = append(set, "name = ?"), append(args, *v)
} }
if v := patch.EndPoint; v != nil { if v := patch.Config; v != nil {
set, args = append(set, "end_point = ?"), append(args, *v) var configBytes []byte
var err error
if patch.Type == api.StorageS3 {
configBytes, err = json.Marshal(patch.Config.S3Config)
if err != nil {
return nil, err
}
} else {
return nil, fmt.Errorf("unsupported storage type %s", string(patch.Type))
}
set, args = append(set, "config = ?"), append(args, string(configBytes))
} }
if v := patch.Region; v != nil {
set, args = append(set, "region = ?"), append(args, *v)
}
if v := patch.AccessKey; v != nil {
set, args = append(set, "access_key = ?"), append(args, *v)
}
if v := patch.SecretKey; v != nil {
set, args = append(set, "secret_key = ?"), append(args, *v)
}
if v := patch.Bucket; v != nil {
set, args = append(set, "bucket = ?"), append(args, *v)
}
if v := patch.URLPrefix; v != nil {
set, args = append(set, "url_prefix = ?"), append(args, *v)
}
args = append(args, patch.ID) args = append(args, patch.ID)
query := ` query := `
UPDATE storage UPDATE storage
SET ` + strings.Join(set, ", ") + ` SET ` + strings.Join(set, ", ") + `
WHERE id = ? WHERE id = ?
RETURNING id, name, end_point, region, access_key, secret_key, bucket, url_prefix RETURNING id, name, type, config
` `
var storageRaw storageRaw var storageRaw storageRaw
var storageConfig string
if err := tx.QueryRowContext(ctx, query, args...).Scan( if err := tx.QueryRowContext(ctx, query, args...).Scan(
&storageRaw.ID, &storageRaw.ID,
&storageRaw.Name, &storageRaw.Name,
&storageRaw.EndPoint, &storageRaw.Type,
&storageRaw.Region, &storageConfig,
&storageRaw.AccessKey,
&storageRaw.SecretKey,
&storageRaw.Bucket,
&storageRaw.URLPrefix,
); err != nil { ); err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
if storageRaw.Type == api.StorageS3 {
s3Config := &api.StorageS3Config{}
if err := json.Unmarshal([]byte(storageConfig), s3Config); err != nil {
return nil, err
}
storageRaw.Config = &api.StorageConfig{
S3Config: s3Config,
}
} else {
return nil, fmt.Errorf("unsupported storage type %s", string(storageRaw.Type))
}
return &storageRaw, nil return &storageRaw, nil
} }
@ -215,20 +218,13 @@ func findStorageRawList(ctx context.Context, tx *sql.Tx, find *api.StorageFind)
if v := find.ID; v != nil { if v := find.ID; v != nil {
where, args = append(where, "id = ?"), append(args, *v) where, args = append(where, "id = ?"), append(args, *v)
} }
if v := find.Name; v != nil {
where, args = append(where, "name = ?"), append(args, *v)
}
query := ` query := `
SELECT SELECT
id, id,
name, name,
end_point, type,
region, config
access_key,
secret_key,
bucket,
url_prefix
FROM storage FROM storage
WHERE ` + strings.Join(where, " AND ") + ` WHERE ` + strings.Join(where, " AND ") + `
ORDER BY id DESC ORDER BY id DESC
@ -242,19 +238,26 @@ func findStorageRawList(ctx context.Context, tx *sql.Tx, find *api.StorageFind)
storageRawList := make([]*storageRaw, 0) storageRawList := make([]*storageRaw, 0)
for rows.Next() { for rows.Next() {
var storageRaw storageRaw var storageRaw storageRaw
var storageConfig string
if err := rows.Scan( if err := rows.Scan(
&storageRaw.ID, &storageRaw.ID,
&storageRaw.Name, &storageRaw.Name,
&storageRaw.EndPoint, &storageRaw.Type,
&storageRaw.Region, &storageConfig,
&storageRaw.AccessKey,
&storageRaw.SecretKey,
&storageRaw.Bucket,
&storageRaw.URLPrefix,
); err != nil { ); err != nil {
return nil, FormatError(err) return nil, FormatError(err)
} }
if storageRaw.Type == api.StorageS3 {
s3Config := &api.StorageS3Config{}
if err := json.Unmarshal([]byte(storageConfig), s3Config); err != nil {
return nil, err
}
storageRaw.Config = &api.StorageConfig{
S3Config: s3Config,
}
} else {
return nil, fmt.Errorf("unsupported storage type %s", string(storageRaw.Type))
}
storageRawList = append(storageRawList, &storageRaw) storageRawList = append(storageRawList, &storageRaw)
} }

View File

@ -7,15 +7,18 @@ import Icon from "./Icon";
import toastHelper from "./Toast"; import toastHelper from "./Toast";
interface Props extends DialogProps { interface Props extends DialogProps {
storage?: Storage; storage?: ObjectStorage;
confirmCallback?: () => void; confirmCallback?: () => void;
} }
const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => { const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
const { destroy, storage, confirmCallback } = props; const { destroy, storage, confirmCallback } = props;
const { t } = useTranslation(); const { t } = useTranslation();
const [storageCreate, setStorageCreate] = useState<StorageCreate>({ const [basicInfo, setBasicInfo] = useState({
name: "", name: "",
});
const [type, setType] = useState<StorageType>("S3");
const [s3Config, setS3Config] = useState<StorageS3Config>({
endPoint: "", endPoint: "",
region: "", region: "",
accessKey: "", accessKey: "",
@ -27,7 +30,13 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
useEffect(() => { useEffect(() => {
if (storage) { if (storage) {
setStorageCreate({ ...storage }); setBasicInfo({
name: storage.name,
});
setType(storage.type);
if (storage.type === "S3") {
setS3Config(storage.config.s3Config);
}
} }
}, []); }, []);
@ -36,27 +45,35 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
}; };
const allowConfirmAction = () => { const allowConfirmAction = () => {
if ( if (basicInfo.name === "") {
storageCreate.name === "" ||
storageCreate.endPoint === "" ||
storageCreate.region === "" ||
storageCreate.accessKey === "" ||
storageCreate.bucket === "" ||
storageCreate.bucket === ""
) {
return false; return false;
} }
if (type === "S3") {
if (s3Config.endPoint === "" || s3Config.region === "" || s3Config.accessKey === "" || s3Config.bucket === "") {
return false;
}
}
return true; return true;
}; };
const handleConfirmBtnClick = async () => { const handleConfirmBtnClick = async () => {
try { try {
if (isCreating) { if (isCreating) {
await api.createStorage(storageCreate); await api.createStorage({
...basicInfo,
type: type,
config: {
s3Config: s3Config,
},
});
} else { } else {
await api.patchStorage({ await api.patchStorage({
id: storage.id, id: storage.id,
...storageCreate, type: type,
...basicInfo,
config: {
s3Config: s3Config,
},
}); });
} }
} catch (error: any) { } catch (error: any) {
@ -69,59 +86,10 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
destroy(); destroy();
}; };
const handleNameChange = (event: React.ChangeEvent<HTMLInputElement>) => { const setPartialS3Config = (state: Partial<StorageS3Config>) => {
const name = event.target.value; setS3Config({
setStorageCreate({ ...s3Config,
...storageCreate, ...state,
name,
});
};
const handleEndPointChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const endPoint = event.target.value;
setStorageCreate({
...storageCreate,
endPoint,
});
};
const handleRegionChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const region = event.target.value;
setStorageCreate({
...storageCreate,
region,
});
};
const handleAccessKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const accessKey = event.target.value;
setStorageCreate({
...storageCreate,
accessKey,
});
};
const handleSecretKeyChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const secretKey = event.target.value;
setStorageCreate({
...storageCreate,
secretKey,
});
};
const handleBucketChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const bucket = event.target.value;
setStorageCreate({
...storageCreate,
bucket,
});
};
const handleURLPrefixChange = (event: React.ChangeEvent<HTMLInputElement>) => {
const urlPrefix = event.target.value;
setStorageCreate({
...storageCreate,
urlPrefix,
}); });
}; };
@ -140,37 +108,84 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
Name Name
<span className="text-sm text-gray-400 ml-1">(Unique identifier)</span> <span className="text-sm text-gray-400 ml-1">(Unique identifier)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="Name" value={storageCreate.name} onChange={handleNameChange} fullWidth /> <Input
className="mb-2"
placeholder="Name"
value={basicInfo.name}
onChange={(e) =>
setBasicInfo({
...basicInfo,
name: e.target.value,
})
}
fullWidth
/>
<Typography className="!mb-1" level="body2"> <Typography className="!mb-1" level="body2">
EndPoint EndPoint
<span className="text-sm text-gray-400 ml-1">(S3-compatible server URL)</span> <span className="text-sm text-gray-400 ml-1">(S3-compatible server URL)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="EndPoint" value={storageCreate.endPoint} onChange={handleEndPointChange} fullWidth /> <Input
className="mb-2"
placeholder="EndPoint"
value={s3Config.endPoint}
onChange={(e) => setPartialS3Config({ endPoint: e.target.value })}
fullWidth
/>
<Typography className="!mb-1" level="body2"> <Typography className="!mb-1" level="body2">
Region Region
<span className="text-sm text-gray-400 ml-1">(Region name)</span> <span className="text-sm text-gray-400 ml-1">(Region name)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="Region" value={storageCreate.region} onChange={handleRegionChange} fullWidth /> <Input
className="mb-2"
placeholder="Region"
value={s3Config.region}
onChange={(e) => setPartialS3Config({ region: e.target.value })}
fullWidth
/>
<Typography className="!mb-1" level="body2"> <Typography className="!mb-1" level="body2">
AccessKey AccessKey
<span className="text-sm text-gray-400 ml-1">(Access Key / Access ID)</span> <span className="text-sm text-gray-400 ml-1">(Access Key / Access ID)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="AccessKey" value={storageCreate.accessKey} onChange={handleAccessKeyChange} fullWidth /> <Input
className="mb-2"
placeholder="AccessKey"
value={s3Config.accessKey}
onChange={(e) => setPartialS3Config({ accessKey: e.target.value })}
fullWidth
/>
<Typography className="!mb-1" level="body2"> <Typography className="!mb-1" level="body2">
SecretKey SecretKey
<span className="text-sm text-gray-400 ml-1">(Secret Key / Secret Access Key)</span> <span className="text-sm text-gray-400 ml-1">(Secret Key / Secret Access Key)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="SecretKey" value={storageCreate.secretKey} onChange={handleSecretKeyChange} fullWidth /> <Input
className="mb-2"
placeholder="SecretKey"
value={s3Config.secretKey}
onChange={(e) => setPartialS3Config({ secretKey: e.target.value })}
fullWidth
/>
<Typography className="!mb-1" level="body2"> <Typography className="!mb-1" level="body2">
Bucket Bucket
<span className="text-sm text-gray-400 ml-1">(Bucket name)</span> <span className="text-sm text-gray-400 ml-1">(Bucket name)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="Bucket" value={storageCreate.bucket} onChange={handleBucketChange} fullWidth /> <Input
className="mb-2"
placeholder="Bucket"
value={s3Config.bucket}
onChange={(e) => setPartialS3Config({ bucket: e.target.value })}
fullWidth
/>
<Typography className="!mb-1" level="body2"> <Typography className="!mb-1" level="body2">
URLPrefix URLPrefix
<span className="text-sm text-gray-400 ml-1">(Custom URL prefix; Optional)</span> <span className="text-sm text-gray-400 ml-1">(Custom URL prefix; Optional)</span>
</Typography> </Typography>
<Input className="mb-2" placeholder="URLPrefix" value={storageCreate.urlPrefix} onChange={handleURLPrefixChange} fullWidth /> <Input
className="mb-2"
placeholder="URLPrefix"
value={s3Config.urlPrefix}
onChange={(e) => setPartialS3Config({ urlPrefix: e.target.value })}
fullWidth
/>
<div className="mt-2 w-full flex flex-row justify-end items-center space-x-1"> <div className="mt-2 w-full flex flex-row justify-end items-center space-x-1">
<Button variant="plain" color="neutral" onClick={handleCloseBtnClick}> <Button variant="plain" color="neutral" onClick={handleCloseBtnClick}>
Cancel Cancel
@ -184,7 +199,7 @@ const CreateStorageServiceDialog: React.FC<Props> = (props: Props) => {
); );
}; };
function showCreateStorageServiceDialog(storage?: Storage, confirmCallback?: () => void) { function showCreateStorageServiceDialog(storage?: ObjectStorage, confirmCallback?: () => void) {
generateDialog( generateDialog(
{ {
className: "create-storage-service-dialog", className: "create-storage-service-dialog",

View File

@ -13,16 +13,12 @@ const StorageSection = () => {
const globalStore = useGlobalStore(); const globalStore = useGlobalStore();
const systemStatus = globalStore.state.systemStatus; const systemStatus = globalStore.state.systemStatus;
const [storageServiceId, setStorageServiceId] = useState(systemStatus.storageServiceId); const [storageServiceId, setStorageServiceId] = useState(systemStatus.storageServiceId);
const [storageList, setStorageList] = useState<Storage[]>([]); const [storageList, setStorageList] = useState<ObjectStorage[]>([]);
useEffect(() => { useEffect(() => {
fetchStorageList(); fetchStorageList();
}, []); }, []);
useEffect(() => {
setStorageServiceId(systemStatus.storageServiceId);
}, [systemStatus]);
const fetchStorageList = async () => { const fetchStorageList = async () => {
const { const {
data: { data: storageList }, data: { data: storageList },
@ -31,6 +27,10 @@ const StorageSection = () => {
}; };
const handleActiveStorageServiceChanged = async (storageId: StorageId) => { const handleActiveStorageServiceChanged = async (storageId: StorageId) => {
if (storageList.length === 0) {
return;
}
await api.upsertSystemSetting({ await api.upsertSystemSetting({
name: "storageServiceId", name: "storageServiceId",
value: JSON.stringify(storageId), value: JSON.stringify(storageId),
@ -38,7 +38,7 @@ const StorageSection = () => {
setStorageServiceId(storageId); setStorageServiceId(storageId);
}; };
const handleDeleteStorage = (storage: Storage) => { const handleDeleteStorage = (storage: ObjectStorage) => {
showCommonDialog({ showCommonDialog({
title: t("setting.storage-section.delete-storage"), title: t("setting.storage-section.delete-storage"),
content: t("setting.storage-section.warning-text"), content: t("setting.storage-section.warning-text"),
@ -68,12 +68,12 @@ const StorageSection = () => {
handleActiveStorageServiceChanged(storageId || 0); handleActiveStorageServiceChanged(storageId || 0);
}} }}
> >
<Option value={0}>Database</Option>
{storageList.map((storage) => ( {storageList.map((storage) => (
<Option key={storage.id} value={storage.id}> <Option key={storage.id} value={storage.id}>
{storage.name} {storage.name}
</Option> </Option>
))} ))}
<Option value={0}>Database</Option>
</Select> </Select>
<Divider /> <Divider />
<div className="mt-4 mb-2 w-full flex flex-row justify-start items-center"> <div className="mt-4 mb-2 w-full flex flex-row justify-start items-center">

View File

@ -215,15 +215,15 @@ export function deleteTag(tagName: string) {
} }
export function getStorageList() { export function getStorageList() {
return axios.get<ResponseObject<Storage[]>>(`/api/storage`); return axios.get<ResponseObject<ObjectStorage[]>>(`/api/storage`);
} }
export function createStorage(storageCreate: StorageCreate) { export function createStorage(storageCreate: StorageCreate) {
return axios.post<ResponseObject<Storage>>(`/api/storage`, storageCreate); return axios.post<ResponseObject<ObjectStorage>>(`/api/storage`, storageCreate);
} }
export function patchStorage(storagePatch: StoragePatch) { export function patchStorage(storagePatch: StoragePatch) {
return axios.patch<ResponseObject<Storage>>(`/api/storage/${storagePatch.id}`, storagePatch); return axios.patch<ResponseObject<ObjectStorage>>(`/api/storage/${storagePatch.id}`, storagePatch);
} }
export function deleteStorage(storageId: StorageId) { export function deleteStorage(storageId: StorageId) {

View File

@ -1,8 +1,8 @@
type StorageId = number; type StorageId = number;
interface Storage { type StorageType = "S3";
id: StorageId;
name: string; interface StorageS3Config {
endPoint: string; endPoint: string;
region: string; region: string;
accessKey: string; accessKey: string;
@ -11,23 +11,27 @@ interface Storage {
urlPrefix: string; urlPrefix: string;
} }
interface StorageConfig {
s3Config: StorageS3Config;
}
// Note: Storage is a reserved word in TypeScript. So we use ObjectStorage instead.
interface ObjectStorage {
id: StorageId;
name: string;
type: StorageType;
config: StorageConfig;
}
interface StorageCreate { interface StorageCreate {
name: string; name: string;
endPoint: string; type: StorageType;
region: string; config: StorageConfig;
accessKey: string;
secretKey: string;
bucket: string;
urlPrefix: string;
} }
interface StoragePatch { interface StoragePatch {
id: StorageId; id: StorageId;
name: string; name: string;
endPoint: string; type: StorageType;
region: string; config: StorageConfig;
accessKey: string;
secretKey: string;
bucket: string;
urlPrefix: string;
} }