github.com/levb/mattermost-server@v5.3.1+incompatible/utils/file_backend_s3.go (about)

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