thread-master/old/backend/internal/logic/threads_account/reply_image.go

88 lines
2.8 KiB
Go

package threads_account
import (
"io"
"path/filepath"
"strings"
app "haixun-backend/internal/library/errors"
"haixun-backend/internal/library/errors/code"
"haixun-backend/internal/library/storage"
"haixun-backend/internal/svc"
)
const maxReplyImageBytes = 8 << 20
type ReplyImageFile struct {
Filename string
ContentType string
Body io.Reader
}
func stageReplyImageURL(svcCtx *svc.ServiceContext, file *ReplyImageFile) (imageURL string, cleanup func(), err error) {
cleanup = func() {}
if file == nil || file.Body == nil {
return "", cleanup, nil
}
ext := strings.ToLower(filepath.Ext(strings.TrimSpace(file.Filename)))
if !allowedPublishAttachmentExt(ext) {
return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat("image must be jpg, png, webp, or gif")
}
contentType := strings.TrimSpace(file.ContentType)
if contentType != "" && !strings.HasPrefix(strings.ToLower(contentType), "image/") {
return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat("file must be an image")
}
if contentType == "" {
switch ext {
case ".jpg", ".jpeg":
contentType = "image/jpeg"
case ".png":
contentType = "image/png"
case ".webp":
contentType = "image/webp"
case ".gif":
contentType = "image/gif"
default:
contentType = "application/octet-stream"
}
}
limited := io.LimitReader(file.Body, maxReplyImageBytes+1)
data, readErr := io.ReadAll(limited)
if readErr != nil {
return "", cleanup, app.For(code.ThreadsAccount).SysInternal("read image failed").WithCause(readErr)
}
if len(data) == 0 {
return "", cleanup, app.For(code.ThreadsAccount).InputMissingRequired("image file is required")
}
if len(data) > maxReplyImageBytes {
return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat("image must be smaller than 8MB")
}
key, regErr := storage.DefaultEphemeralAssetStore().Register(data, contentType, 0)
if regErr != nil {
return "", cleanup, app.For(code.ThreadsAccount).SysInternal("stage image failed").WithCause(regErr)
}
base := strings.TrimSpace(svcCtx.Config.Storage.S3.PublicBaseURL)
imageURL, urlErr := storage.BuildEphemeralPublicURL(base, key)
if urlErr != nil {
storage.DefaultEphemeralAssetStore().Delete(key)
return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat(urlErr.Error())
}
if err := storage.ValidateThreadsFetchableURL(imageURL); err != nil {
storage.DefaultEphemeralAssetStore().Delete(key)
return "", cleanup, app.For(code.ThreadsAccount).InputInvalidFormat(err.Error())
}
cleanup = func() {
storage.DefaultEphemeralAssetStore().Delete(key)
}
return imageURL, cleanup, nil
}
func replyImageURLs(svcCtx *svc.ServiceContext, file *ReplyImageFile) ([]string, func(), error) {
imageURL, cleanup, err := stageReplyImageURL(svcCtx, file)
if err != nil || imageURL == "" {
return nil, cleanup, err
}
return []string{imageURL}, cleanup, nil
}