Skip to content

Commit

Permalink
storage: implement object composition
Browse files Browse the repository at this point in the history
Fixes googleapis#146

Change-Id: I63a657b879851ff8cc658ddec33f29cd52c7f304
Reviewed-on: https://code-review.googlesource.com/7375
Reviewed-by: Jonathan Amsterdam <[email protected]>
  • Loading branch information
okdave committed Sep 9, 2016
1 parent 087edeb commit 4df34ab
Show file tree
Hide file tree
Showing 2 changed files with 235 additions and 0 deletions.
64 changes: 64 additions & 0 deletions storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -437,6 +437,52 @@ func (o *ObjectHandle) CopyTo(ctx context.Context, dst *ObjectHandle, attrs *Obj
return newObject(obj), nil
}

// ComposeFrom concatenates the provided slice of source objects into a new
// object whose destination is the receiver. The provided attrs, if not nil,
// are used to set the attributes on the newly-created object. All source
// objects must reside within the same bucket as the destination.
func (o *ObjectHandle) ComposeFrom(ctx context.Context, srcs []*ObjectHandle, attrs *ObjectAttrs) (*ObjectAttrs, error) {
if o.bucket == "" || o.object == "" {
return nil, errors.New("storage: the destination bucket and object names must be non-empty")
}
if len(srcs) == 0 {
return nil, errors.New("storage: at least one source object must be specified")
}

req := &raw.ComposeRequest{}
if attrs != nil {
req.Destination = attrs.toRawObject(o.bucket)
req.Destination.Name = o.object
}

for _, src := range srcs {
if src.bucket != o.bucket {
return nil, fmt.Errorf("storage: all source objects must be in bucket %q, found %q", o.bucket, src.bucket)
}
if src.object == "" {
return nil, errors.New("storage: all source object names must be non-empty")
}
srcObj := &raw.ComposeRequestSourceObjects{
Name: src.object,
}
if err := applyConds("ComposeFrom source", src.conds, composeSourceObj{srcObj}); err != nil {
return nil, err
}
req.SourceObjects = append(req.SourceObjects, srcObj)
}

call := o.c.raw.Objects.Compose(o.bucket, o.object, req).Context(ctx)
if err := applyConds("ComposeFrom destination", o.conds, call); err != nil {
return nil, err
}

obj, err := call.Do()
if err != nil {
return nil, err
}
return newObject(obj), nil
}

// NewReader creates a new Reader to read the contents of the
// object.
// ErrObjectNotExist will be returned if the object is not found.
Expand Down Expand Up @@ -885,4 +931,22 @@ func (c objectsGetCall) IfMetagenerationNotMatch(gen int64) {
appendParam(c.req, "ifMetagenerationNotMatch", fmt.Sprint(gen))
}

// composeSourceObj wraps a *raw.ComposeRequestSourceObjects, but adds the methods
// that modifyCall searches for by name.
type composeSourceObj struct {
src *raw.ComposeRequestSourceObjects
}

func (c composeSourceObj) Generation(gen int64) {
c.src.Generation = gen
}

func (c composeSourceObj) IfGenerationMatch(gen int64) {
// It's safe to overwrite ObjectPreconditions, since its only field is
// IfGenerationMatch.
c.src.ObjectPreconditions = &raw.ComposeRequestSourceObjectsObjectPreconditions{
IfGenerationMatch: gen,
}
}

// TODO(jbd): Add storage.objects.watch.
171 changes: 171 additions & 0 deletions storage/storage_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -16,19 +16,22 @@ package storage

import (
"crypto/tls"
"encoding/json"
"fmt"
"io"
"io/ioutil"
"log"
"net"
"net/http"
"net/http/httptest"
"reflect"
"strings"
"testing"
"time"

"golang.org/x/net/context"
"google.golang.org/api/option"
raw "google.golang.org/api/storage/v1"
)

func TestSignedURL(t *testing.T) {
Expand Down Expand Up @@ -409,6 +412,174 @@ func TestCondition(t *testing.T) {
}
}

