|
[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.
|