feat(ops): add object storage management and site-mapping CSV import/export
Ship the Ops backstage 对象存储 page (browse / upload / move / delete / folder ops) backed by a new object-storage service + handler, with the shared storage client extended with List/Stat/Copy/Usage/DownloadURL so both MinIO and Aliyun providers cover the new surface. Add ForcePathStyle to the object-storage config and treat r2/s3/custom_s3 as external providers so Cloudflare R2 and other S3-compatibles can share the MinIO client path without spinning up the bundled MinIO. Also add CSV import/export + template download to the site-domain mappings page, raise gin MaxMultipartMemory to 8 MiB for the new multipart endpoints, register Ops swagger routes, and extend the descriptions-parity test to cover the ops router. Co-Authored-By: Claude Opus 4.7 (1M context) <noreply@anthropic.com>
This commit is contained in:
@@ -5,6 +5,7 @@ import (
|
||||
"errors"
|
||||
"fmt"
|
||||
"strings"
|
||||
"time"
|
||||
|
||||
"go.uber.org/zap"
|
||||
|
||||
@@ -13,6 +14,8 @@ import (
|
||||
|
||||
var ErrNotConfigured = errors.New("object storage is not configured")
|
||||
var ErrObjectNotFound = errors.New("object storage object not found")
|
||||
var ErrObjectNotPublic = errors.New("object storage object is not publicly readable")
|
||||
var ErrUsageUnsupported = errors.New("object storage usage stat is not supported")
|
||||
|
||||
type Client interface {
|
||||
Validate() error
|
||||
@@ -21,6 +24,57 @@ type Client interface {
|
||||
Exists(ctx context.Context, objectKey string) (bool, error)
|
||||
Delete(ctx context.Context, objectKey string) error
|
||||
PublicURL(objectKey string) (string, error)
|
||||
DownloadURL(objectKey string) (string, error)
|
||||
}
|
||||
|
||||
type ManagementClient interface {
|
||||
Client
|
||||
List(ctx context.Context, in ListInput) (*ListResult, error)
|
||||
Stat(ctx context.Context, objectKey string) (*ObjectInfo, error)
|
||||
Copy(ctx context.Context, sourceKey, destinationKey string) error
|
||||
}
|
||||
|
||||
type UsageClient interface {
|
||||
Usage(ctx context.Context) (*UsageInfo, error)
|
||||
}
|
||||
|
||||
type PublicReadableClient interface {
|
||||
PublicReadableURL(objectKey string) (string, error)
|
||||
}
|
||||
|
||||
type ListInput struct {
|
||||
Prefix string
|
||||
ContinuationToken string
|
||||
StartAfter string
|
||||
Delimiter string
|
||||
Limit int
|
||||
Recursive bool
|
||||
}
|
||||
|
||||
type ListResult struct {
|
||||
Objects []ObjectInfo `json:"objects"`
|
||||
CommonPrefixes []string `json:"common_prefixes"`
|
||||
ContinuationToken string `json:"continuation_token,omitempty"`
|
||||
NextContinuationToken string `json:"next_continuation_token,omitempty"`
|
||||
IsTruncated bool `json:"is_truncated"`
|
||||
Prefix string `json:"prefix"`
|
||||
Delimiter string `json:"delimiter"`
|
||||
}
|
||||
|
||||
type ObjectInfo struct {
|
||||
Key string `json:"key"`
|
||||
Size int64 `json:"size"`
|
||||
ETag string `json:"etag,omitempty"`
|
||||
LastModified time.Time `json:"last_modified"`
|
||||
ContentType string `json:"content_type,omitempty"`
|
||||
StorageClass string `json:"storage_class,omitempty"`
|
||||
}
|
||||
|
||||
type UsageInfo struct {
|
||||
Available bool `json:"available"`
|
||||
Provider string `json:"provider"`
|
||||
StorageSize int64 `json:"storage_size"`
|
||||
ObjectCount int64 `json:"object_count"`
|
||||
}
|
||||
|
||||
func New(cfg config.ObjectStorageConfig, logger *zap.Logger) Client {
|
||||
@@ -28,7 +82,7 @@ func New(cfg config.ObjectStorageConfig, logger *zap.Logger) Client {
|
||||
switch provider {
|
||||
case "", "disabled":
|
||||
return disabledClient{reason: "object storage is disabled"}
|
||||
case "minio", "mino":
|
||||
case "minio", "mino", "r2", "cloudflare_r2", "cloudflare-r2", "s3", "aws_s3", "aws-s3", "custom", "custom_s3", "custom-s3":
|
||||
return NewMinIOClient(cfg, logger)
|
||||
case "aliyun", "aliyun_oss", "aliyun-oss", "oss":
|
||||
return NewAliyunClient(cfg, logger)
|
||||
@@ -67,3 +121,23 @@ func (c disabledClient) Delete(context.Context, string) error {
|
||||
func (c disabledClient) PublicURL(string) (string, error) {
|
||||
return "", c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) DownloadURL(string) (string, error) {
|
||||
return "", c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) List(context.Context, ListInput) (*ListResult, error) {
|
||||
return nil, c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) Stat(context.Context, string) (*ObjectInfo, error) {
|
||||
return nil, c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) Copy(context.Context, string, string) error {
|
||||
return c.Validate()
|
||||
}
|
||||
|
||||
func (c disabledClient) Usage(context.Context) (*UsageInfo, error) {
|
||||
return nil, c.Validate()
|
||||
}
|
||||
|
||||
Reference in New Issue
Block a user