github.com/openimsdk/tools@v0.0.49/s3/minio/minio.go (about)

     1  // Copyright © 2023 OpenIM. All rights reserved.
     2  //
     3  // Licensed under the Apache License, Version 2.0 (the "License");
     4  // you may not use this file except in compliance with the License.
     5  // You may obtain a copy of the License at
     6  //
     7  //     http://www.apache.org/licenses/LICENSE-2.0
     8  //
     9  // Unless required by applicable law or agreed to in writing, software
    10  // distributed under the License is distributed on an "AS IS" BASIS,
    11  // WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    12  // See the License for the specific language governing permissions and
    13  // limitations under the License.
    14  
    15  package minio
    16  
    17  import (
    18  	"context"
    19  	"errors"
    20  	"fmt"
    21  	"io"
    22  	"net/http"
    23  	"net/url"
    24  	"path"
    25  	"path/filepath"
    26  	"reflect"
    27  	"strconv"
    28  	"strings"
    29  	"sync"
    30  	"time"
    31  	"unsafe"
    32  
    33  	"github.com/minio/minio-go/v7"
    34  	"github.com/openimsdk/tools/s3"
    35  
    36  	"github.com/minio/minio-go/v7/pkg/credentials"
    37  	"github.com/minio/minio-go/v7/pkg/signer"
    38  	"github.com/openimsdk/tools/errs"
    39  	"github.com/openimsdk/tools/log"
    40  )
    41  
    42  const (
    43  	unsignedPayload = "UNSIGNED-PAYLOAD"
    44  )
    45  
    46  const (
    47  	minPartSize int64 = 1024 * 1024 * 5        // 5MB
    48  	maxPartSize int64 = 1024 * 1024 * 1024 * 5 // 5GB
    49  	maxNumSize  int64 = 10000
    50  )
    51  
    52  const (
    53  	maxImageWidth      = 1024
    54  	maxImageHeight     = 1024
    55  	maxImageSize       = 1024 * 1024 * 50
    56  	imageThumbnailPath = "openim/thumbnail"
    57  )
    58  
    59  const successCode = http.StatusOK
    60  
    61  var _ s3.Interface = (*Minio)(nil)
    62  
    63  type Config struct {
    64  	Bucket          string
    65  	Endpoint        string
    66  	AccessKeyID     string
    67  	SecretAccessKey string
    68  	SessionToken    string
    69  	SignEndpoint    string
    70  	PublicRead      bool
    71  }
    72  
    73  func NewMinio(ctx context.Context, cache Cache, conf Config) (*Minio, error) {
    74  	u, err := url.Parse(conf.Endpoint)
    75  	if err != nil {
    76  		return nil, err
    77  	}
    78  	opts := &minio.Options{
    79  		Creds:  credentials.NewStaticV4(conf.AccessKeyID, conf.SecretAccessKey, conf.SessionToken),
    80  		Secure: u.Scheme == "https",
    81  	}
    82  	client, err := minio.New(u.Host, opts)
    83  	if err != nil {
    84  		return nil, err
    85  	}
    86  	m := &Minio{
    87  		conf:   conf,
    88  		bucket: conf.Bucket,
    89  		core:   &minio.Core{Client: client},
    90  		lock:   &sync.Mutex{},
    91  		init:   false,
    92  		cache:  cache,
    93  	}
    94  	if conf.SignEndpoint == "" || conf.SignEndpoint == conf.Endpoint {
    95  		m.opts = opts
    96  		m.sign = m.core.Client
    97  		m.prefix = u.Path
    98  		u.Path = ""
    99  		conf.Endpoint = u.String()
   100  		m.signEndpoint = conf.Endpoint
   101  	} else {
   102  		su, err := url.Parse(conf.SignEndpoint)
   103  		if err != nil {
   104  			return nil, err
   105  		}
   106  		m.opts = &minio.Options{
   107  			Creds:  credentials.NewStaticV4(conf.AccessKeyID, conf.SecretAccessKey, conf.SessionToken),
   108  			Secure: su.Scheme == "https",
   109  		}
   110  		m.sign, err = minio.New(su.Host, m.opts)
   111  		if err != nil {
   112  			return nil, err
   113  		}
   114  		m.prefix = su.Path
   115  		su.Path = ""
   116  		conf.SignEndpoint = su.String()
   117  		m.signEndpoint = conf.SignEndpoint
   118  	}
   119  	if err := m.initMinio(ctx); err != nil {
   120  		return nil, err
   121  	}
   122  	return m, nil
   123  }
   124  
   125  type Minio struct {
   126  	conf         Config
   127  	bucket       string
   128  	signEndpoint string
   129  	location     string
   130  	opts         *minio.Options
   131  	core         *minio.Core
   132  	sign         *minio.Client
   133  	lock         sync.Locker
   134  	init         bool
   135  	prefix       string
   136  	cache        Cache
   137  }
   138  
   139  func (m *Minio) initMinio(ctx context.Context) error {
   140  	if m.init {
   141  		return nil
   142  	}
   143  	m.lock.Lock()
   144  	defer m.lock.Unlock()
   145  	if m.init {
   146  		return nil
   147  	}
   148  	exists, err := m.core.Client.BucketExists(ctx, m.conf.Bucket)
   149  	if err != nil {
   150  		return fmt.Errorf("check bucket exists error: %w", err)
   151  	}
   152  	if !exists {
   153  		if err = m.core.Client.MakeBucket(ctx, m.conf.Bucket, minio.MakeBucketOptions{}); err != nil {
   154  			return fmt.Errorf("make bucket error: %w", err)
   155  		}
   156  	}
   157  	if m.conf.PublicRead {
   158  		policy := fmt.Sprintf(
   159  			`{"Version": "2012-10-17","Statement": [{"Action": ["s3:GetObject","s3:PutObject"],"Effect": "Allow","Principal": {"AWS": ["*"]},"Resource": ["arn:aws:s3:::%s/*"],"Sid": ""}]}`,
   160  			m.conf.Bucket,
   161  		)
   162  		if err = m.core.Client.SetBucketPolicy(ctx, m.conf.Bucket, policy); err != nil {
   163  			return err
   164  		}
   165  	}
   166  	m.location, err = m.core.Client.GetBucketLocation(ctx, m.conf.Bucket)
   167  	if err != nil {
   168  		return err
   169  	}
   170  	func() {
   171  		if m.conf.SignEndpoint == "" || m.conf.SignEndpoint == m.conf.Endpoint {
   172  			return
   173  		}
   174  		defer func() {
   175  			if r := recover(); r != nil {
   176  				m.sign = m.core.Client
   177  				log.ZWarn(
   178  					context.Background(),
   179  					"set sign bucket location cache panic",
   180  					errors.New("failed to get private field value"),
   181  					"recover",
   182  					fmt.Sprintf("%+v", r),
   183  					"development version",
   184  					"github.com/minio/minio-go/v7 v7.0.61",
   185  				)
   186  			}
   187  		}()
   188  		blc := reflect.ValueOf(m.sign).Elem().FieldByName("bucketLocCache")
   189  		vblc := reflect.New(reflect.PtrTo(blc.Type()))
   190  		*(*unsafe.Pointer)(vblc.UnsafePointer()) = unsafe.Pointer(blc.UnsafeAddr())
   191  		vblc.Elem().Elem().Interface().(interface{ Set(string, string) }).Set(m.conf.Bucket, m.location)
   192  	}()
   193  	m.init = true
   194  	return nil
   195  }
   196  
   197  func (m *Minio) Engine() string {
   198  	return "minio"
   199  }
   200  
   201  func (m *Minio) PartLimit() *s3.PartLimit {
   202  	return &s3.PartLimit{
   203  		MinPartSize: minPartSize,
   204  		MaxPartSize: maxPartSize,
   205  		MaxNumSize:  maxNumSize,
   206  	}
   207  }
   208  
   209  func (m *Minio) InitiateMultipartUpload(ctx context.Context, name string) (*s3.InitiateMultipartUploadResult, error) {
   210  	if err := m.initMinio(ctx); err != nil {
   211  		return nil, err
   212  	}
   213  	uploadID, err := m.core.NewMultipartUpload(ctx, m.bucket, name, minio.PutObjectOptions{})
   214  	if err != nil {
   215  		return nil, err
   216  	}
   217  	return &s3.InitiateMultipartUploadResult{
   218  		Bucket:   m.bucket,
   219  		Key:      name,
   220  		UploadID: uploadID,
   221  	}, nil
   222  }
   223  
   224  func (m *Minio) CompleteMultipartUpload(ctx context.Context, uploadID string, name string, parts []s3.Part) (*s3.CompleteMultipartUploadResult, error) {
   225  	if err := m.initMinio(ctx); err != nil {
   226  		return nil, err
   227  	}
   228  	minioParts := make([]minio.CompletePart, len(parts))
   229  	for i, part := range parts {
   230  		minioParts[i] = minio.CompletePart{
   231  			PartNumber: part.PartNumber,
   232  			ETag:       strings.ToLower(part.ETag),
   233  		}
   234  	}
   235  	upload, err := m.core.CompleteMultipartUpload(ctx, m.bucket, name, uploadID, minioParts, minio.PutObjectOptions{})
   236  	if err != nil {
   237  		return nil, err
   238  	}
   239  	m.delObjectImageInfoKey(ctx, name, upload.Size)
   240  	return &s3.CompleteMultipartUploadResult{
   241  		Location: upload.Location,
   242  		Bucket:   upload.Bucket,
   243  		Key:      upload.Key,
   244  		ETag:     strings.ToLower(upload.ETag),
   245  	}, nil
   246  }
   247  
   248  func (m *Minio) PartSize(ctx context.Context, size int64) (int64, error) {
   249  	if size <= 0 {
   250  		return 0, errors.New("size must be greater than 0")
   251  	}
   252  	if size > maxPartSize*maxNumSize {
   253  		return 0, fmt.Errorf("MINIO size must be less than the maximum allowed limit")
   254  	}
   255  	if size <= minPartSize*maxNumSize {
   256  		return minPartSize, nil
   257  	}
   258  	partSize := size / maxNumSize
   259  	if size%maxNumSize != 0 {
   260  		partSize++
   261  	}
   262  	return partSize, nil
   263  }
   264  
   265  func (m *Minio) AuthSign(ctx context.Context, uploadID string, name string, expire time.Duration, partNumbers []int) (*s3.AuthSignResult, error) {
   266  	if err := m.initMinio(ctx); err != nil {
   267  		return nil, err
   268  	}
   269  	creds, err := m.opts.Creds.Get()
   270  	if err != nil {
   271  		return nil, err
   272  	}
   273  	result := s3.AuthSignResult{
   274  		URL:   m.signEndpoint + "/" + m.bucket + "/" + name,
   275  		Query: url.Values{"uploadId": {uploadID}},
   276  		Parts: make([]s3.SignPart, len(partNumbers)),
   277  	}
   278  	for i, partNumber := range partNumbers {
   279  		rawURL := result.URL + "?partNumber=" + strconv.Itoa(partNumber) + "&uploadId=" + uploadID
   280  		request, err := http.NewRequestWithContext(ctx, http.MethodPut, rawURL, nil)
   281  		if err != nil {
   282  			return nil, err
   283  		}
   284  		request.Header.Set("X-Amz-Content-Sha256", unsignedPayload)
   285  		request = signer.SignV4Trailer(*request, creds.AccessKeyID, creds.SecretAccessKey, creds.SessionToken, m.location, nil)
   286  		result.Parts[i] = s3.SignPart{
   287  			PartNumber: partNumber,
   288  			Query:      url.Values{"partNumber": {strconv.Itoa(partNumber)}},
   289  			Header:     request.Header,
   290  		}
   291  	}
   292  	if m.prefix != "" {
   293  		result.URL = m.signEndpoint + m.prefix + "/" + m.bucket + "/" + name
   294  	}
   295  	return &result, nil
   296  }
   297  
   298  func (m *Minio) PresignedPutObject(ctx context.Context, name string, expire time.Duration) (string, error) {
   299  	if err := m.initMinio(ctx); err != nil {
   300  		return "", err
   301  	}
   302  	rawURL, err := m.sign.PresignedPutObject(ctx, m.bucket, name, expire)
   303  	if err != nil {
   304  		return "", err
   305  	}
   306  	if m.prefix != "" {
   307  		rawURL.Path = path.Join(m.prefix, rawURL.Path)
   308  	}
   309  	return rawURL.String(), nil
   310  }
   311  
   312  func (m *Minio) DeleteObject(ctx context.Context, name string) error {
   313  	if err := m.initMinio(ctx); err != nil {
   314  		return err
   315  	}
   316  	return m.core.Client.RemoveObject(ctx, m.bucket, name, minio.RemoveObjectOptions{})
   317  }
   318  
   319  func (m *Minio) StatObject(ctx context.Context, name string) (*s3.ObjectInfo, error) {
   320  	if err := m.initMinio(ctx); err != nil {
   321  		return nil, err
   322  	}
   323  	info, err := m.core.Client.StatObject(ctx, m.bucket, name, minio.StatObjectOptions{})
   324  	if err != nil {
   325  		return nil, err
   326  	}
   327  	return &s3.ObjectInfo{
   328  		ETag:         strings.ToLower(info.ETag),
   329  		Key:          info.Key,
   330  		Size:         info.Size,
   331  		LastModified: info.LastModified,
   332  	}, nil
   333  }
   334  
   335  func (m *Minio) CopyObject(ctx context.Context, src string, dst string) (*s3.CopyObjectInfo, error) {
   336  	if err := m.initMinio(ctx); err != nil {
   337  		return nil, err
   338  	}
   339  	result, err := m.core.Client.CopyObject(ctx, minio.CopyDestOptions{
   340  		Bucket: m.bucket,
   341  		Object: dst,
   342  	}, minio.CopySrcOptions{
   343  		Bucket: m.bucket,
   344  		Object: src,
   345  	})
   346  	if err != nil {
   347  		return nil, err
   348  	}
   349  	return &s3.CopyObjectInfo{
   350  		Key:  dst,
   351  		ETag: strings.ToLower(result.ETag),
   352  	}, nil
   353  }
   354  
   355  func (m *Minio) IsNotFound(err error) bool {
   356  	switch e := errs.Unwrap(err).(type) {
   357  	case minio.ErrorResponse:
   358  		return e.StatusCode == http.StatusNotFound || e.Code == "NoSuchKey"
   359  	case *minio.ErrorResponse:
   360  		return e.StatusCode == http.StatusNotFound || e.Code == "NoSuchKey"
   361  	default:
   362  		return false
   363  	}
   364  }
   365  
   366  func (m *Minio) AbortMultipartUpload(ctx context.Context, uploadID string, name string) error {
   367  	if err := m.initMinio(ctx); err != nil {
   368  		return err
   369  	}
   370  	return m.core.AbortMultipartUpload(ctx, m.bucket, name, uploadID)
   371  }
   372  
   373  func (m *Minio) ListUploadedParts(ctx context.Context, uploadID string, name string, partNumberMarker int, maxParts int) (*s3.ListUploadedPartsResult, error) {
   374  	if err := m.initMinio(ctx); err != nil {
   375  		return nil, err
   376  	}
   377  	result, err := m.core.ListObjectParts(ctx, m.bucket, name, uploadID, partNumberMarker, maxParts)
   378  	if err != nil {
   379  		return nil, err
   380  	}
   381  	res := &s3.ListUploadedPartsResult{
   382  		Key:                  result.Key,
   383  		UploadID:             result.UploadID,
   384  		MaxParts:             result.MaxParts,
   385  		NextPartNumberMarker: result.NextPartNumberMarker,
   386  		UploadedParts:        make([]s3.UploadedPart, len(result.ObjectParts)),
   387  	}
   388  	for i, part := range result.ObjectParts {
   389  		res.UploadedParts[i] = s3.UploadedPart{
   390  			PartNumber:   part.PartNumber,
   391  			LastModified: part.LastModified,
   392  			ETag:         part.ETag,
   393  			Size:         part.Size,
   394  		}
   395  	}
   396  	return res, nil
   397  }
   398  
   399  func (m *Minio) PresignedGetObject(ctx context.Context, name string, expire time.Duration, query url.Values) (string, error) {
   400  	if expire <= 0 {
   401  		expire = time.Hour * 24 * 365 * 99 // 99 years
   402  	} else if expire < time.Second {
   403  		expire = time.Second
   404  	}
   405  	var (
   406  		rawURL *url.URL
   407  		err    error
   408  	)
   409  	if m.conf.PublicRead {
   410  		rawURL, err = makeTargetURL(m.sign, m.bucket, name, m.location, false, query)
   411  	} else {
   412  		rawURL, err = m.sign.PresignedGetObject(ctx, m.bucket, name, expire, query)
   413  	}
   414  	if err != nil {
   415  		return "", err
   416  	}
   417  	if m.prefix != "" {
   418  		rawURL.Path = path.Join(m.prefix, rawURL.Path)
   419  	}
   420  	return rawURL.String(), nil
   421  }
   422  
   423  func (m *Minio) AccessURL(ctx context.Context, name string, expire time.Duration, opt *s3.AccessURLOption) (string, error) {
   424  	if err := m.initMinio(ctx); err != nil {
   425  		return "", err
   426  	}
   427  	reqParams := make(url.Values)
   428  	if opt != nil {
   429  		if opt.ContentType != "" {
   430  			reqParams.Set("response-content-type", opt.ContentType)
   431  		}
   432  		if opt.Filename != "" {
   433  			reqParams.Set("response-content-disposition", `attachment; filename=`+strconv.Quote(opt.Filename))
   434  		}
   435  	}
   436  	if opt.Image == nil || (opt.Image.Width < 0 && opt.Image.Height < 0 && opt.Image.Format == "") || (opt.Image.Width > maxImageWidth || opt.Image.Height > maxImageHeight) {
   437  		return m.PresignedGetObject(ctx, name, expire, reqParams)
   438  	}
   439  	return m.getImageThumbnailURL(ctx, name, expire, opt.Image)
   440  }
   441  
   442  func (m *Minio) getObjectData(ctx context.Context, name string, limit int64) ([]byte, error) {
   443  	object, err := m.core.Client.GetObject(ctx, m.bucket, name, minio.GetObjectOptions{})
   444  	if err != nil {
   445  		return nil, err
   446  	}
   447  	defer object.Close()
   448  	if limit < 0 {
   449  		return io.ReadAll(object)
   450  	}
   451  	return io.ReadAll(io.LimitReader(object, limit))
   452  }
   453  
   454  func (m *Minio) FormData(ctx context.Context, name string, size int64, contentType string, duration time.Duration) (*s3.FormData, error) {
   455  	if err := m.initMinio(ctx); err != nil {
   456  		return nil, err
   457  	}
   458  	policy := minio.NewPostPolicy()
   459  	if err := policy.SetKey(name); err != nil {
   460  		return nil, err
   461  	}
   462  	expires := time.Now().Add(duration)
   463  	if err := policy.SetExpires(expires); err != nil {
   464  		return nil, err
   465  	}
   466  	if size > 0 {
   467  		if err := policy.SetContentLengthRange(0, size); err != nil {
   468  			return nil, err
   469  		}
   470  	}
   471  	if err := policy.SetSuccessStatusAction(strconv.Itoa(successCode)); err != nil {
   472  		return nil, err
   473  	}
   474  	if contentType != "" {
   475  		if err := policy.SetContentType(contentType); err != nil {
   476  			return nil, err
   477  		}
   478  	}
   479  	if err := policy.SetBucket(m.bucket); err != nil {
   480  		return nil, err
   481  	}
   482  	u, fd, err := m.core.PresignedPostPolicy(ctx, policy)
   483  	if err != nil {
   484  		return nil, err
   485  	}
   486  	sign, err := url.Parse(m.signEndpoint)
   487  	if err != nil {
   488  		return nil, err
   489  	}
   490  	u.Scheme = sign.Scheme
   491  	u.Host = sign.Host
   492  	return &s3.FormData{
   493  		URL:          u.String(),
   494  		File:         "file",
   495  		Header:       nil,
   496  		FormData:     fd,
   497  		Expires:      expires,
   498  		SuccessCodes: []int{successCode},
   499  	}, nil
   500  }
   501  
   502  func (m *Minio) GetImageThumbnailKey(ctx context.Context, name string) (string, error) {
   503  	info, img, err := m.getObjectImageInfo(ctx, name)
   504  	if err != nil {
   505  		return "", errs.Wrap(err)
   506  	}
   507  	if !info.IsImg {
   508  		return "", errs.New("object not image").Wrap()
   509  	}
   510  	thumbnailWidth, thumbnailHeight := getThumbnailSize(img)
   511  
   512  	cacheKey := filepath.Join(imageThumbnailPath, info.Etag, fmt.Sprintf("image_w%d_h%d.%s", thumbnailWidth, thumbnailHeight, info.Format))
   513  	return cacheKey, nil
   514  }