Skip to content

Commit

Permalink
Implement Versioning support
Browse files Browse the repository at this point in the history
  • Loading branch information
vadmeste authored and harshavardhana committed Jul 8, 2020
1 parent e39de30 commit c752eca
Show file tree
Hide file tree
Showing 26 changed files with 2,118 additions and 825 deletions.
84 changes: 31 additions & 53 deletions api-compose-object.go
Original file line number Diff line number Diff line change
Expand Up @@ -88,45 +88,7 @@ func filterCustomMeta(userMeta map[string]string) (map[string]string, error) {

// NewDestinationInfo - creates a compose-object/copy-source
// destination info object.
//
// `sse` 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, sse encrypt.ServerSide, 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
}
m, err := filterCustomMeta(userMeta)
if err != nil {
return d, err
}
opts := DestInfoOptions{
Encryption: sse,
UserMeta: m,
UserTags: nil,
ReplaceTags: false,
LegalHold: LegalHoldStatus(""),
Mode: RetentionMode(""),
}
return DestinationInfo{
bucket: bucket,
object: object,
opts: opts,
}, nil
}

// NewDestinationInfoWithOptions - creates a compose-object/copy-source
// destination info object.
func NewDestinationInfoWithOptions(bucket, object string, destOpts DestInfoOptions) (d DestinationInfo, err error) {
func NewDestinationInfo(bucket, object string, destOpts DestInfoOptions) (d DestinationInfo, err error) {
// Input validation.
if err = s3utils.CheckValidBucketName(bucket); err != nil {
return d, err
Expand Down Expand Up @@ -173,6 +135,7 @@ func (d *DestinationInfo) getUserMetaHeadersMap(withCopyDirectiveHeader bool) ma
// server-side copying APIs.
type SourceInfo struct {
bucket, object string
versionID string
start, end int64
encryption encrypt.ServerSide
// Headers to send with the upload-part-copy request involving
Expand Down Expand Up @@ -200,6 +163,16 @@ func NewSourceInfo(bucket, object string, sse encrypt.ServerSide) SourceInfo {
return r
}

// SetVersionID - Set the version ID of the source object
func (s *SourceInfo) SetVersionID(versionID string) error {
if versionID == "" {
return errInvalidArgument("version ID must be non-empty.")
}
s.versionID = versionID
s.Headers.Set("x-amz-copy-source", s3utils.EncodePath(s.bucket+"/"+s.object)+"?versionId="+versionID)
return nil
}

// 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.
Expand Down Expand Up @@ -257,6 +230,9 @@ func (s *SourceInfo) getProps(c Client) (size int64, etag string, userMeta map[s
// headers are added to the stat request if given.
var objInfo ObjectInfo
opts := StatObjectOptions{GetObjectOptions{ServerSideEncryption: encrypt.SSE(s.encryption)}}
if s.versionID != "" {
opts.VersionID = s.versionID
}
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))
Expand Down Expand Up @@ -413,9 +389,9 @@ func (c Client) uploadPartCopy(ctx context.Context, bucket, object, uploadID str
// and concatenates them into a new object using only server-side copying
// operations. Optionally takes progress reader hook for applications to
// look at current progress.
func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationInfo, srcs []SourceInfo, progress io.Reader) error {
func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationInfo, srcs []SourceInfo, progress io.Reader) (UploadInfo, error) {
if len(srcs) < 1 || len(srcs) > maxPartsCount {
return errInvalidArgument("There must be as least one and up to 10000 source objects.")
return UploadInfo{}, errInvalidArgument("There must be as least one and up to 10000 source objects.")
}
srcSizes := make([]int64, len(srcs))
var totalSize, size, totalParts int64
Expand All @@ -425,13 +401,13 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn
for i, src := range srcs {
size, etags[i], srcUserMeta, err = src.getProps(c)
if err != nil {
return err
return UploadInfo{}, 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(
return UploadInfo{}, errInvalidArgument(
fmt.Sprintf("Client side encryption is used in source object %s/%s", src.bucket, src.object))
}

Expand All @@ -442,7 +418,7 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn
// 0 <= src.start <= src.end
// so only invalid case to check is:
if src.end >= size {
return errInvalidArgument(
return UploadInfo{}, errInvalidArgument(
fmt.Sprintf("SourceInfo %d has invalid segment-to-copy [%d, %d] (size is %d)",
i, src.start, src.end, size))
}
Expand All @@ -451,14 +427,14 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn

// Only the last source may be less than `absMinPartSize`
if size < absMinPartSize && i < len(srcs)-1 {
return errInvalidArgument(
return UploadInfo{}, 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))
return UploadInfo{}, errInvalidArgument(fmt.Sprintf("Cannot compose an object of size %d (> 5TiB)", totalSize))
}

// record source size
Expand All @@ -468,7 +444,7 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn
totalParts += partsRequired(size)
// Do we need more parts than we are allowed?
if totalParts > maxPartsCount {
return errInvalidArgument(fmt.Sprintf(
return UploadInfo{}, errInvalidArgument(fmt.Sprintf(
"Your proposed compose object requires more than %d parts", maxPartsCount))
}
}
Expand Down Expand Up @@ -507,7 +483,7 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn

uploadID, err := c.newUploadID(ctx, dst.bucket, dst.object, PutObjectOptions{ServerSideEncryption: dst.opts.Encryption, UserMetadata: metaHeaders})
if err != nil {
return err
return UploadInfo{}, err
}

// 3. Perform copy part uploads
Expand Down Expand Up @@ -538,7 +514,7 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn
complPart, err := c.uploadPartCopy(ctx, dst.bucket,
dst.object, uploadID, partIndex, h)
if err != nil {
return err
return UploadInfo{}, err
}
if progress != nil {
io.CopyN(ioutil.Discard, progress, end-start+1)
Expand All @@ -549,19 +525,21 @@ func (c Client) ComposeObjectWithProgress(ctx context.Context, dst DestinationIn
}

// 4. Make final complete-multipart request.
_, err = c.completeMultipartUpload(ctx, dst.bucket, dst.object, uploadID,
uploadInfo, err := c.completeMultipartUpload(ctx, dst.bucket, dst.object, uploadID,
completeMultipartUpload{Parts: objParts})
if err != nil {
return err
return UploadInfo{}, err
}
return nil

uploadInfo.Size = totalSize
return uploadInfo, 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(ctx context.Context, dst DestinationInfo, srcs []SourceInfo) error {
func (c Client) ComposeObject(ctx context.Context, dst DestinationInfo, srcs []SourceInfo) (UploadInfo, error) {
return c.ComposeObjectWithProgress(ctx, dst, srcs, nil)
}

Expand Down
2 changes: 1 addition & 1 deletion api-compose-object_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -130,7 +130,7 @@ func TestGetUserMetaHeadersMap(t *testing.T) {
"x-amz-grant-write": "[email protected]",
}

destInfo, _ := NewDestinationInfo("bucket", "object", nil, userMetadata)
destInfo, _ := NewDestinationInfo("bucket", "object", DestInfoOptions{UserMeta: userMetadata})

r := destInfo.getUserMetaHeadersMap(true)

Expand Down
26 changes: 21 additions & 5 deletions api-datatypes.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* MinIO Go Library for Amazon S3 Compatible Cloud Storage
* Copyright 2015-2017 MinIO, Inc.
* Copyright 2015-2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -62,6 +62,20 @@ func (m *StringMap) UnmarshalXML(d *xml.Decoder, start xml.StartElement) error {
return nil
}

// Owner name.
type Owner struct {
DisplayName string `json:"name"`
ID string `json:"id"`
}

// UploadInfo contains information about the
// newly uploaded or copied object.
type UploadInfo struct {
ETag string
VersionID string
Size int64
}

// ObjectInfo container for object metadata.
type ObjectInfo struct {
// An ETag is optionally set to md5sum of an object. In case of multipart objects,
Expand All @@ -86,10 +100,7 @@ type ObjectInfo struct {
UserTags map[string]string `json:"userTags"`

// Owner name.
Owner struct {
DisplayName string `json:"name"`
ID string `json:"id"`
} `json:"owner"`
Owner Owner

// ACL grant.
Grant []struct {
Expand All @@ -104,6 +115,11 @@ type ObjectInfo struct {
// The class of storage used to store the object.
StorageClass string `json:"storageClass"`

// Versioning related information
IsLatest bool
IsDeleteMarker bool
VersionID string `xml:"VersionId"`

// Error
Err error `json:"-"`
}
Expand Down
7 changes: 7 additions & 0 deletions api-get-object.go
Original file line number Diff line number Diff line change
Expand Up @@ -23,6 +23,7 @@ import (
"fmt"
"io"
"net/http"
"net/url"
"sync"

"github.com/minio/minio-go/v7/pkg/s3utils"
Expand Down Expand Up @@ -612,10 +613,16 @@ func (c Client) getObject(ctx context.Context, bucketName, objectName string, op
return nil, ObjectInfo{}, nil, err
}

urlValues := make(url.Values)
if opts.VersionID != "" {
urlValues.Set("versionId", opts.VersionID)
}

// Execute GET on objectName.
resp, err := c.executeMethod(ctx, "GET", requestMetadata{
bucketName: bucketName,
objectName: objectName,
queryValues: urlValues,
customHeader: opts.Header(),
contentSHA256Hex: emptySHA256Hex,
})
Expand Down
3 changes: 2 additions & 1 deletion api-get-options.go
Original file line number Diff line number Diff line change
@@ -1,6 +1,6 @@
/*
* MinIO Go Library for Amazon S3 Compatible Cloud Storage
* Copyright 2015-2017 MinIO, Inc.
* Copyright 2015-2020 MinIO, Inc.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
Expand Down Expand Up @@ -30,6 +30,7 @@ import (
type GetObjectOptions struct {
headers map[string]string
ServerSideEncryption encrypt.ServerSide
VersionID string
}

// StatObjectOptions are used to specify additional headers or options
Expand Down
Loading

0 comments on commit c752eca

Please sign in to comment.