Skip to content

Commit

Permalink
feat(storage): add ObjectHandle.Move method (#11302)
Browse files Browse the repository at this point in the history
Support the new MoveObject API in both gRPC and JSON.
  • Loading branch information
tritone authored Dec 20, 2024
1 parent 650b89e commit a3cb8c4
Show file tree
Hide file tree
Showing 8 changed files with 262 additions and 30 deletions.
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
6 changes: 3 additions & 3 deletions storage/go.mod
Original file line number Diff line number Diff line change
Expand Up @@ -6,7 +6,7 @@ retract [v1.25.0, v1.27.0] // due to https://github.com/googleapis/google-cloud-

require (
cloud.google.com/go v0.116.0
cloud.google.com/go/compute/metadata v0.5.2
cloud.google.com/go/compute/metadata v0.6.0
cloud.google.com/go/iam v1.2.2
cloud.google.com/go/longrunning v0.6.2
github.com/GoogleCloudPlatform/opentelemetry-operations-go/exporter/metric v0.48.1
Expand All @@ -20,7 +20,7 @@ require (
go.opentelemetry.io/otel/sdk/metric v1.29.0
golang.org/x/oauth2 v0.24.0
golang.org/x/sync v0.10.0
google.golang.org/api v0.211.0
google.golang.org/api v0.212.0
google.golang.org/genproto v0.0.0-20241118233622-e639e219e697
google.golang.org/genproto/googleapis/api v0.0.0-20241118233622-e639e219e697
google.golang.org/grpc v1.67.3
Expand All @@ -29,7 +29,7 @@ require (

require (
cel.dev/expr v0.16.1 // indirect
cloud.google.com/go/auth v0.12.1 // indirect
cloud.google.com/go/auth v0.13.0 // indirect
cloud.google.com/go/auth/oauth2adapt v0.2.6 // indirect
cloud.google.com/go/monitoring v1.21.2 // indirect
github.com/GoogleCloudPlatform/opentelemetry-operations-go/detectors/gcp v1.25.0 // indirect
Expand Down
12 changes: 6 additions & 6 deletions storage/go.sum
Original file line number Diff line number Diff line change
Expand Up @@ -3,12 +3,12 @@ cel.dev/expr v0.16.1/go.mod h1:AsGA5zb3WruAEQeQng1RZdGEXmBj0jvMWh6l5SnNuC8=
cloud.google.com/go v0.26.0/go.mod h1:aQUYkXzVsufM+DwF1aE+0xfcU+56JwCaLick0ClmMTw=
cloud.google.com/go v0.116.0 h1:B3fRrSDkLRt5qSHWe40ERJvhvnQwdZiHu0bJOpldweE=
cloud.google.com/go v0.116.0/go.mod h1:cEPSRWPzZEswwdr9BxE6ChEn01dWlTaF05LiC2Xs70U=
cloud.google.com/go/auth v0.12.1 h1:n2Bj25BUMM0nvE9D2XLTiImanwZhO3DkfWSYS/SAJP4=
cloud.google.com/go/auth v0.12.1/go.mod h1:BFMu+TNpF3DmvfBO9ClqTR/SiqVIm7LukKF9mbendF4=
cloud.google.com/go/auth v0.13.0 h1:8Fu8TZy167JkW8Tj3q7dIkr2v4cndv41ouecJx0PAHs=
cloud.google.com/go/auth v0.13.0/go.mod h1:COOjD9gwfKNKz+IIduatIhYJQIc0mG3H102r/EMxX6Q=
cloud.google.com/go/auth/oauth2adapt v0.2.6 h1:V6a6XDu2lTwPZWOawrAa9HUK+DB2zfJyTuciBG5hFkU=
cloud.google.com/go/auth/oauth2adapt v0.2.6/go.mod h1:AlmsELtlEBnaNTL7jCj8VQFLy6mbZv0s4Q7NGBeQ5E8=
cloud.google.com/go/compute/metadata v0.5.2 h1:UxK4uu/Tn+I3p2dYWTfiX4wva7aYlKixAHn3fyqngqo=
cloud.google.com/go/compute/metadata v0.5.2/go.mod h1:C66sj2AluDcIqakBq/M8lw8/ybHgOZqin2obFxa/E5k=
cloud.google.com/go/compute/metadata v0.6.0 h1:A6hENjEsCDtC1k8byVsgwvVcioamEHvZ4j01OwKxG9I=
cloud.google.com/go/compute/metadata v0.6.0/go.mod h1:FjyFAW1MW0C203CEOMDTu3Dk1FlqW3Rga40jzHL4hfg=
cloud.google.com/go/iam v1.2.2 h1:ozUSofHUGf/F4tCNy/mu9tHLTaxZFLOUiKzjcgWHGIA=
cloud.google.com/go/iam v1.2.2/go.mod h1:0Ys8ccaZHdI1dEUilwzqng/6ps2YB6vRsjIe00/+6JY=
cloud.google.com/go/logging v1.12.0 h1:ex1igYcGFd4S/RZWOCU51StlIEuey5bjqwH9ZYjHibk=
Expand Down Expand Up @@ -167,8 +167,8 @@ golang.org/x/tools v0.0.0-20190226205152-f727befe758c/go.mod h1:9Yl7xja0Znq3iFh3
golang.org/x/tools v0.0.0-20190311212946-11955173bddd/go.mod h1:LCzVGOaR6xXOjkQ3onu1FJEFr0SW1gC7cKk1uF8kGRs=
golang.org/x/tools v0.0.0-20190524140312-2c0ae7006135/go.mod h1:RgjU9mgBXZiqYHBnxXauZ1Gv1EHHAz9KjViQ78xBX0Q=
golang.org/x/xerrors v0.0.0-20191204190536-9bdfabe68543/go.mod h1:I/5z698sn9Ka8TeJc9MKroUUfqBBauWjQqLJ2OPfmY0=
google.golang.org/api v0.211.0 h1:IUpLjq09jxBSV1lACO33CGY3jsRcbctfGzhj+ZSE/Bg=
google.golang.org/api v0.211.0/go.mod h1:XOloB4MXFH4UTlQSGuNUxw0UT74qdENK8d6JNsXKLi0=
google.golang.org/api v0.212.0 h1:BcRj3MJfHF3FYD29rk7u9kuu1SyfGqfHcA0hSwKqkHg=
google.golang.org/api v0.212.0/go.mod h1:gICpLlpp12/E8mycRMzgy3SQ9cFh2XnVJ6vJi/kQbvI=
google.golang.org/appengine v1.1.0/go.mod h1:EbEs0AVv82hx2wNQdGPgUI5lhzA/G0D9YwlJXL52JkM=
google.golang.org/appengine v1.4.0/go.mod h1:xpcJRLb0r/rnEns0DIKYYv+WjYCduHsrkT7/EB5XEv4=
google.golang.org/genproto v0.0.0-20180817151627-c66870c02cf8/go.mod h1:JiN7NxoALGmiZfu7CAH4rXhgtRTLTxftemlI0sWmxmc=
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
Loading

0 comments on commit a3cb8c4

Please sign in to comment.