answer/internal/service/uploader/upload.go

270 lines
7.9 KiB
Go

package uploader
import (
"bytes"
"fmt"
"io"
"io/ioutil"
"mime/multipart"
"net/http"
"os"
"path"
"path/filepath"
"strings"
"github.com/answerdev/answer/internal/base/reason"
"github.com/answerdev/answer/internal/service/service_config"
"github.com/answerdev/answer/internal/service/siteinfo_common"
"github.com/answerdev/answer/pkg/checker"
"github.com/answerdev/answer/pkg/dir"
"github.com/answerdev/answer/pkg/uid"
"github.com/answerdev/answer/plugin"
"github.com/disintegration/imaging"
"github.com/gin-gonic/gin"
exifremove "github.com/scottleedavis/go-exif-remove"
"github.com/segmentfault/pacman/errors"
"github.com/segmentfault/pacman/log"
)
const (
avatarSubPath = "avatar"
avatarThumbSubPath = "avatar_thumb"
postSubPath = "post"
brandingSubPath = "branding"
)
var (
subPathList = []string{
avatarSubPath,
avatarThumbSubPath,
postSubPath,
brandingSubPath,
}
FormatExts = map[string]imaging.Format{
".jpg": imaging.JPEG,
".jpeg": imaging.JPEG,
".png": imaging.PNG,
//".gif": imaging.GIF,
//".tif": imaging.TIFF,
//".tiff": imaging.TIFF,
//".bmp": imaging.BMP,
}
)
type UploaderService interface {
UploadAvatarFile(ctx *gin.Context) (url string, err error)
UploadPostFile(ctx *gin.Context) (url string, err error)
UploadBrandingFile(ctx *gin.Context) (url string, err error)
AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error)
}
// uploaderService uploader service
type uploaderService struct {
serviceConfig *service_config.ServiceConfig
siteInfoService siteinfo_common.SiteInfoCommonService
}
// NewUploaderService new upload service
func NewUploaderService(serviceConfig *service_config.ServiceConfig,
siteInfoService siteinfo_common.SiteInfoCommonService) UploaderService {
for _, subPath := range subPathList {
err := dir.CreateDirIfNotExist(filepath.Join(serviceConfig.UploadPath, subPath))
if err != nil {
panic(err)
}
}
return &uploaderService{
serviceConfig: serviceConfig,
siteInfoService: siteInfoService,
}
}
// UploadAvatarFile upload avatar file
func (us *uploaderService) UploadAvatarFile(ctx *gin.Context) (url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserAvatar)
if err != nil {
return "", err
}
if len(url) > 0 {
return url, nil
}
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 5*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(avatarSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *uploaderService) AvatarThumbFile(ctx *gin.Context, fileName string, size int) (url string, err error) {
if size > 1024 {
size = 1024
}
thumbFileName := fmt.Sprintf("%d_%d@%s", size, size, fileName)
thumbFilePath := fmt.Sprintf("%s/%s/%s", us.serviceConfig.UploadPath, avatarThumbSubPath, thumbFileName)
avatarfile, err := os.ReadFile(thumbFilePath)
if err == nil {
return thumbFilePath, nil
}
filePath := fmt.Sprintf("%s/avatar/%s", us.serviceConfig.UploadPath, fileName)
avatarfile, err = os.ReadFile(filePath)
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
reader := bytes.NewReader(avatarfile)
img, err := imaging.Decode(reader)
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
new_image := imaging.Fill(img, size, size, imaging.Center, imaging.Linear)
var buf bytes.Buffer
fileSuffix := path.Ext(fileName)
_, ok := FormatExts[fileSuffix]
if !ok {
return "", fmt.Errorf("img extension not exist")
}
err = imaging.Encode(&buf, new_image, FormatExts[fileSuffix])
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
thumbReader := bytes.NewReader(buf.Bytes())
err = dir.CreateDirIfNotExist(path.Join(us.serviceConfig.UploadPath, avatarThumbSubPath))
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
avatarFilePath := path.Join(avatarThumbSubPath, thumbFileName)
savefilePath := path.Join(us.serviceConfig.UploadPath, avatarFilePath)
out, err := os.Create(savefilePath)
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
defer out.Close()
_, err = io.Copy(out, thumbReader)
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
return savefilePath, nil
}
func (us *uploaderService) UploadPostFile(ctx *gin.Context) (
url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.UserPost)
if err != nil {
return "", err
}
if len(url) > 0 {
return url, nil
}
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
if _, ok := FormatExts[fileExt]; !ok {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(postSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *uploaderService) UploadBrandingFile(ctx *gin.Context) (
url string, err error) {
url, err = us.tryToUploadByPlugin(ctx, plugin.AdminBranding)
if err != nil {
return "", err
}
if len(url) > 0 {
return url, nil
}
// max size
ctx.Request.Body = http.MaxBytesReader(ctx.Writer, ctx.Request.Body, 10*1024*1024)
_, file, err := ctx.Request.FormFile("file")
if err != nil {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
fileExt := strings.ToLower(path.Ext(file.Filename))
_, ok := FormatExts[fileExt]
if !ok && fileExt != ".ico" {
return "", errors.BadRequest(reason.RequestFormatError).WithError(err)
}
newFilename := fmt.Sprintf("%s%s", uid.IDStr12(), fileExt)
avatarFilePath := path.Join(brandingSubPath, newFilename)
return us.uploadFile(ctx, file, avatarFilePath)
}
func (us *uploaderService) uploadFile(ctx *gin.Context, file *multipart.FileHeader, fileSubPath string) (
url string, err error) {
siteGeneral, err := us.siteInfoService.GetSiteGeneral(ctx)
if err != nil {
return "", err
}
filePath := path.Join(us.serviceConfig.UploadPath, fileSubPath)
if err := ctx.SaveUploadedFile(file, filePath); err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
src, err := file.Open()
if err != nil {
return "", errors.InternalServer(reason.UnknownError).WithError(err).WithStack()
}
defer src.Close()
Dexif(filePath, filePath)
if !checker.IsSupportedImageFile(src, filepath.Ext(fileSubPath)) {
return "", errors.BadRequest(reason.UploadFileUnsupportedFileFormat)
}
url = fmt.Sprintf("%s/uploads/%s", siteGeneral.SiteUrl, fileSubPath)
return url, nil
}
func (us *uploaderService) tryToUploadByPlugin(ctx *gin.Context, source plugin.UploadSource) (
url string, err error) {
_ = plugin.CallStorage(func(fn plugin.Storage) error {
resp := fn.UploadFile(ctx, source)
if resp.OriginalError != nil {
log.Errorf("upload file by plugin failed, err: %v", resp.OriginalError)
err = errors.BadRequest("").WithMsg(resp.DisplayErrorMsg.Translate(ctx)).WithError(err)
} else {
url = resp.FullURL
}
return nil
})
return url, err
}
func Dexif(filepath string, destpath string) error {
img, err := ioutil.ReadFile(filepath)
if err != nil {
return err
}
noExifBytes, err := exifremove.Remove(img)
if err != nil {
return err
}
err = os.WriteFile(destpath, noExifBytes, 0644)
if err != nil {
return err
}
return nil
}