diff --git a/src/gcp_storage_emulator/handlers/objects.py b/src/gcp_storage_emulator/handlers/objects.py index bbd523a..2271981 100644 --- a/src/gcp_storage_emulator/handlers/objects.py +++ b/src/gcp_storage_emulator/handlers/objects.py @@ -537,3 +537,13 @@ def batch(request, response, storage, *args, **kwargs): response.write("\r\n\r\n") response.write("--{}--".format(boundary)) + + +def options(request, response, storage, *args, **kwargs): + response.write("HTTP/1.1 200 OK\r\n") + response.write("Content-Type: text/html; charset=UTF-8\r\n") + response.write("allow: OPTIONS,GET,POST,PUT,DELETE,PATCH\r\n") + response.write("access-control-allow-origin: *\r\n") + response.write( + "access-control-allow-methods: GET,POST,PUT,PATCH,DELETE,OPTIONS\r\n" + ) diff --git a/src/gcp_storage_emulator/server.py b/src/gcp_storage_emulator/server.py index 428ad24..0942dde 100644 --- a/src/gcp_storage_emulator/server.py +++ b/src/gcp_storage_emulator/server.py @@ -21,6 +21,7 @@ PUT = "PUT" DELETE = "DELETE" PATCH = "PATCH" +OPTIONS = "OPTIONS" def _wipe_data(req, res, storage): @@ -39,56 +40,64 @@ def _health_check(req, res, storage): HANDLERS = ( - (r"^{}/b$".format(settings.API_ENDPOINT), {GET: buckets.ls, POST: buckets.insert}), + ( + r"^{}/b$".format(settings.API_ENDPOINT), + {GET: buckets.ls, POST: buckets.insert, OPTIONS: objects.options}, + ), ( r"^{}/b/(?P[-.\w]+)$".format(settings.API_ENDPOINT), - {GET: buckets.get, DELETE: buckets.delete}, + {GET: buckets.get, DELETE: buckets.delete, OPTIONS: objects.options}, ), ( r"^{}/b/(?P[-.\w]+)/o$".format(settings.API_ENDPOINT), - {GET: objects.ls}, + {GET: objects.ls, OPTIONS: objects.options}, ), ( r"^{}/b/(?P[-.\w]+)/o/(?P.*[^/]+)/copyTo/b/".format( settings.API_ENDPOINT ) + r"(?P[-.\w]+)/o/(?P.*[^/]+)$", - {POST: objects.copy}, + {POST: objects.copy, OPTIONS: objects.options}, ), ( r"^{}/b/(?P[-.\w]+)/o/(?P.*[^/]+)/compose$".format( settings.API_ENDPOINT ), - {POST: objects.compose}, + {POST: objects.compose, OPTIONS: objects.options}, ), ( r"^{}/b/(?P[-.\w]+)/o/(?P.*[^/]+)$".format( settings.API_ENDPOINT ), - {GET: objects.get, DELETE: objects.delete, PATCH: objects.patch}, + { + GET: objects.get, + DELETE: objects.delete, + PATCH: objects.patch, + OPTIONS: objects.options, + }, ), # Non-default API endpoints ( r"^{}/b/(?P[-.\w]+)/o$".format(settings.UPLOAD_API_ENDPOINT), - {POST: objects.insert, PUT: objects.upload_partial}, + {POST: objects.insert, PUT: objects.upload_partial, OPTIONS: objects.options}, ), ( r"^{}/b/(?P[-.\w]+)/o/(?P.*[^/]+)$".format( settings.DOWNLOAD_API_ENDPOINT ), - {GET: objects.download}, + {GET: objects.download, OPTIONS: objects.options}, ), ( r"^{}$".format(settings.BATCH_API_ENDPOINT), - {POST: objects.batch}, + {POST: objects.batch, OPTIONS: objects.options}, ), # Internal API, not supported by the real GCS - (r"^/$", {GET: _health_check}), # Health check endpoint - (r"^/wipe$", {GET: _wipe_data}), # Wipe all data + (r"^/$", {GET: _health_check, OPTIONS: objects.options}), # Health check endpoint + (r"^/wipe$", {GET: _wipe_data, OPTIONS: objects.options}), # Wipe all data # Public file serving, same as object.download and signed URLs ( r"^/(?P[-.\w]+)/(?P.*[^/]+)$", - {GET: objects.download, PUT: objects.xml_upload}, + {GET: objects.download, PUT: objects.xml_upload, OPTIONS: objects.options}, ), ) @@ -318,9 +327,12 @@ def handle(self, method): request = Request(self._request_handler, method) response = Response(self._request_handler) + response["Access-Control-Allow-Origin"] = "*" + for regex, handlers in HANDLERS: pattern = re.compile(regex) match = pattern.fullmatch(request.path) + if match: request.set_match(match) handler = handlers.get(method) @@ -341,7 +353,7 @@ def handle(self, method): "Method not implemented: {} - {}".format(request.method, request.path) ) response.status = HTTPStatus.NOT_IMPLEMENTED - + response.close() @@ -370,6 +382,10 @@ def do_PATCH(self): router = Router(self) router.handle(PATCH) + def do_OPTIONS(self): + router = Router(self) + router.handle(OPTIONS) + def log_message(self, format, *args): logger.info(format % args) diff --git a/tests/test_main.py b/tests/test_main.py index 7a22a8d..1947233 100644 --- a/tests/test_main.py +++ b/tests/test_main.py @@ -76,3 +76,12 @@ def test_path_does_not_exist(self): response = requests.get(url) self.assertEqual(response.status_code, 501) self.assertEqual(response.content, "".encode("utf-8")) + + def test_options(self): + url = self._url("/") + response = requests.options(url) + self.assertEqual(response.status_code, 200) + self.assertTrue( + "GET,POST,PUT,PATCH,DELETE,OPTIONS" + in response.headers["access-control-allow-methods"] + )