package attachments import ( "context" "crypto/md5" "crypto/rand" "encoding/hex" "errors" "io" "net/http" "os" "path/filepath" "strings" "git.118im.com/im_team/olons_server/internal/config" "git.118im.com/im_team/olons_server/internal/store" ) 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 } func New(repository Repository, cfg config.UploadConfig) *Service { return &Service{repository: repository, cfg: cfg} } 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 if err = os.MkdirAll(s.cfg.Dir, 0o750); err != nil { return nil, err } path := filepath.Join(s.cfg.Dir, fileName) if err = os.WriteFile(path, data, 0o640); err != nil { return nil, err } digest := md5.Sum(data) publicPath := strings.TrimRight(s.cfg.PublicPath, "/") + "/" + fileName attachment, err := s.repository.CreateAttachment(ctx, store.AttachmentCreation{ MemberID: memberID, Name: filepath.Base(name), MimeType: mimeType, Path: path, URL: publicPath, Size: int64(len(data)), Ext: strings.TrimPrefix(ext, "."), MD5: hex.EncodeToString(digest[:]), }) if err != nil { _ = os.Remove(path) return nil, err } return attachment, nil }