Skip to content

Commit a3cb8c4

Browse files
authored
feat(storage): add ObjectHandle.Move method (#11302)
Support the new MoveObject API in both gRPC and JSON.
1 parent 650b89e commit a3cb8c4

File tree

8 files changed

+262
-30
lines changed

8 files changed

+262
-30
lines changed

storage/client.go

Lines changed: 8 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -62,6 +62,7 @@ type storageClient interface {
6262
GetObject(ctx context.Context, params *getObjectParams, opts ...storageOption) (*ObjectAttrs, error)
6363
UpdateObject(ctx context.Context, params *updateObjectParams, opts ...storageOption) (*ObjectAttrs, error)
6464
RestoreObject(ctx context.Context, params *restoreObjectParams, opts ...storageOption) (*ObjectAttrs, error)
65+
MoveObject(ctx context.Context, params *moveObjectParams, opts ...storageOption) (*ObjectAttrs, error)
6566

6667
// Default Object ACL methods.
6768

@@ -313,6 +314,13 @@ type restoreObjectParams struct {
313314
copySourceACL bool
314315
}
315316

317+
type moveObjectParams struct {
318+
bucket, srcObject, dstObject string
319+
srcConds *Conditions
320+
dstConds *Conditions
321+
encryptionKey []byte
322+
}
323+
316324
type composeObjectRequest struct {
317325
dstBucket string
318326
dstObject destinationObject

storage/go.mod

Lines changed: 3 additions & 3 deletions
Original file line numberDiff line numberDiff line change
@@ -6,7 +6,7 @@ retract [v1.25.0, v1.27.0] // due to https://github.com/googleapis/google-cloud-
66

77
require (
88
cloud.google.com/go v0.116.0
9-
cloud.google.com/go/compute/metadata v0.5.2
9+
cloud.google.com/go/compute/metadata v0.6.0
1010
cloud.google.com/go/iam v1.2.2
1111
cloud.google.com/go/longrunning v0.6.2
1212
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1
@@ -20,7 +20,7 @@ require (
2020
go.opentelemetry.io/otel/sdk/metric v1.29.0
2121
golang.org/x/oauth2 v0.24.0
2222
golang.org/x/sync v0.10.0
23-
google.golang.org/api v0.211.0
23+
google.golang.org/api v0.212.0
2424
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697
2525
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697
2626
google.golang.org/grpc v1.67.3
@@ -29,7 +29,7 @@ require (
2929

3030
require (
3131
cel.dev/expr v0.16.1 // indirect
32-
cloud.google.com/go/auth v0.12.1 // indirect
32+
cloud.google.com/go/auth v0.13.0 // indirect
3333
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
3434
cloud.google.com/go/monitoring v1.21.2 // indirect
3535
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect

storage/go.sum

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -3,12 +3,12 @@ cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
33
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
44
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
55
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
6-
cloud.google.com/go/auth v0.12.1 h1:n2Bj25BUMM0nvE9D2XLTiImanwZhO3DkfWSYS/SAJP4=
7-
cloud.google.com/go/auth v0.12.1/go.mod h1:BFMu+TNpF3DmvfBO9ClqTR/SiqVIm7LukKF9mbendF4=
6+
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
7+
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
88
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
99
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
10-
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
11-
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
10+
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
11+
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
1212
cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
1313
cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
1414
cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk=
@@ -167,8 +167,8 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
167167
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
168168
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
169169
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
170-
google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg=
171-
google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0=
170+
google.golang.org/api v0.212.0 h1:BcRj3MJfHF3FYD29rk7u9kuu1SyfGqfHcA0hSwKqkHg=
171+
google.golang.org/api v0.212.0/go.mod h1:gICpLlpp12/E8mycRMzgy3SQ9cFh2XnVJ6vJi/kQbvI=
172172
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
173173
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
174174
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=

storage/grpc_client.go

Lines changed: 31 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -662,6 +662,36 @@ func (c *grpcStorageClient) RestoreObject(ctx context.Context, params *restoreOb
662662
return attrs, err
663663
}
664664

665+
func (c *grpcStorageClient) MoveObject(ctx context.Context, params *moveObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
666+
s := callSettings(c.settings, opts...)
667+
req := &storagepb.MoveObjectRequest{
668+
Bucket: bucketResourceName(globalProjectAlias, params.bucket),
669+
SourceObject: params.srcObject,
670+
DestinationObject: params.dstObject,
671+
}
672+
if err := applyCondsProto("MoveObjectDestination", defaultGen, params.dstConds, req); err != nil {
673+
return nil, err
674+
}
675+
if err := applySourceCondsProto("MoveObjectSource", defaultGen, params.srcConds, req); err != nil {
676+
return nil, err
677+
}
678+
679+
if s.userProject != "" {
680+
ctx = setUserProjectMetadata(ctx, s.userProject)
681+
}
682+
683+
var attrs *ObjectAttrs
684+
err := run(ctx, func(ctx context.Context) error {
685+
res, err := c.raw.MoveObject(ctx, req, s.gax...)
686+
attrs = newObjectFromProto(res)
687+
return err
688+
}, s.retry, s.idempotent)
689+
if s, ok := status.FromError(err); ok && s.Code() == codes.NotFound {
690+
return nil, ErrObjectNotExist
691+
}
692+
return attrs, err
693+
}
694+
665695
// Default Object ACL methods.
666696

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

storage/http_client.go

Lines changed: 26 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -593,6 +593,31 @@ func (c *httpStorageClient) RestoreObject(ctx context.Context, params *restoreOb
593593
return newObject(obj), err
594594
}
595595

596+
func (c *httpStorageClient) MoveObject(ctx context.Context, params *moveObjectParams, opts ...storageOption) (*ObjectAttrs, error) {
597+
s := callSettings(c.settings, opts...)
598+
req := c.raw.Objects.Move(params.bucket, params.srcObject, params.dstObject).Context(ctx)
599+
if err := applyConds("MoveObjectDestination", defaultGen, params.dstConds, req); err != nil {
600+
return nil, err
601+
}
602+
if err := applySourceConds("MoveObjectSource", defaultGen, params.srcConds, req); err != nil {
603+
return nil, err
604+
}
605+
if s.userProject != "" {
606+
req.UserProject(s.userProject)
607+
}
608+
if err := setEncryptionHeaders(req.Header(), params.encryptionKey, false); err != nil {
609+
return nil, err
610+
}
611+
var obj *raw.Object
612+
var err error
613+
err = run(ctx, func(ctx context.Context) error { obj, err = req.Context(ctx).Do(); return err }, s.retry, s.idempotent)
614+
var e *googleapi.Error
615+
if ok := errors.As(err, &e); ok && e.Code == http.StatusNotFound {
616+
return nil, ErrObjectNotExist
617+
}
618+
return newObject(obj), err
619+
}
620+
596621
// Default Object ACL methods.
597622

598623
func (c *httpStorageClient) DeleteDefaultObjectACL(ctx context.Context, bucket string, entity ACLEntity, opts ...storageOption) error {
@@ -798,7 +823,7 @@ func (c *httpStorageClient) RewriteObject(ctx context.Context, req *rewriteObjec
798823
if err := applyConds("Copy destination", defaultGen, req.dstObject.conds, call); err != nil {
799824
return nil, err
800825
}
801-
if err := applySourceConds(req.srcObject.gen, req.srcObject.conds, call); err != nil {
826+
if err := applySourceConds("Copy source", req.srcObject.gen, req.srcObject.conds, call); err != nil {
802827
return nil, err
803828
}
804829
if s.userProject != "" {

storage/integration_test.go

Lines changed: 72 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -4603,6 +4603,78 @@ func TestIntegration_SoftDelete(t *testing.T) {
46034603
})
46044604
}
46054605

4606+
func TestIntegration_ObjectMove(t *testing.T) {
4607+
multiTransportTest(skipJSONReads(context.Background(), "no reads in test"), t, func(t *testing.T, ctx context.Context, _, prefix string, client *Client) {
4608+
h := testHelper{t}
4609+
srcObj := "move-src-obj"
4610+
dstObj := "move-dst-obj"
4611+
4612+
// Create bucket with HNS enabled
4613+
bkt := client.Bucket(prefix + uidSpace.New())
4614+
attrs := &BucketAttrs{
4615+
HierarchicalNamespace: &HierarchicalNamespace{Enabled: true},
4616+
UniformBucketLevelAccess: UniformBucketLevelAccess{Enabled: true},
4617+
SoftDeletePolicy: &SoftDeletePolicy{RetentionDuration: 0},
4618+
}
4619+
if err := bkt.Create(ctx, testutil.ProjID(), attrs); err != nil {
4620+
t.Fatalf("error creating bucket with soft delete policy set: %v", err)
4621+
}
4622+
t.Cleanup(func() { h.mustDeleteBucket(bkt) })
4623+
4624+
// Create source object
4625+
obj := bkt.Object(srcObj)
4626+
w := obj.NewWriter(ctx)
4627+
h.mustWrite(w, randomContents())
4628+
t.Cleanup(func() { h.mustDeleteObject(bkt.Object(dstObj)) })
4629+
4630+
// Move object
4631+
objAttrs, err := obj.Move(ctx, MoveObjectDestination{Object: dstObj})
4632+
if err != nil {
4633+
t.Fatalf("ObjectHandle.Move: %v", err)
4634+
}
4635+
// Check attrs are populated.
4636+
if objAttrs == nil || objAttrs.Name == "" {
4637+
t.Errorf("wanted object attrs to be populated; got %+v", objAttrs)
4638+
}
4639+
// Check source object is no longer present.
4640+
if _, err := obj.Attrs(ctx); !errors.Is(err, ErrObjectNotExist) {
4641+
t.Errorf("source object: got err %v, want ErrObjectNotExist", err)
4642+
}
4643+
4644+
// Test that source and destination preconditions are applied appropriately.
4645+
srcObj2 := "move-src-obj2"
4646+
dstObj2 := "move-dst-obj2"
4647+
4648+
obj2 := bkt.Object(srcObj2)
4649+
w2 := obj2.NewWriter(ctx)
4650+
h.mustWrite(w2, randomContents())
4651+
t.Cleanup(func() { h.mustDeleteObject(bkt.Object(dstObj2)) })
4652+
4653+
// Bad source generation should cause 412.
4654+
_, err = obj2.If(Conditions{
4655+
GenerationMatch: 123,
4656+
}).Move(ctx, MoveObjectDestination{Object: dstObj2})
4657+
if err == nil || !(status.Code(err) == codes.FailedPrecondition || extractErrCode(err) == http.StatusPreconditionFailed) {
4658+
t.Errorf("ObjectHandle.Move: got err %v, want failed precondition (412)", err)
4659+
}
4660+
4661+
// Bad dest generation should also cause 412.
4662+
_, err = obj2.Move(ctx, MoveObjectDestination{Object: dstObj2, Conditions: &Conditions{GenerationMatch: 123}})
4663+
if err == nil || !(status.Code(err) == codes.FailedPrecondition || extractErrCode(err) == http.StatusPreconditionFailed) {
4664+
t.Errorf("ObjectHandle.Move: got err %v, want failed precondition (412)", err)
4665+
}
4666+
4667+
// Correctly applied preconditions should work.
4668+
_, err = obj2.If(Conditions{
4669+
GenerationMatch: w2.Attrs().Generation,
4670+
MetagenerationMatch: w2.Attrs().Metageneration,
4671+
}).Move(ctx, MoveObjectDestination{Object: dstObj2, Conditions: &Conditions{DoesNotExist: true}})
4672+
if err != nil {
4673+
t.Fatalf("ObjectHandle.Move: %v", err)
4674+
}
4675+
})
4676+
}
4677+
46064678
func TestIntegration_KMS(t *testing.T) {
46074679
multiTransportTest(context.Background(), t, func(t *testing.T, ctx context.Context, bucket, prefix string, client *Client) {
46084680
h := testHelper{t}

0 commit comments

Comments
 (0)