94f7186cce
- Implemented image upload API in the article service, allowing users to upload images associated with articles. - Added validation for image size and type, ensuring only supported formats (PNG, JPG, GIF, WebP) are accepted. - Created a new Aliyun object storage client to handle image storage and retrieval. - Updated the article handler to include an endpoint for image uploads. - Enhanced error handling for image upload failures and added relevant error messages. - Introduced public URL generation for stored images, allowing access via signed tokens. - Updated localization files to include new messages related to image upload functionality. - Added tests for image upload and retrieval processes.
83 lines
1.9 KiB
Go
83 lines
1.9 KiB
Go
package transport
|
|
|
|
import (
|
|
"crypto/hmac"
|
|
"crypto/sha256"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"strings"
|
|
|
|
"github.com/gin-gonic/gin"
|
|
|
|
"github.com/geo-platform/tenant-api/internal/bootstrap"
|
|
)
|
|
|
|
type AssetHandler struct {
|
|
secret string
|
|
app *bootstrap.App
|
|
}
|
|
|
|
func NewAssetHandler(app *bootstrap.App) *AssetHandler {
|
|
return &AssetHandler{
|
|
secret: strings.TrimSpace(app.Config.JWT.Secret),
|
|
app: app,
|
|
}
|
|
}
|
|
|
|
func (h *AssetHandler) BuildArticleImageURL(objectKey string) string {
|
|
token := h.signObjectKey(objectKey)
|
|
return "/api/public/assets/" + token
|
|
}
|
|
|
|
func (h *AssetHandler) Serve(c *gin.Context) {
|
|
objectKey, ok := h.parseToken(c.Param("token"))
|
|
if !ok {
|
|
c.AbortWithStatus(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
content, err := h.app.ObjectStorage.GetBytes(c.Request.Context(), objectKey)
|
|
if err != nil || len(content) == 0 {
|
|
c.AbortWithStatus(http.StatusNotFound)
|
|
return
|
|
}
|
|
|
|
contentType := http.DetectContentType(content)
|
|
c.Header("Cache-Control", "public, max-age=31536000, immutable")
|
|
c.Header("Content-Type", contentType)
|
|
c.Writer.WriteHeader(http.StatusOK)
|
|
_, _ = c.Writer.Write(content)
|
|
}
|
|
|
|
func (h *AssetHandler) signObjectKey(objectKey string) string {
|
|
encodedKey := base64.RawURLEncoding.EncodeToString([]byte(objectKey))
|
|
mac := hmac.New(sha256.New, []byte(h.secret))
|
|
_, _ = mac.Write([]byte(objectKey))
|
|
signature := base64.RawURLEncoding.EncodeToString(mac.Sum(nil))
|
|
return fmt.Sprintf("%s.%s", encodedKey, signature)
|
|
}
|
|
|
|
func (h *AssetHandler) parseToken(token string) (string, bool) {
|
|
parts := strings.Split(token, ".")
|
|
if len(parts) != 2 {
|
|
return "", false
|
|
}
|
|
|
|
objectKeyBytes, err := base64.RawURLEncoding.DecodeString(parts[0])
|
|
if err != nil {
|
|
return "", false
|
|
}
|
|
objectKey := string(objectKeyBytes)
|
|
if strings.TrimSpace(objectKey) == "" {
|
|
return "", false
|
|
}
|
|
|
|
expected := h.signObjectKey(objectKey)
|
|
if !hmac.Equal([]byte(expected), []byte(token)) {
|
|
return "", false
|
|
}
|
|
|
|
return objectKey, true
|
|
}
|