github.com/minio/minio-go/v6@v6.0.57/api-compose-object.go (about)

     1  /*
     2   * MinIO Go Library for Amazon S3 Compatible Cloud Storage
     3   * Copyright 2017, 2018 MinIO, Inc.
     4   *
     5   * Licensed under the Apache License, Version 2.0 (the "License");
     6   * you may not use this file except in compliance with the License.
     7   * You may obtain a copy of the License at
     8   *
     9   *     http://www.apache.org/licenses/LICENSE-2.0
    10   *
    11   * Unless required by applicable law or agreed to in writing, software
    12   * distributed under the License is distributed on an "AS IS" BASIS,
    13   * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
    14   * See the License for the specific language governing permissions and
    15   * limitations under the License.
    16   */
    17  
    18  package minio
    19  
    20  import (
    21  	"context"
    22  	"fmt"
    23  	"io"
    24  	"io/ioutil"
    25  	"net/http"
    26  	"net/url"
    27  	"strconv"
    28  	"strings"
    29  	"time"
    30  
    31  	"github.com/minio/minio-go/v6/pkg/encrypt"
    32  	"github.com/minio/minio-go/v6/pkg/s3utils"
    33  )
    34  
    35  // DestinationInfo - type with information about the object to be
    36  // created via server-side copy requests, using the Compose API.
    37  type DestinationInfo struct {
    38  	bucket, object string
    39  	opts           DestInfoOptions
    40  }
    41  
    42  // DestInfoOptions represents options specified by user for NewDestinationInfo call
    43  type DestInfoOptions struct {
    44  	// `Encryption` is the key info for server-side-encryption with customer
    45  	// provided key. If it is nil, no encryption is performed.
    46  	Encryption encrypt.ServerSide
    47  
    48  	// `userMeta` is the user-metadata key-value pairs to be set on the
    49  	// destination. The keys are automatically prefixed with `x-amz-meta-`
    50  	// if needed. If nil is passed, and if only a single source (of any
    51  	// size) is provided in the ComposeObject call, then metadata from the
    52  	// source is copied to the destination.
    53  	// if no user-metadata is provided, it is copied from source
    54  	// (when there is only once source object in the compose
    55  	// request)
    56  	UserMeta map[string]string
    57  
    58  	// `userTags` is the user defined object tags to be set on destination.
    59  	// This will be set only if the `replaceTags` field is set to true.
    60  	// Otherwise this field is ignored
    61  	UserTags    map[string]string
    62  	ReplaceTags bool
    63  
    64  	// Specifies whether you want to apply a Legal Hold to the copied object.
    65  	LegalHold LegalHoldStatus
    66  
    67  	// Object Retention related fields
    68  	Mode            RetentionMode
    69  	RetainUntilDate time.Time
    70  }
    71  
    72  // Process custom-metadata to remove a `x-amz-meta-` prefix if
    73  // present and validate that keys are distinct (after this
    74  // prefix removal).
    75  func filterCustomMeta(userMeta map[string]string) (map[string]string, error) {
    76  	m := make(map[string]string)
    77  	for k, v := range userMeta {
    78  		if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") {
    79  			k = k[len("x-amz-meta-"):]
    80  		}
    81  		if _, ok := m[k]; ok {
    82  			return nil, ErrInvalidArgument(fmt.Sprintf("Cannot add both %s and x-amz-meta-%s keys as custom metadata", k, k))
    83  		}
    84  		m[k] = v
    85  	}
    86  	return m, nil
    87  }
    88  
    89  // NewDestinationInfo - creates a compose-object/copy-source
    90  // destination info object.
    91  //
    92  // `sse` is the key info for server-side-encryption with customer
    93  // provided key. If it is nil, no encryption is performed.
    94  //
    95  // `userMeta` is the user-metadata key-value pairs to be set on the
    96  // destination. The keys are automatically prefixed with `x-amz-meta-`
    97  // if needed. If nil is passed, and if only a single source (of any
    98  // size) is provided in the ComposeObject call, then metadata from the
    99  // source is copied to the destination.
   100  func NewDestinationInfo(bucket, object string, sse encrypt.ServerSide, userMeta map[string]string) (d DestinationInfo, err error) {
   101  	// Input validation.
   102  	if err = s3utils.CheckValidBucketName(bucket); err != nil {
   103  		return d, err
   104  	}
   105  	if err = s3utils.CheckValidObjectName(object); err != nil {
   106  		return d, err
   107  	}
   108  	m, err := filterCustomMeta(userMeta)
   109  	if err != nil {
   110  		return d, err
   111  	}
   112  	opts := DestInfoOptions{
   113  		Encryption:  sse,
   114  		UserMeta:    m,
   115  		UserTags:    nil,
   116  		ReplaceTags: false,
   117  		LegalHold:   LegalHoldStatus(""),
   118  		Mode:        RetentionMode(""),
   119  	}
   120  	return DestinationInfo{
   121  		bucket: bucket,
   122  		object: object,
   123  		opts:   opts,
   124  	}, nil
   125  }
   126  
   127  // NewDestinationInfoWithOptions - creates a compose-object/copy-source
   128  // destination info object.
   129  func NewDestinationInfoWithOptions(bucket, object string, destOpts DestInfoOptions) (d DestinationInfo, err error) {
   130  	// Input validation.
   131  	if err = s3utils.CheckValidBucketName(bucket); err != nil {
   132  		return d, err
   133  	}
   134  	if err = s3utils.CheckValidObjectName(object); err != nil {
   135  		return d, err
   136  	}
   137  	destOpts.UserMeta, err = filterCustomMeta(destOpts.UserMeta)
   138  	if err != nil {
   139  		return d, err
   140  	}
   141  	return DestinationInfo{
   142  		bucket: bucket,
   143  		object: object,
   144  		opts:   destOpts,
   145  	}, nil
   146  }
   147  
   148  // getUserMetaHeadersMap - construct appropriate key-value pairs to send
   149  // as headers from metadata map to pass into copy-object request. For
   150  // single part copy-object (i.e. non-multipart object), enable the
   151  // withCopyDirectiveHeader to set the `x-amz-metadata-directive` to
   152  // `REPLACE`, so that metadata headers from the source are not copied
   153  // over.
   154  func (d *DestinationInfo) getUserMetaHeadersMap(withCopyDirectiveHeader bool) map[string]string {
   155  	if len(d.opts.UserMeta) == 0 {
   156  		return nil
   157  	}
   158  	r := make(map[string]string)
   159  	if withCopyDirectiveHeader {
   160  		r["x-amz-metadata-directive"] = "REPLACE"
   161  	}
   162  	for k, v := range d.opts.UserMeta {
   163  		if isAmzHeader(k) || isStandardHeader(k) || isStorageClassHeader(k) {
   164  			r[k] = v
   165  		} else {
   166  			r["x-amz-meta-"+k] = v
   167  		}
   168  	}
   169  	return r
   170  }
   171  
   172  // SourceInfo - represents a source object to be copied, using
   173  // server-side copying APIs.
   174  type SourceInfo struct {
   175  	bucket, object string
   176  	start, end     int64
   177  	encryption     encrypt.ServerSide
   178  	// Headers to send with the upload-part-copy request involving
   179  	// this source object.
   180  	Headers http.Header
   181  }
   182  
   183  // NewSourceInfo - create a compose-object/copy-object source info
   184  // object.
   185  //
   186  // `decryptSSEC` is the decryption key using server-side-encryption
   187  // with customer provided key. It may be nil if the source is not
   188  // encrypted.
   189  func NewSourceInfo(bucket, object string, sse encrypt.ServerSide) SourceInfo {
   190  	r := SourceInfo{
   191  		bucket:     bucket,
   192  		object:     object,
   193  		start:      -1, // range is unspecified by default
   194  		encryption: sse,
   195  		Headers:    make(http.Header),
   196  	}
   197  
   198  	// Set the source header
   199  	r.Headers.Set("x-amz-copy-source", s3utils.EncodePath(bucket+"/"+object))
   200  	return r
   201  }
   202  
   203  // SetRange - Set the start and end offset of the source object to be
   204  // copied. If this method is not called, the whole source object is
   205  // copied.
   206  func (s *SourceInfo) SetRange(start, end int64) error {
   207  	if start > end || start < 0 {
   208  		return ErrInvalidArgument("start must be non-negative, and start must be at most end.")
   209  	}
   210  	// Note that 0 <= start <= end
   211  	s.start, s.end = start, end
   212  	return nil
   213  }
   214  
   215  // SetMatchETagCond - Set ETag match condition. The object is copied
   216  // only if the etag of the source matches the value given here.
   217  func (s *SourceInfo) SetMatchETagCond(etag string) error {
   218  	if etag == "" {
   219  		return ErrInvalidArgument("ETag cannot be empty.")
   220  	}
   221  	s.Headers.Set("x-amz-copy-source-if-match", etag)
   222  	return nil
   223  }
   224  
   225  // SetMatchETagExceptCond - Set the ETag match exception
   226  // condition. The object is copied only if the etag of the source is
   227  // not the value given here.
   228  func (s *SourceInfo) SetMatchETagExceptCond(etag string) error {
   229  	if etag == "" {
   230  		return ErrInvalidArgument("ETag cannot be empty.")
   231  	}
   232  	s.Headers.Set("x-amz-copy-source-if-none-match", etag)
   233  	return nil
   234  }
   235  
   236  // SetModifiedSinceCond - Set the modified since condition.
   237  func (s *SourceInfo) SetModifiedSinceCond(modTime time.Time) error {
   238  	if modTime.IsZero() {
   239  		return ErrInvalidArgument("Input time cannot be 0.")
   240  	}
   241  	s.Headers.Set("x-amz-copy-source-if-modified-since", modTime.Format(http.TimeFormat))
   242  	return nil
   243  }
   244  
   245  // SetUnmodifiedSinceCond - Set the unmodified since condition.
   246  func (s *SourceInfo) SetUnmodifiedSinceCond(modTime time.Time) error {
   247  	if modTime.IsZero() {
   248  		return ErrInvalidArgument("Input time cannot be 0.")
   249  	}
   250  	s.Headers.Set("x-amz-copy-source-if-unmodified-since", modTime.Format(http.TimeFormat))
   251  	return nil
   252  }
   253  
   254  // Helper to fetch size and etag of an object using a StatObject call.
   255  func (s *SourceInfo) getProps(c Client) (size int64, etag string, userMeta map[string]string, err error) {
   256  	// Get object info - need size and etag here. Also, decryption
   257  	// headers are added to the stat request if given.
   258  	var objInfo ObjectInfo
   259  	opts := StatObjectOptions{GetObjectOptions{ServerSideEncryption: encrypt.SSE(s.encryption)}}
   260  	objInfo, err = c.statObject(context.Background(), s.bucket, s.object, opts)
   261  	if err != nil {
   262  		err = ErrInvalidArgument(fmt.Sprintf("Could not stat object - %s/%s: %v", s.bucket, s.object, err))
   263  	} else {
   264  		size = objInfo.Size
   265  		etag = objInfo.ETag
   266  		userMeta = make(map[string]string)
   267  		for k, v := range objInfo.Metadata {
   268  			if strings.HasPrefix(k, "x-amz-meta-") {
   269  				if len(v) > 0 {
   270  					userMeta[k] = v[0]
   271  				}
   272  			}
   273  		}
   274  	}
   275  	return
   276  }
   277  
   278  // Low level implementation of CopyObject API, supports only upto 5GiB worth of copy.
   279  func (c Client) copyObjectDo(ctx context.Context, srcBucket, srcObject, destBucket, destObject string,
   280  	metadata map[string]string) (ObjectInfo, error) {
   281  
   282  	// Build headers.
   283  	headers := make(http.Header)
   284  
   285  	// Set all the metadata headers.
   286  	for k, v := range metadata {
   287  		headers.Set(k, v)
   288  	}
   289  
   290  	// Set the source header
   291  	headers.Set("x-amz-copy-source", s3utils.EncodePath(srcBucket+"/"+srcObject))
   292  
   293  	// Send upload-part-copy request
   294  	resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
   295  		bucketName:   destBucket,
   296  		objectName:   destObject,
   297  		customHeader: headers,
   298  	})
   299  	defer closeResponse(resp)
   300  	if err != nil {
   301  		return ObjectInfo{}, err
   302  	}
   303  
   304  	// Check if we got an error response.
   305  	if resp.StatusCode != http.StatusOK {
   306  		return ObjectInfo{}, httpRespToErrorResponse(resp, srcBucket, srcObject)
   307  	}
   308  
   309  	cpObjRes := copyObjectResult{}
   310  	err = xmlDecoder(resp.Body, &cpObjRes)
   311  	if err != nil {
   312  		return ObjectInfo{}, err
   313  	}
   314  
   315  	objInfo := ObjectInfo{
   316  		Key:          destObject,
   317  		ETag:         strings.Trim(cpObjRes.ETag, "\""),
   318  		LastModified: cpObjRes.LastModified,
   319  	}
   320  	return objInfo, nil
   321  }
   322  
   323  func (c Client) copyObjectPartDo(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string,
   324  	partID int, startOffset int64, length int64, metadata map[string]string) (p CompletePart, err error) {
   325  
   326  	headers := make(http.Header)
   327  
   328  	// Set source
   329  	headers.Set("x-amz-copy-source", s3utils.EncodePath(srcBucket+"/"+srcObject))
   330  
   331  	if startOffset < 0 {
   332  		return p, ErrInvalidArgument("startOffset must be non-negative")
   333  	}
   334  
   335  	if length >= 0 {
   336  		headers.Set("x-amz-copy-source-range", fmt.Sprintf("bytes=%d-%d", startOffset, startOffset+length-1))
   337  	}
   338  
   339  	for k, v := range metadata {
   340  		headers.Set(k, v)
   341  	}
   342  
   343  	queryValues := make(url.Values)
   344  	queryValues.Set("partNumber", strconv.Itoa(partID))
   345  	queryValues.Set("uploadId", uploadID)
   346  
   347  	resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
   348  		bucketName:   destBucket,
   349  		objectName:   destObject,
   350  		customHeader: headers,
   351  		queryValues:  queryValues,
   352  	})
   353  	defer closeResponse(resp)
   354  	if err != nil {
   355  		return
   356  	}
   357  
   358  	// Check if we got an error response.
   359  	if resp.StatusCode != http.StatusOK {
   360  		return p, httpRespToErrorResponse(resp, destBucket, destObject)
   361  	}
   362  
   363  	// Decode copy-part response on success.
   364  	cpObjRes := copyObjectResult{}
   365  	err = xmlDecoder(resp.Body, &cpObjRes)
   366  	if err != nil {
   367  		return p, err
   368  	}
   369  	p.PartNumber, p.ETag = partID, cpObjRes.ETag
   370  	return p, nil
   371  }
   372  
   373  // uploadPartCopy - helper function to create a part in a multipart
   374  // upload via an upload-part-copy request
   375  // https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
   376  func (c Client) uploadPartCopy(ctx context.Context, bucket, object, uploadID string, partNumber int,
   377  	headers http.Header) (p CompletePart, err error) {
   378  
   379  	// Build query parameters
   380  	urlValues := make(url.Values)
   381  	urlValues.Set("partNumber", strconv.Itoa(partNumber))
   382  	urlValues.Set("uploadId", uploadID)
   383  
   384  	// Send upload-part-copy request
   385  	resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
   386  		bucketName:   bucket,
   387  		objectName:   object,
   388  		customHeader: headers,
   389  		queryValues:  urlValues,
   390  	})
   391  	defer closeResponse(resp)
   392  	if err != nil {
   393  		return p, err
   394  	}
   395  
   396  	// Check if we got an error response.
   397  	if resp.StatusCode != http.StatusOK {
   398  		return p, httpRespToErrorResponse(resp, bucket, object)
   399  	}
   400  
   401  	// Decode copy-part response on success.
   402  	cpObjRes := copyObjectResult{}
   403  	err = xmlDecoder(resp.Body, &cpObjRes)
   404  	if err != nil {
   405  		return p, err
   406  	}
   407  	p.PartNumber, p.ETag = partNumber, cpObjRes.ETag
   408  	return p, nil
   409  }
   410  
   411  // ComposeObjectWithProgress - creates an object using server-side copying of
   412  // existing objects. It takes a list of source objects (with optional
   413  // offsets) and concatenates them into a new object using only
   414  // server-side copying operations. Optionally takes progress reader hook
   415  // for applications to look at current progress.
   416  func (c Client) ComposeObjectWithProgress(dst DestinationInfo, srcs []SourceInfo, progress io.Reader) error {
   417  	if len(srcs) < 1 || len(srcs) > maxPartsCount {
   418  		return ErrInvalidArgument("There must be as least one and up to 10000 source objects.")
   419  	}
   420  	ctx := context.Background()
   421  	srcSizes := make([]int64, len(srcs))
   422  	var totalSize, size, totalParts int64
   423  	var srcUserMeta map[string]string
   424  	etags := make([]string, len(srcs))
   425  	var err error
   426  	for i, src := range srcs {
   427  		size, etags[i], srcUserMeta, err = src.getProps(c)
   428  		if err != nil {
   429  			return err
   430  		}
   431  
   432  		// Error out if client side encryption is used in this source object when
   433  		// more than one source objects are given.
   434  		if len(srcs) > 1 && src.Headers.Get("x-amz-meta-x-amz-key") != "" {
   435  			return ErrInvalidArgument(
   436  				fmt.Sprintf("Client side encryption is used in source object %s/%s", src.bucket, src.object))
   437  		}
   438  
   439  		// Check if a segment is specified, and if so, is the
   440  		// segment within object bounds?
   441  		if src.start != -1 {
   442  			// Since range is specified,
   443  			//    0 <= src.start <= src.end
   444  			// so only invalid case to check is:
   445  			if src.end >= size {
   446  				return ErrInvalidArgument(
   447  					fmt.Sprintf("SourceInfo %d has invalid segment-to-copy [%d, %d] (size is %d)",
   448  						i, src.start, src.end, size))
   449  			}
   450  			size = src.end - src.start + 1
   451  		}
   452  
   453  		// Only the last source may be less than `absMinPartSize`
   454  		if size < absMinPartSize && i < len(srcs)-1 {
   455  			return ErrInvalidArgument(
   456  				fmt.Sprintf("SourceInfo %d is too small (%d) and it is not the last part", i, size))
   457  		}
   458  
   459  		// Is data to copy too large?
   460  		totalSize += size
   461  		if totalSize > maxMultipartPutObjectSize {
   462  			return ErrInvalidArgument(fmt.Sprintf("Cannot compose an object of size %d (> 5TiB)", totalSize))
   463  		}
   464  
   465  		// record source size
   466  		srcSizes[i] = size
   467  
   468  		// calculate parts needed for current source
   469  		totalParts += partsRequired(size)
   470  		// Do we need more parts than we are allowed?
   471  		if totalParts > maxPartsCount {
   472  			return ErrInvalidArgument(fmt.Sprintf(
   473  				"Your proposed compose object requires more than %d parts", maxPartsCount))
   474  		}
   475  	}
   476  
   477  	// Single source object case (i.e. when only one source is
   478  	// involved, it is being copied wholly and at most 5GiB in
   479  	// size, emptyfiles are also supported).
   480  	if (totalParts == 1 && srcs[0].start == -1 && totalSize <= maxPartSize) || (totalSize == 0) {
   481  		return c.CopyObjectWithProgress(dst, srcs[0], progress)
   482  	}
   483  
   484  	// Now, handle multipart-copy cases.
   485  
   486  	// 1. Ensure that the object has not been changed while
   487  	//    we are copying data.
   488  	for i, src := range srcs {
   489  		if src.Headers.Get("x-amz-copy-source-if-match") == "" {
   490  			src.SetMatchETagCond(etags[i])
   491  		}
   492  	}
   493  
   494  	// 2. Initiate a new multipart upload.
   495  
   496  	// Set user-metadata on the destination object. If no
   497  	// user-metadata is specified, and there is only one source,
   498  	// (only) then metadata from source is copied.
   499  	userMeta := dst.getUserMetaHeadersMap(false)
   500  	metaMap := userMeta
   501  	if len(userMeta) == 0 && len(srcs) == 1 {
   502  		metaMap = srcUserMeta
   503  	}
   504  	metaHeaders := make(map[string]string)
   505  	for k, v := range metaMap {
   506  		metaHeaders[k] = v
   507  	}
   508  
   509  	uploadID, err := c.newUploadID(ctx, dst.bucket, dst.object, PutObjectOptions{ServerSideEncryption: dst.opts.Encryption, UserMetadata: metaHeaders})
   510  	if err != nil {
   511  		return err
   512  	}
   513  
   514  	// 3. Perform copy part uploads
   515  	objParts := []CompletePart{}
   516  	partIndex := 1
   517  	for i, src := range srcs {
   518  		h := src.Headers
   519  		if src.encryption != nil {
   520  			encrypt.SSECopy(src.encryption).Marshal(h)
   521  		}
   522  		// Add destination encryption headers
   523  		if dst.opts.Encryption != nil {
   524  			dst.opts.Encryption.Marshal(h)
   525  		}
   526  
   527  		// calculate start/end indices of parts after
   528  		// splitting.
   529  		startIdx, endIdx := calculateEvenSplits(srcSizes[i], src)
   530  		for j, start := range startIdx {
   531  			end := endIdx[j]
   532  
   533  			// Add (or reset) source range header for
   534  			// upload part copy request.
   535  			h.Set("x-amz-copy-source-range",
   536  				fmt.Sprintf("bytes=%d-%d", start, end))
   537  
   538  			// make upload-part-copy request
   539  			complPart, err := c.uploadPartCopy(ctx, dst.bucket,
   540  				dst.object, uploadID, partIndex, h)
   541  			if err != nil {
   542  				return err
   543  			}
   544  			if progress != nil {
   545  				io.CopyN(ioutil.Discard, progress, end-start+1)
   546  			}
   547  			objParts = append(objParts, complPart)
   548  			partIndex++
   549  		}
   550  	}
   551  
   552  	// 4. Make final complete-multipart request.
   553  	_, err = c.completeMultipartUpload(ctx, dst.bucket, dst.object, uploadID,
   554  		completeMultipartUpload{Parts: objParts})
   555  	if err != nil {
   556  		return err
   557  	}
   558  	return nil
   559  }
   560  
   561  // ComposeObject - creates an object using server-side copying of
   562  // existing objects. It takes a list of source objects (with optional
   563  // offsets) and concatenates them into a new object using only
   564  // server-side copying operations.
   565  func (c Client) ComposeObject(dst DestinationInfo, srcs []SourceInfo) error {
   566  	return c.ComposeObjectWithProgress(dst, srcs, nil)
   567  }
   568  
   569  // partsRequired is maximum parts possible with
   570  // max part size of ceiling(maxMultipartPutObjectSize / (maxPartsCount - 1))
   571  func partsRequired(size int64) int64 {
   572  	maxPartSize := maxMultipartPutObjectSize / (maxPartsCount - 1)
   573  	r := size / int64(maxPartSize)
   574  	if size%int64(maxPartSize) > 0 {
   575  		r++
   576  	}
   577  	return r
   578  }
   579  
   580  // calculateEvenSplits - computes splits for a source and returns
   581  // start and end index slices. Splits happen evenly to be sure that no
   582  // part is less than 5MiB, as that could fail the multipart request if
   583  // it is not the last part.
   584  func calculateEvenSplits(size int64, src SourceInfo) (startIndex, endIndex []int64) {
   585  	if size == 0 {
   586  		return
   587  	}
   588  
   589  	reqParts := partsRequired(size)
   590  	startIndex = make([]int64, reqParts)
   591  	endIndex = make([]int64, reqParts)
   592  	// Compute number of required parts `k`, as:
   593  	//
   594  	// k = ceiling(size / copyPartSize)
   595  	//
   596  	// Now, distribute the `size` bytes in the source into
   597  	// k parts as evenly as possible:
   598  	//
   599  	// r parts sized (q+1) bytes, and
   600  	// (k - r) parts sized q bytes, where
   601  	//
   602  	// size = q * k + r (by simple division of size by k,
   603  	// so that 0 <= r < k)
   604  	//
   605  	start := src.start
   606  	if start == -1 {
   607  		start = 0
   608  	}
   609  	quot, rem := size/reqParts, size%reqParts
   610  	nextStart := start
   611  	for j := int64(0); j < reqParts; j++ {
   612  		curPartSize := quot
   613  		if j < rem {
   614  			curPartSize++
   615  		}
   616  
   617  		cStart := nextStart
   618  		cEnd := cStart + curPartSize - 1
   619  		nextStart = cEnd + 1
   620  
   621  		startIndex[j], endIndex[j] = cStart, cEnd
   622  	}
   623  	return
   624  }