158 lines
5.3 KiB
Go
158 lines
5.3 KiB
Go
package oci
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"encoding/json"
|
|
"fmt"
|
|
"io"
|
|
"mime/multipart"
|
|
"net/http"
|
|
"net/url"
|
|
"strings"
|
|
)
|
|
|
|
// domainImagesPath 是身份域公开图片上传端点(品牌 / IdP 图标),SDK 未覆盖该操作。
|
|
const domainImagesPath = "/storage/v1/Images"
|
|
|
|
// UploadDomainImage 实现 Client:上传公开图片到身份域存储,返回公网 fileUrl
|
|
// 与域存储内文件名(后者留作后续精确清理)。借 domainsClient 的 BaseClient
|
|
// 发裸 multipart 请求,OCI 签名与代理配置直接复用。
|
|
func (c *RealClient) UploadDomainImage(ctx context.Context, cred Credentials, region, domainID, fileName string, data []byte) (string, string, error) {
|
|
dc, err := c.domainsClient(ctx, cred, region, domainID)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
req, err := newDomainImageUploadRequest(ctx, dc.Endpoint(), fileName, data)
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
resp, callErr := dc.BaseClient.Call(ctx, req)
|
|
return parseImageUploadResponse(resp, callErr)
|
|
}
|
|
|
|
// DeleteDomainImage 实现 Client:按响应 fileName 精确删除公开图片,404 视为幂等成功。
|
|
func (c *RealClient) DeleteDomainImage(ctx context.Context, cred Credentials, region, domainID, fileName string) error {
|
|
dc, err := c.domainsClient(ctx, cred, region, domainID)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
req, err := newDomainImageDeleteRequest(ctx, dc.Endpoint(), fileName)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
resp, callErr := dc.BaseClient.Call(ctx, req)
|
|
return finishDomainImageDelete(resp, callErr)
|
|
}
|
|
|
|
func newDomainImageUploadRequest(ctx context.Context, endpoint, fileName string, data []byte) (*http.Request, error) {
|
|
body, contentType, err := imageMultipart(fileName, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodPost, strings.TrimRight(endpoint, "/")+domainImagesPath, bytes.NewReader(body))
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build image upload request: %w", err)
|
|
}
|
|
req.Header.Set("Content-Type", contentType)
|
|
return req, nil
|
|
}
|
|
|
|
func newDomainImageDeleteRequest(ctx context.Context, endpoint, fileName string) (*http.Request, error) {
|
|
u, err := url.Parse(strings.TrimRight(endpoint, "/") + domainImagesPath)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build image delete URL: %w", err)
|
|
}
|
|
query := u.Query()
|
|
query.Set("fileName", fileName)
|
|
u.RawQuery = query.Encode()
|
|
req, err := http.NewRequestWithContext(ctx, http.MethodDelete, u.String(), nil)
|
|
if err != nil {
|
|
return nil, fmt.Errorf("build image delete request: %w", err)
|
|
}
|
|
return req, nil
|
|
}
|
|
|
|
// imageMultipart 组装上传请求体:file(二进制)与 fileName 两个 part(官方文档要求)。
|
|
func imageMultipart(fileName string, data []byte) ([]byte, string, error) {
|
|
var buf bytes.Buffer
|
|
w := multipart.NewWriter(&buf)
|
|
fw, err := w.CreateFormFile("file", fileName)
|
|
if err != nil {
|
|
return nil, "", fmt.Errorf("build image multipart: %w", err)
|
|
}
|
|
if _, err := fw.Write(data); err != nil {
|
|
return nil, "", fmt.Errorf("build image multipart: %w", err)
|
|
}
|
|
if err := w.WriteField("fileName", fileName); err != nil {
|
|
return nil, "", fmt.Errorf("build image multipart: %w", err)
|
|
}
|
|
if err := w.Close(); err != nil {
|
|
return nil, "", fmt.Errorf("build image multipart: %w", err)
|
|
}
|
|
return buf.Bytes(), w.FormDataContentType(), nil
|
|
}
|
|
|
|
// parseImageUploadResponse 解析上传响应,并确保任何返回路径都关闭响应体。
|
|
func parseImageUploadResponse(resp *http.Response, callErr error) (string, string, error) {
|
|
if resp != nil && resp.Body != nil {
|
|
defer drainAndClose(resp.Body)
|
|
}
|
|
if callErr != nil {
|
|
return "", "", fmt.Errorf("upload domain image: %w", callErr)
|
|
}
|
|
if resp == nil || resp.Body == nil {
|
|
return "", "", fmt.Errorf("upload domain image: empty response")
|
|
}
|
|
if resp.StatusCode != http.StatusCreated && resp.StatusCode != http.StatusOK {
|
|
return "", "", fmt.Errorf("upload domain image: HTTP %d", resp.StatusCode)
|
|
}
|
|
b, err := io.ReadAll(io.LimitReader(resp.Body, (1<<20)+1))
|
|
if err != nil {
|
|
return "", "", fmt.Errorf("read image upload response: %w", err)
|
|
}
|
|
if len(b) > 1<<20 {
|
|
return "", "", fmt.Errorf("upload domain image: response exceeds 1MB")
|
|
}
|
|
return decodeImageUploadResponse(b)
|
|
}
|
|
|
|
func decodeImageUploadResponse(data []byte) (string, string, error) {
|
|
var out struct {
|
|
FileURL string `json:"fileUrl"`
|
|
FileName string `json:"fileName"`
|
|
}
|
|
if err := json.Unmarshal(data, &out); err != nil {
|
|
return "", "", fmt.Errorf("parse image upload response: %w", err)
|
|
}
|
|
if out.FileURL == "" || out.FileName == "" {
|
|
return "", "", fmt.Errorf("upload domain image: response missing fileUrl or fileName")
|
|
}
|
|
return out.FileURL, out.FileName, nil
|
|
}
|
|
|
|
func finishDomainImageDelete(resp *http.Response, callErr error) error {
|
|
if resp != nil && resp.Body != nil {
|
|
defer drainAndClose(resp.Body)
|
|
}
|
|
if resp != nil && resp.StatusCode == http.StatusNotFound {
|
|
return nil
|
|
}
|
|
if callErr != nil {
|
|
return fmt.Errorf("delete domain image: %w", callErr)
|
|
}
|
|
if resp == nil {
|
|
return fmt.Errorf("delete domain image: empty response")
|
|
}
|
|
if resp.StatusCode != http.StatusOK && resp.StatusCode != http.StatusNoContent {
|
|
return fmt.Errorf("delete domain image: HTTP %d", resp.StatusCode)
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// drainAndClose 读完小响应以便 HTTP 连接复用;超大异常响应限制为 64KiB。
|
|
func drainAndClose(body io.ReadCloser) {
|
|
_, _ = io.Copy(io.Discard, io.LimitReader(body, 64<<10))
|
|
_ = body.Close()
|
|
}
|