Merge remote-tracking branch 'refs/remotes/rodney757/file_limits'

Conflicts:
	mediagoblin/db/migrations.py
This commit is contained in:
Christopher Allan Webber 2013-09-18 11:21:57 -05:00
commit 28eab59ace
17 changed files with 315 additions and 37 deletions

View File

@ -75,6 +75,12 @@ theme = string()
plugin_web_path = string(default="/plugin_static/")
plugin_linked_assets_dir = string(default="%(here)s/user_dev/plugin_static/")
# Default user upload limit (in Mb)
upload_limit = integer(default=None)
# Max file size (in Mb)
max_file_size = integer(default=None)
[jinja2]
# Jinja2 supports more directives than the minimum required by mediagoblin.
# This setting allows users creating custom templates to specify a list of

View File

@ -474,3 +474,23 @@ def wants_notifications(db):
col.create(user_table)
db.commit()
@RegisterMigration(16, MIGRATIONS)
def upload_limits(db):
"""Add user upload limit columns"""
metadata = MetaData(bind=db.bind)
user_table = inspect_table(metadata, 'core__users')
media_entry_table = inspect_table(metadata, 'core__media_entries')
col = Column('uploaded', Integer, default=0)
col.create(user_table)
col = Column('upload_limit', Integer)
col.create(user_table)
col = Column('file_size', Integer, default=0)
col.create(media_entry_table)
db.commit()

View File

@ -74,6 +74,8 @@ class User(Base, UserMixin):
is_admin = Column(Boolean, default=False, nullable=False)
url = Column(Unicode)
bio = Column(UnicodeText) # ??
uploaded = Column(Integer, default=0)
upload_limit = Column(Integer)
## TODO
# plugin data would be in a separate model
@ -190,6 +192,7 @@ class MediaEntry(Base, MediaEntryMixin):
# or use sqlalchemy.types.Enum?
license = Column(Unicode)
collected = Column(Integer, default=0)
file_size = Column(Integer, default=0)
fail_error = Column(Unicode)
fail_metadata = Column(JSONEncoded)

View File

@ -0,0 +1,45 @@
/**
* GNU MediaGoblin -- federated, autonomous media hosting
* Copyright (C) 2011, 2012 MediaGoblin contributors. See AUTHORS.
*
* This program is free software: you can redistribute it and/or modify
* it under the terms of the GNU Affero General Public License as published by
* the Free Software Foundation, either version 3 of the License, or
* (at your option) any later version.
*
* This program is distributed in the hope that it will be useful,
* but WITHOUT ANY WARRANTY; without even the implied warranty of
* MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the
* GNU Affero General Public License for more details.
*
* You should have received a copy of the GNU Affero General Public License
* along with this program. If not, see <http://www.gnu.org/licenses/>.
*/
$(document).ready(function(){
var file = document.getElementById('file');
var uploaded = parseInt(document.getElementById('uploaded').value);
var upload_limit = parseInt(document.getElementById('upload_limit').value);
var max_file_size = parseInt(document.getElementById('max_file_size').value);
file.onchange = function() {
var file_size = file.files[0].size / (1024.0 * 1024);
if (file_size >= max_file_size) {
$('#file').after('<p id="file_size_error" class="form_field_error">Sorry, the file size is too big.</p>');
}
else if (document.getElementById('file_size_error')) {
$('#file_size_error').hide();
}
if (upload_limit) {
if ( uploaded + file_size >= upload_limit) {
$('#file').after('<p id="upload_limit_error" class="form_field_error">Sorry, uploading this file will put you over your upload limit.</p>');
}
else if (document.getElementById('upload_limit_error')) {
$('#upload_limit_error').hide();
console.log(file_size >= max_file_size);
}
}
};
});

View File

@ -191,6 +191,13 @@ class StorageInterface(object):
# Copy to storage system in 4M chunks
shutil.copyfileobj(source_file, dest_file, length=4*1048576)
def get_file_size(self, filepath):
"""
Return the size of the file in bytes.
"""
# Subclasses should override this method.
self.__raise_not_implemented()
###########
# Utilities

View File

