Skip to content

Commit

Permalink
Merge branch 'main' into upgrade-api-02140
Browse files Browse the repository at this point in the history
  • Loading branch information
quartzmo authored Dec 20, 2024
2 parents 4f2a1d5 + a3cb8c4 commit 7f61b96
Show file tree
Hide file tree
Showing 9 changed files with 262 additions and 23 deletions.
2 changes: 1 addition & 1 deletion .release-please-manifest-individual.json
Original file line number Diff line number Diff line change
Expand Up @@ -12,5 +12,5 @@
"pubsublite": "1.8.2",
"spanner": "1.73.0",
"storage": "1.48.0",
"vertexai": "0.13.2"
"vertexai": "0.13.3"
}
8 changes: 8 additions & 0 deletions storage/client.go
Original file line number Diff line number Diff line change
Expand Up @@ -62,6 +62,7 @@ type storageClient interface {
GetObject(ctx context.Context, params *getObjectParams, opts ...storageOption) (*ObjectAttrs, error)
UpdateObject(ctx context.Context, params *updateObjectParams, opts ...storageOption) (*ObjectAttrs, error)
RestoreObject(ctx context.Context, params *restoreObjectParams, opts ...storageOption) (*ObjectAttrs, error)
MoveObject(ctx context.Context, params *moveObjectParams, opts ...storageOption) (*ObjectAttrs, error)

// Default Object ACL methods.

Expand Down Expand Up @@ -313,6 +314,13 @@ type restoreObjectParams struct {
copySourceACL bool
}

type moveObjectParams struct {
bucket, srcObject, dstObject string
srcConds *Conditions
dstConds *Conditions
encryptionKey []byte
}

type composeObjectRequest struct {
dstBucket string
dstObject destinationObject
Expand Down
32 changes: 31 additions & 1 deletion storage/grpc_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -662,6 +662,36 @@ func (c *grpcStorageClient) RestoreObject(ctx context.Context, params *restoreOb
return attrs, err
}

func (c *grpcStorageClient) MoveObject(ctx context.Context, params *moveObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
s := callSettings(c.settings, opts...)
req := &storagepb.MoveObjectRequest{
Bucket: bucketResourceName(globalProjectAlias, params.bucket),
SourceObject: params.srcObject,
DestinationObject: params.dstObject,
}
if err := applyCondsProto("MoveObjectDestination", defaultGen, params.dstConds, req); err != nil {
return nil, err
}
if err := applySourceCondsProto("MoveObjectSource", defaultGen, params.srcConds, req); err != nil {
return nil, err
}

if s.userProject != "" {
ctx = setUserProjectMetadata(ctx, s.userProject)
}

var attrs *ObjectAttrs
err := run(ctx, func(ctx context.Context) error {
res, err := c.raw.MoveObject(ctx, req, s.gax...)
attrs = newObjectFromProto(res)
return err
}, s.retry, s.idempotent)
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
return nil, ErrObjectNotExist
}
return attrs, err
}

// Default Object ACL methods.

func (c *grpcStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error {
Expand Down Expand Up @@ -926,7 +956,7 @@ func (c *grpcStorageClient) RewriteObject(ctx context.Context, req *rewriteObjec
if err := applyCondsProto("Copy destination", defaultGen, req.dstObject.conds, call); err != nil {
return nil, err
}
if err := applySourceCondsProto(req.srcObject.gen, req.srcObject.conds, call); err != nil {
if err := applySourceCondsProto("Copy source", req.srcObject.gen, req.srcObject.conds, call); err != nil {
return nil, err
}

Expand Down
27 changes: 26 additions & 1 deletion storage/http_client.go
Original file line number Diff line number Diff line change
Expand Up @@ -593,6 +593,31 @@ func (c *httpStorageClient) RestoreObject(ctx context.Context, params *restoreOb
return newObject(obj), err
}

func (c *httpStorageClient) MoveObject(ctx context.Context, params *moveObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
s := callSettings(c.settings, opts...)
req := c.raw.Objects.Move(params.bucket, params.srcObject, params.dstObject).Context(ctx)
if err := applyConds("MoveObjectDestination", defaultGen, params.dstConds, req); err != nil {
return nil, err
}
if err := applySourceConds("MoveObjectSource", defaultGen, params.srcConds, req); err != nil {
return nil, err
}
if s.userProject != "" {
req.UserProject(s.userProject)
}
if err := setEncryptionHeaders(req.Header(), params.encryptionKey, false); err != nil {
return nil, err
}
var obj *raw.Object
var err error
err = run(ctx, func(ctx context.Context) error { obj, err = req.Context(ctx).Do(); return err }, s.retry, s.idempotent)
var e *googleapi.Error
if ok := errors.As(err, &e); ok && e.Code == http.StatusNotFound {
return nil, ErrObjectNotExist
}
return newObject(obj), err
}

// Default Object ACL methods.

func (c *httpStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error {
Expand Down Expand Up @@ -798,7 +823,7 @@ func (c *httpStorageClient) RewriteObject(ctx context.Context, req *rewriteObjec
if err := applyConds("Copy destination", defaultGen, req.dstObject.conds, call); err != nil {
return nil, err
}
if err := applySourceConds(req.srcObject.gen, req.srcObject.conds, call); err != nil {
if err := applySourceConds("Copy source", req.srcObject.gen, req.srcObject.conds, call); err != nil {
return nil, err
}
if s.userProject != "" {
Expand Down
72 changes: 72 additions & 0 deletions storage/integration_test.go
Original file line number Diff line number Diff line change
Expand Up @@ -4603,6 +4603,78 @@ func TestIntegration_SoftDelete(t *testing.T) {
})
}

func TestIntegration_ObjectMove(t *testing.T) {
multiTransportTest(skipJSONReads(context.Background(), "no reads in test"), t, func(t *testing.T, ctx context.Context, _, prefix string, client *Client) {
h := testHelper{t}
srcObj := "move-src-obj"
dstObj := "move-dst-obj"

// Create bucket with HNS enabled
bkt := client.Bucket(prefix + uidSpace.New())
attrs := &BucketAttrs{
HierarchicalNamespace: &HierarchicalNamespace{Enabled: true},
UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: 0},
}
if err := bkt.Create(ctx, testutil.ProjID(), attrs); err != nil {
t.Fatalf("error creating bucket with soft delete policy set: %v", err)
}
t.Cleanup(func() { h.mustDeleteBucket(bkt) })

// Create source object
obj := bkt.Object(srcObj)
w := obj.NewWriter(ctx)
h.mustWrite(w, randomContents())
t.Cleanup(func() { h.mustDeleteObject(bkt.Object(dstObj)) })

// Move object
objAttrs, err := obj.Move(ctx, MoveObjectDestination{Object: dstObj})
if err != nil {
t.Fatalf("ObjectHandle.Move: %v", err)
}
// Check attrs are populated.
if objAttrs == nil || objAttrs.Name == "" {
t.Errorf("wanted object attrs to be populated; got %+v", objAttrs)
}
// Check source object is no longer present.
if _, err := obj.Attrs(ctx); !errors.Is(err, ErrObjectNotExist) {
t.Errorf("source object: got err %v, want ErrObjectNotExist", err)
}

// Test that source and destination preconditions are applied appropriately.
srcObj2 := "move-src-obj2"
dstObj2 := "move-dst-obj2"

obj2 := bkt.Object(srcObj2)
w2 := obj2.NewWriter(ctx)
h.mustWrite(w2, randomContents())
t.Cleanup(func() { h.mustDeleteObject(bkt.Object(dstObj2)) })

// Bad source generation should cause 412.
_, err = obj2.If(Conditions{
GenerationMatch: 123,
}).Move(ctx, MoveObjectDestination{Object: dstObj2})
if err == nil || !(status.Code(err) == codes.FailedPrecondition || extractErrCode(err) == http.StatusPreconditionFailed) {
t.Errorf("ObjectHandle.Move: got err %v, want failed precondition (412)", err)
}

// Bad dest generation should also cause 412.
_, err = obj2.Move(ctx, MoveObjectDestination{Object: dstObj2, Conditions: &Conditions{GenerationMatch: 123}})
if err == nil || !(status.Code(err) == codes.FailedPrecondition || extractErrCode(err) == http.StatusPreconditionFailed) {
t.Errorf("ObjectHandle.Move: got err %v, want failed precondition (412)", err)
}

// Correctly applied preconditions should work.
_, err = obj2.If(Conditions{
GenerationMatch: w2.Attrs().Generation,
MetagenerationMatch: w2.Attrs().Metageneration,
}).Move(ctx, MoveObjectDestination{Object: dstObj2, Conditions: &Conditions{DoesNotExist: true}})
if err != nil {
t.Fatalf("ObjectHandle.Move: %v", err)
}
})
}

func TestIntegration_KMS(t *testing.T) {
multiTransportTest(context.Background(), t, func(t *testing.T, ctx context.Context, bucket, prefix string, client *Client) {
h := testHelper{t}
Expand Down
120 changes: 104 additions & 16 deletions storage/storage.go
Original file line number Diff line number Diff line change
Expand Up @@ -1156,6 +1156,38 @@ func (o *ObjectHandle) Restore(ctx context.Context, opts *RestoreOptions) (*Obje
}, sOpts...)
}

// Move changes the name of the object to the destination name.
// It can only be used to rename an object within the same bucket. The
// bucket must have [HierarchicalNamespace] enabled to use this method.
//
// Any preconditions set on the ObjectHandle will be applied for the source
// object. Set preconditions on the destination object using
// [MoveObjectDestination.Conditions].
//
// This API is in preview and is not yet publicly available.
func (o *ObjectHandle) Move(ctx context.Context, destination MoveObjectDestination) (*ObjectAttrs, error) {
if err := o.validate(); err != nil {
return nil, err
}

sOpts := makeStorageOpts(true, o.retry, o.userProject)
return o.c.tc.MoveObject(ctx, &moveObjectParams{
bucket: o.bucket,
srcObject: o.object,
dstObject: destination.Object,
srcConds: o.conds,
dstConds: destination.Conditions,
encryptionKey: o.encryptionKey,
}, sOpts...)
}

// MoveObjectDestination provides the destination object name and (optional) preconditions
// for [ObjectHandle.Move].
type MoveObjectDestination struct {
Object string
Conditions *Conditions
}

// NewWriter returns a storage Writer that writes to the GCS object
// associated with this ObjectHandle.
//
Expand Down Expand Up @@ -2055,56 +2087,91 @@ func applyConds(method string, gen int64, conds *Conditions, call interface{}) e
return nil
}

func applySourceConds(gen int64, conds *Conditions, call *raw.ObjectsRewriteCall) error {
// applySourceConds modifies the provided call using the conditions in conds.
// call is something that quacks like a *raw.WhateverCall.
// This is specifically for calls like Rewrite and Move which have a source and destination
// object.
func applySourceConds(method string, gen int64, conds *Conditions, call interface{}) error {
cval := reflect.ValueOf(call)
if gen >= 0 {
call.SourceGeneration(gen)
if !setSourceGeneration(cval, gen) {
return fmt.Errorf("storage: %s: source generation not supported", method)
}
}
if conds == nil {
return nil
}
if err := conds.validate("CopyTo source"); err != nil {
if err := conds.validate(method); err != nil {
return err
}
switch {
case conds.GenerationMatch != 0:
call.IfSourceGenerationMatch(conds.GenerationMatch)
if !setIfSourceGenerationMatch(cval, conds.GenerationMatch) {
return fmt.Errorf("storage: %s: ifSourceGenerationMatch not supported", method)
}
case conds.GenerationNotMatch != 0:
call.IfSourceGenerationNotMatch(conds.GenerationNotMatch)
if !setIfSourceGenerationNotMatch(cval, conds.GenerationNotMatch) {
return fmt.Errorf("storage: %s: ifSourceGenerationNotMatch not supported", method)
}
case conds.DoesNotExist:
call.IfSourceGenerationMatch(0)
if !setIfSourceGenerationMatch(cval, int64(0)) {
return fmt.Errorf("storage: %s: DoesNotExist not supported", method)
}
}
switch {
case conds.MetagenerationMatch != 0:
call.IfSourceMetagenerationMatch(conds.MetagenerationMatch)
if !setIfSourceMetagenerationMatch(cval, conds.MetagenerationMatch) {
return fmt.Errorf("storage: %s: ifSourceMetagenerationMatch not supported", method)
}
case conds.MetagenerationNotMatch != 0:
call.IfSourceMetagenerationNotMatch(conds.MetagenerationNotMatch)
if !setIfSourceMetagenerationNotMatch(cval, conds.MetagenerationNotMatch) {
return fmt.Errorf("storage: %s: ifSourceMetagenerationNotMatch not supported", method)
}
}
return nil
}

func applySourceCondsProto(gen int64, conds *Conditions, call *storagepb.RewriteObjectRequest) error {
// applySourceCondsProto validates and attempts to set the conditions on a protobuf
// message using protobuf reflection. This is specifically for RPCs which have separate
// preconditions for source and destination objects (e.g. Rewrite and Move).
func applySourceCondsProto(method string, gen int64, conds *Conditions, msg proto.Message) error {
rmsg := msg.ProtoReflect()

if gen >= 0 {
call.SourceGeneration = gen
if !setConditionProtoField(rmsg, "source_generation", gen) {
return fmt.Errorf("storage: %s: generation not supported", method)
}
}
if conds == nil {
return nil
}
if err := conds.validate("CopyTo source"); err != nil {
if err := conds.validate(method); err != nil {
return err
}

switch {
case conds.GenerationMatch != 0:
call.IfSourceGenerationMatch = proto.Int64(conds.GenerationMatch)
if !setConditionProtoField(rmsg, "if_source_generation_match", conds.GenerationMatch) {
return fmt.Errorf("storage: %s: ifSourceGenerationMatch not supported", method)
}
case conds.GenerationNotMatch != 0:
call.IfSourceGenerationNotMatch = proto.Int64(conds.GenerationNotMatch)
if !setConditionProtoField(rmsg, "if_source_generation_not_match", conds.GenerationNotMatch) {
return fmt.Errorf("storage: %s: ifSourceGenerationNotMatch not supported", method)
}
case conds.DoesNotExist:
call.IfSourceGenerationMatch = proto.Int64(0)
if !setConditionProtoField(rmsg, "if_source_generation_match", int64(0)) {
return fmt.Errorf("storage: %s: DoesNotExist not supported", method)
}
}
switch {
case conds.MetagenerationMatch != 0:
call.IfSourceMetagenerationMatch = proto.Int64(conds.MetagenerationMatch)
if !setConditionProtoField(rmsg, "if_source_metageneration_match", conds.MetagenerationMatch) {
return fmt.Errorf("storage: %s: ifSourceMetagenerationMatch not supported", method)
}
case conds.MetagenerationNotMatch != 0:
call.IfSourceMetagenerationNotMatch = proto.Int64(conds.MetagenerationNotMatch)
if !setConditionProtoField(rmsg, "if_source_metageneration_not_match", conds.MetagenerationNotMatch) {
return fmt.Errorf("storage: %s: ifSourceMetagenerationNotMatch not supported", method)
}
}
return nil
}
Expand Down Expand Up @@ -2143,6 +2210,27 @@ func setIfMetagenerationNotMatch(cval reflect.Value, value interface{}) bool {
return setCondition(cval.MethodByName("IfMetagenerationNotMatch"), value)
}

// More methods to set source object precondition fields (used by Rewrite and Move APIs).
func setSourceGeneration(cval reflect.Value, value interface{}) bool {
return setCondition(cval.MethodByName("SourceGeneration"), value)
}

func setIfSourceGenerationMatch(cval reflect.Value, value interface{}) bool {
return setCondition(cval.MethodByName("IfSourceGenerationMatch"), value)
}

func setIfSourceGenerationNotMatch(cval reflect.Value, value interface{}) bool {
return setCondition(cval.MethodByName("IfSourceGenerationNotMatch"), value)
}

func setIfSourceMetagenerationMatch(cval reflect.Value, value interface{}) bool {
return setCondition(cval.MethodByName("IfSourceMetagenerationMatch"), value)
}

func setIfSourceMetagenerationNotMatch(cval reflect.Value, value interface{}) bool {
return setCondition(cval.MethodByName("IfSourceMetagenerationNotMatch"), value)
}

func setCondition(setter reflect.Value, value interface{}) bool {
if setter.IsValid() {
setter.Call([]reflect.Value{reflect.ValueOf(value)})
Expand Down
Loading

0 comments on commit 7f61b96

Please sign in to comment.