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.
This commit is contained in:
parent
3d5baa1e24
commit
fd0e91ea39
|
@ -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
|
||||
|
|
|
@ -1130,3 +1130,6 @@ class Users( object ):
|
|||
result[ "invites" ] = []
|
||||
|
||||
return result
|
||||
|
||||
def rate_plan( self, plan_index ):
|
||||
return self.__rate_plans[ plan_index ]
|
||||
|
|
|
@ -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 ) {
|
||||
|
|
|
@ -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(),
|
||||
)
|
||||
|
|
|
@ -73,3 +73,27 @@ def stream_progress( uploading_file, filename, fraction_reported ):
|
|||
</body>
|
||||
</html>
|
||||
"""
|
||||
|
||||
|
||||
stop_upload_script = \
|
||||
"""
|
||||
withDocument( window.parent.document, function () { getElement( 'upload_frame' ).pulldown.shutdown( true, true ); } );
|
||||
"""
|
||||
|
||||
|
||||
def stream_quota_error():
|
||||
yield \
|
||||
u"""
|
||||
<html>
|
||||
<head>
|
||||
<link href="/static/css/upload.css" type="text/css" rel="stylesheet" />
|
||||
<script type="text/javascript" src="/static/js/MochiKit.js"></script>
|
||||
<meta content="text/html; charset=UTF-8" http_equiv="content-type" />
|
||||
</head>
|
||||
<body>
|
||||
<script type="text/javascript">
|
||||
%s
|
||||
</script>
|
||||
</body>
|
||||
</html>
|
||||
""" % stop_upload_script
|
||||
|
|
Reference in New Issue