Plupload is a reusable component which fully takes advantage of the file upload capabilities of your browser and its plugins. It will use HTML5, Flash and Google Gears if they are available, but otherwise, it can gracefully degrade down to HTML4 it it needs to. Here's how it can be integrated within a Django web application.
(I have posted the sample application I will refer to and you may use it anyway you like.)
Creating a basic upload form
The first step is to create a simple one-file upload form that will be used in the case where Javascript is disabled:
class UploadForm(forms.Form):
file = forms.FileField()
def save(self, uploaded_file):
print 'File "%s" would presumably be saved to disk now.' % uploaded_file
pass
Then you can add this form to one of your templates:
<form enctype="multipart/form-data" action="{% url plupload_sample.upload.views.upload_file %}" method="post">
{% csrf_token %}
<div id="uploader">
{{form.file.errors}}{{form.file}}
<input type="submit" value="Upload" />
</div>
</form>
And create a new method to receive the form data:
@csrf_protect
def upload_file(request):
if request.method == 'POST':
form = UploadForm(request.POST, request.FILES)
if form.is_valid():
uploaded_file = request.FILES['file']
form.save(uploaded_file)
return HttpResponseRedirect(reverse('plupload_sample.upload.views.upload_file'))
else:
form = UploadForm()
return render_to_response('upload_file.html', {'form': form}, context_instance=RequestContext(request))
Adding Plupload to the template
In order to display the right Javascript-based upload form, add the following code, based on the official example, to the head of your template:
<link rel="stylesheet" href="/css/plupload.queue.css" type="text/css">
<script type="text/javascript" src="/js/jquery.min.js"></script>
<script type="text/javascript" src="/js/plupload.full.min.js"></script>
<script type="text/javascript" src="/js/jquery.plupload.queue.min.js"></script>
<script type="text/javascript">
$(function() {
$("#uploader").pluploadQueue({
runtimes : 'html5,html4',
url : '{% url plupload_sample.upload.views.upload_file %}',
max_file_size : '1mb',
chunk_size: '1mb',
unique_names : false,
multipart: true,
headers : {'X-Requested-With' : 'XMLHttpRequest', 'X-CSRFToken' : '{{csrf_token}}'},
});
$('form').submit(function(e) {
var uploader = $('#uploader').pluploadQueue();
// Validate number of uploaded files
if (uploader.total.uploaded == 0) {
// Files in queue upload them first
if (uploader.files.length > 0) {
// When all files are uploaded submit form
uploader.bind('UploadProgress', function() {
if (uploader.total.uploaded == uploader.files.length)
$('form').submit();
});
uploader.start();
} else {
alert('You must at least upload one file.');
}
e.preventDefault();
}
});
});
</script>
Pay close attention to the extra headers that need to be added to ensure that the AJAX requests will pass the Django CSRF checks:
X-Requested-With: XMLHttpRequest
X-CSRFToken: {{csrf_token}}
Adding Plupload to the view method
Now in order to properly receive the files uploaded by Plupload via AJAX calls, we need to revise our upload_file() method:
@csrf_protect def upload_file(request): if request.method == 'POST': form = UploadForm(request.POST, request.FILES) if form.is_valid(): uploaded_file = request.FILES['file'] form.save(uploaded_file) if request.is_ajax(): response = HttpResponse('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}', mimetype='text/plain; charset=UTF-8') response['Expires'] = 'Mon, 1 Jan 2000 01:00:00 GMT' response['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' response['Pragma'] = 'no-cache' return response else: return HttpResponseRedirect(reverse('plupload_sample.upload.views.upload_file')) else: form = UploadForm() return render_to_response('upload_file.html', {'form': form}, context_instance=RequestContext(request))
The above includes the response (which is not really documented as far as I can tell) that needs to be sent back to Plupload to make sure it knows that the file has been received successfully:
{"jsonrpc" : "2.0", "result" : null, "id" : "id"}
Adding support for multipart files
Our solution so far works fine except when uploading large files that need to be sent in multiple chunks.
This involves writing to a temporary file until all parts have been received:
@csrf_protect def upload_file(request): if request.method == 'POST': form = UploadForm(request.POST, request.FILES) if form.is_valid(): uploaded_file = request.FILES['file'] chunk = request.REQUEST.get('chunk', '0') chunks = request.REQUEST.get('chunks', '0') name = request.REQUEST.get('name', '') if not name: name = uploaded_file.name temp_file = '/tmp/insecure.tmp' with open(temp_file, ('wb' if chunk == '0' else 'ab')) as f: for content in uploaded_file.chunks(): f.write(content) if int(chunk) + 1 >= int(chunks): form.save(temp_file, name) if request.is_ajax(): response = HttpResponse('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}', mimetype='text/plain; charset=UTF-8') response['Expires'] = 'Mon, 1 Jan 2000 01:00:00 GMT' response['Cache-Control'] = 'no-store, no-cache, must-revalidate, post-check=0, pre-check=0' response['Pragma'] = 'no-cache' return response else: return HttpResponseRedirect(reverse('plupload_sample.upload.views.upload_file')) else: form = UploadForm() return render_to_response('upload_file.html', {'form': form}, context_instance=RequestContext(request))
Note that I have used /tmp/insecure.tmp
for brevity. In a real application, you do need to use a secure mechanism to create the temporary file or you would expose yourself to a tempfile vulnerability.
Hi Francois,
Question of the year: After the completed download I want a redirection to take place, do I have to hook in some JS event? I ask because the else statement of request.is_ajax() seems to never be executed. Unless I am doing something horribly wrong which might very well be
thanks and regards,
Michele
@Michele the else statement of "request.is_ajax()" is for when the non-Javascript version of the form is submitted.
Redirections would indeed have to be done in Javascript.
Hi Francois,
thanks for the hint. It wasn't too obvious to me as there's no queue_finished event, but checking the number of uploads at every FileUploaded event did it.
thanks again and regards,
Michele
Hello,
I have been testing you django plupload integration and it works nice... could you talk a bit more about the secure temp_file issue?
If I simple do a
file_obj, temp_file = tempfile.mkstemp()
it does not work as it creates a multiple tempfiles for an upload?
Do you have a hint on how to go about this?
thanks
Titusz
Titusz: How bout using the session_id ?
Found these in upload.php:
die('{"jsonrpc" : "2.0", "error" : {"code": 101, "message": "Failed to open input stream."}, "id" : "id"}');
die('{"jsonrpc" : "2.0", "error" : {"code": 102, "message": "Failed to open output stream."}, "id" : "id"}');
die('{"jsonrpc" : "2.0", "error" : {"code": 103, "message": "Failed to move uploaded file."}, "id" : "id"}');
die('{"jsonrpc" : "2.0", "result" : null, "id" : "id"}');