Skip to content

Commit b23ddff

Browse files
authored
feat: add MoveObject to json and grpc (#706)
* feat: add MoveObject to json and grpc * format * remove setting of metageneration * remove retry decoratator, add comment * format
1 parent e48a569 commit b23ddff

File tree

5 files changed

+119
-4
lines changed

5 files changed

+119
-4
lines changed

gcs/object.py

+14-4
Original file line numberDiff line numberDiff line change
@@ -88,7 +88,15 @@ def _metadata_etag(cls, metadata):
8888

8989
@classmethod
9090
def init(
91-
cls, request, metadata, media, bucket, is_destination, context, finalize=True
91+
cls,
92+
request,
93+
metadata,
94+
media,
95+
bucket,
96+
is_destination,
97+
context,
98+
finalize=True,
99+
csek=True,
92100
):
93101
instruction = testbench.common.extract_instruction(request, context)
94102
if instruction == "inject-upload-data-error":
@@ -121,9 +129,11 @@ def init(
121129
)
122130
metadata.retention_expire_time.FromDatetime(retention_expiration_time)
123131
metadata.owner.entity = testbench.acl.get_object_entity("OWNER", context)
124-
algorithm, key_b64, key_sha256_b64 = testbench.csek.extract(
125-
request, False, context
126-
)
132+
algorithm, key_b64, key_sha256_b64 = "", "", ""
133+
if csek:
134+
algorithm, key_b64, key_sha256_b64 = testbench.csek.extract(
135+
request, False, context
136+
)
127137
if algorithm != "":
128138
key_sha256 = base64.b64decode(key_sha256_b64)
129139
testbench.csek.check(algorithm, key_b64, key_sha256, context)

testbench/grpc_server.py

+36
Original file line numberDiff line numberDiff line change
@@ -805,6 +805,42 @@ def RestoreObject(self, request, context):
805805
)
806806
return blob.metadata
807807

