mirror of
https://github.com/octoleo/syncthing.git
synced 2024-12-23 11:28:59 +00:00
916ec63af6
This is a new revision of the discovery server. Relevant changes and non-changes: - Protocol towards clients is unchanged. - Recommended large scale design is still to be deployed nehind nginx (I tested, and it's still a lot faster at terminating TLS). - Database backend is leveldb again, only. It scales enough, is easy to setup, and we don't need any backend to take care of. - Server supports replication. This is a simple TCP channel - protect it with a firewall when deploying over the internet. (We deploy this within the same datacenter, and with firewall.) Any incoming client announces are sent over the replication channel(s) to other peer discosrvs. Incoming replication changes are applied to the database as if they came from clients, but without the TLS/certificate overhead. - Metrics are exposed using the prometheus library, when enabled. - The database values and replication protocol is protobuf, because JSON was quite CPU intensive when I tried that and benchmarked it. - The "Retry-After" value for failed lookups gets slowly increased from a default of 120 seconds, by 5 seconds for each failed lookup, independently by each discosrv. This lowers the query load over time for clients that are never seen. The Retry-After maxes out at 3600 after a couple of weeks of this increase. The number of failed lookups is stored in the database, now and then (avoiding making each lookup a database put). All in all this means clients can be pointed towards a cluster using just multiple A / AAAA records to gain both load sharing and redundancy (if one is down, clients will talk to the remaining ones). GitHub-Pull-Request: https://github.com/syncthing/syncthing/pull/4648
630 lines
18 KiB
Go
630 lines
18 KiB
Go
/*
|
|
* Minio Go Library for Amazon S3 Compatible Cloud Storage
|
|
* Copyright 2017 Minio, Inc.
|
|
*
|
|
* Licensed under the Apache License, Version 2.0 (the "License");
|
|
* you may not use this file except in compliance with the License.
|
|
* You may obtain a copy of the License at
|
|
*
|
|
* http://www.apache.org/licenses/LICENSE-2.0
|
|
*
|
|
* Unless required by applicable law or agreed to in writing, software
|
|
* distributed under the License is distributed on an "AS IS" BASIS,
|
|
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
|
|
* See the License for the specific language governing permissions and
|
|
* limitations under the License.
|
|
*/
|
|
|
|
package minio
|
|
|
|
import (
|
|
"context"
|
|
"encoding/base64"
|
|
"fmt"
|
|
"net/http"
|
|
"net/url"
|
|
"strconv"
|
|
"strings"
|
|
"time"
|
|
|
|
"github.com/minio/minio-go/pkg/s3utils"
|
|
)
|
|
|
|
// SSEInfo - represents Server-Side-Encryption parameters specified by
|
|
// a user.
|
|
type SSEInfo struct {
|
|
key []byte
|
|
algo string
|
|
}
|
|
|
|
// NewSSEInfo - specifies (binary or un-encoded) encryption key and
|
|
// algorithm name. If algo is empty, it defaults to "AES256". Ref:
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/dev/ServerSideEncryptionCustomerKeys.html
|
|
func NewSSEInfo(key []byte, algo string) SSEInfo {
|
|
if algo == "" {
|
|
algo = "AES256"
|
|
}
|
|
return SSEInfo{key, algo}
|
|
}
|
|
|
|
// internal method that computes SSE-C headers
|
|
func (s *SSEInfo) getSSEHeaders(isCopySource bool) map[string]string {
|
|
if s == nil {
|
|
return nil
|
|
}
|
|
|
|
cs := ""
|
|
if isCopySource {
|
|
cs = "copy-source-"
|
|
}
|
|
return map[string]string{
|
|
"x-amz-" + cs + "server-side-encryption-customer-algorithm": s.algo,
|
|
"x-amz-" + cs + "server-side-encryption-customer-key": base64.StdEncoding.EncodeToString(s.key),
|
|
"x-amz-" + cs + "server-side-encryption-customer-key-MD5": sumMD5Base64(s.key),
|
|
}
|
|
}
|
|
|
|
// GetSSEHeaders - computes and returns headers for SSE-C as key-value
|
|
// pairs. They can be set as metadata in PutObject* requests (for
|
|
// encryption) or be set as request headers in `Core.GetObject` (for
|
|
// decryption).
|
|
func (s *SSEInfo) GetSSEHeaders() map[string]string {
|
|
return s.getSSEHeaders(false)
|
|
}
|
|
|
|
// DestinationInfo - type with information about the object to be
|
|
// created via server-side copy requests, using the Compose API.
|
|
type DestinationInfo struct {
|
|
bucket, object string
|
|
|
|
// key for encrypting destination
|
|
encryption *SSEInfo
|
|
|
|
// if no user-metadata is provided, it is copied from source
|
|
// (when there is only once source object in the compose
|
|
// request)
|
|
userMetadata map[string]string
|
|
}
|
|
|
|
// NewDestinationInfo - creates a compose-object/copy-source
|
|
// destination info object.
|
|
//
|
|
// `encSSEC` is the key info for server-side-encryption with customer
|
|
// provided key. If it is nil, no encryption is performed.
|
|
//
|
|
// `userMeta` is the user-metadata key-value pairs to be set on the
|
|
// destination. The keys are automatically prefixed with `x-amz-meta-`
|
|
// if needed. If nil is passed, and if only a single source (of any
|
|
// size) is provided in the ComposeObject call, then metadata from the
|
|
// source is copied to the destination.
|
|
func NewDestinationInfo(bucket, object string, encryptSSEC *SSEInfo,
|
|
userMeta map[string]string) (d DestinationInfo, err error) {
|
|
|
|
// Input validation.
|
|
if err = s3utils.CheckValidBucketName(bucket); err != nil {
|
|
return d, err
|
|
}
|
|
if err = s3utils.CheckValidObjectName(object); err != nil {
|
|
return d, err
|
|
}
|
|
|
|
// Process custom-metadata to remove a `x-amz-meta-` prefix if
|
|
// present and validate that keys are distinct (after this
|
|
// prefix removal).
|
|
m := make(map[string]string)
|
|
for k, v := range userMeta {
|
|
if strings.HasPrefix(strings.ToLower(k), "x-amz-meta-") {
|
|
k = k[len("x-amz-meta-"):]
|
|
}
|
|
if _, ok := m[k]; ok {
|
|
return d, ErrInvalidArgument(fmt.Sprintf("Cannot add both %s and x-amz-meta-%s keys as custom metadata", k, k))
|
|
}
|
|
m[k] = v
|
|
}
|
|
|
|
return DestinationInfo{
|
|
bucket: bucket,
|
|
object: object,
|
|
encryption: encryptSSEC,
|
|
userMetadata: m,
|
|
}, nil
|
|
}
|
|
|
|
// getUserMetaHeadersMap - construct appropriate key-value pairs to send
|
|
// as headers from metadata map to pass into copy-object request. For
|
|
// single part copy-object (i.e. non-multipart object), enable the
|
|
// withCopyDirectiveHeader to set the `x-amz-metadata-directive` to
|
|
// `REPLACE`, so that metadata headers from the source are not copied
|
|
// over.
|
|
func (d *DestinationInfo) getUserMetaHeadersMap(withCopyDirectiveHeader bool) map[string]string {
|
|
if len(d.userMetadata) == 0 {
|
|
return nil
|
|
}
|
|
r := make(map[string]string)
|
|
if withCopyDirectiveHeader {
|
|
r["x-amz-metadata-directive"] = "REPLACE"
|
|
}
|
|
for k, v := range d.userMetadata {
|
|
r["x-amz-meta-"+k] = v
|
|
}
|
|
return r
|
|
}
|
|
|
|
// SourceInfo - represents a source object to be copied, using
|
|
// server-side copying APIs.
|
|
type SourceInfo struct {
|
|
bucket, object string
|
|
|
|
start, end int64
|
|
|
|
decryptKey *SSEInfo
|
|
// Headers to send with the upload-part-copy request involving
|
|
// this source object.
|
|
Headers http.Header
|
|
}
|
|
|
|
// NewSourceInfo - create a compose-object/copy-object source info
|
|
// object.
|
|
//
|
|
// `decryptSSEC` is the decryption key using server-side-encryption
|
|
// with customer provided key. It may be nil if the source is not
|
|
// encrypted.
|
|
func NewSourceInfo(bucket, object string, decryptSSEC *SSEInfo) SourceInfo {
|
|
r := SourceInfo{
|
|
bucket: bucket,
|
|
object: object,
|
|
start: -1, // range is unspecified by default
|
|
decryptKey: decryptSSEC,
|
|
Headers: make(http.Header),
|
|
}
|
|
|
|
// Set the source header
|
|
r.Headers.Set("x-amz-copy-source", s3utils.EncodePath(bucket+"/"+object))
|
|
|
|
// Assemble decryption headers for upload-part-copy request
|
|
for k, v := range decryptSSEC.getSSEHeaders(true) {
|
|
r.Headers.Set(k, v)
|
|
}
|
|
|
|
return r
|
|
}
|
|
|
|
// SetRange - Set the start and end offset of the source object to be
|
|
// copied. If this method is not called, the whole source object is
|
|
// copied.
|
|
func (s *SourceInfo) SetRange(start, end int64) error {
|
|
if start > end || start < 0 {
|
|
return ErrInvalidArgument("start must be non-negative, and start must be at most end.")
|
|
}
|
|
// Note that 0 <= start <= end
|
|
s.start, s.end = start, end
|
|
return nil
|
|
}
|
|
|
|
// SetMatchETagCond - Set ETag match condition. The object is copied
|
|
// only if the etag of the source matches the value given here.
|
|
func (s *SourceInfo) SetMatchETagCond(etag string) error {
|
|
if etag == "" {
|
|
return ErrInvalidArgument("ETag cannot be empty.")
|
|
}
|
|
s.Headers.Set("x-amz-copy-source-if-match", etag)
|
|
return nil
|
|
}
|
|
|
|
// SetMatchETagExceptCond - Set the ETag match exception
|
|
// condition. The object is copied only if the etag of the source is
|
|
// not the value given here.
|
|
func (s *SourceInfo) SetMatchETagExceptCond(etag string) error {
|
|
if etag == "" {
|
|
return ErrInvalidArgument("ETag cannot be empty.")
|
|
}
|
|
s.Headers.Set("x-amz-copy-source-if-none-match", etag)
|
|
return nil
|
|
}
|
|
|
|
// SetModifiedSinceCond - Set the modified since condition.
|
|
func (s *SourceInfo) SetModifiedSinceCond(modTime time.Time) error {
|
|
if modTime.IsZero() {
|
|
return ErrInvalidArgument("Input time cannot be 0.")
|
|
}
|
|
s.Headers.Set("x-amz-copy-source-if-modified-since", modTime.Format(http.TimeFormat))
|
|
return nil
|
|
}
|
|
|
|
// SetUnmodifiedSinceCond - Set the unmodified since condition.
|
|
func (s *SourceInfo) SetUnmodifiedSinceCond(modTime time.Time) error {
|
|
if modTime.IsZero() {
|
|
return ErrInvalidArgument("Input time cannot be 0.")
|
|
}
|
|
s.Headers.Set("x-amz-copy-source-if-unmodified-since", modTime.Format(http.TimeFormat))
|
|
return nil
|
|
}
|
|
|
|
// Helper to fetch size and etag of an object using a StatObject call.
|
|
func (s *SourceInfo) getProps(c Client) (size int64, etag string, userMeta map[string]string, err error) {
|
|
// Get object info - need size and etag here. Also, decryption
|
|
// headers are added to the stat request if given.
|
|
var objInfo ObjectInfo
|
|
opts := StatObjectOptions{}
|
|
for k, v := range s.decryptKey.getSSEHeaders(false) {
|
|
opts.Set(k, v)
|
|
}
|
|
objInfo, err = c.statObject(context.Background(), s.bucket, s.object, opts)
|
|
if err != nil {
|
|
err = ErrInvalidArgument(fmt.Sprintf("Could not stat object - %s/%s: %v", s.bucket, s.object, err))
|
|
} else {
|
|
size = objInfo.Size
|
|
etag = objInfo.ETag
|
|
userMeta = make(map[string]string)
|
|
for k, v := range objInfo.Metadata {
|
|
if strings.HasPrefix(k, "x-amz-meta-") {
|
|
if len(v) > 0 {
|
|
userMeta[k] = v[0]
|
|
}
|
|
}
|
|
}
|
|
}
|
|
return
|
|
}
|
|
|
|
// Low level implementation of CopyObject API, supports only upto 5GiB worth of copy.
|
|
func (c Client) copyObjectDo(ctx context.Context, srcBucket, srcObject, destBucket, destObject string,
|
|
metadata map[string]string) (ObjectInfo, error) {
|
|
|
|
// Build headers.
|
|
headers := make(http.Header)
|
|
|
|
// Set all the metadata headers.
|
|
for k, v := range metadata {
|
|
headers.Set(k, v)
|
|
}
|
|
|
|
// Set the source header
|
|
headers.Set("x-amz-copy-source", s3utils.EncodePath(srcBucket+"/"+srcObject))
|
|
|
|
// Send upload-part-copy request
|
|
resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
|
|
bucketName: destBucket,
|
|
objectName: destObject,
|
|
customHeader: headers,
|
|
})
|
|
defer closeResponse(resp)
|
|
if err != nil {
|
|
return ObjectInfo{}, err
|
|
}
|
|
|
|
// Check if we got an error response.
|
|
if resp.StatusCode != http.StatusOK {
|
|
return ObjectInfo{}, httpRespToErrorResponse(resp, srcBucket, srcObject)
|
|
}
|
|
|
|
cpObjRes := copyObjectResult{}
|
|
err = xmlDecoder(resp.Body, &cpObjRes)
|
|
if err != nil {
|
|
return ObjectInfo{}, err
|
|
}
|
|
|
|
objInfo := ObjectInfo{
|
|
Key: destObject,
|
|
ETag: strings.Trim(cpObjRes.ETag, "\""),
|
|
LastModified: cpObjRes.LastModified,
|
|
}
|
|
return objInfo, nil
|
|
}
|
|
|
|
func (c Client) copyObjectPartDo(ctx context.Context, srcBucket, srcObject, destBucket, destObject string, uploadID string,
|
|
partID int, startOffset int64, length int64, metadata map[string]string) (p CompletePart, err error) {
|
|
|
|
headers := make(http.Header)
|
|
|
|
// Set source
|
|
headers.Set("x-amz-copy-source", s3utils.EncodePath(srcBucket+"/"+srcObject))
|
|
|
|
if startOffset < 0 {
|
|
return p, ErrInvalidArgument("startOffset must be non-negative")
|
|
}
|
|
|
|
if length >= 0 {
|
|
headers.Set("x-amz-copy-source-range", fmt.Sprintf("bytes=%d-%d", startOffset, startOffset+length-1))
|
|
}
|
|
|
|
for k, v := range metadata {
|
|
headers.Set(k, v)
|
|
}
|
|
|
|
queryValues := make(url.Values)
|
|
queryValues.Set("partNumber", strconv.Itoa(partID))
|
|
queryValues.Set("uploadId", uploadID)
|
|
|
|
resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
|
|
bucketName: destBucket,
|
|
objectName: destObject,
|
|
customHeader: headers,
|
|
queryValues: queryValues,
|
|
})
|
|
defer closeResponse(resp)
|
|
if err != nil {
|
|
return
|
|
}
|
|
|
|
// Check if we got an error response.
|
|
if resp.StatusCode != http.StatusOK {
|
|
return p, httpRespToErrorResponse(resp, destBucket, destObject)
|
|
}
|
|
|
|
// Decode copy-part response on success.
|
|
cpObjRes := copyObjectResult{}
|
|
err = xmlDecoder(resp.Body, &cpObjRes)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
p.PartNumber, p.ETag = partID, cpObjRes.ETag
|
|
return p, nil
|
|
}
|
|
|
|
// uploadPartCopy - helper function to create a part in a multipart
|
|
// upload via an upload-part-copy request
|
|
// https://docs.aws.amazon.com/AmazonS3/latest/API/mpUploadUploadPartCopy.html
|
|
func (c Client) uploadPartCopy(ctx context.Context, bucket, object, uploadID string, partNumber int,
|
|
headers http.Header) (p CompletePart, err error) {
|
|
|
|
// Build query parameters
|
|
urlValues := make(url.Values)
|
|
urlValues.Set("partNumber", strconv.Itoa(partNumber))
|
|
urlValues.Set("uploadId", uploadID)
|
|
|
|
// Send upload-part-copy request
|
|
resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
|
|
bucketName: bucket,
|
|
objectName: object,
|
|
customHeader: headers,
|
|
queryValues: urlValues,
|
|
})
|
|
defer closeResponse(resp)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
|
|
// Check if we got an error response.
|
|
if resp.StatusCode != http.StatusOK {
|
|
return p, httpRespToErrorResponse(resp, bucket, object)
|
|
}
|
|
|
|
// Decode copy-part response on success.
|
|
cpObjRes := copyObjectResult{}
|
|
err = xmlDecoder(resp.Body, &cpObjRes)
|
|
if err != nil {
|
|
return p, err
|
|
}
|
|
p.PartNumber, p.ETag = partNumber, cpObjRes.ETag
|
|
return p, nil
|
|
}
|
|
|
|
// ComposeObject - creates an object using server-side copying of
|
|
// existing objects. It takes a list of source objects (with optional
|
|
// offsets) and concatenates them into a new object using only
|
|
// server-side copying operations.
|
|
func (c Client) ComposeObject(dst DestinationInfo, srcs []SourceInfo) error {
|
|
if len(srcs) < 1 || len(srcs) > maxPartsCount {
|
|
return ErrInvalidArgument("There must be as least one and up to 10000 source objects.")
|
|
}
|
|
ctx := context.Background()
|
|
srcSizes := make([]int64, len(srcs))
|
|
var totalSize, size, totalParts int64
|
|
var srcUserMeta map[string]string
|
|
var etag string
|
|
var err error
|
|
for i, src := range srcs {
|
|
size, etag, srcUserMeta, err = src.getProps(c)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// Error out if client side encryption is used in this source object when
|
|
// more than one source objects are given.
|
|
if len(srcs) > 1 && src.Headers.Get("x-amz-meta-x-amz-key") != "" {
|
|
return ErrInvalidArgument(
|
|
fmt.Sprintf("Client side encryption is used in source object %s/%s", src.bucket, src.object))
|
|
}
|
|
|
|
// Since we did a HEAD to get size, we use the ETag
|
|
// value to make sure the object has not changed by
|
|
// the time we perform the copy. This is done, only if
|
|
// the user has not set their own ETag match
|
|
// condition.
|
|
if src.Headers.Get("x-amz-copy-source-if-match") == "" {
|
|
src.SetMatchETagCond(etag)
|
|
}
|
|
|
|
// Check if a segment is specified, and if so, is the
|
|
// segment within object bounds?
|
|
if src.start != -1 {
|
|
// Since range is specified,
|
|
// 0 <= src.start <= src.end
|
|
// so only invalid case to check is:
|
|
if src.end >= size {
|
|
return ErrInvalidArgument(
|
|
fmt.Sprintf("SourceInfo %d has invalid segment-to-copy [%d, %d] (size is %d)",
|
|
i, src.start, src.end, size))
|
|
}
|
|
size = src.end - src.start + 1
|
|
}
|
|
|
|
// Only the last source may be less than `absMinPartSize`
|
|
if size < absMinPartSize && i < len(srcs)-1 {
|
|
return ErrInvalidArgument(
|
|
fmt.Sprintf("SourceInfo %d is too small (%d) and it is not the last part", i, size))
|
|
}
|
|
|
|
// Is data to copy too large?
|
|
totalSize += size
|
|
if totalSize > maxMultipartPutObjectSize {
|
|
return ErrInvalidArgument(fmt.Sprintf("Cannot compose an object of size %d (> 5TiB)", totalSize))
|
|
}
|
|
|
|
// record source size
|
|
srcSizes[i] = size
|
|
|
|
// calculate parts needed for current source
|
|
totalParts += partsRequired(size)
|
|
// Do we need more parts than we are allowed?
|
|
if totalParts > maxPartsCount {
|
|
return ErrInvalidArgument(fmt.Sprintf(
|
|
"Your proposed compose object requires more than %d parts", maxPartsCount))
|
|
}
|
|
}
|
|
|
|
// Single source object case (i.e. when only one source is
|
|
// involved, it is being copied wholly and at most 5GiB in
|
|
// size).
|
|
if totalParts == 1 && srcs[0].start == -1 && totalSize <= maxPartSize {
|
|
h := srcs[0].Headers
|
|
// Add destination encryption headers
|
|
for k, v := range dst.encryption.getSSEHeaders(false) {
|
|
h.Set(k, v)
|
|
}
|
|
|
|
// If no user metadata is specified (and so, the
|
|
// for-loop below is not entered), metadata from the
|
|
// source is copied to the destination (due to
|
|
// single-part copy-object PUT request behaviour).
|
|
for k, v := range dst.getUserMetaHeadersMap(true) {
|
|
h.Set(k, v)
|
|
}
|
|
|
|
// Send copy request
|
|
resp, err := c.executeMethod(ctx, "PUT", requestMetadata{
|
|
bucketName: dst.bucket,
|
|
objectName: dst.object,
|
|
customHeader: h,
|
|
})
|
|
defer closeResponse(resp)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
// Check if we got an error response.
|
|
if resp.StatusCode != http.StatusOK {
|
|
return httpRespToErrorResponse(resp, dst.bucket, dst.object)
|
|
}
|
|
|
|
// Return nil on success.
|
|
return nil
|
|
}
|
|
|
|
// Now, handle multipart-copy cases.
|
|
|
|
// 1. Initiate a new multipart upload.
|
|
|
|
// Set user-metadata on the destination object. If no
|
|
// user-metadata is specified, and there is only one source,
|
|
// (only) then metadata from source is copied.
|
|
userMeta := dst.getUserMetaHeadersMap(false)
|
|
metaMap := userMeta
|
|
if len(userMeta) == 0 && len(srcs) == 1 {
|
|
metaMap = srcUserMeta
|
|
}
|
|
metaHeaders := make(map[string]string)
|
|
for k, v := range metaMap {
|
|
metaHeaders[k] = v
|
|
}
|
|
uploadID, err := c.newUploadID(ctx, dst.bucket, dst.object, PutObjectOptions{UserMetadata: metaHeaders})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
|
|
// 2. Perform copy part uploads
|
|
objParts := []CompletePart{}
|
|
partIndex := 1
|
|
for i, src := range srcs {
|
|
h := src.Headers
|
|
// Add destination encryption headers
|
|
for k, v := range dst.encryption.getSSEHeaders(false) {
|
|
h.Set(k, v)
|
|
}
|
|
|
|
// calculate start/end indices of parts after
|
|
// splitting.
|
|
startIdx, endIdx := calculateEvenSplits(srcSizes[i], src)
|
|
for j, start := range startIdx {
|
|
end := endIdx[j]
|
|
|
|
// Add (or reset) source range header for
|
|
// upload part copy request.
|
|
h.Set("x-amz-copy-source-range",
|
|
fmt.Sprintf("bytes=%d-%d", start, end))
|
|
|
|
// make upload-part-copy request
|
|
complPart, err := c.uploadPartCopy(ctx, dst.bucket,
|
|
dst.object, uploadID, partIndex, h)
|
|
if err != nil {
|
|
return err
|
|
}
|
|
objParts = append(objParts, complPart)
|
|
partIndex++
|
|
}
|
|
}
|
|
|
|
// 3. Make final complete-multipart request.
|
|
_, err = c.completeMultipartUpload(ctx, dst.bucket, dst.object, uploadID,
|
|
completeMultipartUpload{Parts: objParts})
|
|
if err != nil {
|
|
return err
|
|
}
|
|
return nil
|
|
}
|
|
|
|
// partsRequired is ceiling(size / copyPartSize)
|
|
func partsRequired(size int64) int64 {
|
|
r := size / copyPartSize
|
|
if size%copyPartSize > 0 {
|
|
r++
|
|
}
|
|
return r
|
|
}
|
|
|
|
// calculateEvenSplits - computes splits for a source and returns
|
|
// start and end index slices. Splits happen evenly to be sure that no
|
|
// part is less than 5MiB, as that could fail the multipart request if
|
|
// it is not the last part.
|
|
func calculateEvenSplits(size int64, src SourceInfo) (startIndex, endIndex []int64) {
|
|
if size == 0 {
|
|
return
|
|
}
|
|
|
|
reqParts := partsRequired(size)
|
|
startIndex = make([]int64, reqParts)
|
|
endIndex = make([]int64, reqParts)
|
|
// Compute number of required parts `k`, as:
|
|
//
|
|
// k = ceiling(size / copyPartSize)
|
|
//
|
|
// Now, distribute the `size` bytes in the source into
|
|
// k parts as evenly as possible:
|
|
//
|
|
// r parts sized (q+1) bytes, and
|
|
// (k - r) parts sized q bytes, where
|
|
//
|
|
// size = q * k + r (by simple division of size by k,
|
|
// so that 0 <= r < k)
|
|
//
|
|
start := src.start
|
|
if start == -1 {
|
|
start = 0
|
|
}
|
|
quot, rem := size/reqParts, size%reqParts
|
|
nextStart := start
|
|
for j := int64(0); j < reqParts; j++ {
|
|
curPartSize := quot
|
|
if j < rem {
|
|
curPartSize++
|
|
}
|
|
|
|
cStart := nextStart
|
|
cEnd := cStart + curPartSize - 1
|
|
nextStart = cEnd + 1
|
|
|
|
startIndex[j], endIndex[j] = cStart, cEnd
|
|
}
|
|
return
|
|
}
|