Files
cmroubao/backend-api/internal/platform/shunyunbao/image.go
T

201 lines
5.0 KiB
Go

package shunyunbao
import (
"bufio"
"bytes"
"context"
"errors"
"io"
"mime"
"net/http"
"net/url"
"strconv"
"strings"
"cmroubao/backend-api/internal/domain"
"cmroubao/backend-api/internal/usecase"
)
const (
ProductImagePath = "/api/p/file"
maxProductImageBytes = 20 << 20
productImageSniffLen = 512
)
func (manager *SessionManager) FetchProductImage(
ctx context.Context,
productThumbRef string,
) (usecase.FreightSourceImage, error) {
productThumbRef = strings.TrimSpace(productThumbRef)
value, err := strconv.ParseUint(productThumbRef, 10, 64)
if err != nil || value == 0 ||
strconv.FormatUint(value, 10) != productThumbRef {
return usecase.FreightSourceImage{}, domain.ErrFreightImageInvalid
}
manager.mu.Lock()
if !manager.configuredLocked() {
manager.mu.Unlock()
return usecase.FreightSourceImage{},
domain.ErrFreightSourceNotConfigured
}
if !manager.authenticated {
manager.mu.Unlock()
return usecase.FreightSourceImage{},
domain.ErrFreightSourceSessionNeeded
}
baseURL := manager.baseURL
headers := manager.headers.Clone()
client := manager.http
manager.mu.Unlock()
query := url.Values{}
query.Set("id", productThumbRef)
request, err := http.NewRequestWithContext(
ctx,
http.MethodGet,
baseURL+ProductImagePath+"?"+query.Encode(),
nil,
)
if err != nil {
return usecase.FreightSourceImage{},
domain.ErrFreightSourceUnavailable
}
for name, values := range headers {
request.Header[name] = append([]string(nil), values...)
}
request.Header.Set("Accept", "image/jpeg, image/png, image/webp")
manager.logERPRequest(request)
response, err := client.Do(request)
if err != nil {
manager.logERPTransportFailure(request)
return usecase.FreightSourceImage{},
domain.ErrFreightSourceUnavailable
}
if response.StatusCode < http.StatusOK ||
response.StatusCode >= http.StatusMultipleChoices {
defer response.Body.Close()
manager.logERPBinaryResponse(request, response)
switch response.StatusCode {
case http.StatusUnauthorized, http.StatusForbidden:
manager.mu.Lock()
manager.clearAuthenticatedLocked()
manager.mu.Unlock()
return usecase.FreightSourceImage{},
domain.ErrFreightSourceSessionNeeded
case http.StatusNotFound:
return usecase.FreightSourceImage{},
domain.ErrFreightImageNotFound
default:
return usecase.FreightSourceImage{},
domain.ErrFreightSourceUnavailable
}
}
if response.ContentLength > maxProductImageBytes {
response.Body.Close()
manager.logERPBinaryResponse(request, response)
return usecase.FreightSourceImage{},
domain.ErrFreightImageInvalid
}
mediaType, content, err := productImageContent(
response.Header.Get("Content-Type"),
response.Body,
)
if err != nil {
response.Body.Close()
manager.logERPBinaryResponse(request, response)
return usecase.FreightSourceImage{},
domain.ErrFreightImageInvalid
}
manager.logERPBinaryResponse(request, response)
return usecase.FreightSourceImage{
Content: content,
MediaType: mediaType,
}, nil
}
func productImageContent(
contentType string,
body io.ReadCloser,
) (string, io.ReadCloser, error) {
contentType = strings.TrimSpace(contentType)
if contentType != "" {
mediaType, _, err := mime.ParseMediaType(contentType)
if err != nil {
return "", nil, err
}
mediaType = strings.ToLower(mediaType)
if supportedProductImageType(mediaType) {
return mediaType, body, nil
}
if mediaType != "application/octet-stream" {
return "", nil, domain.ErrFreightImageInvalid
}
}
reader := bufio.NewReaderSize(body, productImageSniffLen)
prefix, err := reader.Peek(productImageSniffLen)
if err != nil && !errors.Is(err, io.EOF) {
return "", nil, err
}
if len(prefix) == 0 {
return "", nil, domain.ErrFreightImageInvalid
}
mediaType := detectProductImageType(prefix)
if !supportedProductImageType(mediaType) {
return "", nil, domain.ErrFreightImageInvalid
}
return mediaType, struct {
io.Reader
io.Closer
}{
Reader: reader,
Closer: body,
}, nil
}
func detectProductImageType(prefix []byte) string {
mediaType := strings.ToLower(http.DetectContentType(prefix))
if supportedProductImageType(mediaType) {
return mediaType
}
if len(prefix) >= 12 &&
bytes.Equal(prefix[:4], []byte("RIFF")) &&
bytes.Equal(prefix[8:12], []byte("WEBP")) {
return "image/webp"
}
return mediaType
}
func supportedProductImageType(value string) bool {
switch strings.ToLower(value) {
case "image/jpeg", "image/png", "image/webp":
return true
default:
return false
}
}
func (manager *SessionManager) logERPBinaryResponse(
request *http.Request,
response *http.Response,
) {
if !manager.diagnosticsOn {
return
}
byteCount := "unknown"
if response.ContentLength >= 0 {
byteCount = strconv.FormatInt(response.ContentLength, 10)
}
manager.diagnosticLog(
"erp_response method=" + request.Method +
" path=" + request.URL.EscapedPath() +
" status=" + strconv.Itoa(response.StatusCode) +
" content_type=" +
diagnosticContentType(response.Header.Get("Content-Type")) +
" bytes=" + byteCount + " body=omitted_binary",
)
}
var _ usecase.FreightImageSource = (*SessionManager)(nil)