Allow users to download files without revealing link and without buffering - django

I am hosting some video files in rackspace cloud files, and each user is allowed to download the files that are assigned to them.
Because of the file sizes it is not feasible to buffer the object in the webserver(webfaction)
I tried a redirect to the file, with Content-Disposition set to attachment, but to no avail.
What kind of options do I have, if any?
Ideally the file download would pop as coming from my domain after clicking a link that points to something like example.com/video/42/download/ so I can handle authentication ect. but im not sure how to structure my view for that to happen.

You are probably best served by using an HttpResponseRedirect unless there is something I am misunderstanding...?
# urls.py
from django.http import HttpResponseRedirect
url(r'^applications/(?P<id>\d+)/image\.png$', 'core.views.serve_image', name='image'),
This will serve a view at http://localhost/application/12345/image.png.
# core/views.py
def serve_application_image(request, id):
# redirect to temp_url
application = Application.objects.get(id=id)
return HttpResponseRedirect(application.image.temp_url)
And this will redirect users that hit that URL to the Rackspace URL. It can work for embedding videos, images, etc, in html <img> tags and such. Browser clients will be able to see the redirected URL (at rackcdn.com).
I have configured my apps to serve a temp_url property that expires after 15 minutes. The temporary URL is created for the CDN at Rackspace.com and their documentation may be out of the scope for this question so I'll leave it off for now... but the code I use to sub-class ImageField to serve image attributes with the .temp_url code follows:
import hmac
from hashlib import sha1
from time import time
class ImageFieldFile_With_Temp_Url(ImageFieldFile):
#property
def temp_url(self):
container_name, file_name = (self.storage.container.name, self.name)
key = settings.CUMULUS['CUSTOM__X_ACCOUNT_META_TEMP_URL_KEY']
public_url = settings.CUMULUS['CUSTOM__X_STORAGE_URL']
method = 'GET'
expires = int(time() + settings.CUMULUS['CUSTOM__X_TEMP_URL_TIMEOUT'])
url = '%s/%s/%s' % (public_url, container_name, file_name)
base_url, object_path = url.split('/v1/')
object_path = '/v1/' + object_path
hmac_body = '%s\n%s\n%s' % (method, expires, object_path)
sig = hmac.new(key, hmac_body, sha1).hexdigest()
return '%s%s?temp_url_sig=%s&temp_url_expires=%s' % (base_url, object_path, sig, expires)
class ImageField_With_Temp_Url(models.ImageField):
attr_class = ImageFieldFile_With_Temp_Url
models.ImageField = ImageField_With_Temp_Url
Note that I am using the django-cumulus project in this approach.
Importing this function anywhere at the top of your models.py will extend ImageField with a new temp_url property (since I assign it to models.ImageField ...).

Related

google cloud storage images differ for authenticated url and public url

I cant seem to understand how is it possible that for GCS the authenticated URL shows a different image then the public URL ?
Im uploading the images via a python django script
def upload_to_cloud(blob_name, file_obj):
file_type = imghdr.what(file_obj)
blob_name = str(blob_name) + '.' + file_type # concatenate string to create 'file_name.format'
stats = storage.Blob(bucket=bucket, name=blob_name).exists(client) # check if logo with the same reg.nr exists
if stats is True: # if exists then delete before uploading new logo
storage.Blob(bucket=bucket, name=blob_name).delete()
blob = bucket.blob(blob_name)
blob.upload_from_file(file_obj=file_obj, content_type=f'image/{file_type}')
path = blob.public_url
return path
class CompanyProfile(SuccessMessageMixin, UpdateView): # TODO why company logo differs from the one in ads_list?
model = Company
form_class = CompanyProfileCreationForm
def form_valid(self, form):
"""
Check if user uploaded a new logo. If yes
then upload the new logo to google cloud
"""
if 'logo' in self.request.FILES:
blob_name = self.request.user.company.reg_nr # get company registration number
file_obj = self.request.FILES['logo'] # store uploaded file in variable
form.instance.logo_url = upload_to_cloud(blob_name, file_obj) # update company.logo_url with path to uploaded file
company = Company.objects.get(pk=self.request.user.company.pk)
company.save()
return super().form_valid(form)
else:
return super().form_valid(form)
Any ideas on what Im doing wrong and how its even possible? The file that I actually uploaded is the one under authenticated url. The file thats under public url is a file that I uploaded for a different blob
EDIT
Im adding screenshot of the different images because after some time the images appear to be the same as they should be. Some people are confused by this and comment that the images are the same after all
Public URL
Authenticated URL
Note that caching issue is ruled out since I sent the public URL to my friend and he also saw that the image is the HTML text although the image in the authenticated URL (the correct image) was a light bulb. He also noted that the URL preview in fb messenger showed the light bulb image but when he actually opened the URL the HTML text image appeared
This problem persists in case a file is uploaded with the same blob name. This happens regardless if its overwritten by gcs or if I previously execute blob delete function and then create a new file with the same name as the deleted blob.
In general the same object will be served by storage.googleapis.com and storage.cloud.google.com.
The only exception is if there is some caching (either in your browser, in a proxy, with Cloud CDN or in GCS). If you read the object via storage.cloud.google.com before uploading a new version, then reading after by storage.cloud.google.com may serve the old version while storage.googleapis.com returns the new one. Caching can also be location dependent.
If you can't allow an hour of caching, set Cache control to no-cache.

