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 }