New support for nginx's X-Accel-Redirect for downloading files. Also made Files.thumbnail() non-streaming.
This commit is contained in:
parent
503625bdea
commit
3013beddbd
|
@ -23,6 +23,7 @@ settings = {
|
||||||
"luminotes.https_proxy_ip": "127.0.0.2",
|
"luminotes.https_proxy_ip": "127.0.0.2",
|
||||||
"luminotes.db_host": "localhost", # hostname for PostgreSQL or None (no quotes) for SQLite
|
"luminotes.db_host": "localhost", # hostname for PostgreSQL or None (no quotes) for SQLite
|
||||||
"luminotes.db_ssl_mode": "allow", # "disallow", "allow", "prefer", or "require"
|
"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.support_email": "",
|
||||||
"luminotes.payment_email": "",
|
"luminotes.payment_email": "",
|
||||||
"luminotes.rate_plans": [
|
"luminotes.rate_plans": [
|
||||||
|
@ -136,7 +137,6 @@ settings = {
|
||||||
"encoding_filter.on": False,
|
"encoding_filter.on": False,
|
||||||
},
|
},
|
||||||
"/files/thumbnail": {
|
"/files/thumbnail": {
|
||||||
"stream_response": True,
|
|
||||||
"encoding_filter.on": False,
|
"encoding_filter.on": False,
|
||||||
},
|
},
|
||||||
"/files/image": {
|
"/files/image": {
|
||||||
|
|
|
@ -66,8 +66,8 @@ def expose( view = None, rss = None ):
|
||||||
cherrypy.root.report_traceback()
|
cherrypy.root.report_traceback()
|
||||||
result = dict( error = u"An error occurred when processing your request. Please try again or contact support." )
|
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 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 ):
|
if isinstance( result, ( types.GeneratorType, basestring ) ):
|
||||||
return result
|
return result
|
||||||
|
|
||||||
redirect = result.get( u"redirect" )
|
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 using the supplied view to render the result
|
||||||
try:
|
try:
|
||||||
if view_override is None:
|
if view_override is not 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:
|
|
||||||
return render( view_override, result, encoding )
|
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:
|
except:
|
||||||
if redirect is None:
|
if redirect is None:
|
||||||
if original_error:
|
if original_error:
|
||||||
|
|
|
@ -244,7 +244,7 @@ class Files( object ):
|
||||||
"""
|
"""
|
||||||
Controller for dealing with uploaded files, corresponding to the "/files" URL.
|
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.
|
Create a new Files object.
|
||||||
|
|
||||||
|
@ -254,12 +254,15 @@ class Files( object ):
|
||||||
@param users: controller for all users
|
@param users: controller for all users
|
||||||
@type download_products: [ { "name": unicode, ... } ]
|
@type download_products: [ { "name": unicode, ... } ]
|
||||||
@param download_products: list of configured downloadable products
|
@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
|
@rtype: Files
|
||||||
@return: newly constructed Files
|
@return: newly constructed Files
|
||||||
"""
|
"""
|
||||||
self.__database = database
|
self.__database = database
|
||||||
self.__users = users
|
self.__users = users
|
||||||
self.__download_products = download_products
|
self.__download_products = download_products
|
||||||
|
self.__web_server = web_server
|
||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
@weakly_expire
|
@weakly_expire
|
||||||
|
@ -312,9 +315,14 @@ class Files( object ):
|
||||||
cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % filename
|
cherrypy.response.headerMap[ u"Content-Disposition" ] = 'attachment; filename="%s"' % filename
|
||||||
cherrypy.response.headerMap[ u"Content-Length" ] = db_file.size_bytes
|
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():
|
def stream():
|
||||||
CHUNK_SIZE = 8192
|
CHUNK_SIZE = 8192
|
||||||
local_file = Upload_file.open_file( file_id )
|
local_file = Upload_file.open_file( file_id )
|
||||||
|
local_file.seek(0)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = local_file.read( CHUNK_SIZE )
|
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-Disposition" ] = 'attachment; filename="%s"' % public_filename
|
||||||
cherrypy.response.headerMap[ u"Content-Length" ] = os.path.getsize( local_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():
|
def stream():
|
||||||
CHUNK_SIZE = 8192
|
CHUNK_SIZE = 8192
|
||||||
local_file = file( local_filename, "rb" )
|
local_file = file( local_filename, "rb" )
|
||||||
|
local_file.seek(0)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = local_file.read( CHUNK_SIZE )
|
data = local_file.read( CHUNK_SIZE )
|
||||||
|
@ -463,15 +476,7 @@ class Files( object ):
|
||||||
image.save( image_buffer, "PNG" )
|
image.save( image_buffer, "PNG" )
|
||||||
image_buffer.seek( 0 )
|
image_buffer.seek( 0 )
|
||||||
|
|
||||||
def stream( image_buffer ):
|
return image_buffer.getvalue()
|
||||||
CHUNK_SIZE = 8192
|
|
||||||
|
|
||||||
while True:
|
|
||||||
data = image_buffer.read( CHUNK_SIZE )
|
|
||||||
if len( data ) == 0: break
|
|
||||||
yield data
|
|
||||||
|
|
||||||
return stream( image_buffer )
|
|
||||||
|
|
||||||
@expose()
|
@expose()
|
||||||
@weakly_expire
|
@weakly_expire
|
||||||
|
@ -501,9 +506,14 @@ class Files( object ):
|
||||||
|
|
||||||
cherrypy.response.headerMap[ u"Content-Type" ] = db_file.content_type
|
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():
|
def stream():
|
||||||
CHUNK_SIZE = 8192
|
CHUNK_SIZE = 8192
|
||||||
local_file = Upload_file.open_file( file_id )
|
local_file = Upload_file.open_file( file_id )
|
||||||
|
local_file.seek(0)
|
||||||
|
|
||||||
while True:
|
while True:
|
||||||
data = local_file.read( CHUNK_SIZE )
|
data = local_file.read( CHUNK_SIZE )
|
||||||
|
|
|
@ -57,6 +57,7 @@ class Root( object ):
|
||||||
database,
|
database,
|
||||||
self.__users,
|
self.__users,
|
||||||
settings[ u"global" ].get( u"luminotes.download_products", [] ),
|
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.__notebooks = Notebooks( database, self.__users, self.__files, settings[ u"global" ].get( u"luminotes.https_url", u"" ) )
|
||||||
self.__forums = Forums( database, self.__notebooks, self.__users )
|
self.__forums = Forums( database, self.__notebooks, self.__users )
|
||||||
|
|
|
@ -137,9 +137,14 @@ class Test_files( Test_controller ):
|
||||||
|
|
||||||
os.remove( u"products/test.exe" )
|
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()
|
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(
|
self.http_upload(
|
||||||
"/files/upload?file_id=%s" % self.file_id,
|
"/files/upload?file_id=%s" % self.file_id,
|
||||||
dict(
|
dict(
|
||||||
|
@ -191,8 +196,17 @@ class Test_files( Test_controller ):
|
||||||
if u"session_storage" not in str( exc ):
|
if u"session_storage" not in str( exc ):
|
||||||
raise exc
|
raise exc
|
||||||
|
|
||||||
file_data = "".join( pieces )
|
received_file_data = "".join( pieces )
|
||||||
assert file_data == ( file_data or self.file_data )
|
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 ):
|
def test_download_with_unicode_filename( self ):
|
||||||
self.test_download( self.unicode_filename )
|
self.test_download( self.unicode_filename )
|
||||||
|
@ -372,6 +386,44 @@ class Test_files( Test_controller ):
|
||||||
file_data = "".join( pieces )
|
file_data = "".join( pieces )
|
||||||
assert file_data == self.file_data
|
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 ):
|
def test_download_product_without_login( self ):
|
||||||
access_id = u"wheeaccessid"
|
access_id = u"wheeaccessid"
|
||||||
item_number = u"5000"
|
item_number = u"5000"
|
||||||
|
@ -924,6 +976,35 @@ class Test_files( Test_controller ):
|
||||||
|
|
||||||
assert "".join( result[ u"body" ] ) == self.IMAGE_DATA
|
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 ):
|
def test_image_with_non_image( self ):
|
||||||
self.login()
|
self.login()
|
||||||
|
|
||||||
|
|
Reference in New Issue