Commit 07c93a8e by Mushtaq Ali Committed by GitHub

Merge pull request #14140 from edx/mushtaq/video-upload-restrict

Video upload restrictions
parents a56b3049 2c7941d1
......@@ -286,6 +286,27 @@ class VideosHandlerTestCase(VideoUploadTestMixin, CourseTestCase):
self.assertIn('error', response)
self.assertEqual(response['error'], "Request 'files' entry contain unsupported content_type")
@override_settings(AWS_ACCESS_KEY_ID='test_key_id', AWS_SECRET_ACCESS_KEY='test_secret')
@patch('boto.s3.connection.S3Connection')
def test_upload_with_non_ascii_charaters(self, mock_conn):
"""
Test that video uploads throws error message when file name contains special characters.
"""
file_name = u'test\u2019_file.mp4'
files = [{'file_name': file_name, 'content_type': 'video/mp4'}]
bucket = Mock()
mock_conn.return_value = Mock(get_bucket=Mock(return_value=bucket))
response = self.client.post(
self.url,
json.dumps({'files': files}),
content_type='application/json'
)
self.assertEqual(response.status_code, 400)
response = json.loads(response.content)
self.assertEqual(response['error'], 'The file name for %s must contain only ASCII characters.' % file_name)
@override_settings(AWS_ACCESS_KEY_ID="test_key_id", AWS_SECRET_ACCESS_KEY="test_secret")
@patch("boto.s3.key.Key")
@patch("boto.s3.connection.S3Connection")
......
......@@ -34,6 +34,8 @@ VIDEO_SUPPORTED_FILE_FORMATS = {
'.mov': 'video/quicktime',
}
VIDEO_UPLOAD_MAX_FILE_SIZE_GB = 5
class StatusDisplayStrings(object):
"""
......@@ -262,7 +264,8 @@ def videos_index_html(course):
"encodings_download_url": reverse_course_url("video_encodings_download", unicode(course.id)),
"previous_uploads": _get_index_videos(course),
"concurrent_upload_limit": settings.VIDEO_UPLOAD_PIPELINE.get("CONCURRENT_UPLOAD_LIMIT", 0),
"video_supported_file_formats": VIDEO_SUPPORTED_FILE_FORMATS.keys()
"video_supported_file_formats": VIDEO_SUPPORTED_FILE_FORMATS.keys(),
"video_upload_max_file_size": VIDEO_UPLOAD_MAX_FILE_SIZE_GB
}
)
......@@ -328,6 +331,12 @@ def videos_post(course, request):
for req_file in req_files:
file_name = req_file["file_name"]
try:
file_name.encode('ascii')
except UnicodeEncodeError:
error_msg = 'The file name for %s must contain only ASCII characters.' % file_name
return JsonResponse({'error': error_msg}, status=400)
edx_video_id = unicode(uuid4())
key = storage_service_key(bucket, file_name=edx_video_id)
for metadata_name, value in [
......
......@@ -10,13 +10,15 @@ define([
concurrentUploadLimit,
uploadButton,
previousUploads,
videoSupportedFileFormats
videoSupportedFileFormats,
videoUploadMaxFileSizeInGB
) {
var activeView = new ActiveVideoUploadListView({
postUrl: videoHandlerUrl,
concurrentUploadLimit: concurrentUploadLimit,
uploadButton: uploadButton,
videoSupportedFileFormats: videoSupportedFileFormats,
videoUploadMaxFileSizeInGB: videoUploadMaxFileSizeInGB,
onFileUploadDone: function(activeVideos) {
$.ajax({
url: videoHandlerUrl,
......
......@@ -18,16 +18,16 @@ define(
this.postUrl = '/test/post/url';
this.uploadButton = $('<button>');
this.videoSupportedFileFormats = ['.mp4', '.mov'];
this.videoUploadMaxFileSizeInGB = 5;
this.view = new ActiveVideoUploadListView({
concurrentUploadLimit: concurrentUploadLimit,
postUrl: this.postUrl,
uploadButton: this.uploadButton,
videoSupportedFileFormats: this.videoSupportedFileFormats
videoSupportedFileFormats: this.videoSupportedFileFormats,
videoUploadMaxFileSizeInGB: this.videoUploadMaxFileSizeInGB
});
this.view.render();
jasmine.Ajax.install();
this.globalAjaxError = jasmine.createSpy();
$(document).ajaxError(this.globalAjaxError);
});
// Remove window unload handler triggered by the upload requests
......@@ -89,6 +89,38 @@ define(
});
});
describe('Upload file', function() {
_.each(
[
{desc: 'larger than', additionalBytes: 1},
{desc: 'equal to', additionalBytes: 0},
{desc: 'smaller than', additionalBytes: - 1}
],
function(caseInfo) {
it(caseInfo.desc + 'max file size', function() {
var maxFileSizeInBytes = this.view.getMaxFileSizeInBytes(),
fileSize = maxFileSizeInBytes + caseInfo.additionalBytes,
fileToUpload = {
files: [
{name: 'file.mp4', size: fileSize}
]
};
this.view.$uploadForm.fileupload('add', fileToUpload);
if (fileSize > maxFileSizeInBytes) {
expect(this.view.fileErrorMsg).toBeDefined();
expect(this.view.fileErrorMsg.options.title).toEqual('Your file could not be uploaded');
expect(this.view.fileErrorMsg.options.message).toEqual(
'file.mp4 exceeds maximum size of ' + this.videoUploadMaxFileSizeInGB + ' GB.'
);
} else {
this.view.$uploadForm.fileupload('add', fileToUpload);
expect(this.view.fileErrorMsg).toBeNull();
}
});
}
);
});
_.each(
[
{desc: 'a single file', numFiles: 1},
......@@ -122,21 +154,25 @@ define(
});
it('should trigger the correct request', function() {
expect(this.request.url).toEqual(this.postUrl);
expect(this.request.method).toEqual('POST');
expect(this.request.requestHeaders['Content-Type']).toEqual('application/json');
expect(this.request.requestHeaders['Accept']).toContain('application/json');
expect(JSON.parse(this.request.params)).toEqual({
'files': _.map(
fileNames,
function(fileName) { return {'file_name': fileName}; }
)
var request,
self = this;
expect(jasmine.Ajax.requests.count()).toEqual(caseInfo.numFiles);
_.each(_.range(caseInfo.numFiles), function(index) {
request = jasmine.Ajax.requests.at(index);
expect(request.url).toEqual(self.postUrl);
expect(request.method).toEqual('POST');
expect(request.requestHeaders['Content-Type']).toEqual('application/json');
expect(request.requestHeaders.Accept).toContain('application/json');
expect(JSON.parse(request.params)).toEqual({
files: [{file_name: fileNames[index]}]
});
});
});
it('should trigger the global AJAX error handler on server error', function() {
it('should trigger the notification error handler on server error', function() {
this.request.respondWith({status: 500});
expect(this.globalAjaxError).toHaveBeenCalled();
expect(this.view.fileErrorMsg).toBeDefined();
expect(this.view.fileErrorMsg.options.title).toEqual('Your file could not be uploaded');
});
describe('and successful server response', function() {
......@@ -269,8 +305,8 @@ define(
}
});
it('should not trigger the global AJAX error handler', function() {
expect(this.globalAjaxError).not.toHaveBeenCalled();
it('should not trigger the notification error handler', function() {
expect(this.view.fileErrorMsg).toBeNull();
});
if (caseInfo.numFiles > concurrentUploadLimit) {
......
define([
'jquery',
'underscore',
'underscore.string',
'backbone',
'js/models/active_video_upload',
'js/views/baseview',
......@@ -10,10 +11,12 @@ define([
'text!templates/active-video-upload-list.underscore',
'jquery.fileupload'
],
function($, _, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView, NotificationView, HtmlUtils,
function($, _, str, Backbone, ActiveVideoUpload, BaseView, ActiveVideoUploadView, NotificationView, HtmlUtils,
activeVideoUploadListTemplate) {
'use strict';
var ActiveVideoUploadListView = BaseView.extend({
var ActiveVideoUploadListView,
CONVERSION_FACTOR_GBS_TO_BYTES = 1000 * 1000 * 1000;
ActiveVideoUploadListView = BaseView.extend({
tagName: 'div',
events: {
'click .file-drop-area': 'chooseFile',
......@@ -29,6 +32,7 @@ define([
this.concurrentUploadLimit = options.concurrentUploadLimit || 0;
this.postUrl = options.postUrl;
this.videoSupportedFileFormats = options.videoSupportedFileFormats;
this.videoUploadMaxFileSizeInGB = options.videoUploadMaxFileSizeInGB;
this.onFileUploadDone = options.onFileUploadDone;
if (options.uploadButton) {
options.uploadButton.click(this.chooseFile.bind(this));
......@@ -136,34 +140,48 @@ define([
uploadData.cid = model.cid; // eslint-disable-line no-param-reassign
uploadData.submit();
} else {
$.ajax({
url: this.postUrl,
contentType: 'application/json',
data: JSON.stringify({
files: _.map(
uploadData.files,
function(file) {
return {file_name: file.name, content_type: file.type};
_.each(
uploadData.files,
function(file) {
$.ajax({
url: view.postUrl,
contentType: 'application/json',
data: JSON.stringify({
files: [{file_name: file.name, content_type: file.type}]
}),
dataType: 'json',
type: 'POST',
global: false // Do not trigger global AJAX error handler
}).done(function(responseData) {
_.each(
responseData.files,
function(file) { // eslint-disable-line no-shadow
view.$uploadForm.fileupload('add', {
files: _.filter(uploadData.files, function(fileObj) {
return file.file_name === fileObj.name;
}),
url: file.upload_url,
videoId: file.edx_video_id,
multipart: false,
global: false, // Do not trigger global AJAX error handler
redirected: true
});
}
);
}).fail(function(response) {
if (response.responseText) {
try {
errorMsg = JSON.parse(response.responseText).error;
} catch (error) {
errorMsg = str.truncate(response.responseText, 300);
}
} else {
errorMsg = gettext('This may be happening because of an error with our server or your internet connection. Try refreshing the page or making sure you are online.'); // eslint-disable-line max-len
}
)
}),
dataType: 'json',
type: 'POST'
}).done(function(responseData) {
_.each(
responseData.files,
function(file, index) {
view.$uploadForm.fileupload('add', {
files: [uploadData.files[index]],
url: file.upload_url,
videoId: file.edx_video_id,
multipart: false,
global: false, // Do not trigger global AJAX error handler
redirected: true
});
}
);
});
view.showErrorMessage(errorMsg);
});
}
);
}
}
},
......@@ -198,6 +216,10 @@ define([
this.setStatus(data.cid, ActiveVideoUpload.STATUS_FAILED);
},
getMaxFileSizeInBytes: function() {
return this.videoUploadMaxFileSizeInGB * CONVERSION_FACTOR_GBS_TO_BYTES;
},
hideErrorMessage: function() {
if (this.fileErrorMsg) {
this.fileErrorMsg.hide();
......@@ -212,13 +234,16 @@ define([
},
showErrorMessage: function(errorMsg) {
var titleMsg = gettext('Your file could not be uploaded');
this.fileErrorMsg = new NotificationView.Error({
title: titleMsg,
message: errorMsg
});
this.fileErrorMsg.show();
this.readMessages([titleMsg, errorMsg]);
var titleMsg;
if (!this.fileErrorMsg) {
titleMsg = gettext('Your file could not be uploaded');
this.fileErrorMsg = new NotificationView.Error({
title: titleMsg,
message: errorMsg
});
this.fileErrorMsg.show();
this.readMessages([titleMsg, errorMsg]);
}
},
validateFile: function(data) {
......@@ -240,6 +265,14 @@ define([
.replace('{supportedFileFormats}', self.videoSupportedFileFormats.join(' and '));
return false;
}
if (file.size > self.getMaxFileSizeInBytes()) {
error = gettext(
'{filename} exceeds maximum size of {maxFileSizeInGB} GB.'
)
.replace('{filename}', fileName)
.replace('{maxFileSizeInGB}', self.videoUploadMaxFileSizeInGB);
return false;
}
});
return error;
},
......
......@@ -34,7 +34,8 @@
${concurrent_upload_limit | n, dump_js_escaped_json},
$(".nav-actions .upload-button"),
$contentWrapper.data("previous-uploads"),
${video_supported_file_formats | n, dump_js_escaped_json}
${video_supported_file_formats | n, dump_js_escaped_json},
${video_upload_max_file_size | n, dump_js_escaped_json}
);
});
</%block>
......@@ -70,7 +71,10 @@
file_formats=' or '.join(video_supported_file_formats)
)}</p>
<h3 class="title-3">${_("Maximum Video File Size")}</h3>
<p>${_("The maximum size for each video file that you upload is 5 GB. The upload process fails for larger files.")}</p>
<p>${Text(_("The maximum size for each video file that you upload is {em_start}5 GB{em_end}. The upload process fails for larger files.")).format(
em_start=HTML('<strong>'),
em_end=HTML('</strong>')
)}</p>
<h3 class="title-3">${_("Monitoring files as they upload")}</h3>
<p>${_("Each video file that you upload needs to reach the video processing servers successfully before additional work can begin. You can monitor the progress of files as they upload, and try again if the upload fails.")}</p>
<h3 class="title-3">${_("Managing uploaded files")}</h3>
......
Markdown is supported
0% or
You are about to add 0 people to the discussion. Proceed with caution.
Finish editing this message first!
Please register or to comment