From fd0e91ea3909f9c380964c9424e520387e9b084b Mon Sep 17 00:00:00 2001 From: Dan Helfman Date: Wed, 20 Feb 2008 20:21:54 +0000 Subject: [PATCH] Implemented quota enforcement when uploading a file. This occurs in two places: 1. In progress(), around the time when the file starts uploading. This causes an upload that's too large to bail before the whole file uploads, but the quota calculation is only an estimate and relies on the client actually calling progress(). 2. In upload(), when the file finishes uploading. This quota calculation is exact, but only happens after the entire upload completes. --- controller/Files.py | 58 +++++++++++++++++++++++++++++++++----------- controller/Users.py | 3 +++ static/js/Wiki.js | 19 +++++++++++---- view/Blank_page.py | 23 ++++++++++++++---- view/Progress_bar.py | 24 ++++++++++++++++++ 5 files changed, 103 insertions(+), 24 deletions(-) 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