125 lines
3.7 KiB
Go
125 lines
3.7 KiB
Go
package attachments
|
|
|
|
import (
|
|
"bytes"
|
|
"context"
|
|
"crypto/md5"
|
|
"crypto/rand"
|
|
"encoding/hex"
|
|
"errors"
|
|
"io"
|
|
"net/http"
|
|
"net/url"
|
|
"os"
|
|
"path"
|
|
"path/filepath"
|
|
"strings"
|
|
"time"
|
|
|
|
"git.118im.com/im_team/olons_server/internal/config"
|
|
"git.118im.com/im_team/olons_server/internal/store"
|
|
"github.com/minio/minio-go/v7"
|
|
"github.com/minio/minio-go/v7/pkg/credentials"
|
|
)
|
|
|
|
const maxImageSize = 5 << 20
|
|
|
|
var ErrImageInvalid = errors.New("image attachment invalid")
|
|
|
|
type Repository interface {
|
|
CreateAttachment(context.Context, store.AttachmentCreation) (*store.Attachment, error)
|
|
}
|
|
|
|
type Service struct {
|
|
repository Repository
|
|
cfg config.UploadConfig
|
|
minio *minio.Client
|
|
}
|
|
|
|
func New(repository Repository, cfg config.UploadConfig) (*Service, error) {
|
|
service := &Service{repository: repository, cfg: cfg}
|
|
if strings.EqualFold(cfg.Driver, "minio") {
|
|
client, err := minio.New(strings.TrimSpace(cfg.Endpoint), &minio.Options{
|
|
Creds: credentials.NewStaticV4(cfg.AccessKey, cfg.SecretKey, ""),
|
|
Secure: cfg.Secure,
|
|
Region: cfg.Region,
|
|
})
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
service.minio = client
|
|
}
|
|
return service, nil
|
|
}
|
|
|
|
func (s *Service) UploadImage(ctx context.Context, memberID int64, name string, reader io.Reader) (*store.Attachment, error) {
|
|
if s.repository == nil || memberID <= 0 {
|
|
return nil, ErrImageInvalid
|
|
}
|
|
data, err := io.ReadAll(io.LimitReader(reader, maxImageSize+1))
|
|
if err != nil || len(data) == 0 || len(data) > maxImageSize {
|
|
return nil, ErrImageInvalid
|
|
}
|
|
mimeType := http.DetectContentType(data)
|
|
extensions := map[string]string{"image/jpeg": ".jpg", "image/png": ".png", "image/webp": ".webp"}
|
|
ext, ok := extensions[mimeType]
|
|
if !ok {
|
|
return nil, ErrImageInvalid
|
|
}
|
|
random := make([]byte, 16)
|
|
if _, err = rand.Read(random); err != nil {
|
|
return nil, err
|
|
}
|
|
fileName := hex.EncodeToString(random) + ext
|
|
storedPath, publicURL, err := s.store(ctx, fileName, mimeType, data)
|
|
if err != nil {
|
|
return nil, err
|
|
}
|
|
digest := md5.Sum(data)
|
|
attachment, err := s.repository.CreateAttachment(ctx, store.AttachmentCreation{
|
|
MemberID: memberID, Drive: strings.ToLower(strings.TrimSpace(s.cfg.Driver)), Name: filepath.Base(name), MimeType: mimeType, Path: storedPath,
|
|
URL: publicURL, Size: int64(len(data)), Ext: strings.TrimPrefix(ext, "."), MD5: hex.EncodeToString(digest[:]),
|
|
})
|
|
if err != nil {
|
|
s.remove(ctx, storedPath)
|
|
return nil, err
|
|
}
|
|
return attachment, nil
|
|
}
|
|
|
|
func (s *Service) store(ctx context.Context, fileName, mimeType string, data []byte) (string, string, error) {
|
|
if s.minio != nil {
|
|
objectName := path.Join(strings.Trim(s.cfg.Path, "/"), time.Now().UTC().Format("2006/01/02"), fileName)
|
|
_, err := s.minio.PutObject(ctx, s.cfg.Bucket, objectName, bytes.NewReader(data), int64(len(data)), minio.PutObjectOptions{ContentType: mimeType})
|
|
if err != nil {
|
|
return "", "", err
|
|
}
|
|
return objectName, minioPublicURL(s.cfg.Domain, s.cfg.Bucket, objectName), nil
|
|
}
|
|
if err := os.MkdirAll(s.cfg.Dir, 0o750); err != nil {
|
|
return "", "", err
|
|
}
|
|
localPath := filepath.Join(s.cfg.Dir, fileName)
|
|
if err := os.WriteFile(localPath, data, 0o640); err != nil {
|
|
return "", "", err
|
|
}
|
|
return localPath, strings.TrimRight(s.cfg.PublicPath, "/") + "/" + fileName, nil
|
|
}
|
|
|
|
func (s *Service) remove(ctx context.Context, storedPath string) {
|
|
if s.minio != nil {
|
|
_ = s.minio.RemoveObject(ctx, s.cfg.Bucket, storedPath, minio.RemoveObjectOptions{})
|
|
return
|
|
}
|
|
_ = os.Remove(storedPath)
|
|
}
|
|
|
|
func minioPublicURL(domain, bucket, objectName string) string {
|
|
base := strings.TrimRight(strings.TrimSpace(domain), "/")
|
|
segments := strings.Split(strings.Trim(objectName, "/"), "/")
|
|
for i := range segments {
|
|
segments[i] = url.PathEscape(segments[i])
|
|
}
|
|
return base + "/" + url.PathEscape(bucket) + "/" + strings.Join(segments, "/")
|
|
}
|