Files
oci-portal/internal/oci/domainimage.go
T
2026-07-22 16:51:23 +08:00

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()
}