Cache-Control not being set in response header

I am trying to optimize my Django web application by leveraging browser caching. I set a Cache-Control header for max-age equal to a year in my home view function returned response. When I load up my site however, and check the response header of some of the images on my home page, the cache-control header isn't there. I tried two different methods of setting the response header. First I tried using Django's built in cache-control decorator. I also tried simply taking the rendered response, and setting the header in my view function before its return statement. Are static images cached differently ?
View Function
def view_home(request, page=None):
# FIND THE HOME PAGE IF WE DO NOT HAVE ONE
# IF NOT FOUND RETURN 404
if not page:
try:
page = WebPage.objects.get(template='home')
except WebPage.DoesNotExist:
raise Http404
try:
billboards = Billboard.get_published_objects()
except Exception as e:
logging.error(e)
billboards = None
project_types = ProjectType.get_published_objects()
budgets = Budget.get_published_objects()
deadlines = Deadline.get_published_objects()
contact_descriptions = ContactDescription.get_published_objects()
contact_form = ContactForm(type_list=project_types, budget_list=budgets,
deadline_list=deadlines, description_list=contact_descriptions)
context = {'page': page, 'billboards': billboards, 'contact_form': contact_form}
set_detail_context(request, context)
template = 'home.html'
# Add Cache control to response header
expiry_date = datetime.datetime.now() + datetime.timedelta(days=7)
response = render(request, template, context)
response['Cache-Control'] = 'max-age=602000'
response['Expires'] = expiry_date
return response
It sounds like you're setting the headers on a per-view basis. But those views are handling specific URLs, which presumably are not the URLs for your static image files. So it won't have any effect on those.
How you set the headers for your static files depends on how you're serving them.
The most straightforward solution is to use the whitenoise app. This serves static files from Django in the same way in both development and production, and has a setting to control the max-age.
If you're using an external server (e.g. ngnix or Apache) you'll need to configure it to set any custom headers. It doesn't have anything to do with Django.
If you're using the Django development server you'll have to opt out of having it handle the static files automatically, and instead use a custom view that sets the headers. (Or you could just not bother when using the development server.)

Add request header before redirection

I am retrofitting a Django web app for a new client. To this end I have added a url pattern that redirects requests from new client to old url patterns.
from:-
(('api/(?P<phone>\w+)/MessageA', handle_a_message),
('api/(?P<phone>\w+)/MessageB', handle_b_message),
...)
to:-
(('api/(?P<phone>\w+)/MessageA', handle_a_message),
('api/(?P<phone>\w+)/MessageB', handle_b_message),
('api/newclient', handle_newclient)
...)
views.handle_newclient
def handle_newclient(request):
return redirect('/api/%(phone)s/%(msg)s' % request.GET)
This somewhat works. However the new client doesn't do basic auth which those url's need. Also the default output is json where the new client needs plain text. Is there a way I can tweak the headers before redirecting to the existing url's?
Django FBV's should return an HTTPResponse object (or subclass thereof). The Django shorcut redirect returns HttpResponseRedirect which is a subclass of HTTPResponse. This means we can set the headers for redirect() the way we will set headers for a typical HTTPResponse object. We can do that like so:
def my_view(request):
response = redirect('http://www.gamefaqs.com')
# Set 'Test' header and then delete
response['Test'] = 'Test'
del response['Test']
# Set 'Test Header' header
response['Test Header'] = 'Test Header'
return response
Relevant docs here and here.

