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

Comment by Michael

@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.

Comment by Fran├žois

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

Comment by Michael

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

Comment by 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"}');

Comment by Unknown