diff --git a/controller/Files.py b/controller/Files.py index 2924762..163ac8c 100644 --- a/controller/Files.py +++ b/controller/Files.py @@ -14,7 +14,7 @@ from model.User import User from view.Upload_page import Upload_page from view.Blank_page import Blank_page from view.Json import Json -from view.Progress_bar import stream_progress +from view.Progress_bar import stream_progress, stream_quota_error, stop_upload_script class Access_error( Exception ): @@ -126,14 +126,12 @@ class FieldStorage( cherrypy._cpcgifs.FieldStorage ): @type binary: NoneType @param binary: ignored - @type user_id: unicode or NoneType - @param user_id: id of current logged-in user (if any) @rtype: Upload_file @return: wrapped temporary file used to store the upload @raise Upload_error: the provided file_id value is invalid, or the filename or Content-Length is missing """ - def make_file( self, binary = None, user_id = None ): + def make_file( self, binary = None ): global current_uploads, current_uploads_lock cherrypy.server.max_request_body_size = 0 # remove CherryPy default file size limit of 100 MB @@ -256,7 +254,6 @@ class Files( object ): return stream() - @expose( view = Upload_page ) @strongly_expire @grab_user_id @@ -327,6 +324,12 @@ class Files( object ): if not uploaded_file: raise Upload_error() + current_uploads_lock.acquire() + try: + del( current_uploads[ file_id ] ) + finally: + current_uploads_lock.release() + if not self.__users.check_access( user_id, notebook_id, read_write = True ): uploaded_file.delete() raise Access_error() @@ -336,7 +339,18 @@ class Files( object ): # if we didn't receive all of the expected data, abort if uploaded_file.total_received_bytes < uploaded_file.content_length: uploaded_file.delete() - raise Upload_error( "The upload did not complete." ) + raise Upload_error( u"The file did not complete uploading." ) + + user = self.__database.load( User, user_id ) + if not user: + uploaded_file.delete() + raise Access_error() + + # if the uploaded file's size would put the user over quota, bail and inform the user + rate_plan = self.__users.rate_plan( user.rate_plan ) + if user.storage_bytes + uploaded_file.total_received_bytes > rate_plan.get( u"storage_quota_bytes", 0 ): + uploaded_file.delete() + return dict( script = stop_upload_script ) # record metadata on the upload in the database db_file = File.create( file_id, notebook_id, note_id, uploaded_file.filename, uploaded_file.file_received_bytes, content_type ) @@ -345,21 +359,17 @@ class Files( object ): self.__database.commit() uploaded_file.close() - current_uploads_lock.acquire() - try: - del( current_uploads[ file_id ] ) - finally: - current_uploads_lock.release() - return dict() @expose() @strongly_expire + @grab_user_id @validate( file_id = Valid_id(), filename = unicode, + user_id = Valid_id( none_okay = True ), ) - def progress( self, file_id, filename ): + def progress( self, file_id, filename, user_id = None ): """ Stream information on a file that is in the process of being uploaded. This method does not perform any access checks, but the only information streamed is a progress bar and upload @@ -369,6 +379,8 @@ class Files( object ): @param file_id: id of a currently uploading file @type filename: unicode @param filename: name of the file to report on + @type user_id: unicode or NoneType + @param user_id: id of current logged-in user (if any) @rtype: unicode @return: streaming HTML progress bar """ @@ -395,6 +407,19 @@ class Files( object ): fraction_reported = 1.0 break + # if the uploaded file's size would put the user over quota, bail and inform the user + if uploading_file: + SOFT_QUOTA_FACTOR = 1.05 # fudge factor since content_length isn't really the file's actual size + + user = self.__database.load( User, user_id ) + if not user: + raise Access_error() + + rate_plan = self.__users.rate_plan( user.rate_plan ) + + if user.storage_bytes + uploading_file.content_length > rate_plan.get( u"storage_quota_bytes", 0 ) * SOFT_QUOTA_FACTOR: + return stream_quota_error() + return stream_progress( uploading_file, filename, fraction_reported ) @expose( view = Json ) @@ -427,6 +452,8 @@ class Files( object ): raise Access_error() user = self.__database.load( User, user_id ) + if not user: + raise Access_error() return dict( filename = db_file.filename, @@ -434,5 +461,8 @@ class Files( object ): storage_bytes = user.storage_bytes, ) - def rename( file_id, filename ): + def delete( self, file_id ): + pass # TODO + + def rename( self, file_id, filename ): pass # TODO diff --git a/controller/Users.py b/controller/Users.py index ee59c90..20d61be 100644 --- a/controller/Users.py +++ b/controller/Users.py @@ -1130,3 +1130,6 @@ class Users( object ): result[ "invites" ] = [] return result + + def rate_plan( self, plan_index ): + return self.__rate_plans[ plan_index ] diff --git a/static/js/Wiki.js b/static/js/Wiki.js index 8bb5337..a2a1138 100644 --- a/static/js/Wiki.js +++ b/static/js/Wiki.js @@ -141,7 +141,7 @@ Wiki.prototype.display_storage_usage = function( storage_bytes ) { this.display_message( "You are currently using " + usage_percent + - "% of your available storage space. Please delete some notes, empty the trash, or", + "% of your available storage space. Please delete some notes or files, empty the trash, or", [ createDOM( "a", { "href": "/upgrade" }, "upgrade" ), " your account." ] ); this.storage_usage_high = true; @@ -2352,19 +2352,28 @@ Upload_pulldown.prototype.update_position = function ( anchor, relative_to ) { Pulldown.prototype.update_position.call( this, anchor, relative_to ); } -Upload_pulldown.prototype.shutdown = function ( force ) { +Upload_pulldown.prototype.shutdown = function ( force, display_quota_error ) { // if there's an upload in progress and the force flag is not set, then bail without performing a // shutdown if ( this.uploading ) { - if ( force ) - this.wiki.display_message( "The file upload has been cancelled." ) - else + if ( force ) { + if ( !display_quota_error ) + this.wiki.display_message( "The file upload has been cancelled." ) + } else { return; + } } Pulldown.prototype.shutdown.call( this ); if ( this.link ) this.link.pulldown = null; + + if ( display_quota_error ) { + this.wiki.display_message( + "That file is too large for your available storage space. Before uploading, please delete some notes or files, empty the trash, or", + [ createDOM( "a", { "href": "/upgrade" }, "upgrade" ), " your account." ] + ); + } } function File_link_pulldown( wiki, notebook_id, invoker, editor, link ) { diff --git a/view/Blank_page.py b/view/Blank_page.py index a1b160e..05a72e9 100644 --- a/view/Blank_page.py +++ b/view/Blank_page.py @@ -1,8 +1,21 @@ -from Tags import Html +from Tags import Html, Head, Body, Script class Blank_page( Html ): - def __init__( self ): - Html.__init__( - self, - ) + def __init__( self, script = None ): + if script: + Html.__init__( + self, + Head( + Script( type = u"text/javascript", src = u"/static/js/MochiKit.js" ), + ), + Body( + Script( script, type = u"text/javascript" ), + ), + ) + else: + Html.__init__( + self, + Head(), + Body(), + ) diff --git a/view/Progress_bar.py b/view/Progress_bar.py index 51330ad..6ecbdd9 100644 --- a/view/Progress_bar.py +++ b/view/Progress_bar.py @@ -73,3 +73,27 @@ def stream_progress( uploading_file, filename, fraction_reported ): """ + + +stop_upload_script = \ + """ + withDocument( window.parent.document, function () { getElement( 'upload_frame' ).pulldown.shutdown( true, true ); } ); + """ + + +def stream_quota_error(): + yield \ + u""" + + + + + + + + + + + """ % stop_upload_script