新增 Aliyun OSS 与 S3 协议、通道内重试和按优先级跨通道切换,保留 server-main 兼容与环境 OSS 内存通道。 将请求及结果中的 Base64、Data URI、Buffer、multipart 和内联二进制统一对象化,生产路径不再写入本机静态目录,历史本地资源仅保留只读兼容。 引入 PublicErrorV1 并统一 API、异步查询、兼容协议和失败回调的安全错误输出,同时补充迁移、管理端、指标、OpenAPI 与本地模拟验收。 验证:go test ./... -count=1;go vet ./...;pnpm lint;pnpm test;pnpm build;pnpm openapi;tests/ci/migrations-test.sh。
531 lines
19 KiB
Go
531 lines
19 KiB
Go
package runner
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/hmac"
|
|
"crypto/sha1"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"encoding/hex"
|
|
"fmt"
|
|
"io"
|
|
"net"
|
|
"net/http"
|
|
"net/url"
|
|
"path"
|
|
"sort"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/clients"
|
|
"github.com/easyai/easyai-ai-gateway/apps/api/internal/store"
|
|
"github.com/google/uuid"
|
|
)
|
|
|
|
const (
|
|
objectStorageRequestTimeout = 120 * time.Second
|
|
objectStorageReadLimit = 256 << 20
|
|
objectStorageSignedURLTTL = 15 * time.Minute
|
|
)
|
|
|
|
var sharedObjectStorageHTTPClient = &http.Client{
|
|
Timeout: objectStorageRequestTimeout,
|
|
Transport: &http.Transport{
|
|
MaxIdleConns: 256,
|
|
MaxIdleConnsPerHost: 256,
|
|
MaxConnsPerHost: 256,
|
|
IdleConnTimeout: 90 * time.Second,
|
|
},
|
|
}
|
|
|
|
type objectStorageAdapter struct {
|
|
channel store.FileStorageChannel
|
|
client *http.Client
|
|
now func() time.Time
|
|
}
|
|
|
|
type FileStorageChannelTestResult struct {
|
|
Provider string `json:"provider"`
|
|
PutSucceeded bool `json:"putSucceeded"`
|
|
HeadSucceeded bool `json:"headSucceeded"`
|
|
DeleteSucceeded bool `json:"deleteSucceeded"`
|
|
DurationMS int64 `json:"durationMs"`
|
|
}
|
|
|
|
// TestFileStorageChannel performs an isolated write, metadata read and cleanup
|
|
// against one object-storage channel. The random probe avoids overwriting a
|
|
// content-addressed business object and no object key or credential is exposed.
|
|
func (s *Service) TestFileStorageChannel(ctx context.Context, channel store.FileStorageChannel) (FileStorageChannelTestResult, error) {
|
|
startedAt := time.Now()
|
|
result := FileStorageChannelTestResult{Provider: strings.ToLower(strings.TrimSpace(channel.Provider))}
|
|
adapter, err := newObjectStorageAdapter(channel)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
payload := FileUploadPayload{
|
|
Bytes: []byte("easyai-storage-probe:" + uuid.NewString()),
|
|
ContentType: "application/octet-stream",
|
|
FileName: "probe.bin",
|
|
Scene: store.FileStorageSceneUpload,
|
|
Source: "admin-connection-test",
|
|
}
|
|
upload, err := adapter.put(ctx, payload)
|
|
if err != nil {
|
|
return result, err
|
|
}
|
|
result.PutSucceeded = true
|
|
objectKey := stringFromAny(upload["objectKey"])
|
|
if objectKey == "" {
|
|
return result, storageClientError("storage_write_failed", "object storage probe did not return an object reference", 0, false)
|
|
}
|
|
if err := adapter.head(ctx, objectKey); err != nil {
|
|
_ = adapter.delete(context.WithoutCancel(ctx), objectKey)
|
|
return result, err
|
|
}
|
|
result.HeadSucceeded = true
|
|
if err := adapter.delete(ctx, objectKey); err != nil {
|
|
return result, err
|
|
}
|
|
result.DeleteSucceeded = true
|
|
result.DurationMS = time.Since(startedAt).Milliseconds()
|
|
return result, nil
|
|
}
|
|
|
|
func newObjectStorageAdapter(channel store.FileStorageChannel) (*objectStorageAdapter, error) {
|
|
provider := strings.ToLower(strings.TrimSpace(channel.Provider))
|
|
if provider != "aliyun_oss" && provider != "s3" {
|
|
return nil, &clients.ClientError{Code: "storage_config_invalid", Message: "unsupported object storage provider", Retryable: false}
|
|
}
|
|
if objectStorageConfigString(channel.Config, "endpoint") == "" || objectStorageConfigString(channel.Config, "bucket") == "" {
|
|
return nil, &clients.ClientError{Code: "storage_config_invalid", Message: "object storage endpoint and bucket are required", Retryable: false}
|
|
}
|
|
if strings.TrimSpace(channel.AccessKeyID) == "" || strings.TrimSpace(channel.AccessKeySecret) == "" {
|
|
return nil, &clients.ClientError{Code: "storage_auth_failed", Message: "object storage credentials are not configured", Retryable: false}
|
|
}
|
|
return &objectStorageAdapter{
|
|
channel: channel,
|
|
client: sharedObjectStorageHTTPClient,
|
|
now: time.Now,
|
|
}, nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) put(ctx context.Context, payload FileUploadPayload) (map[string]any, error) {
|
|
objectKey := a.objectKey(payload)
|
|
requestURL, err := a.objectURL(objectKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
contentType := strings.TrimSpace(payload.ContentType)
|
|
if contentType == "" {
|
|
contentType = "application/octet-stream"
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPut, requestURL, bytes.NewReader(payload.Bytes))
|
|
if err != nil {
|
|
return nil, storageClientError("storage_config_invalid", err.Error(), 0, false)
|
|
}
|
|
req.Header.Set("Content-Type", contentType)
|
|
if err := a.sign(req, sha256Hex(payload.Bytes), a.now().UTC()); err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return nil, storageClientError("storage_write_failed", err.Error(), 0, true)
|
|
}
|
|
defer resp.Body.Close()
|
|
responseBody, readErr := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
|
if readErr != nil {
|
|
return nil, storageClientError("storage_write_failed", readErr.Error(), resp.StatusCode, storageStatusRetryable(resp.StatusCode))
|
|
}
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return nil, objectStorageHTTPError("storage_write_failed", resp.StatusCode, responseBody)
|
|
}
|
|
accessScope := objectStorageAccessScope(a.channel, payload.Scene)
|
|
publicURL := ""
|
|
if accessScope == "public" {
|
|
publicURL = a.publicURL(objectKey)
|
|
}
|
|
urlExpiresAt := ""
|
|
if publicURL == "" {
|
|
publicURL, err = a.presignGet(objectKey, objectStorageSignedURLTTL)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
urlExpiresAt = a.now().UTC().Add(objectStorageSignedURLTTL).Format(time.RFC3339)
|
|
}
|
|
digest := sha256.Sum256(payload.Bytes)
|
|
result := map[string]any{
|
|
"url": publicURL,
|
|
"fileName": path.Base(objectKey),
|
|
"objectKey": objectKey,
|
|
"contentType": contentType,
|
|
"size": len(payload.Bytes),
|
|
"sha256": hex.EncodeToString(digest[:]),
|
|
"accessScope": accessScope,
|
|
"storageChannel": map[string]any{
|
|
"id": a.channel.ID,
|
|
"channelKey": a.channel.ChannelKey,
|
|
"name": a.channel.Name,
|
|
"provider": a.channel.Provider,
|
|
},
|
|
}
|
|
if urlExpiresAt != "" {
|
|
result["urlExpiresAt"] = urlExpiresAt
|
|
}
|
|
return result, nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) get(ctx context.Context, objectKey string) ([]byte, error) {
|
|
requestURL, err := a.objectURL(objectKey)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodGet, requestURL, nil)
|
|
if err != nil {
|
|
return nil, storageClientError("storage_config_invalid", err.Error(), 0, false)
|
|
}
|
|
if err := a.sign(req, sha256Hex(nil), a.now().UTC()); err != nil {
|
|
return nil, err
|
|
}
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return nil, storageClientError("storage_read_failed", err.Error(), 0, true)
|
|
}
|
|
defer resp.Body.Close()
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
body, _ := io.ReadAll(io.LimitReader(resp.Body, 64<<10))
|
|
return nil, objectStorageHTTPError("storage_read_failed", resp.StatusCode, body)
|
|
}
|
|
payload, err := io.ReadAll(io.LimitReader(resp.Body, objectStorageReadLimit+1))
|
|
if err != nil {
|
|
return nil, storageClientError("storage_read_failed", err.Error(), resp.StatusCode, true)
|
|
}
|
|
if len(payload) > objectStorageReadLimit {
|
|
return nil, storageClientError("storage_read_failed", "stored object exceeds the read limit", resp.StatusCode, false)
|
|
}
|
|
return payload, nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) head(ctx context.Context, objectKey string) error {
|
|
return a.emptyObjectRequest(ctx, http.MethodHead, objectKey)
|
|
}
|
|
|
|
func (a *objectStorageAdapter) delete(ctx context.Context, objectKey string) error {
|
|
return a.emptyObjectRequest(ctx, http.MethodDelete, objectKey)
|
|
}
|
|
|
|
func (a *objectStorageAdapter) emptyObjectRequest(ctx context.Context, method string, objectKey string) error {
|
|
requestURL, err := a.objectURL(objectKey)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, method, requestURL, nil)
|
|
if err != nil {
|
|
return storageClientError("storage_config_invalid", err.Error(), 0, false)
|
|
}
|
|
if err := a.sign(req, sha256Hex(nil), a.now().UTC()); err != nil {
|
|
return err
|
|
}
|
|
resp, err := a.client.Do(req)
|
|
if err != nil {
|
|
return storageClientError("storage_read_failed", err.Error(), 0, true)
|
|
}
|
|
defer resp.Body.Close()
|
|
_, _ = io.Copy(io.Discard, io.LimitReader(resp.Body, 64<<10))
|
|
if resp.StatusCode < 200 || resp.StatusCode >= 300 {
|
|
return objectStorageHTTPError("storage_read_failed", resp.StatusCode, nil)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) presignGet(objectKey string, ttl time.Duration) (string, error) {
|
|
requestURL, err := a.objectURL(objectKey)
|
|
if err != nil {
|
|
return "", err
|
|
}
|
|
if ttl <= 0 {
|
|
ttl = objectStorageSignedURLTTL
|
|
}
|
|
if strings.EqualFold(a.channel.Provider, "s3") {
|
|
return a.presignS3Get(requestURL, ttl, a.now().UTC())
|
|
}
|
|
return a.presignOSSGet(requestURL, objectKey, ttl, a.now().UTC())
|
|
}
|
|
|
|
func (a *objectStorageAdapter) objectKey(payload FileUploadPayload) string {
|
|
now := a.now().UTC()
|
|
digest := sha256.Sum256(payload.Bytes)
|
|
extension := uploadFileExtension(payload.ContentType, path.Ext(payload.FileName))
|
|
prefix := strings.Trim(objectStorageConfigString(a.channel.Config, "objectPrefix"), "/")
|
|
parts := make([]string, 0, 7)
|
|
if prefix != "" {
|
|
parts = append(parts, prefix)
|
|
}
|
|
parts = append(parts,
|
|
firstNonEmptyString(strings.TrimSpace(payload.Scene), store.FileStorageSceneUpload),
|
|
fmt.Sprintf("%04d", now.Year()),
|
|
fmt.Sprintf("%02d", now.Month()),
|
|
fmt.Sprintf("%02d", now.Day()),
|
|
hex.EncodeToString(digest[:])+extension,
|
|
)
|
|
return strings.Join(parts, "/")
|
|
}
|
|
|
|
func (a *objectStorageAdapter) objectURL(objectKey string) (string, error) {
|
|
endpoint, err := url.Parse(strings.TrimRight(objectStorageConfigString(a.channel.Config, "endpoint"), "/"))
|
|
if err != nil || endpoint.Scheme == "" || endpoint.Host == "" || endpoint.User != nil {
|
|
return "", storageClientError("storage_config_invalid", "invalid object storage endpoint", 0, false)
|
|
}
|
|
bucket := objectStorageConfigString(a.channel.Config, "bucket")
|
|
if strings.EqualFold(a.channel.Provider, "s3") {
|
|
if objectStorageConfigBool(a.channel.Config, "forcePathStyle") {
|
|
endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/" + bucket + "/" + strings.TrimLeft(objectKey, "/")
|
|
} else {
|
|
endpoint.Host = bucket + "." + endpoint.Host
|
|
endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/" + strings.TrimLeft(objectKey, "/")
|
|
}
|
|
} else {
|
|
hostname := endpoint.Hostname()
|
|
forcePathStyle := objectStorageConfigBool(a.channel.Config, "forcePathStyle")
|
|
if !forcePathStyle && net.ParseIP(hostname) == nil && hostname != "localhost" && !strings.HasPrefix(strings.ToLower(hostname), strings.ToLower(bucket)+".") {
|
|
if port := endpoint.Port(); port != "" {
|
|
endpoint.Host = bucket + "." + hostname + ":" + port
|
|
} else {
|
|
endpoint.Host = bucket + "." + endpoint.Host
|
|
}
|
|
}
|
|
endpoint.Path = strings.TrimRight(endpoint.Path, "/") + "/" + strings.TrimLeft(objectKey, "/")
|
|
}
|
|
return endpoint.String(), nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) publicURL(objectKey string) string {
|
|
baseURL := strings.TrimRight(objectStorageConfigString(a.channel.Config, "publicBaseUrl"), "/")
|
|
if baseURL == "" {
|
|
baseURL = strings.TrimRight(objectStorageConfigString(a.channel.Config, "publicBaseURL"), "/")
|
|
}
|
|
if baseURL == "" {
|
|
return ""
|
|
}
|
|
return baseURL + "/" + escapeObjectKey(objectKey)
|
|
}
|
|
|
|
func (a *objectStorageAdapter) sign(req *http.Request, payloadHash string, now time.Time) error {
|
|
if strings.EqualFold(a.channel.Provider, "s3") {
|
|
a.signS3Request(req, payloadHash, now)
|
|
return nil
|
|
}
|
|
a.signOSSRequest(req, now)
|
|
return nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) signOSSRequest(req *http.Request, now time.Time) {
|
|
date := now.UTC().Format(http.TimeFormat)
|
|
contentType := req.Header.Get("Content-Type")
|
|
canonicalHeaders := ""
|
|
if token := strings.TrimSpace(a.channel.SessionToken); token != "" {
|
|
req.Header.Set("x-oss-security-token", token)
|
|
canonicalHeaders = "x-oss-security-token:" + token + "\n"
|
|
}
|
|
canonicalResource := "/" + objectStorageConfigString(a.channel.Config, "bucket") + req.URL.EscapedPath()
|
|
stringToSign := req.Method + "\n\n" + contentType + "\n" + date + "\n" + canonicalHeaders + canonicalResource
|
|
signature := hmacSHA1Base64([]byte(a.channel.AccessKeySecret), stringToSign)
|
|
req.Header.Set("Authorization", "OSS "+a.channel.AccessKeyID+":"+signature)
|
|
req.Header.Set("Date", date)
|
|
}
|
|
|
|
func (a *objectStorageAdapter) presignOSSGet(requestURL string, objectKey string, ttl time.Duration, now time.Time) (string, error) {
|
|
parsed, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
return "", storageClientError("storage_config_invalid", err.Error(), 0, false)
|
|
}
|
|
expires := strconv.FormatInt(now.Add(ttl).Unix(), 10)
|
|
canonicalResource := "/" + objectStorageConfigString(a.channel.Config, "bucket") + "/" + strings.TrimLeft(objectKey, "/")
|
|
signature := hmacSHA1Base64([]byte(a.channel.AccessKeySecret), "GET\n\n\n"+expires+"\n"+canonicalResource)
|
|
query := parsed.Query()
|
|
query.Set("OSSAccessKeyId", a.channel.AccessKeyID)
|
|
query.Set("Expires", expires)
|
|
query.Set("Signature", signature)
|
|
if token := strings.TrimSpace(a.channel.SessionToken); token != "" {
|
|
query.Set("security-token", token)
|
|
}
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func (a *objectStorageAdapter) signS3Request(req *http.Request, payloadHash string, now time.Time) {
|
|
region := firstNonEmptyString(objectStorageConfigString(a.channel.Config, "region"), "us-east-1")
|
|
amzDate := now.Format("20060102T150405Z")
|
|
date := now.Format("20060102")
|
|
req.Header.Set("x-amz-date", amzDate)
|
|
req.Header.Set("x-amz-content-sha256", payloadHash)
|
|
if token := strings.TrimSpace(a.channel.SessionToken); token != "" {
|
|
req.Header.Set("x-amz-security-token", token)
|
|
}
|
|
canonicalHeaders, signedHeaders := s3CanonicalHeaders(req)
|
|
canonicalRequest := strings.Join([]string{
|
|
req.Method,
|
|
s3CanonicalURI(req.URL),
|
|
s3CanonicalQuery(req.URL.Query()),
|
|
canonicalHeaders,
|
|
signedHeaders,
|
|
payloadHash,
|
|
}, "\n")
|
|
scope := date + "/" + region + "/s3/aws4_request"
|
|
stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + sha256Hex([]byte(canonicalRequest))
|
|
signature := hex.EncodeToString(s3SigningHMAC(a.channel.AccessKeySecret, date, region, stringToSign))
|
|
req.Header.Set("Authorization", "AWS4-HMAC-SHA256 Credential="+a.channel.AccessKeyID+"/"+scope+", SignedHeaders="+signedHeaders+", Signature="+signature)
|
|
}
|
|
|
|
func (a *objectStorageAdapter) presignS3Get(requestURL string, ttl time.Duration, now time.Time) (string, error) {
|
|
parsed, err := url.Parse(requestURL)
|
|
if err != nil {
|
|
return "", storageClientError("storage_config_invalid", err.Error(), 0, false)
|
|
}
|
|
region := firstNonEmptyString(objectStorageConfigString(a.channel.Config, "region"), "us-east-1")
|
|
amzDate := now.Format("20060102T150405Z")
|
|
date := now.Format("20060102")
|
|
scope := date + "/" + region + "/s3/aws4_request"
|
|
seconds := int64(ttl / time.Second)
|
|
if seconds < 1 {
|
|
seconds = 1
|
|
}
|
|
if seconds > 7*24*60*60 {
|
|
seconds = 7 * 24 * 60 * 60
|
|
}
|
|
query := parsed.Query()
|
|
query.Set("X-Amz-Algorithm", "AWS4-HMAC-SHA256")
|
|
query.Set("X-Amz-Credential", a.channel.AccessKeyID+"/"+scope)
|
|
query.Set("X-Amz-Date", amzDate)
|
|
query.Set("X-Amz-Expires", strconv.FormatInt(seconds, 10))
|
|
query.Set("X-Amz-SignedHeaders", "host")
|
|
if token := strings.TrimSpace(a.channel.SessionToken); token != "" {
|
|
query.Set("X-Amz-Security-Token", token)
|
|
}
|
|
canonicalRequest := "GET\n" + s3CanonicalURI(parsed) + "\n" + s3CanonicalQuery(query) + "\nhost:" + strings.ToLower(parsed.Host) + "\n\nhost\nUNSIGNED-PAYLOAD"
|
|
stringToSign := "AWS4-HMAC-SHA256\n" + amzDate + "\n" + scope + "\n" + sha256Hex([]byte(canonicalRequest))
|
|
query.Set("X-Amz-Signature", hex.EncodeToString(s3SigningHMAC(a.channel.AccessKeySecret, date, region, stringToSign)))
|
|
parsed.RawQuery = query.Encode()
|
|
return parsed.String(), nil
|
|
}
|
|
|
|
func s3CanonicalHeaders(req *http.Request) (string, string) {
|
|
headers := map[string]string{"host": strings.ToLower(req.URL.Host)}
|
|
for key, values := range req.Header {
|
|
lower := strings.ToLower(strings.TrimSpace(key))
|
|
if lower == "content-type" || strings.HasPrefix(lower, "x-amz-") {
|
|
headers[lower] = strings.Join(values, ",")
|
|
}
|
|
}
|
|
keys := make([]string, 0, len(headers))
|
|
for key := range headers {
|
|
keys = append(keys, key)
|
|
}
|
|
sort.Strings(keys)
|
|
var canonical strings.Builder
|
|
for _, key := range keys {
|
|
canonical.WriteString(key)
|
|
canonical.WriteByte(':')
|
|
canonical.WriteString(strings.Join(strings.Fields(headers[key]), " "))
|
|
canonical.WriteByte('\n')
|
|
}
|
|
return canonical.String(), strings.Join(keys, ";")
|
|
}
|
|
|
|
func s3CanonicalURI(value *url.URL) string {
|
|
uri := value.EscapedPath()
|
|
if uri == "" {
|
|
return "/"
|
|
}
|
|
return uri
|
|
}
|
|
|
|
func s3CanonicalQuery(values url.Values) string {
|
|
return strings.ReplaceAll(values.Encode(), "+", "%20")
|
|
}
|
|
|
|
func s3SigningHMAC(secret string, date string, region string, stringToSign string) []byte {
|
|
dateKey := hmacSHA256([]byte("AWS4"+secret), date)
|
|
regionKey := hmacSHA256(dateKey, region)
|
|
serviceKey := hmacSHA256(regionKey, "s3")
|
|
signingKey := hmacSHA256(serviceKey, "aws4_request")
|
|
return hmacSHA256(signingKey, stringToSign)
|
|
}
|
|
|
|
func hmacSHA256(key []byte, value string) []byte {
|
|
mac := hmac.New(sha256.New, key)
|
|
_, _ = mac.Write([]byte(value))
|
|
return mac.Sum(nil)
|
|
}
|
|
|
|
func hmacSHA1Base64(key []byte, value string) string {
|
|
mac := hmac.New(sha1.New, key)
|
|
_, _ = mac.Write([]byte(value))
|
|
return base64.StdEncoding.EncodeToString(mac.Sum(nil))
|
|
}
|
|
|
|
func sha256Hex(value []byte) string {
|
|
digest := sha256.Sum256(value)
|
|
return hex.EncodeToString(digest[:])
|
|
}
|
|
|
|
func objectStorageConfigString(config map[string]any, key string) string {
|
|
if config == nil {
|
|
return ""
|
|
}
|
|
value, _ := config[key].(string)
|
|
return strings.TrimSpace(value)
|
|
}
|
|
|
|
func objectStorageConfigBool(config map[string]any, key string) bool {
|
|
if config == nil {
|
|
return false
|
|
}
|
|
value, _ := config[key].(bool)
|
|
return value
|
|
}
|
|
|
|
func objectStorageAccessScope(channel store.FileStorageChannel, scene string) string {
|
|
if scope := strings.ToLower(objectStorageConfigString(channel.Config, "accessScope")); scope == "public" || scope == "private" {
|
|
return scope
|
|
}
|
|
if strings.TrimSpace(scene) == store.FileStorageSceneRequestAsset {
|
|
return "private"
|
|
}
|
|
if strings.TrimSpace(objectStorageConfigString(channel.Config, "publicBaseUrl")) != "" || strings.TrimSpace(objectStorageConfigString(channel.Config, "publicBaseURL")) != "" {
|
|
return "public"
|
|
}
|
|
return "private"
|
|
}
|
|
|
|
func escapeObjectKey(objectKey string) string {
|
|
parts := strings.Split(strings.TrimLeft(objectKey, "/"), "/")
|
|
for index, part := range parts {
|
|
parts[index] = url.PathEscape(part)
|
|
}
|
|
return strings.Join(parts, "/")
|
|
}
|
|
|
|
func storageStatusRetryable(status int) bool {
|
|
return status == http.StatusRequestTimeout || status == http.StatusTooManyRequests || status >= 500
|
|
}
|
|
|
|
func objectStorageHTTPError(code string, status int, body []byte) error {
|
|
if status == http.StatusUnauthorized || status == http.StatusForbidden {
|
|
code = "storage_auth_failed"
|
|
}
|
|
message := http.StatusText(status)
|
|
if message == "" {
|
|
message = "object storage request failed"
|
|
}
|
|
// The provider response may contain object names, endpoints or credential
|
|
// diagnostics. Keep it out of the public error chain; channel health stores
|
|
// only the stable status description as well.
|
|
return storageClientError(code, message, status, storageStatusRetryable(status))
|
|
}
|
|
|
|
func storageClientError(code string, message string, status int, retryable bool) error {
|
|
return &clients.ClientError{Code: code, Message: message, StatusCode: status, Retryable: retryable}
|
|
}
|