github.com/haalcala/mattermost-server-change-repo@v0.0.0-20210713015153-16753fbeee5f/services/filesstore/s3store.go (about)

     1  // Copyright (c) 2015-present Mattermost, Inc. All Rights Reserved.
     2  // See LICENSE.txt for license information.
     3  
     4  package filesstore
     5  
     6  import (
     7  	"context"
     8  	"io"
     9  	"io/ioutil"
    10  	"os"
    11  	"path/filepath"
    12  	"strings"
    13  	"time"
    14  
    15  	s3 "github.com/minio/minio-go/v7"
    16  	"github.com/minio/minio-go/v7/pkg/credentials"
    17  	"github.com/minio/minio-go/v7/pkg/encrypt"
    18  	"github.com/pkg/errors"
    19  
    20  	"github.com/mattermost/mattermost-server/v5/mlog"
    21  )
    22  
    23  // S3FileBackend contains all necessary information to communicate with
    24  // an AWS S3 compatible API backend.
    25  type S3FileBackend struct {
    26  	endpoint   string
    27  	accessKey  string
    28  	secretKey  string
    29  	secure     bool
    30  	signV2     bool
    31  	region     string
    32  	bucket     string
    33  	pathPrefix string
    34  	encrypt    bool
    35  	trace      bool
    36  	client     *s3.Client
    37  }
    38  
    39  const (
    40  	// This is not exported by minio. See: https://github.com/minio/minio-go/issues/1339
    41  	bucketNotFound = "NoSuchBucket"
    42  )
    43  
    44  var (
    45  	imageExtensions = map[string]bool{".jpg": true, ".jpeg": true, ".gif": true, ".bmp": true, ".png": true, ".tiff": true, "tif": true}
    46  	imageMimeTypes  = map[string]string{".jpg": "image/jpeg", ".jpeg": "image/jpeg", ".gif": "image/gif", ".bmp": "image/bmp", ".png": "image/png", ".tiff": "image/tiff", ".tif": "image/tif"}
    47  )
    48  
    49  func isFileExtImage(ext string) bool {
    50  	ext = strings.ToLower(ext)
    51  	return imageExtensions[ext]
    52  }
    53  
    54  func getImageMimeType(ext string) string {
    55  	ext = strings.ToLower(ext)
    56  	if imageMimeTypes[ext] == "" {
    57  		return "image"
    58  	}
    59  	return imageMimeTypes[ext]
    60  }
    61  
    62  // NewS3FileBackend returns an instance of an S3FileBackend.
    63  func NewS3FileBackend(settings FileBackendSettings) (*S3FileBackend, error) {
    64  	backend := &S3FileBackend{
    65  		endpoint:   settings.AmazonS3Endpoint,
    66  		accessKey:  settings.AmazonS3AccessKeyId,
    67  		secretKey:  settings.AmazonS3SecretAccessKey,
    68  		secure:     settings.AmazonS3SSL,
    69  		signV2:     settings.AmazonS3SignV2,
    70  		region:     settings.AmazonS3Region,
    71  		bucket:     settings.AmazonS3Bucket,
    72  		pathPrefix: settings.AmazonS3PathPrefix,
    73  		encrypt:    settings.AmazonS3SSE,
    74  		trace:      settings.AmazonS3Trace,
    75  	}
    76  	cli, err := backend.s3New()
    77  	if err != nil {
    78  		return nil, err
    79  	}
    80  	backend.client = cli
    81  	return backend, nil
    82  }
    83  
    84  // Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
    85  // If signV2 input is false, function always returns signature v4.
    86  //
    87  // Additionally this function also takes a user defined region, if set
    88  // disables automatic region lookup.
    89  func (b *S3FileBackend) s3New() (*s3.Client, error) {
    90  	var creds *credentials.Credentials
    91  
    92  	isCloud := os.Getenv("MM_CLOUD_FILESTORE_BIFROST") != ""
    93  	if isCloud {
    94  		creds = credentials.New(customProvider{isSignV2: b.signV2})
    95  	} else if b.accessKey == "" && b.secretKey == "" {
    96  		creds = credentials.NewIAM("")
    97  	} else if b.signV2 {
    98  		creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2)
    99  	} else {
   100  		creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4)
   101  	}
   102  
   103  	opts := s3.Options{
   104  		Creds:  creds,
   105  		Secure: b.secure,
   106  		Region: b.region,
   107  	}
   108  
   109  	// If this is a cloud installation, we override the default transport.
   110  	if isCloud {
   111  		tr, err := s3.DefaultTransport(b.secure)
   112  		if err != nil {
   113  			return nil, err
   114  		}
   115  		scheme := "http"
   116  		if b.secure {
   117  			scheme = "https"
   118  		}
   119  		opts.Transport = &customTransport{
   120  			base:   tr,
   121  			host:   b.endpoint,
   122  			scheme: scheme,
   123  		}
   124  	}
   125  
   126  	s3Clnt, err := s3.New(b.endpoint, &opts)
   127  	if err != nil {
   128  		return nil, err
   129  	}
   130  
   131  	if b.trace {
   132  		s3Clnt.TraceOn(os.Stdout)
   133  	}
   134  
   135  	return s3Clnt, nil
   136  }
   137  
   138  func (b *S3FileBackend) TestConnection() error {
   139  	exists := true
   140  	var err error
   141  	// If a path prefix is present, we attempt to test the bucket by listing objects under the path
   142  	// and just checking the first response. This is because the BucketExists call is only at a bucket level
   143  	// and sometimes the user might only be allowed access to the specified path prefix.
   144  	if b.pathPrefix != "" {
   145  		obj := <-b.client.ListObjects(context.Background(), b.bucket, s3.ListObjectsOptions{Prefix: b.pathPrefix})
   146  		if obj.Err != nil {
   147  			typedErr := s3.ToErrorResponse(obj.Err)
   148  			if typedErr.Code != bucketNotFound {
   149  				return errors.Wrap(err, "unable to list objects in the s3 bucket")
   150  			}
   151  			exists = false
   152  		}
   153  	} else {
   154  		exists, err = b.client.BucketExists(context.Background(), b.bucket)
   155  		if err != nil {
   156  			return errors.Wrap(err, "unable to check if the s3 bucket exists")
   157  		}
   158  	}
   159  
   160  	if exists {
   161  		mlog.Debug("Connection to S3 or minio is good. Bucket exists.")
   162  	} else {
   163  		mlog.Warn("Bucket specified does not exist. Attempting to create...")
   164  		err := b.client.MakeBucket(context.Background(), b.bucket, s3.MakeBucketOptions{Region: b.region})
   165  		if err != nil {
   166  			return errors.Wrap(err, "unable to create the s3 bucket")
   167  		}
   168  	}
   169  
   170  	return nil
   171  }
   172  
   173  // Caller must close the first return value
   174  func (b *S3FileBackend) Reader(path string) (ReadCloseSeeker, error) {
   175  	path = filepath.Join(b.pathPrefix, path)
   176  	minioObject, err := b.client.GetObject(context.Background(), b.bucket, path, s3.GetObjectOptions{})
   177  	if err != nil {
   178  		return nil, errors.Wrapf(err, "unable to open file %s", path)
   179  	}
   180  
   181  	return minioObject, nil
   182  }
   183  
   184  func (b *S3FileBackend) ReadFile(path string) ([]byte, error) {
   185  	path = filepath.Join(b.pathPrefix, path)
   186  	minioObject, err := b.client.GetObject(context.Background(), b.bucket, path, s3.GetObjectOptions{})
   187  	if err != nil {
   188  		return nil, errors.Wrapf(err, "unable to open file %s", path)
   189  	}
   190  
   191  	defer minioObject.Close()
   192  	f, err := ioutil.ReadAll(minioObject)
   193  	if err != nil {
   194  		return nil, errors.Wrapf(err, "unable to read file %s", path)
   195  	}
   196  	return f, nil
   197  }
   198  
   199  func (b *S3FileBackend) FileExists(path string) (bool, error) {
   200  	path = filepath.Join(b.pathPrefix, path)
   201  
   202  	_, err := b.client.StatObject(context.Background(), b.bucket, path, s3.StatObjectOptions{})
   203  	if err == nil {
   204  		return true, nil
   205  	}
   206  
   207  	var s3Err s3.ErrorResponse
   208  	if errors.As(err, &s3Err); s3Err.Code == "NoSuchKey" {
   209  		return false, nil
   210  	}
   211  
   212  	return false, errors.Wrapf(err, "unable to know if file %s exists", path)
   213  }
   214  
   215  func (b *S3FileBackend) FileSize(path string) (int64, error) {
   216  	path = filepath.Join(b.pathPrefix, path)
   217  
   218  	info, err := b.client.StatObject(context.Background(), b.bucket, path, s3.StatObjectOptions{})
   219  	if err != nil {
   220  		return 0, errors.Wrapf(err, "unable to get file size for %s", path)
   221  	}
   222  
   223  	return info.Size, nil
   224  }
   225  
   226  func (b *S3FileBackend) FileModTime(path string) (time.Time, error) {
   227  	path = filepath.Join(b.pathPrefix, path)
   228  
   229  	info, err := b.client.StatObject(context.Background(), b.bucket, path, s3.StatObjectOptions{})
   230  	if err != nil {
   231  		return time.Time{}, errors.Wrapf(err, "unable to get modification time for file %s", path)
   232  	}
   233  
   234  	return info.LastModified, nil
   235  }
   236  
   237  func (b *S3FileBackend) CopyFile(oldPath, newPath string) error {
   238  	oldPath = filepath.Join(b.pathPrefix, oldPath)
   239  	newPath = filepath.Join(b.pathPrefix, newPath)
   240  	srcOpts := s3.CopySrcOptions{
   241  		Bucket:     b.bucket,
   242  		Object:     oldPath,
   243  		Encryption: encrypt.NewSSE(),
   244  	}
   245  	dstOpts := s3.CopyDestOptions{
   246  		Bucket:     b.bucket,
   247  		Object:     newPath,
   248  		Encryption: encrypt.NewSSE(),
   249  	}
   250  	if _, err := b.client.CopyObject(context.Background(), dstOpts, srcOpts); err != nil {
   251  		return errors.Wrapf(err, "unable to copy file from %s to %s", oldPath, newPath)
   252  	}
   253  	return nil
   254  }
   255  
   256  func (b *S3FileBackend) MoveFile(oldPath, newPath string) error {
   257  	oldPath = filepath.Join(b.pathPrefix, oldPath)
   258  	newPath = filepath.Join(b.pathPrefix, newPath)
   259  	srcOpts := s3.CopySrcOptions{
   260  		Bucket:     b.bucket,
   261  		Object:     oldPath,
   262  		Encryption: encrypt.NewSSE(),
   263  	}
   264  	dstOpts := s3.CopyDestOptions{
   265  		Bucket:     b.bucket,
   266  		Object:     newPath,
   267  		Encryption: encrypt.NewSSE(),
   268  	}
   269  
   270  	if _, err := b.client.CopyObject(context.Background(), dstOpts, srcOpts); err != nil {
   271  		return errors.Wrapf(err, "unable to copy the file to %s to the new destionation", newPath)
   272  	}
   273  
   274  	if err := b.client.RemoveObject(context.Background(), b.bucket, oldPath, s3.RemoveObjectOptions{}); err != nil {
   275  		return errors.Wrapf(err, "unable to remove the file old file %s", oldPath)
   276  	}
   277  
   278  	return nil
   279  }
   280  
   281  func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, error) {
   282  	var contentType string
   283  	path = filepath.Join(b.pathPrefix, path)
   284  	if ext := filepath.Ext(path); isFileExtImage(ext) {
   285  		contentType = getImageMimeType(ext)
   286  	} else {
   287  		contentType = "binary/octet-stream"
   288  	}
   289  
   290  	options := s3PutOptions(b.encrypt, contentType)
   291  	info, err := b.client.PutObject(context.Background(), b.bucket, path, fr, -1, options)
   292  	if err != nil {
   293  		return info.Size, errors.Wrapf(err, "unable write the data in the file %s", path)
   294  	}
   295  
   296  	return info.Size, nil
   297  }
   298  
   299  func (b *S3FileBackend) AppendFile(fr io.Reader, path string) (int64, error) {
   300  	fp := filepath.Join(b.pathPrefix, path)
   301  	if _, err := b.client.StatObject(context.Background(), b.bucket, fp, s3.StatObjectOptions{}); err != nil {
   302  		return 0, errors.Wrapf(err, "unable to find the file %s to append the data", path)
   303  	}
   304  
   305  	var contentType string
   306  	if ext := filepath.Ext(fp); isFileExtImage(ext) {
   307  		contentType = getImageMimeType(ext)
   308  	} else {
   309  		contentType = "binary/octet-stream"
   310  	}
   311  
   312  	options := s3PutOptions(b.encrypt, contentType)
   313  	sse := options.ServerSideEncryption
   314  	partName := fp + ".part"
   315  	info, err := b.client.PutObject(context.Background(), b.bucket, partName, fr, -1, options)
   316  	defer b.client.RemoveObject(context.Background(), b.bucket, partName, s3.RemoveObjectOptions{})
   317  	if info.Size > 0 {
   318  		src1Opts := s3.CopySrcOptions{
   319  			Bucket: b.bucket,
   320  			Object: fp,
   321  		}
   322  		src2Opts := s3.CopySrcOptions{
   323  			Bucket: b.bucket,
   324  			Object: partName,
   325  		}
   326  		dstOpts := s3.CopyDestOptions{
   327  			Bucket:     b.bucket,
   328  			Object:     fp,
   329  			Encryption: sse,
   330  		}
   331  		_, err = b.client.ComposeObject(context.Background(), dstOpts, src1Opts, src2Opts)
   332  		if err != nil {
   333  			return 0, errors.Wrapf(err, "unable append the data in the file %s", path)
   334  		}
   335  		return info.Size, nil
   336  	}
   337  
   338  	return 0, errors.Wrapf(err, "unable append the data in the file %s", path)
   339  }
   340  
   341  func (b *S3FileBackend) RemoveFile(path string) error {
   342  	path = filepath.Join(b.pathPrefix, path)
   343  	if err := b.client.RemoveObject(context.Background(), b.bucket, path, s3.RemoveObjectOptions{}); err != nil {
   344  		return errors.Wrapf(err, "unable to remove the file %s", path)
   345  	}
   346  
   347  	return nil
   348  }
   349  
   350  func getPathsFromObjectInfos(in <-chan s3.ObjectInfo) <-chan s3.ObjectInfo {
   351  	out := make(chan s3.ObjectInfo, 1)
   352  
   353  	go func() {
   354  		defer close(out)
   355  
   356  		for {
   357  			info, done := <-in
   358  
   359  			if !done {
   360  				break
   361  			}
   362  
   363  			out <- info
   364  		}
   365  	}()
   366  
   367  	return out
   368  }
   369  
   370  func (b *S3FileBackend) ListDirectory(path string) ([]string, error) {
   371  	path = filepath.Join(b.pathPrefix, path)
   372  	if !strings.HasSuffix(path, "/") && path != "" {
   373  		// s3Clnt returns only the path itself when "/" is not present
   374  		// appending "/" to make it consistent across all filesstores
   375  		path = path + "/"
   376  	}
   377  
   378  	opts := s3.ListObjectsOptions{
   379  		Prefix: path,
   380  	}
   381  	var paths []string
   382  	for object := range b.client.ListObjects(context.Background(), b.bucket, opts) {
   383  		if object.Err != nil {
   384  			return nil, errors.Wrapf(object.Err, "unable to list the directory %s", path)
   385  		}
   386  		// We strip the path prefix that gets applied,
   387  		// so that it remains transparent to the application.
   388  		object.Key = strings.TrimPrefix(object.Key, b.pathPrefix)
   389  		trimmed := strings.Trim(object.Key, "/")
   390  		if trimmed != "" {
   391  			paths = append(paths, trimmed)
   392  		}
   393  	}
   394  
   395  	return paths, nil
   396  }
   397  
   398  func (b *S3FileBackend) RemoveDirectory(path string) error {
   399  	opts := s3.ListObjectsOptions{
   400  		Prefix:    filepath.Join(b.pathPrefix, path),
   401  		Recursive: true,
   402  	}
   403  	list := b.client.ListObjects(context.Background(), b.bucket, opts)
   404  	objectsCh := b.client.RemoveObjects(context.Background(), b.bucket, getPathsFromObjectInfos(list), s3.RemoveObjectsOptions{})
   405  	for err := range objectsCh {
   406  		if err.Err != nil {
   407  			return errors.Wrapf(err.Err, "unable to remove the directory %s", path)
   408  		}
   409  	}
   410  
   411  	return nil
   412  }
   413  
   414  func s3PutOptions(encrypted bool, contentType string) s3.PutObjectOptions {
   415  	options := s3.PutObjectOptions{}
   416  	if encrypted {
   417  		options.ServerSideEncryption = encrypt.NewSSE()
   418  	}
   419  	options.ContentType = contentType
   420  	// We set the part size to the minimum allowed value of 5MBs
   421  	// to avoid an excessive allocation in minio.PutObject implementation.
   422  	options.PartSize = 1024 * 1024 * 5
   423  
   424  	return options
   425  }