808+
# The MoveObject gRPC call only performas a copy + delete of a single object
809+
# as testbench does not have the concept of folders.
810+
# This will suffice for a very basic test but lacks the full depth of the production API.
811+
def MoveObject(self, request, context):
812+
preconditions = testbench.common.make_grpc_preconditions(request)
813+
bucket = self.db.get_bucket(request.bucket, context).metadata
814+
src_object = self.db.get_object(
815+
request.bucket,
816+
request.source_object,
817+
preconditions=preconditions,
818+
context=context,
819+
)
820+
dst_metadata = storage_pb2.Object()
821+
dst_metadata.CopyFrom(src_object.metadata)
822+
dst_metadata.bucket = request.bucket
823+
dst_metadata.name = request.destination_object
824+
dst_media = b""
825+
dst_media += src_object.media
826+
dst_object, _ = gcs.object.Object.init(
827+
request, dst_metadata, dst_media, bucket, False, context, csek=False
828+
)
829+
self.db.insert_object(
830+
request.bucket,
831+
dst_object,
832+
context=context,
833+
preconditions=preconditions,
834+
)
835+
self.db.delete_object(
836+
request.bucket,
837+
request.source_object,
838+
context=context,
839+
preconditions=preconditions,
840+
)
841+
842+
return dst_object.metadata
843+
808844
@retry_test(method="storage.objects.insert")
809845
def WriteObject(self, request_iterator, context):
810846
upload, is_resumable = gcs.upload.Upload.init_write_object_grpc(

testbench/rest_server.py

+15
Original file line numberDiff line numberDiff line change
@@ -704,6 +704,21 @@ def objects_copy(src_bucket_name, src_object_name, dst_bucket_name, dst_object_n
704704
return dst_object.rest_metadata()
705705

706706

707+
# The objects_move endpoint only performas a copy + delete of a single object
708+
# as testbench does not have the concept of folders.
709+
# This will suffice for a very basic test but lacks the full depth of the production API.
710+
@gcs.route(
711+
"/b/<bucket_name>/o/<path:src_object_name>/moveTo/o/<path:dst_object_name>",
712+
methods=["POST"],
713+
)
714+
def objects_move(bucket_name, src_object_name, dst_object_name):
715+
moved_object_metadata = objects_copy(
716+
bucket_name, src_object_name, bucket_name, dst_object_name
717+
)
718+
object_delete(bucket_name, src_object_name)
719+
return moved_object_metadata
720+
721+
707722
@gcs.route(
708723
"/b/<src_bucket_name>/o/<path:src_object_name>/rewriteTo/b/<dst_bucket_name>/o/<path:dst_object_name>",
709724
methods=["POST"],

tests/test_grpc_server.py

+22
Original file line numberDiff line numberDiff line change
@@ -1302,6 +1302,28 @@ def test_restore_object(self):
13021302
self.assertIsNotNone(response)
13031303
self.assertNotEqual(initial_generation, response.generation)
13041304

1305+
def test_move_object(self):
1306+
media = b"The quick brown fox jumps over the lazy dog"
1307+
request = testbench.common.FakeRequest(
1308+
args={"name": "object-to-move"}, data=media, headers={}, environ={}
1309+
)
1310+
blob, _ = gcs.object.Object.init_media(request, self.bucket.metadata)
1311+
self.db.insert_object("bucket-name", blob, None)
1312+
1313+
context = unittest.mock.Mock()
1314+
context.invocation_metadata = unittest.mock.MagicMock(return_value=dict())
1315+
response = self.grpc.MoveObject(
1316+
storage_pb2.MoveObjectRequest(
1317+
bucket="projects/_/buckets/bucket-name",
1318+
source_object="object-to-move",
1319+
destination_object="destination-object-to-move",
1320+
),
1321+
context=context,
1322+
)
1323+
context.abort.assert_not_called()
1324+
self.assertIsNotNone(response)
1325+
self.assertEqual(response.name, "destination-object-to-move")
1326+
13051327
def test_rewrite_object(self):
13061328
# We need a large enough payload to make sure the first rewrite does
13071329
# not complete. The minimum is 1 MiB

tests/test_testbench_object_special.py

+32
Original file line numberDiff line numberDiff line change
@@ -156,6 +156,38 @@ def test_object_copy(self):
156156
response.data.decode("utf-8"), "The quick brown fox jumps over the lazy dog"
157157
)
158158

159+
def test_object_move(self):
160+
response = self.client.post(
161+
"/storage/v1/b", data=json.dumps({"name": "bucket-name"})
162+
)
163+
self.assertEqual(response.status_code, 200)
164+
165+
payload = "The quick brown fox jumps over the lazy dog"
166+
response = self.client.put(
167+
"/bucket-name/fox",
168+
content_type="text/plain",
169+
data=payload,
170+
)
171+
self.assertEqual(response.status_code, 200)
172+
173+
response = self.client.post("/storage/v1/b/bucket-name/o/fox/moveTo/o/fox2")
174+
self.assertEqual(response.status_code, 200, msg=response.data)
175+
self.assertTrue(
176+
response.headers.get("content-type").startswith("application/json")
177+
)
178+
move_rest = json.loads(response.data)
179+
move_rest.pop("acl")
180+
move_rest.pop("owner")
181+
182+
response = self.client.get("/storage/v1/b/bucket-name/o/fox2")
183+
self.assertEqual(response.status_code, 200)
184+
self.assertTrue(
185+
response.headers.get("content-type").startswith("application/json")
186+
)
187+
get_rest = json.loads(response.data)
188+
189+
self.assertEqual(get_rest, move_rest)
190+
159191
def test_object_copy_with_metadata(self):
160192
response = self.client.post(
161193
"/storage/v1/b", data=json.dumps({"name": "bucket-name"})

0 commit comments

Comments
 (0)