How do I serve a text file from Django?

I am writing a Django based website, but need to serve a a simple text file. Is the correct way to so this by putting it in the static directory and bypassing Django?
If the file is static (not generated by the django app) then you can put it in the static directory.
If the content of this file is generated by Django then you can return it in a HttpResponse with text/plain as mimetype.
content = 'any string generated by django'
return HttpResponse(content, content_type='text/plain')
You can also give a name to the file by setting the Content-Disposition of the response.
filename = "my-file.txt"
content = 'any string generated by django'
response = HttpResponse(content, content_type='text/plain')
response['Content-Disposition'] = 'attachment; filename={0}'.format(filename)
return response
I agree with #luc, however another alternative is to use X-Accel-Redirect header.
Imagine that you have to serve big protected (have to login to view it) static files. If you put the file in static directory, then it's access is open and anybody can view it. If you serve it in Django by opening the file and then serving it, there is too much IO and Django will use more RAM since it has to load the file into RAM. The solution is to have a view, which will authenticate a user against a database, however instead of returning a file, Django will add X-Accel-Redirect header to it's response. Now since Django is behind nginx, nginx will see this header and it will then serve the protected static file. That's much better because nginx is much better and much faste at serving static files compared to Django. Here are nginx docs on how to do that. You can also do a similar thing in Apache, however I don't remember the header.
I was using a more complex method until recently, then I discovered this and this:
path('file.txt', TemplateView.as_view(template_name='file.txt',
content_type='text/plain')),
Then put file.txt in the root of your templates directory in your Django project.
I'm now using this method for robots.txt, a text file like the original asker, and a pre-generated sitemap.xml (eg, change to content_type='text/xml').
Unless I'm missing something, this is pretty simple and powerful.
I had a similar requirement for getting a text template for a form via AJAX. I choose to implement it with a model based view (Django 1.6.1) like this:
from django.http import HttpResponse
from django.views.generic import View
from django.views.generic.detail import SingleObjectMixin
from .models import MyModel
class TextFieldView(SingleObjectMixin, View):
model = MyModel
def get(self, request, *args, **kwargs):
myinstance = self.get_object()
content = myinstance.render_text_content()
return HttpResponse(content, content_type='text/plain; charset=utf8')
The rendered text is quite small and dynamically generated from other fields in the model.

Allow users to play mp3 files but don't expose them directly on the web

I want to store some mp3s in a folder which is not public, can't be directly accessed through the web and allow users to hear/download the songs with a browser only if they are logged in.
How can I do that?
I do my web development with Django, but if I know how it works is enough.
You first need to setup authentication. The django tutorials thoroughly explore this.
You don't' link the mp3's directly, You link to a django script that checks the auth, then reads the mp3 and serves it to the client with a mp3 content type header.
http://yourserver.com/listen?file=Fat+Boys+Greatest+Hits
I assume you use django. Then you can try something like this:
from django.conf import settings
from django.contrib.auth.decorators import login_required
from django.http import HttpResponse
#login_required
def listen(request, file_name):
# note that MP3_STORAGE should not be in MEDIA_ROOT
file = open("%smp3/%s" % (settings.MP3_STORAGE, file_name))
response = HttpResponse(file.read(), mimetype="audio/mpeg")
return response
Note that you will get dramatic speed decrease. Using generator to read file in blocks may help to save memory.
Lazy Method for Reading Big File in Python?
File outside of public access (not in
MEDIA_URL folders)
Check if user logged in
Serve files only via a view, with
unique links for every user
Pseudocode:
class Mp3(models.Model):
file = models.FileField(upload_to=path_outside_of_public_access)
hash = models.CharField(unique=True)
def generate_link_hash(request, file):
return hashlib.md5("%s_%i_%s_%s" % (request.session.session_key, file.id, str(file.date_added), file.hash)) # or however u like
def files_list(request)
""" view to show files list """
for file in files:
file.link_hash = generate_link_hash(request, file)
#login_required
def download_file(request, file_hash, link_hash):
""" view to download file """
file = Mp3.objects.get(hash=file_hash)
if link_hash == generate_link_hash(request, file):
file = open(file.file)
return HttpResponse(file.read(), mimetype="audio/mpeg")
else:
raise Http404
Should do the job enough, but remember - what is once accessed, you have no control where it goes from now on. And that every file download needs reading the file through the app (it's not given statically), which will affect the performance of your app.