Skip to content

Commit 646ea0c

Browse files
authored
Initial support for signed URLs (#129)
1 parent 1956d44 commit 646ea0c

3 files changed

Lines changed: 85 additions & 6 deletions

File tree

gcp_storage_emulator/handlers/objects.py

Lines changed: 22 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -231,6 +231,28 @@ def _patch(storage, bucket_name, object_id, metadata):
231231
return None
232232

233233

234+
def xml_upload(request, response, storage, *args, **kwargs):
235+
content_type = request.get_header("Content-Type", "application/octet-stream")
236+
obj = _make_object_resource(
237+
request.base_url,
238+
request.params["bucket_name"],
239+
request.params["object_id"],
240+
content_type,
241+
str(len(request.data)),
242+
)
243+
try:
244+
obj = _checksums(request.data, obj)
245+
storage.create_file(
246+
request.params["bucket_name"],
247+
request.params["object_id"],
248+
request.data,
249+
obj,
250+
)
251+
252+
except NotFound:
253+
response.status = HTTPStatus.NOT_FOUND
254+
255+
234256
def insert(request, response, storage, *args, **kwargs):
235257
uploadType = request.query.get("uploadType")
236258

gcp_storage_emulator/server.py

Lines changed: 6 additions & 6 deletions
Original file line numberDiff line numberDiff line change
@@ -85,8 +85,11 @@ def _health_check(req, res, storage):
8585
# Internal API, not supported by the real GCS
8686
(r"^/$", {GET: _health_check}), # Health check endpoint
8787
(r"^/wipe$", {GET: _wipe_data}), # Wipe all data
88-
# Public file serving, same as object.download
89-
(r"^/(?P<bucket_name>[-.\w]+)/(?P<object_id>.*[^/]+)$", {GET: objects.download}),
88+
# Public file serving, same as object.download and signed URLs
89+
(
90+
r"^/(?P<bucket_name>[-.\w]+)/(?P<object_id>.*[^/]+)$",
91+
{GET: objects.download, PUT: objects.xml_upload},
92+
),
9093
)
9194

9295
BATCH_HANDLERS = (
@@ -161,15 +164,12 @@ def _decode_raw_data(raw_data, request_handler):
161164

162165

163166
def _read_data(request_handler):
164-
if not request_handler.headers["Content-Type"]:
165-
return None
166-
167167
raw_data = _decode_raw_data(_read_raw_data(request_handler), request_handler)
168168

169169
if not raw_data:
170170
return None
171171

172-
content_type = request_handler.headers["Content-Type"]
172+
content_type = request_handler.headers["Content-Type"] or "application/octet-stream"
173173

174174
if content_type.startswith("application/json"):
175175
return json.loads(raw_data)

tests/test_server.py

Lines changed: 57 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -7,11 +7,25 @@
77
import fs
88
import requests
99
from google.api_core.exceptions import BadRequest, Conflict, NotFound
10+
from google.auth.credentials import AnonymousCredentials, Signing
1011

1112
from gcp_storage_emulator.server import create_server
1213
from gcp_storage_emulator.settings import STORAGE_BASE, STORAGE_DIR
1314

1415

16+
class FakeSigningCredentials(Signing, AnonymousCredentials):
17+
def sign_bytes(self, message):
18+
return b"foobar"
19+
20+
@property
21+
def signer_email(self):
22+
return "foobar@example.tld"
23+
24+
@property
25+
def signer(self):
26+
pass
27+
28+
1529
def _get_storage_client(http):
1630
"""Gets a python storage client"""
1731
os.environ["STORAGE_EMULATOR_HOST"] = "http://localhost:9023"
@@ -780,6 +794,49 @@ def test_empty_blob(self):
780794
fetched_content = blob.download_as_bytes()
781795
self.assertEqual(fetched_content, b"")
782796

797+
def test_signed_url_download(self):
798+
content = b"The quick brown fox jumps over the lazy dog"
799+
bucket = self._client.create_bucket("testbucket")
800+
801+
blob = bucket.blob("signed-download")
802+
blob.upload_from_string(content)
803+
804+
url = blob.generate_signed_url(
805+
api_access_endpoint="http://localhost:9023",
806+
credentials=FakeSigningCredentials(),
807+
version="v4",
808+
expiration=datetime.timedelta(minutes=15),
809+
method="GET",
810+
)
811+
812+
response = requests.get(url)
813+
self.assertEqual(response.content, content)
814+
815+
def test_signed_url_upload(self):
816+
test_text = os.path.join(
817+
os.path.dirname(os.path.abspath(__file__)), "test_text.txt"
818+
)
819+
bucket = self._client.create_bucket("testbucket")
820+
821+
blob = bucket.blob("signed-upload")
822+
url = blob.generate_signed_url(
823+
api_access_endpoint="http://localhost:9023",
824+
credentials=FakeSigningCredentials(),
825+
version="v4",
826+
expiration=datetime.timedelta(minutes=15),
827+
method="PUT",
828+
)
829+
830+
with open(test_text, "rb") as file:
831+
headers = {"Content-type": "text/plain"}
832+
response = requests.put(url, data=file, headers=headers)
833+
self.assertEqual(response.status_code, 200)
834+
835+
blob_content = blob.download_as_bytes()
836+
file.seek(0)
837+
self.assertEqual(blob_content, file.read())
838+
self.assertEqual(blob.content_type, "text/plain")
839+
783840

784841
class HttpEndpointsTest(ServerBaseCase):
785842
"""Tests for the HTTP endpoints defined by server.HANDLERS."""

0 commit comments

Comments
 (0)