// Test object compose.
func TestObjectCompose(t *testing.T) {
gotURL := make(chan string, 1)
gotBody := make(chan []byte, 1)
hc, close := newTestServer(func(w http.ResponseWriter, r *http.Request) {
body, _ := ioutil.ReadAll(r.Body)
gotURL <- r.URL.String()
gotBody <- body
w.Write([]byte("{}"))
})
defer close()
ctx := context.Background()
c, err := NewClient(ctx, option.WithHTTPClient(hc))
if err != nil {
t.Fatal(err)
}

testCases := []struct {
desc string
dst *ObjectHandle
srcs []*ObjectHandle
attrs *ObjectAttrs
wantReq raw.ComposeRequest
wantURL string
wantErr bool
}{
{
desc: "basic case",
dst: c.Bucket("foo").Object("bar"),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz"),
c.Bucket("foo").Object("quux"),
},
wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json",
wantReq: raw.ComposeRequest{
SourceObjects: []*raw.ComposeRequestSourceObjects{
{Name: "baz"},
{Name: "quux"},
},
},
},
{
desc: "with object attrs",
dst: c.Bucket("foo").Object("bar"),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz"),
c.Bucket("foo").Object("quux"),
},
attrs: &ObjectAttrs{
Name: "not-bar",
ContentType: "application/json",
},
wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json",
wantReq: raw.ComposeRequest{
Destination: &raw.Object{
Bucket: "foo",
Name: "bar",
ContentType: "application/json",
},
SourceObjects: []*raw.ComposeRequestSourceObjects{
{Name: "baz"},
{Name: "quux"},
},
},
},
{
desc: "with conditions",
dst: c.Bucket("foo").Object("bar").WithConditions(IfGenerationMatch(12), IfMetaGenerationMatch(34)),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz").WithConditions(Generation(56)),
c.Bucket("foo").Object("quux").WithConditions(IfGenerationMatch(78)),
},
wantURL: "/storage/v1/b/foo/o/bar/compose?alt=json&ifGenerationMatch=12&ifMetagenerationMatch=34",
wantReq: raw.ComposeRequest{
SourceObjects: []*raw.ComposeRequestSourceObjects{
{
Name: "baz",
Generation: 56,
},
{
Name: "quux",
ObjectPreconditions: &raw.ComposeRequestSourceObjectsObjectPreconditions{
IfGenerationMatch: 78,
},
},
},
},
},
{
desc: "no sources",
dst: c.Bucket("foo").Object("bar"),
wantErr: true,
},
{
desc: "destination, no bucket",
dst: c.Bucket("").Object("bar"),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz"),
},
wantErr: true,
},
{
desc: "destination, no object",
dst: c.Bucket("foo").Object(""),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz"),
},
wantErr: true,
},
{
desc: "source, different bucket",
dst: c.Bucket("foo").Object("bar"),
srcs: []*ObjectHandle{
c.Bucket("otherbucket").Object("baz"),
},
wantErr: true,
},
{
desc: "source, no object",
dst: c.Bucket("foo").Object("bar"),
srcs: []*ObjectHandle{
c.Bucket("foo").Object(""),
},
wantErr: true,
},
{
desc: "destination, bad condition",
dst: c.Bucket("foo").Object("bar").WithConditions(Generation(12)),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz"),
},
wantErr: true,
},
{
desc: "source, bad condition",
dst: c.Bucket("foo").Object("bar"),
srcs: []*ObjectHandle{
c.Bucket("foo").Object("baz").WithConditions(IfMetaGenerationMatch(12)),
},
wantErr: true,
},
}

for _, tt := range testCases {
_, err := tt.dst.ComposeFrom(ctx, tt.srcs, tt.attrs)
if gotErr := err != nil; gotErr != tt.wantErr {
t.Errorf("%s: got error %v; want err %t", tt.desc, err, tt.wantErr)
continue
}
if tt.wantErr {
continue
}
url, body := <-gotURL, <-gotBody
if url != tt.wantURL {
t.Errorf("%s: request URL\ngot %q\nwant %q", tt.desc, url, tt.wantURL)
}
var req raw.ComposeRequest
if err := json.Unmarshal(body, &req); err != nil {
t.Errorf("%s: json.Unmarshal %v (body %s)", tt.desc, err, body)
}
if !reflect.DeepEqual(req, tt.wantReq) {
// Print to JSON.
wantReq, _ := json.Marshal(tt.wantReq)
t.Errorf("%s: request body\ngot %s\nwant %s", tt.desc, body, wantReq)
}
}
}

// Test that ObjectIterator's Next and NextPage methods correctly terminate
// if there is nothing to iterate over.
func TestEmptyObjectIterator(t *testing.T) {
Expand Down

0 comments on commit 4df34ab

Please sign in to comment.