diff --git a/config/Common.py b/config/Common.py index b6a63b3..d77f92f 100644 --- a/config/Common.py +++ b/config/Common.py @@ -23,6 +23,7 @@ settings = { "luminotes.https_proxy_ip": "127.0.0.2", "luminotes.db_host": "localhost", # hostname for PostgreSQL or None (no quotes) for SQLite "luminotes.db_ssl_mode": "allow", # "disallow", "allow", "prefer", or "require" + "luminotes.web_server": "", # "", "apache", or "nginx" to use specific server support (optional) "luminotes.support_email": "", "luminotes.payment_email": "", "luminotes.rate_plans": [ @@ -136,7 +137,6 @@ settings = { "encoding_filter.on": False, }, "/files/thumbnail": { - "stream_response": True, "encoding_filter.on": False, }, "/files/image": { diff --git a/controller/Expose.py b/controller/Expose.py index 487054a..a5996a7 100644 --- a/controller/Expose.py +++ b/controller/Expose.py @@ -66,8 +66,8 @@ def expose( view = None, rss = None ): cherrypy.root.report_traceback() result = dict( error = u"An error occurred when processing your request. Please try again or contact support." ) - # if the result is a generator, it's streaming data, so just let CherryPy handle it - if isinstance( result, types.GeneratorType ): + # if the result is a generator or a string, it's streaming data or just data, so just let CherryPy handle it + if isinstance( result, ( types.GeneratorType, basestring ) ): return result redirect = result.get( u"redirect" ) @@ -83,18 +83,17 @@ def expose( view = None, rss = None ): # try using the supplied view to render the result try: - if view_override is None: - if rss and use_rss: - cherrypy.response.headers[ u"Content-Type" ] = u"application/xml" - return render( rss, result, encoding or "utf8" ) - elif view: - return render( view, result, encoding ) - elif result.get( "view" ): - result_view = result.get( "view" ) - del( result[ "view" ] ) - return render( result_view, result, encoding ) - else: + if view_override is not None: return render( view_override, result, encoding ) + elif rss and use_rss: + cherrypy.response.headers[ u"Content-Type" ] = u"application/xml" + return render( rss, result, encoding or "utf8" ) + elif view: + return render( view, result, encoding ) + elif result.get( "view" ): + result_view = result.get( "view" ) + del( result[ "view" ] ) + return render( result_view, result, encoding ) except: if redirect is None: if original_error: diff --git a/controller/Files.py b/controller/Files.py index 2b51c6d..6a3f48c 100644 --- a/controller/Files.py +++ b/controller/Files.py @@ -244,7 +244,7 @@ class Files( object ): """ Controller for dealing with uploaded files, corresponding to the "/files" URL. """ - def __init__( self, database, users, download_products ): + def __init__( self, database, users, download_products, web_server ): """ Create a new Files object. @@ -254,12 +254,15 @@ class Files( object ): @param users: controller for all users @type download_products: [ { "name": unicode, ... } ] @param download_products: list of configured downloadable products + @type web_server: unicode + @param web_server: front-end web server (determines specific support for various features) @rtype: Files @return: newly constructed Files """ self.__database = database self.__users = users self.__download_products = download_products + self.__web_server = web_server @expose() @weakly_expire @@ -312,9 +315,14 @@ class Files( object ): cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % filename cherrypy.response.headerMap[ u"Content-Length" ] = db_file.size_bytes + if self.__web_server == u"nginx": + cherrypy.response.headerMap[ u"X-Accel-Redirect" ] = "/download/%s" % file_id + return "" + def stream(): CHUNK_SIZE = 8192 local_file = Upload_file.open_file( file_id ) + local_file.seek(0) while True: data = local_file.read( CHUNK_SIZE ) @@ -364,9 +372,14 @@ class Files( object ): cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % public_filename cherrypy.response.headerMap[ u"Content-Length" ] = os.path.getsize( local_filename ) + if self.__web_server == u"nginx": + cherrypy.response.headerMap[ u"X-Accel-Redirect" ] = "/download_product/%s" % product[ u"filename" ] + return "" + def stream(): CHUNK_SIZE = 8192 local_file = file( local_filename, "rb" ) + local_file.seek(0) while True: data = local_file.read( CHUNK_SIZE ) @@ -463,15 +476,7 @@ class Files( object ): image.save( image_buffer, "PNG" ) image_buffer.seek( 0 ) - def stream( image_buffer ): - CHUNK_SIZE = 8192 - - while True: - data = image_buffer.read( CHUNK_SIZE ) - if len( data ) == 0: break - yield data - - return stream( image_buffer ) + return image_buffer.getvalue() @expose() @weakly_expire @@ -501,9 +506,14 @@ class Files( object ): cherrypy.response.headerMap[ u"Content-Type" ] = db_file.content_type + if self.__web_server == u"nginx": + cherrypy.response.headerMap[ u"X-Accel-Redirect" ] = "/download/%s" % file_id + return "" + def stream(): CHUNK_SIZE = 8192 local_file = Upload_file.open_file( file_id ) + local_file.seek(0) while True: data = local_file.read( CHUNK_SIZE ) diff --git a/controller/Root.py b/controller/Root.py index a7dd410..d0db4a4 100644 --- a/controller/Root.py +++ b/controller/Root.py @@ -57,6 +57,7 @@ class Root( object ): database, self.__users, settings[ u"global" ].get( u"luminotes.download_products", [] ), + settings[ u"global" ].get( u"luminotes.web_server", "" ), ) self.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) ) self.__forums = Forums( database, self.__notebooks, self.__users ) diff --git a/controller/test/Test_files.py b/controller/test/Test_files.py index 4fd5b91..f3973c9 100644 --- a/controller/test/Test_files.py +++ b/controller/test/Test_files.py @@ -137,9 +137,14 @@ class Test_files( Test_controller ): os.remove( u"products/test.exe" ) - def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None ): + def test_download( self, filename = None, quote_filename = None, file_data = None, preview = None, expected_file_data = None ): self.login() + if expected_file_data is None: + expected_file_data = file_data + if file_data is None: + expected_file_data = self.file_data + self.http_upload( "/files/upload?file_id=%s" % self.file_id, dict( @@ -191,8 +196,17 @@ class Test_files( Test_controller ): if u"session_storage" not in str( exc ): raise exc - file_data = "".join( pieces ) - assert file_data == ( file_data or self.file_data ) + received_file_data = "".join( pieces ) + assert received_file_data == expected_file_data + + return result + + def test_download_with_nginx( self ): + cherrypy.root.files._Files__web_server = u"nginx" + result = self.test_download( self.filename, expected_file_data = "" ) + + headers = result[ u"headers" ] + assert headers[ u"X-Accel-Redirect" ] == u"/download/%s" % self.file_id def test_download_with_unicode_filename( self ): self.test_download( self.unicode_filename ) @@ -372,6 +386,44 @@ class Test_files( Test_controller ): file_data = "".join( pieces ) assert file_data == self.file_data + def test_download_product_with_nginx( self ): + cherrypy.root.files._Files__web_server = u"nginx" + access_id = u"wheeaccessid" + item_number = u"5000" + transaction_id = u"txn" + + self.login() + + download_access = Download_access.create( access_id, item_number, transaction_id ) + self.database.save( download_access ) + + result = self.http_get( + "/files/download_product?access_id=%s" % access_id, + session_id = self.session_id, + ) + + headers = result[ u"headers" ] + assert headers + assert headers[ u"Content-Type" ] == u"application/octet-stream" + + filename = u"test.exe".encode( "utf8" ) + assert headers[ u"Content-Disposition" ] == 'attachment; filename="%s"' % filename + assert headers[ u"X-Accel-Redirect" ] == u"/download_product/test.exe" + + gen = result[ u"body" ] + assert isinstance( gen, types.GeneratorType ) + pieces = [] + + try: + for piece in gen: + pieces.append( piece ) + except AttributeError, exc: + if u"session_storage" not in str( exc ): + raise exc + + file_data = "".join( pieces ) + assert file_data == u"" + def test_download_product_without_login( self ): access_id = u"wheeaccessid" item_number = u"5000" @@ -924,6 +976,35 @@ class Test_files( Test_controller ): assert "".join( result[ u"body" ] ) == self.IMAGE_DATA + def test_image_with_nginx( self ): + cherrypy.root.files._Files__web_server = u"nginx" + self.login() + + self.http_upload( + "/files/upload?file_id=%s" % self.file_id, + dict( + notebook_id = self.notebook.object_id, + note_id = self.note.object_id, + ), + filename = self.filename, + file_data = self.IMAGE_DATA, + content_type = self.content_type, + session_id = self.session_id, + ) + + result = self.http_get( + "/files/image?file_id=%s" % self.file_id, + session_id = self.session_id, + ) + + headers = result[ u"headers" ] + assert headers + assert headers[ u"Content-Type" ] == self.content_type + assert u"Content-Disposition" not in headers + assert headers[ u"X-Accel-Redirect" ] == u"/download/%s" % self.file_id + + assert "".join( result[ u"body" ] ) == u"" + def test_image_with_non_image( self ): self.login()