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