@ -168,6 +168,12 @@ class CloudFilesStorage(StorageInterface):
# Copy to storage system in 4096 byte chunks
dest_file.send(source_file)
def get_file_size(self, filepath):
"""Returns the file size in bytes"""
obj = self.container.get_object(
self._resolve_filepath(filepath))
return obj.total_bytes
class CloudFilesStorageObjectWrapper():
"""
Wrapper for python-cloudfiles's cloudfiles.storage_object.Object

View File

@ -111,3 +111,6 @@ class BasicFileStorage(StorageInterface):
os.makedirs(directory)
# This uses chunked copying of 16kb buffers (Py2.7):
shutil.copy(filename, self.get_local_path(filepath))
def get_file_size(self, filepath):
return os.stat(self._resolve_filepath(filepath)).st_size

View File

@ -17,30 +17,44 @@
import wtforms
from mediagoblin import mg_globals
from mediagoblin.tools.text import tag_length_validator
from mediagoblin.tools.translate import lazy_pass_to_ugettext as _
from mediagoblin.tools.licenses import licenses_as_choices
class SubmitStartForm(wtforms.Form):
file = wtforms.FileField(_('File'))
title = wtforms.TextField(
_('Title'),
[wtforms.validators.Length(min=0, max=500)])
description = wtforms.TextAreaField(
_('Description of this work'),
description=_("""You can use
<a href="http://daringfireball.net/projects/markdown/basics">
Markdown</a> for formatting."""))
tags = wtforms.TextField(
_('Tags'),
[tag_length_validator],
description=_(
"Separate tags by commas."))
license = wtforms.SelectField(
_('License'),
[wtforms.validators.Optional(),],
choices=licenses_as_choices())
def get_submit_start_form(form, **kwargs):
max_file_size = kwargs.get('max_file_size')
desc = None
if max_file_size:
desc = _('Max file size: {0} mb'.format(max_file_size))
class SubmitStartForm(wtforms.Form):
file = wtforms.FileField(
_('File'),
description=desc)
title = wtforms.TextField(
_('Title'),
[wtforms.validators.Length(min=0, max=500)])
description = wtforms.TextAreaField(
_('Description of this work'),
description=_("""You can use
<a href="http://daringfireball.net/projects/markdown/basics">
Markdown</a> for formatting."""))
tags = wtforms.TextField(
_('Tags'),
[tag_length_validator],
description=_(
"Separate tags by commas."))
license = wtforms.SelectField(
_('License'),
[wtforms.validators.Optional(),],
choices=licenses_as_choices())
max_file_size = wtforms.HiddenField('')
upload_limit = wtforms.HiddenField('')
uploaded = wtforms.HiddenField('')
return SubmitStartForm(form, **kwargs)
class AddCollectionForm(wtforms.Form):
title = wtforms.TextField(

View File

@ -43,8 +43,28 @@ def submit_start(request):
"""
First view for submitting a file.
"""
submit_form = submit_forms.SubmitStartForm(request.form,
license=request.user.license_preference)
user = request.user
if user.upload_limit >= 0:
upload_limit = user.upload_limit
else:
upload_limit = mg_globals.app_config.get('upload_limit', None)
if upload_limit and user.uploaded >= upload_limit:
messages.add_message(
request,
messages.WARNING,
_('Sorry, you have reached your upload limit.'))
return redirect(request, "mediagoblin.user_pages.user_home",
user=request.user.username)
max_file_size = mg_globals.app_config.get('max_file_size', None)
submit_form = submit_forms.get_submit_start_form(
request.form,
license=request.user.license_preference,
max_file_size=max_file_size,
upload_limit=upload_limit,
uploaded=user.uploaded)
if request.method == 'POST' and submit_form.validate():
if not check_file_field(request, 'file'):
@ -86,24 +106,49 @@ def submit_start(request):
with queue_file:
queue_file.write(request.files['file'].stream.read())
# Save now so we have this data before kicking off processing
entry.save()
# Get file size and round to 2 decimal places
file_size = request.app.queue_store.get_file_size(
entry.queued_media_file) / (1024.0 * 1024)
file_size = float('{0:.2f}'.format(file_size))
# Pass off to async processing
#
# (... don't change entry after this point to avoid race
# conditions with changes to the document via processing code)
feed_url = request.urlgen(
'mediagoblin.user_pages.atom_feed',
qualified=True, user=request.user.username)
run_process_media(entry, feed_url)
error = False
add_message(request, SUCCESS, _('Woohoo! Submitted!'))
# Check if file size is over the limit
if max_file_size and file_size >= max_file_size:
submit_form.file.errors.append(
_(u'Sorry, the file size is too big.'))
error = True
add_comment_subscription(request.user, entry)
# Check if user is over upload limit
if upload_limit and (user.uploaded + file_size) >= upload_limit:
submit_form.file.errors.append(
_('Sorry, uploading this file will put you over your'
' upload limit.'))
error = True
return redirect(request, "mediagoblin.user_pages.user_home",
user=request.user.username)
if not error:
user.uploaded = user.uploaded + file_size
user.save()
entry.file_size = file_size
# Save now so we have this data before kicking off processing
entry.save()
# Pass off to processing
#
# (... don't change entry after this point to avoid race
# conditions with changes to the document via processing code)
feed_url = request.urlgen(
'mediagoblin.user_pages.atom_feed',
qualified=True, user=request.user.username)
run_process_media(entry, feed_url)
add_message(request, SUCCESS, _('Woohoo! Submitted!'))
add_comment_subscription(request.user, entry)
return redirect(request, "mediagoblin.user_pages.user_home",
user=user.username)
except Exception as e:
'''
This section is intended to catch exceptions raised in

View File

@ -19,6 +19,11 @@
{% import "/mediagoblin/utils/wtforms.html" as wtforms_util %}
{% block mediagoblin_head %}
<script type="text/javascript"
src="{{ request.staticdirect('/js/file_size.js') }}"></script>
{% endblock %}
{% block title -%}
{% trans %}Add your media{% endtrans %} &mdash; {{ super() }}
{%- endblock %}

View File

@ -29,6 +29,8 @@ EVIL_JPG = resource('evil.jpg')
EVIL_PNG = resource('evil.png')
BIG_BLUE = resource('bigblue.png')
GOOD_PDF = resource('good.pdf')
MED_PNG = resource('medium.png')
BIG_PNG = resource('big.png')
def resource_exif(f):

View File

@ -13,6 +13,10 @@ tags_max_length = 50
# So we can start to test attachments:
allow_attachments = True
upload_limit = 500
max_file_size = 2
[storage:publicstore]
base_dir = %(here)s/user_dev/media/public
base_url = /mgoblin_media/

View File

@ -24,13 +24,14 @@ import pytest
from mediagoblin.tests.tools import fixture_add_user
from mediagoblin import mg_globals
from mediagoblin.db.models import MediaEntry
from mediagoblin.db.models import MediaEntry, User
from mediagoblin.db.base import Session
from mediagoblin.tools import template
from mediagoblin.media_types.image import ImageMediaManager
from mediagoblin.media_types.pdf.processing import check_prerequisites as pdf_check_prerequisites
from .resources import GOOD_JPG, GOOD_PNG, EVIL_FILE, EVIL_JPG, EVIL_PNG, \
BIG_BLUE, GOOD_PDF, GPS_JPG
BIG_BLUE, GOOD_PDF, GPS_JPG, MED_PNG, BIG_PNG
GOOD_TAG_STRING = u'yin,yang'
BAD_TAG_STRING = unicode('rage,' + 'f' * 26 + 'u' * 26)
@ -107,9 +108,38 @@ class TestSubmission:
self.logout()
self.test_app.get(url)
def user_upload_limits(self, uploaded=None, upload_limit=None):
if uploaded:
self.test_user.uploaded = uploaded
if upload_limit:
self.test_user.upload_limit = upload_limit
self.test_user.save()
# Reload
self.test_user = User.query.filter_by(
username=self.test_user.username
).first()
# ... and detach from session:
Session.expunge(self.test_user)
def test_normal_jpg(self):
# User uploaded should be 0
assert self.test_user.uploaded == 0
self.check_normal_upload(u'Normal upload 1', GOOD_JPG)
# User uploaded should be the same as GOOD_JPG size in Mb
file_size = os.stat(GOOD_JPG).st_size / (1024.0 * 1024)
file_size = float('{0:.2f}'.format(file_size))
# Reload user
self.test_user = User.query.filter_by(
username=self.test_user.username
).first()
assert self.test_user.uploaded == file_size
def test_normal_png(self):
self.check_normal_upload(u'Normal upload 2', GOOD_PNG)
@ -121,6 +151,75 @@ class TestSubmission:
self.check_url(response, '/u/{0}/'.format(self.test_user.username))
assert 'mediagoblin/user_pages/user.html' in context
def test_default_upload_limits(self):
self.user_upload_limits(uploaded=500)
# User uploaded should be 500
assert self.test_user.uploaded == 500
response, context = self.do_post({'title': u'Normal upload 4'},
do_follow=True,
**self.upload_data(GOOD_JPG))
self.check_url(response, '/u/{0}/'.format(self.test_user.username))
assert 'mediagoblin/user_pages/user.html' in context
# Reload user
self.test_user = User.query.filter_by(
username=self.test_user.username
).first()
# Shouldn't have uploaded
assert self.test_user.uploaded == 500
def test_user_upload_limit(self):
self.user_upload_limits(uploaded=25, upload_limit=25)
# User uploaded should be 25
assert self.test_user.uploaded == 25
response, context = self.do_post({'title': u'Normal upload 5'},
do_follow=True,
**self.upload_data(GOOD_JPG))
self.check_url(response, '/u/{0}/'.format(self.test_user.username))
assert 'mediagoblin/user_pages/user.html' in context
# Reload user
self.test_user = User.query.filter_by(
username=self.test_user.username
).first()
# Shouldn't have uploaded
assert self.test_user.uploaded == 25
def test_user_under_limit(self):
self.user_upload_limits(uploaded=499)
# User uploaded should be 499
assert self.test_user.uploaded == 499
response, context = self.do_post({'title': u'Normal upload 6'},
do_follow=False,
**self.upload_data(MED_PNG))
form = context['mediagoblin/submit/start.html']['submit_form']
assert form.file.errors == [u'Sorry, uploading this file will put you'
' over your upload limit.']
# Reload user
self.test_user = User.query.filter_by(
username=self.test_user.username
).first()
# Shouldn't have uploaded
assert self.test_user.uploaded == 499
def test_big_file(self):
response, context = self.do_post({'title': u'Normal upload 7'},
do_follow=False,
**self.upload_data(BIG_PNG))
form = context['mediagoblin/submit/start.html']['submit_form']
assert form.file.errors == [u'Sorry, the file size is too big.']
def check_media(self, request, find_data, count=None):
media = MediaEntry.query.filter_by(**find_data)
if count is not None:
@ -155,6 +254,7 @@ class TestSubmission:
'ffffffffffffffffffffffffffuuuuuuuuuuuuuuuuuuuuuuuuuu']
def test_delete(self):
self.user_upload_limits(uploaded=50)
response, request = self.do_post({'title': u'Balanced Goblin'},
*REQUEST_CONTEXT, do_follow=True,
**self.upload_data(GOOD_JPG))
@ -199,6 +299,14 @@ class TestSubmission:
self.check_media(request, {'id': media_id}, 0)
self.check_comments(request, media_id, 0)
# Reload user
self.test_user = User.query.filter_by(
username = self.test_user.username
).first()
# Check that user.uploaded is the same as before the upload
assert self.test_user.uploaded == 50
def test_evil_file(self):
# Test non-suppoerted file with non-supported extension
# -----------------------------------------------------

View File

@ -0,0 +1,5 @@
Images located in this directory tree are released under a GPLv3 license
and CC BY-SA 3.0 license. To the extent possible under law, the author(s)
have dedicated all copyright and related and neighboring rights to these
files to the public domain worldwide. These files are distributed without
any warranty.

Binary file not shown.

After

Width:  |  Height:  |  Size: 2.1 MiB

Binary file not shown.

After

Width:  |  Height:  |  Size: 1.7 MiB

View File

@ -296,6 +296,11 @@ def media_confirm_delete(request, media):
if request.method == 'POST' and form.validate():
if form.confirm.data is True:
username = media.get_uploader.username
media.get_uploader.uploaded = media.get_uploader.uploaded - \
media.file_size
media.get_uploader.save()
# Delete MediaEntry and all related files, comments etc.
media.delete()
messages.add_message(