github.com/mattermosttest/mattermost-server/v5@v5.0.0-20200917143240-9dfa12e121f9/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  	"bytes"
     8  	"io"
     9  	"io/ioutil"
    10  	"net/http"
    11  	"os"
    12  	"path/filepath"
    13  	"strings"
    14  
    15  	s3 "github.com/minio/minio-go/v6"
    16  	"github.com/minio/minio-go/v6/pkg/credentials"
    17  	"github.com/minio/minio-go/v6/pkg/encrypt"
    18  
    19  	"github.com/mattermost/mattermost-server/v5/mlog"
    20  	"github.com/mattermost/mattermost-server/v5/model"
    21  )
    22  
    23  type S3FileBackend struct {
    24  	endpoint   string
    25  	accessKey  string
    26  	secretKey  string
    27  	secure     bool
    28  	signV2     bool
    29  	region     string
    30  	bucket     string
    31  	pathPrefix string
    32  	encrypt    bool
    33  	trace      bool
    34  }
    35  
    36  // Similar to s3.New() but allows initialization of signature v2 or signature v4 client.
    37  // If signV2 input is false, function always returns signature v4.
    38  //
    39  // Additionally this function also takes a user defined region, if set
    40  // disables automatic region lookup.
    41  func (b *S3FileBackend) s3New() (*s3.Client, error) {
    42  	var creds *credentials.Credentials
    43  
    44  	if b.accessKey == "" && b.secretKey == "" {
    45  		creds = credentials.NewIAM("")
    46  	} else if b.signV2 {
    47  		creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV2)
    48  	} else {
    49  		creds = credentials.NewStatic(b.accessKey, b.secretKey, "", credentials.SignatureV4)
    50  	}
    51  
    52  	s3Clnt, err := s3.NewWithCredentials(b.endpoint, creds, b.secure, b.region)
    53  	if err != nil {
    54  		return nil, err
    55  	}
    56  
    57  	if b.trace {
    58  		s3Clnt.TraceOn(os.Stdout)
    59  	}
    60  
    61  	return s3Clnt, nil
    62  }
    63  
    64  func (b *S3FileBackend) TestConnection() *model.AppError {
    65  	s3Clnt, err := b.s3New()
    66  	if err != nil {
    67  		return model.NewAppError("TestFileConnection", "api.file.test_connection.s3.connection.app_error", nil, err.Error(), http.StatusInternalServerError)
    68  	}
    69  
    70  	exists, err := s3Clnt.BucketExists(b.bucket)
    71  	if err != nil {
    72  		return model.NewAppError("TestFileConnection", "api.file.test_connection.s3.bucket_exists.app_error", nil, err.Error(), http.StatusInternalServerError)
    73  	}
    74  
    75  	if !exists {
    76  		mlog.Warn("Bucket specified does not exist. Attempting to create...")
    77  		err := s3Clnt.MakeBucket(b.bucket, b.region)
    78  		if err != nil {
    79  			mlog.Error("Unable to create bucket.")
    80  			return model.NewAppError("TestFileConnection", "api.file.test_connection.s3.bucked_create.app_error", nil, err.Error(), http.StatusInternalServerError)
    81  		}
    82  	}
    83  	mlog.Debug("Connection to S3 or minio is good. Bucket exists.")
    84  	return nil
    85  }
    86  
    87  // Caller must close the first return value
    88  func (b *S3FileBackend) Reader(path string) (ReadCloseSeeker, *model.AppError) {
    89  	s3Clnt, err := b.s3New()
    90  	if err != nil {
    91  		return nil, model.NewAppError("Reader", "api.file.reader.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
    92  	}
    93  
    94  	path = filepath.Join(b.pathPrefix, path)
    95  	minioObject, err := s3Clnt.GetObject(b.bucket, path, s3.GetObjectOptions{})
    96  	if err != nil {
    97  		return nil, model.NewAppError("Reader", "api.file.reader.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
    98  	}
    99  
   100  	return minioObject, nil
   101  }
   102  
   103  func (b *S3FileBackend) ReadFile(path string) ([]byte, *model.AppError) {
   104  	s3Clnt, err := b.s3New()
   105  	if err != nil {
   106  		return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   107  	}
   108  
   109  	path = filepath.Join(b.pathPrefix, path)
   110  	minioObject, err := s3Clnt.GetObject(b.bucket, path, s3.GetObjectOptions{})
   111  	if err != nil {
   112  		return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   113  	}
   114  
   115  	defer minioObject.Close()
   116  	if f, err := ioutil.ReadAll(minioObject); err != nil {
   117  		return nil, model.NewAppError("ReadFile", "api.file.read_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   118  	} else {
   119  		return f, nil
   120  	}
   121  }
   122  
   123  func (b *S3FileBackend) FileExists(path string) (bool, *model.AppError) {
   124  	s3Clnt, err := b.s3New()
   125  
   126  	if err != nil {
   127  		return false, model.NewAppError("FileExists", "api.file.file_exists.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   128  	}
   129  
   130  	path = filepath.Join(b.pathPrefix, path)
   131  	_, err = s3Clnt.StatObject(b.bucket, path, s3.StatObjectOptions{})
   132  
   133  	if err == nil {
   134  		return true, nil
   135  	}
   136  
   137  	if err.(s3.ErrorResponse).Code == "NoSuchKey" {
   138  		return false, nil
   139  	}
   140  
   141  	return false, model.NewAppError("FileExists", "api.file.file_exists.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   142  }
   143  
   144  func (b *S3FileBackend) CopyFile(oldPath, newPath string) *model.AppError {
   145  	s3Clnt, err := b.s3New()
   146  	if err != nil {
   147  		return model.NewAppError("copyFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   148  	}
   149  
   150  	oldPath = filepath.Join(b.pathPrefix, oldPath)
   151  	newPath = filepath.Join(b.pathPrefix, newPath)
   152  
   153  	source := s3.NewSourceInfo(b.bucket, oldPath, nil)
   154  	destination, err := s3.NewDestinationInfo(b.bucket, newPath, encrypt.NewSSE(), nil)
   155  	if err != nil {
   156  		return model.NewAppError("copyFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   157  	}
   158  
   159  	if err = s3Clnt.CopyObject(destination, source); err != nil {
   160  		return model.NewAppError("copyFile", "api.file.move_file.copy_within_s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   161  	}
   162  	return nil
   163  }
   164  
   165  func (b *S3FileBackend) MoveFile(oldPath, newPath string) *model.AppError {
   166  	s3Clnt, err := b.s3New()
   167  	if err != nil {
   168  		return model.NewAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   169  	}
   170  
   171  	oldPath = filepath.Join(b.pathPrefix, oldPath)
   172  	newPath = filepath.Join(b.pathPrefix, newPath)
   173  
   174  	source := s3.NewSourceInfo(b.bucket, oldPath, nil)
   175  	destination, err := s3.NewDestinationInfo(b.bucket, newPath, encrypt.NewSSE(), nil)
   176  	if err != nil {
   177  		return model.NewAppError("moveFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   178  	}
   179  
   180  	if err = s3Clnt.CopyObject(destination, source); err != nil {
   181  		return model.NewAppError("moveFile", "api.file.move_file.copy_within_s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   182  	}
   183  
   184  	if err = s3Clnt.RemoveObject(b.bucket, oldPath); err != nil {
   185  		return model.NewAppError("moveFile", "api.file.move_file.delete_from_s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   186  	}
   187  
   188  	return nil
   189  }
   190  
   191  func (b *S3FileBackend) WriteFile(fr io.Reader, path string) (int64, *model.AppError) {
   192  	s3Clnt, err := b.s3New()
   193  	if err != nil {
   194  		return 0, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   195  	}
   196  
   197  	var contentType string
   198  	path = filepath.Join(b.pathPrefix, path)
   199  	if ext := filepath.Ext(path); model.IsFileExtImage(ext) {
   200  		contentType = model.GetImageMimeType(ext)
   201  	} else {
   202  		contentType = "binary/octet-stream"
   203  	}
   204  
   205  	options := s3PutOptions(b.encrypt, contentType)
   206  	var buf bytes.Buffer
   207  	_, err = buf.ReadFrom(fr)
   208  	if err != nil {
   209  		return 0, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   210  	}
   211  	written, err := s3Clnt.PutObject(b.bucket, path, &buf, int64(buf.Len()), options)
   212  	if err != nil {
   213  		return written, model.NewAppError("WriteFile", "api.file.write_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   214  	}
   215  
   216  	return written, nil
   217  }
   218  
   219  func (b *S3FileBackend) RemoveFile(path string) *model.AppError {
   220  	s3Clnt, err := b.s3New()
   221  	if err != nil {
   222  		return model.NewAppError("RemoveFile", "utils.file.remove_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   223  	}
   224  
   225  	path = filepath.Join(b.pathPrefix, path)
   226  	if err := s3Clnt.RemoveObject(b.bucket, path); err != nil {
   227  		return model.NewAppError("RemoveFile", "utils.file.remove_file.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   228  	}
   229  
   230  	return nil
   231  }
   232  
   233  func getPathsFromObjectInfos(in <-chan s3.ObjectInfo) <-chan string {
   234  	out := make(chan string, 1)
   235  
   236  	go func() {
   237  		defer close(out)
   238  
   239  		for {
   240  			info, done := <-in
   241  
   242  			if !done {
   243  				break
   244  			}
   245  
   246  			out <- info.Key
   247  		}
   248  	}()
   249  
   250  	return out
   251  }
   252  
   253  func (b *S3FileBackend) ListDirectory(path string) (*[]string, *model.AppError) {
   254  	var paths []string
   255  
   256  	s3Clnt, err := b.s3New()
   257  	if err != nil {
   258  		return nil, model.NewAppError("ListDirectory", "utils.file.list_directory.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   259  	}
   260  
   261  	doneCh := make(chan struct{})
   262  	defer close(doneCh)
   263  
   264  	path = filepath.Join(b.pathPrefix, path)
   265  	if !strings.HasSuffix(path, "/") && len(path) > 0 {
   266  		// s3Clnt returns only the path itself when "/" is not present
   267  		// appending "/" to make it consistent across all filesstores
   268  		path = path + "/"
   269  	}
   270  
   271  	for object := range s3Clnt.ListObjects(b.bucket, path, false, doneCh) {
   272  		if object.Err != nil {
   273  			return nil, model.NewAppError("ListDirectory", "utils.file.list_directory.s3.app_error", nil, object.Err.Error(), http.StatusInternalServerError)
   274  		}
   275  		paths = append(paths, strings.Trim(object.Key, "/"))
   276  	}
   277  
   278  	return &paths, nil
   279  }
   280  
   281  func (b *S3FileBackend) RemoveDirectory(path string) *model.AppError {
   282  	s3Clnt, err := b.s3New()
   283  	if err != nil {
   284  		return model.NewAppError("RemoveDirectory", "utils.file.remove_directory.s3.app_error", nil, err.Error(), http.StatusInternalServerError)
   285  	}
   286  
   287  	doneCh := make(chan struct{})
   288  
   289  	path = filepath.Join(b.pathPrefix, path)
   290  	for err := range s3Clnt.RemoveObjects(b.bucket, getPathsFromObjectInfos(s3Clnt.ListObjects(b.bucket, path, true, doneCh))) {
   291  		if err.Err != nil {
   292  			doneCh <- struct{}{}
   293  			return model.NewAppError("RemoveDirectory", "utils.file.remove_directory.s3.app_error", nil, err.Err.Error(), http.StatusInternalServerError)
   294  		}
   295  	}
   296  
   297  	close(doneCh)
   298  	return nil
   299  }
   300  
   301  func s3PutOptions(encrypted bool, contentType string) s3.PutObjectOptions {
   302  	options := s3.PutObjectOptions{}
   303  	if encrypted {
   304  		options.ServerSideEncryption = encrypt.NewSSE()
   305  	}
   306  	options.ContentType = contentType
   307  
   308  	return options
   309  }
   310  
   311  func CheckMandatoryS3Fields(settings *model.FileSettings) *model.AppError {
   312  	if settings.AmazonS3Bucket == nil || len(*settings.AmazonS3Bucket) == 0 {
   313  		return model.NewAppError("S3File", "api.admin.test_s3.missing_s3_bucket", nil, "", http.StatusBadRequest)
   314  	}
   315  
   316  	// if S3 endpoint is not set call the set defaults to set that
   317  	if settings.AmazonS3Endpoint == nil || len(*settings.AmazonS3Endpoint) == 0 {
   318  		settings.SetDefaults(true)
   319  	}
   320  
   321  	return nil
   322  }