github.com/jlevesy/mattermost-server@v5.3.2-0.20181003190404-7468f35cb0c8+incompatible/services/filesstore/s3store.go (about) 1 // Copyright (c) 2017-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" 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 }