[sword-app-changelog] SF.net SVN: sword-app:[444]
sss/branches/sss-2/sss/pylons_sword_controller. py
From: SVN c. m. f. t. SWORD-A. p. <swo...@li...> - 2012-01-13 09:17:21
|
Revision: 444 http://sword-app.svn.sourceforge.net/sword-app/?rev=444&view=rev Author: richard-jones Date: 2012-01-13 09:17:11 +0000 (Fri, 13 Jan 2012) Log Message: ----------- add list collection content request support, and prove concept of request routing inside pylons controller Modified Paths: -------------- sss/branches/sss-2/sss/pylons_sword_controller.py Modified: sss/branches/sss-2/sss/pylons_sword_controller.py =================================================================== --- sss/branches/sss-2/sss/pylons_sword_controller.py 2012-01-12 22:31:35 UTC (rev 443) +++ sss/branches/sss-2/sss/pylons_sword_controller.py 2012-01-13 09:17:11 UTC (rev 444) @@ -80,12 +80,195 @@ if not sword_error.empty: response.content_type = "text/xml" return sword_error.error_document - return "" + return + def _map_webpy_headers(self, headers): + return dict([(c[0][5:].replace("_", "-") if c[0].startswith("HTTP_") else c[0].replace("_", "-"), c[1]) for c in headers.items()]) + + def validate_delete_request(self, section): + h = HttpHeaders() + + # map the headers to standard http + mapped_headers = self._map_webpy_headers(request.environ) + ssslog.debug("Validating on header dictionary: " + str(mapped_headers)) + + try: + # now validate the http headers + h.validate(mapped_headers, section) + except ValidationError as e: + raise SwordError(error_uri=Errors.bad_request, msg=e.message) + + def validate_deposit_request(self, entry_section=None, binary_section=None, multipart_section=None, empty_section=None, allow_multipart=True, allow_empty=False): + h = HttpHeaders() + + # map the headers to standard http + mapped_headers = self._map_webpy_headers(request.environ) + ssslog.debug("Validating on header dictionary: " + str(mapped_headers)) + + # run the validation + try: + # there must be both an "atom" and "payload" input or data in web.data() + webin = request.POST + if len(webin) != 2 and len(webin) > 0: + raise ValidationException("Multipart request does not contain exactly 2 parts") + if len(webin) >= 2 and not webin.has_key("atom") and not webin.has_key("payload"): + raise ValidationException("Multipart request must contain Content-Dispositions with names 'atom' and 'payload'") + if len(webin) > 0 and not allow_multipart: + raise ValidationException("Multipart request not permitted in this context") + + # if we get to here then we have a valid multipart or no multipart + is_multipart = False + is_empty = False + if len(webin) != 2: # if it is not multipart + # FIXME: this is reading everything in, and should be re-evaluated for performance/scalability + data = request.environ['wsgi.input'].read(int(request.environ['CONTENT_LENGTH'])) + + if data is None or data.strip() == "": # FIXME: this does not look safe to scale + if allow_empty: + ssslog.info("Validating an empty deposit (could be a control operation)") + is_empty = True + else: + raise ValidationException("No content sent to the server") + else: + ssslog.info("Validating a multipart deposit") + is_multipart = True + + is_entry = False + content_type = mapped_headers.get("CONTENT-TYPE") + if content_type is not None and content_type.startswith("application/atom+xml"): + ssslog.info("Validating a atom-only deposit") + is_entry = True + + if not is_entry and not is_multipart and not is_empty: + ssslog.info("Validating a binary deposit") + + section = entry_section if is_entry else multipart_section if is_multipart else empty_section if is_empty else binary_section + + # now validate the http headers + h.validate(mapped_headers, section) + + except ValidationException as e: + raise SwordError(error_uri=Errors.bad_request, msg=e.message) + + def get_deposit(self, auth=None, atom_only=False): + # FIXME: this reads files into memory, and therefore does not scale + # FIXME: this does not deal with the Media Part headers on a multipart deposit + """ + Take a web.py web object and extract from it the parameters and content required for a SWORD deposit. This + includes determining whether this is an Atom Multipart request or not, and extracting the atom/payload where + appropriate. It also includes extracting the HTTP headers which are relevant to deposit, and for those not + supplied providing their defaults in the returned DepositRequest object + """ + d = DepositRequest() + + # map the webpy headers to something more standard + mapped_headers = self._map_webpy_headers(request.environ) + + # get the headers that have been provided. Any headers which have not been provided will + # will have default values applied + h = HttpHeaders() + d.set_from_headers(h.get_sword_headers(mapped_headers)) + + if d.content_type.startswith("application/atom+xml"): + atom_only=True + + empty_request = False + if d.content_length == 0: + empty_request = True + if d.content_length > config.max_upload_size: + raise SwordError(error_uri=Errors.max_upload_size_exceeded, + msg="Max upload size is " + config.max_upload_size + + "; incoming content length was " + str(cl)) + + # find out if this is a multipart or not + is_multipart = False + + # FIXME: these headers aren't populated yet, because the webpy api doesn't + # appear to have a mechanism to retrieve them. urgh. + entry_part_headers = {} + media_part_headers = {} + webin = request.POST + if len(webin) == 2: + ssslog.info("Received multipart deposit request") + d.atom = webin['atom'] + # FIXME: this reads the payload into memory, we need to sort that out + # read the zip file from the base64 encoded string + d.content = base64.decodestring(webin['payload']) + is_multipart = True + elif not empty_request: + # if this wasn't a multipart, and isn't an empty request, then the data is in web.data(). This could be a binary deposit or + # an atom entry deposit - reply on the passed/determined argument to determine which + if atom_only: + ssslog.info("Received Entry deposit request") + # FIXME: this is reading everything in, and should be re-evaluated for performance/scalability + data = request.environ['wsgi.input'].read(int(request.environ['CONTENT_LENGTH'])) + d.atom = data + else: + ssslog.info("Received Binary deposit request") + # FIXME: this is reading everything in, and should be re-evaluated for performance/scalability + data = request.environ['wsgi.input'].read(int(request.environ['CONTENT_LENGTH'])) + d.content = data + + if is_multipart: + d.filename = h.extract_filename(media_part_headers) + else: + d.filename = h.extract_filename(mapped_headers) + + # now just attach the authentication data and return + d.auth = auth + return d + + def get_delete(self, web, auth=None): + """ + Take a web.py web object and extract from it the parameters and content required for a SWORD delete request. + It mainly extracts the HTTP headers which are relevant to delete, and for those not supplied provides thier + defaults in the returned DeleteRequest object + """ + d = DeleteRequest() + + # map the webpy headers to something more standard + mapped_headers = self._map_webpy_headers(web.ctx.environ) + + h = HttpHeaders() + d.set_from_headers(h.get_sword_headers(mapped_headers)) + + # now just attach the authentication data and return + d.auth = auth + return d + + # Request Routing Methods (used by URL Routing) + ############################################### + + def service_document(self, sub_path=None): + http_method = request.environ['REQUEST_METHOD'] + if http_method == "GET": + return self._GET_service_document(sub_path) + else: + abort(405, "Method Not Allowed") + return + + def collection(self, path=None): + http_method = request.environ['REQUEST_METHOD'] + if http_method == "GET": + return self._GET_collection(path) + elif http_method == "POST": + return self._POST_collection(path) + else: + abort(405, "Method Not Allowed") + return + + def media_resource(self, path=None): pass + def container(self, path=None): pass + def statement(self, path=None): pass + + def aggregation(self, path=None): pass + def part(self, path=None): pass + def webui(self, path=None): pass + # SWORD Protocol Operations ########################### - - def service_document(self, sub_path=None): + + def _GET_service_document(self, sub_path=None): """ GET the service document - returns an XML document - sub_path - the path provided for the sub-service document @@ -103,3 +286,29 @@ sd = ss.service_document(sub_path) response.content_type = "text/xml" return sd + + def _GET_collection(self, path=None): + """ + GET a representation of the collection in XML + Args: + - collection: The ID of the collection as specified in the requested URL + Returns an XML document with some metadata about the collection and the contents of that collection + """ + ssslog.debug("GET on Collection (list collection contents); Incoming HTTP headers: " + str(request.environ)) + + # authenticate + try: + auth = self.http_basic_authenticate() + except SwordError as e: + return self.manage_error(e) + + # if we get here authentication was successful and we carry on (we don't care who authenticated) + ss = SwordServer(config, auth) + cl = ss.list_collection(path) + response.content_type = "text/xml" + return cl + + def _POST_collection(self, path=None): + pass + + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
[sword-app-changelog] SF.net SVN: sword-app:[447]
sss/branches/sss-2/sss/pylons_sword_controller. py
From: SVN c. m. f. t. SWORD-A. p. <swo...@li...> - 2012-01-13 14:41:36
|
Revision: 447 http://sword-app.svn.sourceforge.net/sword-app/?rev=447&view=rev Author: richard-jones Date: 2012-01-13 14:41:25 +0000 (Fri, 13 Jan 2012) Log Message: ----------- add GET on container and PUT on media resource Modified Paths: -------------- sss/branches/sss-2/sss/pylons_sword_controller.py Modified: sss/branches/sss-2/sss/pylons_sword_controller.py =================================================================== --- sss/branches/sss-2/sss/pylons_sword_controller.py 2012-01-13 12:29:00 UTC (rev 446) +++ sss/branches/sss-2/sss/pylons_sword_controller.py 2012-01-13 14:41:25 UTC (rev 447) @@ -277,7 +277,7 @@ if http_method == "GET": return self._GET_media_resource(path) elif http_method == "PUT": - return self._PUT_media_resource + return self._PUT_media_resource(path) elif http_method == "POST": return self._POST_media_resource(path) elif http_method == "DELETE": @@ -286,7 +286,20 @@ abort(405, "Method Not Allowed") return - def container(self, path=None): pass + def container(self, path=None): + http_method = request.environ['REQUEST_METHOD'] + if http_method == "GET": + return self._GET_container(path) + elif http_method == "PUT": + return self._PUT_container(path) + elif http_method == "POST": + return self._POST_container(path) + elif http_method == "DELETE": + return self._DELETE_container(path) + else: + abort(405, "Method Not Allowed") + return + def statement(self, path=None): pass def aggregation(self, path=None): pass @@ -427,5 +440,88 @@ response.status = "200 OK" return f.read() + def _PUT_media_resource(self, path=None): + """ + PUT a new package onto the object identified by the supplied id + Args: + - id: the ID of the media resource as specified in the URL + Returns a Deposit Receipt + """ + ssslog.debug("PUT on Media Resource (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) + + # authenticate + try: + auth = self.http_basic_authenticate() + + # check the validity of the request (note that multipart requests + # and atom-only are not permitted in this method) + self.validate_deposit_request(None, "6.5.1", None, allow_multipart=False) + + # get a deposit object. The PUT operation only supports a single binary deposit, not an Atom Multipart one + # so if the deposit object has an atom part we should return an error + deposit = self.get_deposit(auth) + + # now replace the content of the container + ss = SwordServer(config, auth) + result = ss.replace(path, deposit) + + # replaced + ssslog.info("Content replaced") + response.status_int = 204 + response.status = "204 No Content" # notice that this is different from the POST as per AtomPub + return + + except SwordError as e: + return self.manage_error(e) + def _GET_container(self, path=None): + """ + GET a representation of the container in the appropriate (content negotiated) format as identified by + the supplied id + Args: + - id: The ID of the container as supplied in the request URL + Returns a representation of the container: SSS will return either the Atom Entry identical to the one supplied + as a deposit receipt or the pure RDF/XML Statement depending on the Accept header + """ + ssslog.debug("GET on Container (retrieve deposit receipt or statement); Incoming HTTP headers: " + str(request.environ)) + + # authenticate + try: + 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): + abort(404) + return + + # get the content negotiation headers + accept_header = request.environ.get("HTTP_ACCEPT") + accept_packaging_header = request.environ.get("HTTP_ACCEPT_PACKAGING") + + # do the negotiation + default_accept_parameters, acceptable = config.get_container_formats() + cn = ContentNegotiator(default_accept_parameters, acceptable) + accept_parameters = cn.negotiate(accept=accept_header) + ssslog.info("Container requested in format: " + str(accept_parameters)) + + # did we successfully negotiate a content type? + if accept_parameters is None: + raise SwordError(error_uri=Error.content, status=415, empty=True) + + # now actually get hold of the representation of the container and send it to the client + cont = ss.get_container(path, accept_parameters) + return cont + + except SwordError as e: + return self.manage_error(e) + This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
[sword-app-changelog] SF.net SVN: sword-app:[467]
sss/branches/sss-2/sss/pylons_sword_controller. py
From: SVN c. m. f. t. SWORD-A. p. <swo...@li...> - 2012-02-21 16:50:25
|
Revision: 467 http://sword-app.svn.sourceforge.net/sword-app/?rev=467&view=rev Author: richard-jones Date: 2012-02-21 16:50:14 +0000 (Tue, 21 Feb 2012) Log Message: ----------- add minor logging improvements Modified Paths: -------------- sss/branches/sss-2/sss/pylons_sword_controller.py Modified: sss/branches/sss-2/sss/pylons_sword_controller.py =================================================================== --- sss/branches/sss-2/sss/pylons_sword_controller.py 2012-02-09 16:56:15 UTC (rev 466) +++ sss/branches/sss-2/sss/pylons_sword_controller.py 2012-02-21 16:50:14 UTC (rev 467) @@ -112,8 +112,10 @@ response.status_int = sword_error.status ssslog.info("Returning error (" + str(sword_error.status) + ") - " + str(sword_error.error_uri)) if not sword_error.empty: + ssslog.debug("Returning error document: " + sword_error.error_document) response.content_type = "text/xml" return sword_error.error_document + ssslog.debug("Returning empty body in error response") return def _map_webpy_headers(self, headers): This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
[sword-app-changelog] SF.net SVN: sword-app:[503]
sss/branches/sss-2/sss/pylons_sword_controller. py
From: SVN c. m. f. t. SWORD-A. p. <swo...@li...> - 2012-04-25 13:40:06
|
Revision: 503 http://sword-app.svn.sourceforge.net/sword-app/?rev=503&view=rev Author: richard-jones Date: 2012-04-25 13:39:56 +0000 (Wed, 25 Apr 2012) Log Message: ----------- replace uses of redirect_to with redirect Modified Paths: -------------- sss/branches/sss-2/sss/pylons_sword_controller.py Modified: sss/branches/sss-2/sss/pylons_sword_controller.py =================================================================== --- sss/branches/sss-2/sss/pylons_sword_controller.py 2012-04-25 11:53:04 UTC (rev 502) +++ sss/branches/sss-2/sss/pylons_sword_controller.py 2012-04-25 13:39:56 UTC (rev 503) @@ -1,5 +1,5 @@ from pylons import request, response, session, tmpl_context as c -from pylons.controllers.util import abort, redirect_to +from pylons.controllers.util import abort, redirect from pylons.controllers import WSGIController from pylons.templating import render_mako as render @@ -664,7 +664,7 @@ # either send the client a redirect, or stream the content out if media_resource.redirect: - redirect_to(media_resource.url, _code=302) # FOUND (not SEE OTHER) + redirect(media_resource.url, _code=302) # FOUND (not SEE OTHER) return else: response.content_type = media_resource.content_type This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
[sword-app-changelog] SF.net SVN: sword-app:[509]
sss/branches/sss-2/sss/pylons_sword_controller. py
From: SVN c. m. f. t. SWORD-A. p. <swo...@li...> - 2012-05-01 15:51:59
|
Revision: 509 http://sword-app.svn.sourceforge.net/sword-app/?rev=509&view=rev Author: richard-jones Date: 2012-05-01 15:51:53 +0000 (Tue, 01 May 2012) Log Message: ----------- explicitly cast location headers to string for supporting mod_wsgi Modified Paths: -------------- sss/branches/sss-2/sss/pylons_sword_controller.py Modified: sss/branches/sss-2/sss/pylons_sword_controller.py =================================================================== --- sss/branches/sss-2/sss/pylons_sword_controller.py 2012-04-30 15:23:48 UTC (rev 508) +++ sss/branches/sss-2/sss/pylons_sword_controller.py 2012-05-01 15:51:53 UTC (rev 509) @@ -610,7 +610,7 @@ # created ssslog.info("Item created") response.content_type = "application/atom+xml;type=entry" - response.headers["Location"] = result.location + response.headers["Location"] = str(result.location) # explicit cast to string response.status_int = 201 response.status = "201 Created" if config.return_deposit_receipt: @@ -744,7 +744,7 @@ result = ss.add_content(path, deposit) response.content_type = "application/atom+xml;type=entry" - response.headers["Location"] = result.location + response.headers["Location"] = str(result.location) # explict cast to str response.status_int = 201 response.status = "201 Created" if config.return_deposit_receipt: @@ -869,7 +869,7 @@ ss = SwordServer(config, auth) result = ss.replace(path, deposit) - response.headers["Location"] = result.location + response.headers["Location"] = str(result.location) # explicit cast to str if config.return_deposit_receipt: response.content_type = "application/atom+xml;type=entry" response.status_int = 200 @@ -919,7 +919,7 @@ # in this case the spec is incorrect (correction need to be implemented # asap) - response.headers["Location"] = result.location + response.headers["Location"] = str(result.location) # explict cast to str response.status_int = 200 response.status = "200 OK" if config.return_deposit_receipt: This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |
[sword-app-changelog] SF.net SVN: sword-app:[510]
sss/branches/sss-2/sss/pylons_sword_controller. py
From: SVN c. m. f. t. SWORD-A. p. <swo...@li...> - 2012-05-01 16:18:33
|
Revision: 510 http://sword-app.svn.sourceforge.net/sword-app/?rev=510&view=rev Author: richard-jones Date: 2012-05-01 16:18:27 +0000 (Tue, 01 May 2012) Log Message: ----------- explicitly cast location headers to string for supporting mod_wsgi Modified Paths: -------------- sss/branches/sss-2/sss/pylons_sword_controller.py Modified: sss/branches/sss-2/sss/pylons_sword_controller.py =================================================================== --- sss/branches/sss-2/sss/pylons_sword_controller.py 2012-05-01 15:51:53 UTC (rev 509) +++ sss/branches/sss-2/sss/pylons_sword_controller.py 2012-05-01 16:18:27 UTC (rev 510) @@ -669,7 +669,7 @@ else: response.content_type = media_resource.content_type if media_resource.packaging is not None: - response.headers["Packaging"] = media_resource.packaging + response.headers["Packaging"] = str(media_resource.packaging) f = open(media_resource.filepath, "r") response.status_int = 200 response.status = "200 OK" @@ -838,7 +838,7 @@ cont = ss.get_container(path, accept_parameters) ssslog.info("Returning " + response.status + " from request on " + inspect.stack()[0][3]) if cont is not None: - response.headers["Content-Type"] = accept_parameters.content_type.mimetype() + response.headers["Content-Type"] = str(accept_parameters.content_type.mimetype()) return cont except SwordError as e: @@ -1007,7 +1007,7 @@ edit_uri = ss.get_edit_uri() response.status_int = 303 response.status = "303 See Other" - response.headers["Content-Location"] = edit_uri + response.headers["Content-Location"] = str(edit_uri) return def _GET_webui(self, path=None): This was sent by the SourceForge.net collaborative development platform, the world's largest Open Source development site. |