Skip to content

Commit b0020c1

Browse files
authored
test(storage): add emulator rewrite, iam, project (#5392)
1 parent 608f16e commit b0020c1

File tree

15 files changed

+626
-108
lines changed

15 files changed

+626
-108
lines changed

google/cloud/storage/emulator/database.py

Lines changed: 35 additions & 14 deletions
Original file line numberDiff line numberDiff line change
@@ -12,23 +12,26 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15+
import json
16+
import os
17+
1518
import gcs
1619
import utils
17-
import simdjson
18-
import os
20+
1921
from google.cloud.storage_v1.proto import storage_pb2 as storage_pb2
2022

2123

2224
class Database:
23-
def __init__(self, buckets, objects, live_generations, uploads):
25+
def __init__(self, buckets, objects, live_generations, uploads, rewrites):
2426
self.buckets = buckets
2527
self.objects = objects
2628
self.live_generations = live_generations
2729
self.uploads = uploads
30+
self.rewrites = rewrites
2831

2932
@classmethod
3033
def init(cls):
31-
return cls({}, {}, {}, {})
34+
return cls({}, {}, {}, {}, {})
3235

3336
# === BUCKET === #
3437

@@ -64,7 +67,7 @@ def list_bucket(self, request, project_id, context):
6467

6568
def delete_bucket(self, request, bucket_name, context):
6669
bucket = self.get_bucket(request, bucket_name, context)
67-
if len(self.objects[bucket.metadata.name]) > 0:
70+
if len(self.live_generations[bucket.metadata.name]) > 0:
6871
utils.error.invalid("Deleting non-empty bucket", context)
6972
del self.buckets[bucket.metadata.name]
7073
del self.objects[bucket.metadata.name]
@@ -79,7 +82,7 @@ def insert_test_bucket(self, context):
7982
request = storage_pb2.InsertBucketRequest(bucket={"name": bucket_name})
8083
else:
8184
request = utils.common.FakeRequest(
82-
args={}, data=simdjson.dumps({"name": bucket_name})
85+
args={}, data=json.dumps({"name": bucket_name})
8386
)
8487
bucket_test, _ = gcs.bucket.Bucket.init(request, context)
8588
self.insert_bucket(request, bucket_test, context)
@@ -120,6 +123,7 @@ def list_object(self, request, bucket_name, context):
120123
end_offset,
121124
) = self.__extract_list_object_request(request, context)
122125
items = []
126+
rest_onlys = []
123127
prefixes = set()
124128
for obj in bucket.values():
125129
generation = obj.metadata.generation
@@ -139,7 +143,8 @@ def list_object(self, request, bucket_name, context):
139143
prefixes.add(name[: delimiter_index + 1])
140144
continue
141145
items.append(obj.metadata)
142-
return items, list(prefixes)
146+
rest_onlys.append(obj.rest_only)
147+
return items, list(prefixes), rest_onlys
143148

