storj.io/minio@v0.0.0-20230509071714-0cbc90f649b1/cmd/gateway/s3/gateway-s3-sse.go (about) 1 /* 2 * MinIO Cloud Storage, (C) 2018 MinIO, Inc. 3 * 4 * Licensed under the Apache License, Version 2.0 (the "License"); 5 * you may not use this file except in compliance with the License. 6 * You may obtain a copy of the License at 7 * 8 * http://www.apache.org/licenses/LICENSE-2.0 9 * 10 * Unless required by applicable law or agreed to in writing, software 11 * distributed under the License is distributed on an "AS IS" BASIS, 12 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. 13 * See the License for the specific language governing permissions and 14 * limitations under the License. 15 */ 16 17 package s3 18 19 import ( 20 "bytes" 21 "context" 22 "io" 23 "net/http" 24 "path" 25 "strconv" 26 "strings" 27 "time" 28 29 "github.com/minio/minio-go/v7/pkg/encrypt" 30 31 minio "storj.io/minio/cmd" 32 33 "storj.io/minio/cmd/logger" 34 ) 35 36 const ( 37 // name of custom multipart metadata file for s3 backend. 38 gwdareMetaJSON string = "dare.meta" 39 40 // name of temporary per part metadata file 41 gwpartMetaJSON string = "part.meta" 42 // custom multipart files are stored under the defaultMinioGWPrefix 43 defaultMinioGWPrefix = ".minio" 44 defaultGWContentFileName = "data" 45 ) 46 47 // s3EncObjects is a wrapper around s3Objects and implements gateway calls for 48 // custom large objects encrypted at the gateway 49 type s3EncObjects struct { 50 s3Objects 51 } 52 53 /* 54 NOTE: 55 Custom gateway encrypted objects are stored on backend as follows: 56 obj/.minio/data <= encrypted content 57 obj/.minio/dare.meta <= metadata 58 59 When a multipart upload operation is in progress, the metadata set during 60 NewMultipartUpload is stored in obj/.minio/uploadID/dare.meta and each 61 UploadPart operation saves additional state of the part's encrypted ETag and 62 encrypted size in obj/.minio/uploadID/part1/part.meta 63 64 All the part metadata and temp dare.meta are cleaned up when upload completes 65 */ 66 67 // ListObjects lists all blobs in S3 bucket filtered by prefix 68 func (l *s3EncObjects) ListObjects(ctx context.Context, bucket string, prefix string, marker string, delimiter string, maxKeys int) (loi minio.ListObjectsInfo, e error) { 69 var startAfter string 70 res, err := l.ListObjectsV2(ctx, bucket, prefix, marker, delimiter, maxKeys, false, startAfter) 71 if err != nil { 72 return loi, err 73 } 74 loi.IsTruncated = res.IsTruncated 75 loi.NextMarker = res.NextContinuationToken 76 loi.Objects = res.Objects 77 loi.Prefixes = res.Prefixes 78 return loi, nil 79 80 } 81 82 // ListObjectsV2 lists all blobs in S3 bucket filtered by prefix 83 func (l *s3EncObjects) ListObjectsV2(ctx context.Context, bucket, prefix, continuationToken, delimiter string, maxKeys int, fetchOwner bool, startAfter string) (loi minio.ListObjectsV2Info, e error) { 84 85 var objects []minio.ObjectInfo 86 var prefixes []string 87 var isTruncated bool 88 89 // filter out objects that contain a .minio prefix, but is not a dare.meta metadata file. 90 for { 91 loi, e = l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, fetchOwner, startAfter) 92 if e != nil { 93 return loi, minio.ErrorRespToObjectError(e, bucket) 94 } 95 96 continuationToken = loi.NextContinuationToken 97 isTruncated = loi.IsTruncated 98 99 for _, obj := range loi.Objects { 100 startAfter = obj.Name 101 102 if !isGWObject(obj.Name) { 103 continue 104 } 105 // get objectname and ObjectInfo from the custom metadata file 106 if strings.HasSuffix(obj.Name, gwdareMetaJSON) { 107 objSlice := strings.Split(obj.Name, minio.SlashSeparator+defaultMinioGWPrefix) 108 gwMeta, e := l.getGWMetadata(ctx, bucket, getDareMetaPath(objSlice[0])) 109 if e != nil { 110 continue 111 } 112 oInfo := gwMeta.ToObjectInfo(bucket, objSlice[0]) 113 objects = append(objects, oInfo) 114 } else { 115 objects = append(objects, obj) 116 } 117 if len(objects) > maxKeys { 118 break 119 } 120 } 121 for _, p := range loi.Prefixes { 122 objName := strings.TrimSuffix(p, minio.SlashSeparator) 123 gm, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(objName)) 124 // if prefix is actually a custom multi-part object, append it to objects 125 if err == nil { 126 objects = append(objects, gm.ToObjectInfo(bucket, objName)) 127 continue 128 } 129 isPrefix := l.isPrefix(ctx, bucket, p, fetchOwner, startAfter) 130 if isPrefix { 131 prefixes = append(prefixes, p) 132 } 133 } 134 if (len(objects) > maxKeys) || !loi.IsTruncated { 135 break 136 } 137 } 138 139 loi.IsTruncated = isTruncated 140 loi.ContinuationToken = continuationToken 141 loi.Objects = make([]minio.ObjectInfo, 0) 142 loi.Prefixes = make([]string, 0) 143 loi.Objects = append(loi.Objects, objects...) 144 145 for _, pfx := range prefixes { 146 if pfx != prefix { 147 loi.Prefixes = append(loi.Prefixes, pfx) 148 } 149 } 150 // Set continuation token if s3 returned truncated list 151 if isTruncated { 152 if len(objects) > 0 { 153 loi.NextContinuationToken = objects[len(objects)-1].Name 154 } 155 } 156 return loi, nil 157 } 158 159 // isGWObject returns true if it is a custom object 160 func isGWObject(objName string) bool { 161 isEncrypted := strings.Contains(objName, defaultMinioGWPrefix) 162 if !isEncrypted { 163 return true 164 } 165 // ignore temp part.meta files 166 if strings.Contains(objName, gwpartMetaJSON) { 167 return false 168 } 169 170 pfxSlice := strings.Split(objName, minio.SlashSeparator) 171 var i1, i2 int 172 for i := len(pfxSlice) - 1; i >= 0; i-- { 173 p := pfxSlice[i] 174 if p == defaultMinioGWPrefix { 175 i1 = i 176 } 177 if p == gwdareMetaJSON { 178 i2 = i 179 } 180 if i1 > 0 && i2 > 0 { 181 break 182 } 183 } 184 // incomplete uploads would have a uploadID between defaultMinioGWPrefix and gwdareMetaJSON 185 return i2 > 0 && i1 > 0 && i2-i1 == 1 186 } 187 188 // isPrefix returns true if prefix exists and is not an incomplete multipart upload entry 189 func (l *s3EncObjects) isPrefix(ctx context.Context, bucket, prefix string, fetchOwner bool, startAfter string) bool { 190 var continuationToken, delimiter string 191 192 for { 193 loi, e := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, fetchOwner, startAfter) 194 if e != nil { 195 return false 196 } 197 for _, obj := range loi.Objects { 198 if isGWObject(obj.Name) { 199 return true 200 } 201 } 202 203 continuationToken = loi.NextContinuationToken 204 if !loi.IsTruncated { 205 break 206 } 207 } 208 return false 209 } 210 211 // GetObject reads an object from S3. Supports additional 212 // parameters like offset and length which are synonymous with 213 // HTTP Range requests. 214 func (l *s3EncObjects) GetObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error { 215 return l.getObject(ctx, bucket, key, startOffset, length, writer, etag, opts) 216 } 217 218 func (l *s3EncObjects) isGWEncrypted(ctx context.Context, bucket, object string) bool { 219 _, err := l.s3Objects.GetObjectInfo(ctx, bucket, getDareMetaPath(object), minio.ObjectOptions{}) 220 return err == nil 221 } 222 223 // getDaremetadata fetches dare.meta from s3 backend and marshals into a structured format. 224 func (l *s3EncObjects) getGWMetadata(ctx context.Context, bucket, metaFileName string) (m gwMetaV1, err error) { 225 oi, err1 := l.s3Objects.GetObjectInfo(ctx, bucket, metaFileName, minio.ObjectOptions{}) 226 if err1 != nil { 227 return m, err1 228 } 229 var buffer bytes.Buffer 230 err = l.s3Objects.getObject(ctx, bucket, metaFileName, 0, oi.Size, &buffer, oi.ETag, minio.ObjectOptions{}) 231 if err != nil { 232 return m, err 233 } 234 return readGWMetadata(ctx, buffer) 235 } 236 237 // writes dare metadata to the s3 backend 238 func (l *s3EncObjects) writeGWMetadata(ctx context.Context, bucket, metaFileName string, m gwMetaV1, o minio.ObjectOptions) error { 239 reader, err := getGWMetadata(ctx, bucket, metaFileName, m) 240 if err != nil { 241 logger.LogIf(ctx, err) 242 return err 243 } 244 _, err = l.s3Objects.PutObject(ctx, bucket, metaFileName, reader, o) 245 return err 246 } 247 248 // returns path of temporary metadata json file for the upload 249 func getTmpDareMetaPath(object, uploadID string) string { 250 return path.Join(getGWMetaPath(object), uploadID, gwdareMetaJSON) 251 } 252 253 // returns path of metadata json file for encrypted objects 254 func getDareMetaPath(object string) string { 255 return path.Join(getGWMetaPath(object), gwdareMetaJSON) 256 } 257 258 // returns path of temporary part metadata file for multipart uploads 259 func getPartMetaPath(object, uploadID string, partID int) string { 260 return path.Join(object, defaultMinioGWPrefix, uploadID, strconv.Itoa(partID), gwpartMetaJSON) 261 } 262 263 // deletes the custom dare metadata file saved at the backend 264 func (l *s3EncObjects) deleteGWMetadata(ctx context.Context, bucket, metaFileName string) (minio.ObjectInfo, error) { 265 return l.s3Objects.DeleteObject(ctx, bucket, metaFileName, minio.ObjectOptions{}) 266 } 267 268 func (l *s3EncObjects) getObject(ctx context.Context, bucket string, key string, startOffset int64, length int64, writer io.Writer, etag string, opts minio.ObjectOptions) error { 269 var o minio.ObjectOptions 270 if minio.GlobalGatewaySSE.SSEC() { 271 o = opts 272 } 273 dmeta, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(key)) 274 if err != nil { 275 // unencrypted content 276 return l.s3Objects.getObject(ctx, bucket, key, startOffset, length, writer, etag, o) 277 } 278 if startOffset < 0 { 279 logger.LogIf(ctx, minio.InvalidRange{}) 280 } 281 282 // For negative length read everything. 283 if length < 0 { 284 length = dmeta.Stat.Size - startOffset 285 } 286 // Reply back invalid range if the input offset and length fall out of range. 287 if startOffset > dmeta.Stat.Size || startOffset+length > dmeta.Stat.Size { 288 logger.LogIf(ctx, minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size}) 289 return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size} 290 } 291 // Get start part index and offset. 292 _, partOffset, err := dmeta.ObjectToPartOffset(ctx, startOffset) 293 if err != nil { 294 return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size} 295 } 296 297 // Calculate endOffset according to length 298 endOffset := startOffset 299 if length > 0 { 300 endOffset += length - 1 301 } 302 303 // Get last part index to read given length. 304 if _, _, err := dmeta.ObjectToPartOffset(ctx, endOffset); err != nil { 305 return minio.InvalidRange{OffsetBegin: startOffset, OffsetEnd: length, ResourceSize: dmeta.Stat.Size} 306 } 307 return l.s3Objects.getObject(ctx, bucket, key, partOffset, endOffset, writer, dmeta.ETag, o) 308 } 309 310 // GetObjectNInfo - returns object info and locked object ReadCloser 311 func (l *s3EncObjects) GetObjectNInfo(ctx context.Context, bucket, object string, rs *minio.HTTPRangeSpec, h http.Header, lockType minio.LockType, o minio.ObjectOptions) (gr *minio.GetObjectReader, err error) { 312 var opts minio.ObjectOptions 313 if minio.GlobalGatewaySSE.SSEC() { 314 opts = o 315 } 316 objInfo, err := l.GetObjectInfo(ctx, bucket, object, opts) 317 if err != nil { 318 return l.s3Objects.GetObjectNInfo(ctx, bucket, object, rs, h, lockType, opts) 319 } 320 fn, off, length, err := minio.NewGetObjectReader(rs, objInfo, opts) 321 if err != nil { 322 return nil, minio.ErrorRespToObjectError(err, bucket, object) 323 } 324 if l.isGWEncrypted(ctx, bucket, object) { 325 object = getGWContentPath(object) 326 } 327 pr, pw := io.Pipe() 328 go func() { 329 // Do not set an `If-Match` header for the ETag when 330 // the ETag is encrypted. The ETag at the backend never 331 // matches an encrypted ETag and there is in any case 332 // no way to make two consecutive S3 calls safe for concurrent 333 // access. 334 // However, the encrypted object changes concurrently then the 335 // gateway will not be able to decrypt it since the key (obtained 336 // from dare.meta) will not work for any new created object. Therefore, 337 // we will in any case not return invalid data to the client. 338 etag := objInfo.ETag 339 if len(etag) > 32 && strings.Count(etag, "-") == 0 { 340 etag = "" 341 } 342 err := l.getObject(ctx, bucket, object, off, length, pw, etag, opts) 343 pw.CloseWithError(err) 344 }() 345 346 // Setup cleanup function to cause the above go-routine to 347 // exit in case of partial read 348 pipeCloser := func() { pr.Close() } 349 return fn(pr, h, o.CheckPrecondFn, pipeCloser) 350 } 351 352 // GetObjectInfo reads object info and replies back ObjectInfo 353 // For custom gateway encrypted large objects, the ObjectInfo is retrieved from the dare.meta file. 354 func (l *s3EncObjects) GetObjectInfo(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) { 355 var opts minio.ObjectOptions 356 if minio.GlobalGatewaySSE.SSEC() { 357 opts = o 358 } 359 360 gwMeta, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(object)) 361 if err != nil { 362 return l.s3Objects.GetObjectInfo(ctx, bucket, object, opts) 363 } 364 return gwMeta.ToObjectInfo(bucket, object), nil 365 } 366 367 // CopyObject copies an object from source bucket to a destination bucket. 368 func (l *s3EncObjects) CopyObject(ctx context.Context, srcBucket string, srcObject string, dstBucket string, dstObject string, srcInfo minio.ObjectInfo, s, d minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) { 369 cpSrcDstSame := path.Join(srcBucket, srcObject) == path.Join(dstBucket, dstObject) 370 if cpSrcDstSame { 371 var gwMeta gwMetaV1 372 if s.ServerSideEncryption != nil && d.ServerSideEncryption != nil && 373 ((s.ServerSideEncryption.Type() == encrypt.SSEC && d.ServerSideEncryption.Type() == encrypt.SSEC) || 374 (s.ServerSideEncryption.Type() == encrypt.S3 && d.ServerSideEncryption.Type() == encrypt.S3)) { 375 gwMeta, err = l.getGWMetadata(ctx, srcBucket, getDareMetaPath(srcObject)) 376 if err != nil { 377 return 378 } 379 header := make(http.Header) 380 if d.ServerSideEncryption != nil { 381 d.ServerSideEncryption.Marshal(header) 382 } 383 for k, v := range header { 384 srcInfo.UserDefined[k] = v[0] 385 } 386 gwMeta.Meta = srcInfo.UserDefined 387 if err = l.writeGWMetadata(ctx, dstBucket, getDareMetaPath(dstObject), gwMeta, minio.ObjectOptions{}); err != nil { 388 return objInfo, minio.ErrorRespToObjectError(err) 389 } 390 return gwMeta.ToObjectInfo(dstBucket, dstObject), nil 391 } 392 } 393 dstOpts := minio.ObjectOptions{ServerSideEncryption: d.ServerSideEncryption, UserDefined: srcInfo.UserDefined} 394 return l.PutObject(ctx, dstBucket, dstObject, srcInfo.PutObjReader, dstOpts) 395 } 396 397 // DeleteObject deletes a blob in bucket 398 // For custom gateway encrypted large objects, cleans up encrypted content and metadata files 399 // from the backend. 400 func (l *s3EncObjects) DeleteObject(ctx context.Context, bucket string, object string, opts minio.ObjectOptions) (minio.ObjectInfo, error) { 401 // Get dare meta json 402 if _, err := l.getGWMetadata(ctx, bucket, getDareMetaPath(object)); err != nil { 403 logger.LogIf(minio.GlobalContext, err) 404 return l.s3Objects.DeleteObject(ctx, bucket, object, opts) 405 } 406 // delete encrypted object 407 l.s3Objects.DeleteObject(ctx, bucket, getGWContentPath(object), opts) 408 return l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object)) 409 } 410 411 func (l *s3EncObjects) DeleteObjects(ctx context.Context, bucket string, objects []minio.ObjectToDelete, opts minio.ObjectOptions) ([]minio.DeletedObject, []error) { 412 errs := make([]error, len(objects)) 413 dobjects := make([]minio.DeletedObject, len(objects)) 414 for idx, object := range objects { 415 _, errs[idx] = l.DeleteObject(ctx, bucket, object.ObjectName, opts) 416 if errs[idx] == nil { 417 dobjects[idx] = minio.DeletedObject{ 418 ObjectName: object.ObjectName, 419 } 420 } 421 } 422 return dobjects, errs 423 } 424 425 // ListMultipartUploads lists all multipart uploads. 426 func (l *s3EncObjects) ListMultipartUploads(ctx context.Context, bucket string, prefix string, keyMarker string, uploadIDMarker string, delimiter string, maxUploads int) (lmi minio.ListMultipartsInfo, e error) { 427 428 lmi, e = l.s3Objects.ListMultipartUploads(ctx, bucket, prefix, keyMarker, uploadIDMarker, delimiter, maxUploads) 429 if e != nil { 430 return 431 } 432 lmi.KeyMarker = strings.TrimSuffix(lmi.KeyMarker, getGWContentPath(minio.SlashSeparator)) 433 lmi.NextKeyMarker = strings.TrimSuffix(lmi.NextKeyMarker, getGWContentPath(minio.SlashSeparator)) 434 for i := range lmi.Uploads { 435 lmi.Uploads[i].Object = strings.TrimSuffix(lmi.Uploads[i].Object, getGWContentPath(minio.SlashSeparator)) 436 } 437 return 438 } 439 440 // NewMultipartUpload uploads object in multiple parts 441 func (l *s3EncObjects) NewMultipartUpload(ctx context.Context, bucket string, object string, o minio.ObjectOptions) (uploadID string, err error) { 442 var sseOpts encrypt.ServerSide 443 if o.ServerSideEncryption == nil { 444 return l.s3Objects.NewMultipartUpload(ctx, bucket, object, minio.ObjectOptions{UserDefined: o.UserDefined}) 445 } 446 // Decide if sse options needed to be passed to backend 447 if (minio.GlobalGatewaySSE.SSEC() && o.ServerSideEncryption.Type() == encrypt.SSEC) || 448 (minio.GlobalGatewaySSE.SSES3() && o.ServerSideEncryption.Type() == encrypt.S3) { 449 sseOpts = o.ServerSideEncryption 450 } 451 452 uploadID, err = l.s3Objects.NewMultipartUpload(ctx, bucket, getGWContentPath(object), minio.ObjectOptions{ServerSideEncryption: sseOpts}) 453 if err != nil { 454 return 455 } 456 // Create uploadID and write a temporary dare.meta object under object/uploadID prefix 457 gwmeta := newGWMetaV1() 458 gwmeta.Meta = o.UserDefined 459 gwmeta.Stat.ModTime = time.Now().UTC() 460 err = l.writeGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID), gwmeta, minio.ObjectOptions{}) 461 if err != nil { 462 return uploadID, minio.ErrorRespToObjectError(err) 463 } 464 return uploadID, nil 465 } 466 467 // PutObject creates a new object with the incoming data, 468 func (l *s3EncObjects) PutObject(ctx context.Context, bucket string, object string, data *minio.PutObjReader, opts minio.ObjectOptions) (objInfo minio.ObjectInfo, err error) { 469 var sseOpts encrypt.ServerSide 470 // Decide if sse options needed to be passed to backend 471 if opts.ServerSideEncryption != nil && 472 ((minio.GlobalGatewaySSE.SSEC() && opts.ServerSideEncryption.Type() == encrypt.SSEC) || 473 (minio.GlobalGatewaySSE.SSES3() && opts.ServerSideEncryption.Type() == encrypt.S3) || 474 opts.ServerSideEncryption.Type() == encrypt.KMS) { 475 sseOpts = opts.ServerSideEncryption 476 } 477 if opts.ServerSideEncryption == nil { 478 defer l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object)) 479 defer l.DeleteObject(ctx, bucket, getGWContentPath(object), opts) 480 return l.s3Objects.PutObject(ctx, bucket, object, data, minio.ObjectOptions{UserDefined: opts.UserDefined}) 481 } 482 483 oi, err := l.s3Objects.PutObject(ctx, bucket, getGWContentPath(object), data, minio.ObjectOptions{ServerSideEncryption: sseOpts}) 484 if err != nil { 485 return objInfo, minio.ErrorRespToObjectError(err) 486 } 487 488 gwMeta := newGWMetaV1() 489 gwMeta.Meta = make(map[string]string) 490 for k, v := range opts.UserDefined { 491 gwMeta.Meta[k] = v 492 } 493 encMD5 := data.MD5CurrentHexString() 494 495 gwMeta.ETag = encMD5 496 gwMeta.Stat.Size = oi.Size 497 gwMeta.Stat.ModTime = time.Now().UTC() 498 if err = l.writeGWMetadata(ctx, bucket, getDareMetaPath(object), gwMeta, minio.ObjectOptions{}); err != nil { 499 return objInfo, minio.ErrorRespToObjectError(err) 500 } 501 objInfo = gwMeta.ToObjectInfo(bucket, object) 502 // delete any unencrypted content of the same name created previously 503 l.s3Objects.DeleteObject(ctx, bucket, object, opts) 504 return objInfo, nil 505 } 506 507 // PutObjectPart puts a part of object in bucket 508 func (l *s3EncObjects) PutObjectPart(ctx context.Context, bucket string, object string, uploadID string, partID int, data *minio.PutObjReader, opts minio.ObjectOptions) (pi minio.PartInfo, e error) { 509 510 if opts.ServerSideEncryption == nil { 511 return l.s3Objects.PutObjectPart(ctx, bucket, object, uploadID, partID, data, opts) 512 } 513 514 var s3Opts minio.ObjectOptions 515 // for sse-s3 encryption options should not be passed to backend 516 if opts.ServerSideEncryption != nil && opts.ServerSideEncryption.Type() == encrypt.SSEC && minio.GlobalGatewaySSE.SSEC() { 517 s3Opts = opts 518 } 519 520 uploadPath := getTmpGWMetaPath(object, uploadID) 521 tmpDareMeta := path.Join(uploadPath, gwdareMetaJSON) 522 _, err := l.s3Objects.GetObjectInfo(ctx, bucket, tmpDareMeta, minio.ObjectOptions{}) 523 if err != nil { 524 return pi, minio.InvalidUploadID{UploadID: uploadID} 525 } 526 527 pi, e = l.s3Objects.PutObjectPart(ctx, bucket, getGWContentPath(object), uploadID, partID, data, s3Opts) 528 if e != nil { 529 return 530 } 531 gwMeta := newGWMetaV1() 532 gwMeta.Parts = make([]minio.ObjectPartInfo, 1) 533 // Add incoming part. 534 gwMeta.Parts[0] = minio.ObjectPartInfo{ 535 Number: partID, 536 ETag: pi.ETag, 537 Size: pi.Size, 538 } 539 gwMeta.ETag = data.MD5CurrentHexString() // encrypted ETag 540 gwMeta.Stat.Size = pi.Size 541 gwMeta.Stat.ModTime = pi.LastModified 542 543 if err = l.writeGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, partID), gwMeta, minio.ObjectOptions{}); err != nil { 544 return pi, minio.ErrorRespToObjectError(err) 545 } 546 return minio.PartInfo{ 547 Size: gwMeta.Stat.Size, 548 ETag: minio.CanonicalizeETag(gwMeta.ETag), 549 LastModified: gwMeta.Stat.ModTime, 550 PartNumber: partID, 551 }, nil 552 } 553 554 // CopyObjectPart creates a part in a multipart upload by copying 555 // existing object or a part of it. 556 func (l *s3EncObjects) CopyObjectPart(ctx context.Context, srcBucket, srcObject, destBucket, destObject, uploadID string, 557 partID int, startOffset, length int64, srcInfo minio.ObjectInfo, srcOpts, dstOpts minio.ObjectOptions) (p minio.PartInfo, err error) { 558 return l.PutObjectPart(ctx, destBucket, destObject, uploadID, partID, srcInfo.PutObjReader, dstOpts) 559 } 560 561 // GetMultipartInfo returns multipart info of the uploadId of the object 562 func (l *s3EncObjects) GetMultipartInfo(ctx context.Context, bucket, object, uploadID string, opts minio.ObjectOptions) (result minio.MultipartInfo, err error) { 563 result.Bucket = bucket 564 result.Object = object 565 result.UploadID = uploadID 566 // We do not store parts uploaded so far in the dare.meta. Only CompleteMultipartUpload finalizes the parts under upload prefix.Otherwise, 567 // there could be situations of dare.meta getting corrupted by competing upload parts. 568 dm, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID)) 569 if err != nil { 570 return l.s3Objects.GetMultipartInfo(ctx, bucket, object, uploadID, opts) 571 } 572 result.UserDefined = dm.ToObjectInfo(bucket, object).UserDefined 573 return result, nil 574 } 575 576 // ListObjectParts returns all object parts for specified object in specified bucket 577 func (l *s3EncObjects) ListObjectParts(ctx context.Context, bucket string, object string, uploadID string, partNumberMarker int, maxParts int, opts minio.ObjectOptions) (lpi minio.ListPartsInfo, e error) { 578 // We do not store parts uploaded so far in the dare.meta. Only CompleteMultipartUpload finalizes the parts under upload prefix.Otherwise, 579 // there could be situations of dare.meta getting corrupted by competing upload parts. 580 dm, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID)) 581 if err != nil { 582 return l.s3Objects.ListObjectParts(ctx, bucket, object, uploadID, partNumberMarker, maxParts, opts) 583 } 584 585 lpi, err = l.s3Objects.ListObjectParts(ctx, bucket, getGWContentPath(object), uploadID, partNumberMarker, maxParts, opts) 586 if err != nil { 587 return lpi, err 588 } 589 for i, part := range lpi.Parts { 590 partMeta, err := l.getGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, part.PartNumber)) 591 if err != nil || len(partMeta.Parts) == 0 { 592 return lpi, minio.InvalidPart{} 593 } 594 lpi.Parts[i].ETag = partMeta.ETag 595 } 596 lpi.UserDefined = dm.ToObjectInfo(bucket, object).UserDefined 597 lpi.Object = object 598 return lpi, nil 599 } 600 601 // AbortMultipartUpload aborts a ongoing multipart upload 602 func (l *s3EncObjects) AbortMultipartUpload(ctx context.Context, bucket string, object string, uploadID string, opts minio.ObjectOptions) error { 603 if _, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID)); err != nil { 604 return l.s3Objects.AbortMultipartUpload(ctx, bucket, object, uploadID, opts) 605 } 606 607 if err := l.s3Objects.AbortMultipartUpload(ctx, bucket, getGWContentPath(object), uploadID, opts); err != nil { 608 return err 609 } 610 611 uploadPrefix := getTmpGWMetaPath(object, uploadID) 612 var continuationToken, startAfter, delimiter string 613 for { 614 loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, uploadPrefix, continuationToken, delimiter, 1000, false, startAfter) 615 if err != nil { 616 return minio.InvalidUploadID{UploadID: uploadID} 617 } 618 for _, obj := range loi.Objects { 619 if _, err := l.s3Objects.DeleteObject(ctx, bucket, obj.Name, minio.ObjectOptions{}); err != nil { 620 return minio.ErrorRespToObjectError(err) 621 } 622 startAfter = obj.Name 623 } 624 continuationToken = loi.NextContinuationToken 625 if !loi.IsTruncated { 626 break 627 } 628 } 629 return nil 630 } 631 632 // CompleteMultipartUpload completes ongoing multipart upload and finalizes object 633 func (l *s3EncObjects) CompleteMultipartUpload(ctx context.Context, bucket, object, uploadID string, uploadedParts []minio.CompletePart, opts minio.ObjectOptions) (oi minio.ObjectInfo, e error) { 634 635 tmpMeta, err := l.getGWMetadata(ctx, bucket, getTmpDareMetaPath(object, uploadID)) 636 if err != nil { 637 oi, e = l.s3Objects.CompleteMultipartUpload(ctx, bucket, object, uploadID, uploadedParts, opts) 638 if e == nil { 639 // delete any encrypted version of object that might exist 640 defer l.deleteGWMetadata(ctx, bucket, getDareMetaPath(object)) 641 defer l.DeleteObject(ctx, bucket, getGWContentPath(object), opts) 642 } 643 return oi, e 644 } 645 gwMeta := newGWMetaV1() 646 gwMeta.Meta = make(map[string]string) 647 for k, v := range tmpMeta.Meta { 648 gwMeta.Meta[k] = v 649 } 650 // Allocate parts similar to incoming slice. 651 gwMeta.Parts = make([]minio.ObjectPartInfo, len(uploadedParts)) 652 653 bkUploadedParts := make([]minio.CompletePart, len(uploadedParts)) 654 // Calculate full object size. 655 var objectSize int64 656 657 // Validate each part and then commit to disk. 658 for i, part := range uploadedParts { 659 partMeta, err := l.getGWMetadata(ctx, bucket, getPartMetaPath(object, uploadID, part.PartNumber)) 660 if err != nil || len(partMeta.Parts) == 0 { 661 return oi, minio.InvalidPart{} 662 } 663 bkUploadedParts[i] = minio.CompletePart{PartNumber: part.PartNumber, ETag: partMeta.Parts[0].ETag} 664 gwMeta.Parts[i] = partMeta.Parts[0] 665 objectSize += partMeta.Parts[0].Size 666 } 667 oi, e = l.s3Objects.CompleteMultipartUpload(ctx, bucket, getGWContentPath(object), uploadID, bkUploadedParts, opts) 668 if e != nil { 669 return oi, e 670 } 671 672 //delete any unencrypted version of object that might be on the backend 673 defer l.s3Objects.DeleteObject(ctx, bucket, object, opts) 674 675 // Save the final object size and modtime. 676 gwMeta.Stat.Size = objectSize 677 gwMeta.Stat.ModTime = time.Now().UTC() 678 gwMeta.ETag = oi.ETag 679 680 if err = l.writeGWMetadata(ctx, bucket, getDareMetaPath(object), gwMeta, minio.ObjectOptions{}); err != nil { 681 return oi, minio.ErrorRespToObjectError(err) 682 } 683 // Clean up any uploaded parts that are not being committed by this CompleteMultipart operation 684 var continuationToken, startAfter, delimiter string 685 uploadPrefix := getTmpGWMetaPath(object, uploadID) 686 done := false 687 for { 688 loi, lerr := l.s3Objects.ListObjectsV2(ctx, bucket, uploadPrefix, continuationToken, delimiter, 1000, false, startAfter) 689 if lerr != nil { 690 break 691 } 692 for _, obj := range loi.Objects { 693 if !strings.HasPrefix(obj.Name, uploadPrefix) { 694 done = true 695 break 696 } 697 startAfter = obj.Name 698 l.s3Objects.DeleteObject(ctx, bucket, obj.Name, opts) 699 } 700 continuationToken = loi.NextContinuationToken 701 if !loi.IsTruncated || done { 702 break 703 } 704 } 705 706 return gwMeta.ToObjectInfo(bucket, object), nil 707 } 708 709 // getTmpGWMetaPath returns the prefix under which uploads in progress are stored on backend 710 func getTmpGWMetaPath(object, uploadID string) string { 711 return path.Join(object, defaultMinioGWPrefix, uploadID) 712 } 713 714 // getGWMetaPath returns the prefix under which custom object metadata and object are stored on backend after upload completes 715 func getGWMetaPath(object string) string { 716 return path.Join(object, defaultMinioGWPrefix) 717 } 718 719 // getGWContentPath returns the prefix under which custom object is stored on backend after upload completes 720 func getGWContentPath(object string) string { 721 return path.Join(object, defaultMinioGWPrefix, defaultGWContentFileName) 722 } 723 724 // Clean-up the stale incomplete encrypted multipart uploads. Should be run in a Go routine. 725 func (l *s3EncObjects) cleanupStaleEncMultipartUploads(ctx context.Context, cleanupInterval, expiry time.Duration) { 726 ticker := time.NewTicker(cleanupInterval) 727 defer ticker.Stop() 728 729 for { 730 select { 731 case <-ctx.Done(): 732 return 733 case <-ticker.C: 734 l.cleanupStaleUploads(ctx, expiry) 735 } 736 } 737 } 738 739 // cleanupStaleUploads removes old custom encryption multipart uploads on backend 740 func (l *s3EncObjects) cleanupStaleUploads(ctx context.Context, expiry time.Duration) { 741 buckets, err := l.s3Objects.ListBuckets(ctx) 742 if err != nil { 743 logger.LogIf(ctx, err) 744 return 745 } 746 for _, b := range buckets { 747 expParts := l.getStalePartsForBucket(ctx, b.Name, expiry) 748 for k := range expParts { 749 l.s3Objects.DeleteObject(ctx, b.Name, k, minio.ObjectOptions{}) 750 } 751 } 752 } 753 754 func (l *s3EncObjects) getStalePartsForBucket(ctx context.Context, bucket string, expiry time.Duration) (expParts map[string]string) { 755 var prefix, continuationToken, delimiter, startAfter string 756 expParts = make(map[string]string) 757 now := time.Now() 758 for { 759 loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, false, startAfter) 760 if err != nil { 761 logger.LogIf(ctx, err) 762 break 763 } 764 for _, obj := range loi.Objects { 765 startAfter = obj.Name 766 if !strings.Contains(obj.Name, defaultMinioGWPrefix) { 767 continue 768 } 769 770 if isGWObject(obj.Name) { 771 continue 772 } 773 774 // delete temporary part.meta or dare.meta files for incomplete uploads that are past expiry 775 if (strings.HasSuffix(obj.Name, gwpartMetaJSON) || strings.HasSuffix(obj.Name, gwdareMetaJSON)) && 776 now.Sub(obj.ModTime) > expiry { 777 expParts[obj.Name] = "" 778 } 779 } 780 continuationToken = loi.NextContinuationToken 781 if !loi.IsTruncated { 782 break 783 } 784 } 785 return 786 } 787 788 func (l *s3EncObjects) DeleteBucket(ctx context.Context, bucket string, forceDelete bool) error { 789 var prefix, continuationToken, delimiter, startAfter string 790 expParts := make(map[string]string) 791 792 for { 793 loi, err := l.s3Objects.ListObjectsV2(ctx, bucket, prefix, continuationToken, delimiter, 1000, false, startAfter) 794 if err != nil { 795 break 796 } 797 for _, obj := range loi.Objects { 798 startAfter = obj.Name 799 if !strings.Contains(obj.Name, defaultMinioGWPrefix) { 800 return minio.BucketNotEmpty{} 801 } 802 if isGWObject(obj.Name) { 803 return minio.BucketNotEmpty{} 804 } 805 // delete temporary part.meta or dare.meta files for incomplete uploads 806 if strings.HasSuffix(obj.Name, gwpartMetaJSON) || strings.HasSuffix(obj.Name, gwdareMetaJSON) { 807 expParts[obj.Name] = "" 808 } 809 } 810 continuationToken = loi.NextContinuationToken 811 if !loi.IsTruncated { 812 break 813 } 814 } 815 for k := range expParts { 816 l.s3Objects.DeleteObject(ctx, bucket, k, minio.ObjectOptions{}) 817 } 818 err := l.Client.RemoveBucket(ctx, bucket) 819 if err != nil { 820 return minio.ErrorRespToObjectError(err, bucket) 821 } 822 return nil 823 }