From c41f17d7b5da89fbf5a2613bca0595224f49eace Mon Sep 17 00:00:00 2001 From: richard-jones Date: Sat, 14 Jan 2012 12:08:07 +0000 Subject: [PATCH] finish protocol operation implementations for pylon (still no multipart support), plus more logging. Have also duplicated any useful logging changes from pylons into the webpy impl git-svn-id: http://sword-app.svn.sourceforge.net/svnroot/sword-app/sss/branches/sss-2@448 2bf6ea0f-123d-0410-b71a-f1a21eb24612 --- sss/pylons_sword_controller.py | 265 ++++++++++++++++++++++++++++++++- sss/webpy.py | 4 +- 2 files changed, 260 insertions(+), 9 deletions(-) diff --git a/sss/pylons_sword_controller.py b/sss/pylons_sword_controller.py index 2a8be7c..48fa846 100644 --- a/sss/pylons_sword_controller.py +++ b/sss/pylons_sword_controller.py @@ -61,7 +61,7 @@ def http_basic_authenticate(self): ssslog.error("unable to interpret authentication header: " + auth_header) raise SwordError(error_uri=Errors.bad_request, msg="unable to interpret authentication header") - ssslog.info("Authentication details: " + str(username) + ":" + str(password) + "; On Behalf Of: " + str(obo)) + ssslog.info("Authentication details: " + str(username) + ":[**password**]; On Behalf Of: " + str(obo)) authenticator = Authenticator(config) try: @@ -76,7 +76,7 @@ def http_basic_authenticate(self): def manage_error(self, sword_error): response.status_int = sword_error.status - ssslog.info("Returning error status: " + str(sword_error.status)) + ssslog.info("Returning error (" + str(sword_error.status) + ") - " + str(sword_error.error_uri)) if not sword_error.empty: response.content_type = "text/xml" return sword_error.error_document @@ -129,7 +129,10 @@ def validate_deposit_request(self, entry_section=None, binary_section=None, mult #if len(webin) != 2: # if it is not multipart # FIXME: this is reading everything in, and should be re-evaluated for performance/scalability wsgi_input = request.environ['wsgi.input'] - wsgi_input.seek(0, 0) + if hasattr(wsgi_input, "seek"): + # in empty requests, the wsgi input object doesn't have a seek() method + # so we have to check for it + wsgi_input.seek(0, 0) if wsgi_input is None or wsgi_input.read().strip() == "": # FIXME: this IS NOT safe to scale if allow_empty: @@ -182,6 +185,7 @@ def get_deposit(self, auth=None, atom_only=False): empty_request = False if d.content_length == 0: + ssslog.info("Received empty deposit request") empty_request = True if d.content_length > config.max_upload_size: raise SwordError(error_uri=Errors.max_upload_size_exceeded, @@ -259,6 +263,7 @@ def service_document(self, sub_path=None): if http_method == "GET": return self._GET_service_document(sub_path) else: + ssslog.info("Returning (405) Method Not Allowed; Received " + http_method + " request on " + __name__) abort(405, "Method Not Allowed") return @@ -269,6 +274,7 @@ def collection(self, path=None): elif http_method == "POST": return self._POST_collection(path) else: + ssslog.info("Returning (405) Method Not Allowed; Received " + http_method + " request on " + __name__) abort(405, "Method Not Allowed") return @@ -283,6 +289,7 @@ def media_resource(self, path=None): elif http_method == "DELETE": return self._DELETE_media_resource(path) else: + ssslog.info("Returning (405) Method Not Allowed; Received " + http_method + " request on " + __name__) abort(405, "Method Not Allowed") return @@ -297,10 +304,18 @@ def container(self, path=None): elif http_method == "DELETE": return self._DELETE_container(path) else: + ssslog.info("Returning (405) Method Not Allowed; Received " + http_method + " request on " + __name__) abort(405, "Method Not Allowed") return - def statement(self, path=None): pass + def statement(self, path=None): + http_method = request.environ['REQUEST_METHOD'] + if http_method == "GET": + return self._GET_statement(path) + else: + ssslog.info("Returning (405) Method Not Allowed; Received " + http_method + " request on " + __name__) + abort(405, "Method Not Allowed") + return def aggregation(self, path=None): pass def part(self, path=None): pass @@ -326,6 +341,7 @@ def _GET_service_document(self, path=None): ss = SwordServer(config, auth) sd = ss.service_document(path) response.content_type = "text/xml" + ssslog.info("Returning " + response.status + " from request on " + __name__) return sd def _GET_collection(self, path=None): @@ -347,6 +363,7 @@ def _GET_collection(self, path=None): ss = SwordServer(config, auth) cl = ss.list_collection(path) response.content_type = "text/xml" + ssslog.info("Returning " + response.status + " from request on " + __name__) return cl def _POST_collection(self, path=None): @@ -381,9 +398,11 @@ def _POST_collection(self, path=None): response.status = "201 Created" if config.return_deposit_receipt: ssslog.info("Returning deposit receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) return result.receipt else: ssslog.info("Omitting deposit receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) return except SwordError as e: @@ -407,8 +426,7 @@ def _GET_media_resource(self, path=None): # 406 Not Acceptable without looking first to see if there is even any media to content negotiate for # which would be weird from a client perspective if not ss.media_resource_exists(path): - abort(404) - return + return self.manage_error(SwordError(status=404, empty=True)) # get the content negotiation headers accept_header = request.environ.get("HTTP_ACCEPT") @@ -438,6 +456,7 @@ def _GET_media_resource(self, path=None): f = open(media_resource.filepath, "r") response.status_int = 200 response.status = "200 OK" + ssslog.info("Returning " + response.status + " from request on " + __name__) return f.read() def _PUT_media_resource(self, path=None): @@ -474,11 +493,92 @@ def _PUT_media_resource(self, path=None): ssslog.info("Content replaced") response.status_int = 204 response.status = "204 No Content" # notice that this is different from the POST as per AtomPub + ssslog.info("Returning " + response.status + " from request on " + __name__) return except SwordError as e: return self.manage_error(e) + def _POST_media_resource(self, path=None): + """ + POST a simple package into the specified media resource + Args: + - id: The ID of the media resource as specified in the requested URL + Returns a Deposit Receipt + """ + ssslog.debug("POST to Media Resource (add new file); Incoming HTTP headers: " + str(request.environ)) + + # find out if update is allowed + if not config.allow_update: + error = SwordError(error_uri=Errors.method_not_allowed, msg="Update operations not currently permitted") + return self.manage_error(error) + + # authenticate + try: + auth = self.http_basic_authenticate() + + # check the validity of the request + self.validate_deposit_request(None, "6.7.1", None, allow_multipart=False) + + deposit = self.get_deposit(auth) + + # if we get here authentication was successful and we carry on + ss = SwordServer(config, auth) + result = ss.add_content(path, deposit) + + response.content_type = "application/atom+xml;type=entry" + response.headers["Location"] = result.location + response.status_int = 201 + response.status = "201 Created" + if config.return_deposit_receipt: + ssslog.info("Returning Receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) + return result.receipt + else: + ssslog.info("Omitting Receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) + return + + except SwordError as e: + return self.manage_error(e) + + def _DELETE_media_resource(self, path=None): + """ + DELETE the contents of an object in the store (but not the object's container), leaving behind an empty + container for further use + Args: + - id: the ID of the object to have its content removed as per the requested URI + Return a Deposit Receipt + """ + ssslog.debug("DELETE on Media Resource (remove content, leave container); Incoming HTTP headers: " + str(request.environ)) + + # find out if delete is allowed + if not config.allow_delete: + error = SwordError(error_uri=Errors.method_not_allowed, msg="Delete operations not currently permitted") + return self.manage_error(error) + + # authenticate + try: + auth = self.http_basic_authenticate() + + # check the validity of the request + self.validate_delete_request("6.6") + + # parse the delete request out of the HTTP request + delete = self.get_delete(auth) + + # carry out the delete + ss = SwordServer(config, auth) + result = ss.delete_content(path, delete) + + # just return, no need to give any more feedback + response.status_int = 204 + response.status = "204 No Content" # No Content + ssslog.info("Returning " + response.status + " from request on " + __name__) + return + + except SwordError as e: + return self.manage_error(e) def _GET_container(self, path=None): """ @@ -501,8 +601,7 @@ def _GET_container(self, path=None): # 415 Unsupported Media Type without looking first to see if there is even any media to content negotiate for # which would be weird from a client perspective if not ss.container_exists(path): - abort(404) - return + return self.manage_error(SwordError(status=404, empty=True)) # get the content negotiation headers accept_header = request.environ.get("HTTP_ACCEPT") @@ -520,8 +619,158 @@ def _GET_container(self, path=None): # now actually get hold of the representation of the container and send it to the client cont = ss.get_container(path, accept_parameters) + ssslog.info("Returning " + response.status + " from request on " + __name__) return cont except SwordError as e: return self.manage_error(e) + + def _PUT_container(self, path=None): + """ + PUT a new Entry over the existing entry, or a multipart request over + both the existing metadata and the existing content + """ + ssslog.debug("PUT on Container (replace); Incoming HTTP headers: " + str(request.environ)) + + # find out if update is allowed + if not config.allow_update: + error = SwordError(error_uri=Errors.method_not_allowed, msg="Update operations not currently permitted") + return self.manage_error(error) + + try: + # authenticate + auth = self.http_basic_authenticate() + + # check the validity of the request + self.validate_deposit_request("6.5.2", None, "6.5.3") + + # get the deposit object + deposit = self.get_deposit(auth) + + ss = SwordServer(config, auth) + result = ss.replace(path, deposit) + + response.headers["Location"] = result.location + if config.return_deposit_receipt: + response.content_type = "application/atom+xml;type=entry" + response.status_int = 200 + response.status = "200 OK" + ssslog.info("Returning Deposit Receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) + return result.receipt + else: + response.status_int = 204 + response.status = "204 No Content" + ssslog.info("Omitting Deposit Receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) + return + + except SwordError as e: + return self.manage_error(e) + + def _POST_container(self, path=None): + """ + POST some new content into the container identified by the supplied id, + or complete an existing deposit (using the In-Progress header) + Args: + - id: The ID of the container as contained in the URL + Returns a Deposit Receipt + """ + ssslog.debug("POST to Container (add new content and metadata); Incoming HTTP headers: " + str(request.environ)) + + # find out if update is allowed + if not config.allow_update: + error = SwordError(error_uri=Errors.method_not_allowed, msg="Update operations not currently permitted") + return self.manage_error(error) + + try: + # authenticate + auth = self.http_basic_authenticate() + + # check the validity of the request + self.validate_deposit_request("6.7.2", None, "6.7.3", "9.3", allow_empty=True) + + deposit = self.get_deposit(auth) + + ss = SwordServer(config, auth) + result = ss.deposit_existing(path, deposit) + + # NOTE: spec says 201 Created for multipart and 200 Ok for metadata only + # we have implemented 200 OK across the board, in the understanding that + # in this case the spec is incorrect (correction need to be implemented + # asap) + + response.headers["Location"] = result.location + response.status_int = 200 + response.status = "200 OK" + if config.return_deposit_receipt: + response.content_type = "application/atom+xml;type=entry" + ssslog.info("Returning Deposit Receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) + return result.receipt + else: + ssslog.info("Omitting Deposit Receipt") + ssslog.info("Returning " + response.status + " from request on " + __name__) + return + + except SwordError as e: + return self.manage_error(e) + + def _DELETE_container(self, path=None): + """ + DELETE the container (and everything in it) from the store, as identified by the supplied id + Args: + - id: the ID of the container + Returns nothing, as there is nothing to return (204 No Content) + """ + ssslog.debug("DELETE on Container (remove); Incoming HTTP headers: " + str(request.environ)) + + try: + # find out if update is allowed + if not config.allow_delete: + raise SwordError(error_uri=Errors.method_not_allowed, msg="Delete operations not currently permitted") + + # authenticate + auth = self.http_basic_authenticate() + + # check the validity of the request + self.validate_delete_request("6.8") + + # get the delete request + delete = self.get_delete(auth) + + # do the delete + ss = SwordServer(config, auth) + result = ss.delete_container(path, delete) + + # no need to return any content + response.status_int = 204 + response.status = "204 No Content" + ssslog.info("Returning " + response.status + " from request on " + __name__) + return + + except SwordError as e: + return self.manage_error(e) + def _GET_statement(self, path=None): + ssslog.debug("GET on Statement (retrieve); Incoming HTTP headers: " + str(request.environ)) + + try: + # authenticate + auth = self.http_basic_authenticate() + + ss = SwordServer(config, auth) + + # first thing we need to do is check that there is an object to return, because otherwise we may throw a + # 415 Unsupported Media Type without looking first to see if there is even any media to content negotiate for + # which would be weird from a client perspective + if not ss.container_exists(path): + raise SwordError(status=404, empty=True) + + # now actually get hold of the representation of the statement and send it to the client + cont = ss.get_statement(path) + ssslog.info("Returning " + response.status + " from request on " + __name__) + return cont + + except SwordError as e: + return self.manage_error(e) diff --git a/sss/webpy.py b/sss/webpy.py index 01ecedb..5e26ef4 100644 --- a/sss/webpy.py +++ b/sss/webpy.py @@ -94,7 +94,7 @@ def http_basic_authenticate(self, web): ssslog.error("unable to interpret authentication header: " + auth_header) raise SwordError(error_uri=Errors.bad_request, msg="unable to interpret authentication header") - ssslog.info("Authentication details: " + str(username) + ":" + str(password) + "; On Behalf Of: " + str(obo)) + ssslog.info("Authentication details: " + str(username) + ":[**password**]; On Behalf Of: " + str(obo)) authenticator = Authenticator(config) try: @@ -109,6 +109,7 @@ def http_basic_authenticate(self, web): def manage_error(self, sword_error): status = STATUS_MAP.get(sword_error.status, "400 Bad Request") + ssslog.info("Returning error (" + str(sword_error.status) + ") - " + str(sword_error.error_uri)) web.ctx.status = status if not sword_error.empty: web.header("Content-Type", "text/xml") @@ -204,6 +205,7 @@ def get_deposit(self, web, auth=None, atom_only=False): empty_request = False if d.content_length == 0: + ssslog.info("Received empty deposit request") empty_request = True if d.content_length > config.max_upload_size: raise SwordError(error_uri=Errors.max_upload_size_exceeded,