144149
def check_object_generation(
145150
self, request, bucket_name, object_name, is_source, context
@@ -188,19 +193,20 @@ def insert_object(self, request, bucket_name, blob, context):
188193
self.live_generations[bucket_name][name] = generation
189194

190195
def delete_object(self, request, bucket_name, object_name, context):
191-
blob = self.get_object(request, bucket_name, object_name, False, context)
192-
generation = blob.metadata.generation
193-
live_generation = self.live_generations[bucket_name][object_name]
194-
if generation == live_generation:
195-
del self.live_generations[bucket_name][object_name]
196-
del self.objects[bucket_name]["%s#%d" % (object_name, generation)]
196+
_ = self.get_object(request, bucket_name, object_name, False, context)
197+
generation = utils.generation.extract_generation(request, False, context)
198+
live_generation = self.live_generations[bucket_name].get(object_name)
199+
if generation == 0 or live_generation == generation:
200+
self.live_generations[bucket_name].pop(object_name, None)
201+
if generation != 0:
202+
del self.objects[bucket_name]["%s#%d" % (object_name, generation)]
197203

198204
# === UPLOAD === #
199205

200206
def get_upload(self, upload_id, context):
201207
upload = self.uploads.get(upload_id)
202208
if upload is None:
203-
utils.error.notfound("Upload %s does not exist." % upload_id, context)
209+
utils.error.notfound("Upload %s" % upload_id, context)
204210
return upload
205211

206212
def insert_upload(self, upload):
@@ -209,3 +215,18 @@ def insert_upload(self, upload):
209215
def delete_upload(self, upload_id, context):
210216
self.get_upload(upload_id, context)
211217
del self.uploads[upload_id]
218+
219+
# === REWRITE === #
220+
221+
def get_rewrite(self, token, context):
222+
rewrite = self.rewrites.get(token)
223+
if rewrite is None:
224+
utils.error.notfound(404, "Rewrite %s" % token, context)
225+
return rewrite
226+
227+
def insert_rewrite(self, rewrite):
228+
self.rewrites[rewrite.token] = rewrite
229+
230+
def delete_rewrite(self, token, context):
231+
self.get_rewrite(token, context)
232+
del self.rewrites[token]

google/cloud/storage/emulator/rest_server.py renamed to google/cloud/storage/emulator/emulator.py

Lines changed: 100 additions & 15 deletions
Original file line numberDiff line numberDiff line change
@@ -12,18 +12,20 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
import database
15+
import json
1616
import logging
17+
18+
import database
1719
import flask
20+
import gcs as gcs_type
1821
import httpbin
19-
import simdjson
2022
import utils
21-
import gcs as gcs_type
22-
from google.protobuf import json_format
2323
from werkzeug import serving
2424
from werkzeug.middleware.dispatcher import DispatcherMiddleware
25-
from google.cloud.storage_v1.proto.storage_resources_pb2 import CommonEnums
25+
2626
from google.cloud.storage_v1.proto import storage_resources_pb2 as resources_pb2
27+
from google.cloud.storage_v1.proto.storage_resources_pb2 import CommonEnums
28+
from google.protobuf import json_format
2729

2830
db = None
2931

@@ -327,11 +329,14 @@ def bucket_lock_retention_policy(bucket_name):
327329
@gcs.route("/b/<bucket_name>/o")
328330
def object_list(bucket_name):
329331
db.insert_test_bucket(None)
330-
items, prefixes = db.list_object(flask.request, bucket_name, None)
332+
items, prefixes, rest_onlys = db.list_object(flask.request, bucket_name, None)
331333
response = {
332334
"kind": "storage#objects",
333335
"nextPageToken": "",
334-
"items": [gcs_type.object.Object.rest(blob) for blob in items],
336+
"items": [
337+
gcs_type.object.Object.rest(blob, rest_only)
338+
for blob, rest_only in zip(items, rest_onlys)
339+
],
335340
"prefixes": prefixes,
336341
}
337342
fields = flask.request.args.get("fields", None)
@@ -372,7 +377,7 @@ def object_delete(bucket_name, object_name):
372377
@gcs.route("/b/<bucket_name>/o/<path:object_name>/compose", methods=["POST"])
373378
def objects_compose(bucket_name, object_name):
374379
bucket = db.get_bucket_without_generation(bucket_name, None).metadata
375-
payload = simdjson.loads(flask.request.data)
380+
payload = json.loads(flask.request.data)
376381
source_objects = payload["sourceObjects"]
377382
if source_objects is None:
378383
utils.error.missing("source component", None)
@@ -427,8 +432,9 @@ def objects_copy(src_bucket_name, src_object_name, dst_bucket_name, dst_object_n
427432
dst_metadata.name = dst_object_name
428433
dst_media = b""
429434
dst_media += src_object.media
435+
dst_rest_only = dict(src_object.rest_only)
430436
dst_object, _ = gcs_type.object.Object.init(
431-
flask.request, dst_metadata, dst_media, dst_bucket, True, None
437+
flask.request, dst_metadata, dst_media, dst_bucket, True, None, dst_rest_only
432438
)
433439
db.insert_object(flask.request, dst_bucket_name, dst_object, None)
434440
dst_object.patch(flask.request, None)
@@ -439,6 +445,70 @@ def objects_copy(src_bucket_name, src_object_name, dst_bucket_name, dst_object_n
439445
return dst_object.rest_metadata()
440446

441447

448+
@gcs.route(
449+
"/b/<src_bucket_name>/o/<path:src_object_name>/rewriteTo/b/<dst_bucket_name>/o/<path:dst_object_name>",
450+
methods=["POST"],
451+
)
452+
def objects_rewrite(src_bucket_name, src_object_name, dst_bucket_name, dst_object_name):
453+
db.insert_test_bucket(None)
454+
token, rewrite = flask.request.args.get("rewriteToken"), None
455+
src_object = None
456+
if token is None:
457+
rewrite = gcs_type.holder.DataHolder.init_rewrite_rest(
458+
flask.request,
459+
src_bucket_name,
460+
src_object_name,
461+
dst_bucket_name,
462+
dst_object_name,
463+
)
464+
db.insert_rewrite(rewrite)
465+
else:
466+
rewrite = db.get_rewrite(token, None)
467+
src_object = db.get_object(
468+
rewrite.request, src_bucket_name, src_object_name, True, None
469+
)
470+
total_bytes_rewritten = len(rewrite.media)
471+
total_bytes_rewritten += min(
472+
rewrite.max_bytes_rewritten_per_call, len(src_object.media) - len(rewrite.media)
473+
)
474+
rewrite.media += src_object.media[len(rewrite.media) : total_bytes_rewritten]
475+
done, dst_object = total_bytes_rewritten == len(src_object.media), None
476+
response = {
477+
"kind": "storage#rewriteResponse",
478+
"totalBytesRewritten": len(rewrite.media),
479+
"objectSize": len(src_object.media),
480+
"done": done,
481+
}
482+
if done:
483+
dst_bucket = db.get_bucket_without_generation(dst_bucket_name, None).metadata
484+
dst_metadata = resources_pb2.Object()
485+
dst_metadata.CopyFrom(src_object.metadata)
486+
dst_rest_only = dict(src_object.rest_only)
487+
dst_metadata.bucket = dst_bucket_name
488+
dst_metadata.name = dst_object_name
489+
dst_media = rewrite.media
490+
dst_object, _ = gcs_type.object.Object.init(
491+
flask.request,
492+
dst_metadata,
493+
dst_media,
494+
dst_bucket,
495+
True,
496+
None,
497+
dst_rest_only,
498+
)
499+
db.insert_object(flask.request, dst_bucket_name, dst_object, None)
500+
dst_object.patch(rewrite.request, None)
501+
dst_object.metadata.metageneration = 1
502+
dst_object.metadata.updated.FromDatetime(
503+
dst_object.metadata.time_created.ToDatetime()
504+
)
505+
resources = dst_object.rest_metadata()
506+
response["resource"] = resources
507+
else:
508+
response["rewriteToken"] = rewrite.token
509+
return response
510+
511+
442512
# === OBJECT ACCESS CONTROL === #
443513

444514

@@ -564,10 +634,11 @@ def resumable_upload_chunk(bucket_name):
564634
utils.error.missing("upload_id in resumable_upload_chunk", None)
565635
upload = db.get_upload(upload_id, None)
566636
if upload.complete:
567-
return gcs_type.object.Object.rest(upload.metadata)
637+
return gcs_type.object.Object.rest(upload.metadata, upload.rest_only)
568638
upload.transfer.add(request.environ.get("HTTP_TRANSFER_ENCODING", ""))
569639
content_length = int(request.headers.get("content-length", 0))
570-
if content_length != len(request.data):
640+
data = utils.common.extract_media(request)
641+
if content_length != len(data):
571642
utils.error.invalid("content-length header", None)
572643
content_range = request.headers.get("content-range")
573644
if content_range is not None:
@@ -576,7 +647,7 @@ def resumable_upload_chunk(bucket_name):
576647
utils.error.invalid("content-range header", None)
577648
if items[0] == "*":
578649
if upload.complete:
579-
return gcs_type.object.Object.rest(upload.metadata)
650+
return gcs_type.object.Object.rest(upload.metadata, upload.rest_only)
580651
if items[1] != "*" and int(items[1]) == len(upload.media):
581652
upload.complete = True
582653
blob, _ = gcs_type.object.Object.init(
@@ -618,16 +689,22 @@ def resumable_upload_chunk(bucket_name):
618689
None,
619690
rest_code=400,
620691
)
621-
upload.media += request.data
692+
upload.media += data
622693
upload.complete = total_object_size == len(upload.media) or (
623694
chunk_last_byte + 1 == total_object_size
624695
)
625696
else:
626-
upload.media += request.data
697+
upload.media += data
627698
upload.complete = True
628699
if upload.complete:
629700
blob, _ = gcs_type.object.Object.init(
630-
upload.request, upload.metadata, upload.media, upload.bucket, False, None
701+
upload.request,
702+
upload.metadata,
703+
upload.media,
704+
upload.bucket,
705+
False,
706+
None,
707+
upload.rest_only,
631708
)
632709
blob.metadata.metadata["x_testbench_transfer_encoding"] = ":".join(
633710
upload.transfer
@@ -672,6 +749,12 @@ def xml_get_object(bucket_name, object_name):
672749

673750
# === SERVER === #
674751

752+
# Define the WSGI application to handle HMAC key requests
753+
(PROJECTS_HANDLER_PATH, projects_app) = gcs_type.project.get_projects_app()
754+
755+
# Define the WSGI application to handle IAM requests
756+
(IAM_HANDLER_PATH, iam_app) = gcs_type.iam.get_iam_app()
757+
675758

676759
server = DispatcherMiddleware(
677760
root,
@@ -680,6 +763,8 @@ def xml_get_object(bucket_name, object_name):
680763
GCS_HANDLER_PATH: gcs,
681764
DOWNLOAD_HANDLER_PATH: download,
682765
UPLOAD_HANDLER_PATH: upload,
766+
PROJECTS_HANDLER_PATH: projects_app,
767+
IAM_HANDLER_PATH: iam_app,
683768
},
684769
)
685770

google/cloud/storage/emulator/gcs/__init__.py

Lines changed: 1 addition & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -12,4 +12,4 @@
1212
# See the License for the specific language governing permissions and
1313
# limitations under the License.
1414

15-
from gcs import bucket, object, holder
15+
from gcs import bucket, object, holder, project, iam

0 commit comments

Comments
 (0)