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 }