Uploading files to Google Cloud Storage with Django
Intro
On of the features of Django Appengine Toolkit is simplifying the work needed to configure Google Cloud Storage as a static files storage for Django applications running on Google App Engine. Infact all you have to do is writing something like this in your settings.py module:
APPENGINE_TOOLKIT = {
'APP_YAML': os.path.join(BASE_DIR, 'app.yaml'),
'BUCKET_NAME': 'media-uploads',
}
DEFAULT_FILE_STORAGE = 'appengine_toolkit.storage.GoogleCloudStorage'
STATICFILE_STORAGE = 'appengine_toolkit.storage.GoogleCloudStorage'
A complete example
This repo contains a minimalistic Django project
implementing a file storage application that lets users upload, listing, retrieve and delete files. The project has just
one app implementing all the logic, defining the model and exposing the views. For detailed instructions on how to
setup a Django project on App Engine with django-appengine-toolkit
please check out
this blog post.
Now let’s take a look at the code.
The Model
class Document(models.Model):
docfile = models.FileField(upload_to='documents/%Y/%m/%d')
def delete(self, *args, **kwargs):
storage, path = self.docfile.storage, self.docfile.path
super(Document, self).delete(*args, **kwargs)
storage.delete(path)
Pretty easy, we have just one field containing the file. Notice the delete method we’re going to use so that when an instance is deleted, the same will happen to corresponding file on Cloud Storage.
The views
Hail to the Class Based Views! Look at how few lines of code we need for the main view, implementing the listing and the logic for the uploads, form included:
class FileManagerView(CreateView):
model = Document
success_url = reverse_lazy('main')
def get_context_data(self, **kwargs):
kwargs['object_list'] = Document.objects.all()
kwargs['fava'] = 'rava'
return super(FileManagerView, self).get_context_data(**kwargs)
Since we need to show the list of files and the form to upload them on the same page, we cannot use a CreateView
as is,
what we need is a CreateView
and ListView
hybrid instead, thus the hack of overriding get_context_data
: we inject the queryset
in the context so the template can render properly.
The relevant html code in the template looks like this:
<ul>
{% for object in object_list %}
<li>
<form action="{% url 'delete' object.id %}" method="post">{% csrf_token %}
<a href="{{ object.docfile.url }}">{{ object.docfile.name }}</a>
<input type="submit" value="Delete" />
</form>
</li>
{% empty %}
<li>No documents.</li>
{% endfor %}
</ul>
<form enctype="multipart/form-data" action="" method="post">{% csrf_token %}
{{ form.as_p }}
<input type="submit" value="Upload" />
</form>
Notice we render a form for each file listed, so we can make a POST
request directly, without passing for a confirmation view
as usual when using DeleteView
generics. Let’s see the View code:
class FileRemoveView(DeleteView):
model = Document
success_url = reverse_lazy('main')
Ok, this was short. Basically we only need to tell to the class based view which is the model and where to go once the istance is deleted. Wow.
The urls
Quick and dirty: mount the two views to the urls:
urlpatterns = patterns('',
url(r'^$', FileManagerView.as_view(), name='main'),
url(r'^delete/(?P<pk>\d+)/$', FileRemoveView.as_view(), name='delete'),
)
That’s all, have fun deploying on App Engine!