201 lines
5.0 KiB
Go
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)
|