Skip to content

Commit

Permalink
feat: add MoveObject to json and grpc (#706)
Browse files Browse the repository at this point in the history
* feat: add MoveObject to json and grpc

* format

* remove setting of metageneration

* remove retry decoratator, add comment

* format
  • Loading branch information
ddelgrosso1 authored Jan 14, 2025
1 parent e48a569 commit b23ddff
Show file tree
Hide file tree
Showing 5 changed files with 119 additions and 4 deletions.
18 changes: 14 additions & 4 deletions gcs/object.py
Original file line number Diff line number Diff line change
Expand Up @@ -88,7 +88,15 @@ def _metadata_etag(cls, metadata):

@classmethod
def init(
cls, request, metadata, media, bucket, is_destination, context, finalize=True
cls,
request,
metadata,
media,
bucket,
is_destination,
context,
finalize=True,
csek=True,
):
instruction = testbench.common.extract_instruction(request, context)
if instruction == "inject-upload-data-error":
Expand Down Expand Up @@ -121,9 +129,11 @@ def init(
)
metadata.retention_expire_time.FromDatetime(retention_expiration_time)
metadata.owner.entity = testbench.acl.get_object_entity("OWNER", context)
algorithm, key_b64, key_sha256_b64 = testbench.csek.extract(
request, False, context
)
algorithm, key_b64, key_sha256_b64 = "", "", ""
if csek:
algorithm, key_b64, key_sha256_b64 = testbench.csek.extract(
request, False, context
)
if algorithm != "":
key_sha256 = base64.b64decode(key_sha256_b64)
testbench.csek.check(algorithm, key_b64, key_sha256, context)
Expand Down
36 changes: 36 additions & 0 deletions testbench/grpc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -805,6 +805,42 @@ def RestoreObject(self, request, context):
)
return blob.metadata

# The MoveObject gRPC call only performas a copy + delete of a single object
# as testbench does not have the concept of folders.
# This will suffice for a very basic test but lacks the full depth of the production API.
def MoveObject(self, request, context):
preconditions = testbench.common.make_grpc_preconditions(request)
bucket = self.db.get_bucket(request.bucket, context).metadata
src_object = self.db.get_object(
request.bucket,
request.source_object,
preconditions=preconditions,
context=context,
)
dst_metadata = storage_pb2.Object()
dst_metadata.CopyFrom(src_object.metadata)
dst_metadata.bucket = request.bucket
dst_metadata.name = request.destination_object
dst_media = b""
dst_media += src_object.media
dst_object, _ = gcs.object.Object.init(
request, dst_metadata, dst_media, bucket, False, context, csek=False
)
self.db.insert_object(
request.bucket,
dst_object,
context=context,
preconditions=preconditions,
)
self.db.delete_object(
request.bucket,
request.source_object,
context=context,
preconditions=preconditions,
)

return dst_object.metadata

@retry_test(method="storage.objects.insert")
def WriteObject(self, request_iterator, context):
upload, is_resumable = gcs.upload.Upload.init_write_object_grpc(
Expand Down
15 changes: 15 additions & 0 deletions testbench/rest_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -704,6 +704,21 @@ def objects_copy(src_bucket_name, src_object_name, dst_bucket_name, dst_object_n
return dst_object.rest_metadata()


# The objects_move endpoint only performas a copy + delete of a single object
# as testbench does not have the concept of folders.
# This will suffice for a very basic test but lacks the full depth of the production API.
@gcs.route(
"/b/<bucket_name>/o/<path:src_object_name>/moveTo/o/<path:dst_object_name>",
methods=["POST"],
)
def objects_move(bucket_name, src_object_name, dst_object_name):
moved_object_metadata = objects_copy(
bucket_name, src_object_name, bucket_name, dst_object_name
)
object_delete(bucket_name, src_object_name)
return moved_object_metadata


@gcs.route(
"/b/<src_bucket_name>/o/<path:src_object_name>/rewriteTo/b/<dst_bucket_name>/o/<path:dst_object_name>",
methods=["POST"],
Expand Down
22 changes: 22 additions & 0 deletions tests/test_grpc_server.py
Original file line number Diff line number Diff line change
Expand Up @@ -1302,6 +1302,28 @@ def test_restore_object(self):
self.assertIsNotNone(response)
self.assertNotEqual(initial_generation, response.generation)

def test_move_object(self):
media = b"The quick brown fox jumps over the lazy dog"
request = testbench.common.FakeRequest(
args={"name": "object-to-move"}, data=media, headers={}, environ={}
)
blob, _ = gcs.object.Object.init_media(request, self.bucket.metadata)
self.db.insert_object("bucket-name", blob, None)

context = unittest.mock.Mock()
context.invocation_metadata = unittest.mock.MagicMock(return_value=dict())
response = self.grpc.MoveObject(
storage_pb2.MoveObjectRequest(
bucket="projects/_/buckets/bucket-name",
source_object="object-to-move",
destination_object="destination-object-to-move",
),
context=context,
)
context.abort.assert_not_called()
self.assertIsNotNone(response)
self.assertEqual(response.name, "destination-object-to-move")

def test_rewrite_object(self):
# We need a large enough payload to make sure the first rewrite does
# not complete. The minimum is 1 MiB
Expand Down
32 changes: 32 additions & 0 deletions tests/test_testbench_object_special.py
Original file line number Diff line number Diff line change
Expand Up @@ -156,6 +156,38 @@ def test_object_copy(self):
response.data.decode("utf-8"), "The quick brown fox jumps over the lazy dog"
)

def test_object_move(self):
response = self.client.post(
"/storage/v1/b", data=json.dumps({"name": "bucket-name"})
)
self.assertEqual(response.status_code, 200)

payload = "The quick brown fox jumps over the lazy dog"
response = self.client.put(
"/bucket-name/fox",
content_type="text/plain",
data=payload,
)
self.assertEqual(response.status_code, 200)

response = self.client.post("/storage/v1/b/bucket-name/o/fox/moveTo/o/fox2")
self.assertEqual(response.status_code, 200, msg=response.data)
self.assertTrue(
response.headers.get("content-type").startswith("application/json")
)
move_rest = json.loads(response.data)
move_rest.pop("acl")
move_rest.pop("owner")

response = self.client.get("/storage/v1/b/bucket-name/o/fox2")
self.assertEqual(response.status_code, 200)
self.assertTrue(
response.headers.get("content-type").startswith("application/json")
)
get_rest = json.loads(response.data)

self.assertEqual(get_rest, move_rest)

def test_object_copy_with_metadata(self):
response = self.client.post(
"/storage/v1/b", data=json.dumps({"name": "bucket-name"})
Expand Down

0 comments on commit b23ddff

Please sign in to comment.