mirror of
https://github.com/usememos/memos.git
synced 2025-02-19 04:40:40 +01:00
feat: update storage schema (#1142)
This commit is contained in:
parent
84fb8b2288
commit
9c5b44d070
@ -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 {
|
||||||
|
@ -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
|
||||||
}
|
}
|
||||||
|
@ -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")
|
||||||
}
|
}
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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 '{}'
|
||||||
|
143
store/storage.go
143
store/storage.go
@ -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)
|
||||||
}
|
}
|
||||||
|
|
||||||
|
@ -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",
|
||||||
|
@ -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">
|
||||||
|
@ -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) {
|
||||||
|
34
web/src/types/modules/storage.d.ts
vendored
34
web/src/types/modules/storage.d.ts
vendored
@ -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;
|
|
||||||
}
|
}
|
||||||
|
Loading…
x
Reference in New Issue
Block a user