88 lines
2.8 KiB
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
|
|
} |