Prevent Force Download of AWS S3 Files Django - django

I am using
storages.backends.s3boto3.S3Boto3Storage
storage backend to upload files in my django project.
field declaration in model:
document = models.FileField(upload_to=s3_directory_path.user_directory_path)
user_directory_path
def user_directory_path(instance, filename):
# TODO: Try to include this along with check filetype on the request object
document = instance.document
mime = magic.from_buffer(document.read(), mime=True)
extension = mimetypes.guess_extension(mime, strict=False)
file_name = str(uuid.uuid4()) + extension
document.seek(0)
return os.path.join("users", str(instance.user.id), file_name)
The saving of the document works perfectly fine, but the link which is generated force downloads the file. How can i avoid that?

Have a look at this answer to a general question about forcing file downloads via HTTP response headers. See also the MDN docs about Content-Disposition.
Can you show us the response headers you get when visiting the document URL?
It would be interesting to see how S3 delivers your files.
If you cannot change the headers in S3, you have the option to write a Django view that proxies the file download. Alternatively, configure your webserver (i.e. NGINX) to act as a proxy and set the required headers).
For Django, this section of the docs will show you how to set the headers.
response = HttpResponse(
document,
headers={
'Content-Type': mimetype,
'Content-Disposition': f'attachment; filename="{document.name}"',
}
)

Related

Django download links only working in new tab

This is driving me crazy, I'm developing a django app and need to provide a download link to a file located in the media folder. If I type the url in a blank tab it works fine, the file is downloaded, but if I click on the link from the website nothing happens. I realize this might be because the url to the file doesn't have the same origin that the website and from what I understood the browser won't allow the download, is this correct? Is there any way around?
url of the website (in development): http://localhost:8000/django_app/view_name
url of the file: http://localhost:8000/django_app/media/file.ext
I've tried the following html href:
href="../media/file.ext" download target="_blank"
And the following view:
def download_file(request):
fl_path = settings.MEDIA_ROOT + "\\filename.ext"
filename = "file_name"
mime_type, _ = mimetypes.guess_type(fl_path)
response = HttpResponse(fl, content_type=mime_type)
response['Content-Disposition'] = "attachment; filename=%s" % filename
return response
Nothing happens when I click on the link, no error generated. If I open the link in a new tab it downloads the file normally... Note that I need to be able to change the href link dynamically.
Thanks!

How to efficiently allow for users to view Amazon S3 content?

I am currently creating a basic app with React-Native (frontend) and Flask/MongoDB (backend). I am planning on using AWS S3 as cheap cloud storage for all the images and videos that are going to be uploaded and viewed. My current idea (and this could be totally off), is when a user uploads content, it will go through my Flask API and then to the S3 storage. When a user wants to view content, I am not sure what the plan of attack is here. Should I use my Flask API as a proxy, or is there a way to simply send a link to the content directly on S3 (which would avoid the extra traffic through my API)?
I am quite new to using AWS and if there is already a post discussing this topic, please let me know, and I'd be more than happy to take down this duplicate. I just can't seem to find anything.
Should I use my Flask API as a proxy, or is there a way to simply send a link to the content directly on S3 (which would avoid the extra traffic through my API)?
If the content is public, you just provide an URL which points directly to the file on the S3 bucket.
If the content is private, you generate presigned url on your backend for the file for which you want to give access. This URL should be valid for a short amount of time (for example: 15/30 minutes). You can regenerate it, if it becomes unavailable.
Moreover, you can generate a presigned URL which can be used for uploads directly from the front-end to the S3 bucket. This might be an option if you don't want the upload traffic to go through the backend or you want faster uploads.
There is an API boto3, try to use it.
It is not so difficult, I have done something similar, will post code here.
I have done like #Ervin said.
frontend asks backend to generate credentials
backend sends to frontend the credentials
Frontend upload file to S3
Frontend warns backend it has done.
Backend validate if everything is ok.
Backend will create a link to download, you have a lot of security options.
example of item 6) To generate a presigned url to download content.
bucket = app.config.get('BOTO3_BUCKET', None)
client = boto_flask.clients.get('s3')
params = {}
params['Bucket'] = bucket
params['Key'] = attachment_model.s3_filename
params['ResponseContentDisposition'] = 'attachment; filename={0}'.format(attachment_model.filename)
if attachment_model.mimetype is not None:
params['ResponseContentType'] = attachment_model.mimetype
url = client.generate_presigned_url('get_object', ExpiresIn=3600, Params=params)
example of item 2) Backend will create presigned credentials to post your file on S3, send s3_credentials to frontend
acl_permission = 'private' if private_attachment else 'public-read'
condition = [{'acl': acl_permission},
["starts-with", "$key", '{0}/'.format(folder_name)],
{'Content-Type': mimetype }]
bucket = app.config.get('BOTO3_BUCKET', None)
fields = {"acl": acl_permission, 'Bucket': bucket, 'Content-Type': mimetype}
client = boto_flask.clients.get('s3')
s3_credentials = client.generate_presigned_post(bucket, s3_filename, Fields=fields, Conditions=condition, ExpiresIn=3600)
example of item 5) Here are an example how backend can check if file on S3 are ok.
bucket = app.config.get('BOTO3_BUCKET', None)
client = boto_flask.clients.get('s3')
response = client.head_object(Bucket=bucket, Key=s3_filename)
if response is None:
return None, None
md5 = response.get('ETag').replace('"', '')
size = response.get('ContentLength')
Here are an example how frontend will ask for credentials, upload file to S3 and inform backend it is done.
I tried to remove a lot of particular code.
//frontend asking backend to create credentials, frontend will send some file metadata
AttachmentService.createPostUrl(payload).then((responseCredentials) => {
let form = new FormData();
Object.keys(responseCredentials.s3.fields).forEach(key => {
form.append(key, responseCredentials.s3.fields[key]);
});
form.append("file", file);
let payload = {
data: form,
url: responseCredentials.s3.url
}
//Frontend will send file to S3
axios.post(payload.url, payload.data).then((res) => {
return Promise.resolve(true);
}).then((result) => {
//when it is done, frontend informs backend
AttachmentService.uploadSuccess(...).then((refreshCase) => {
//Success
});
});
});

Add headers to a Django redirect

I am returning a file to the client via redirect(s3_url). According to the docs I should be able to add a custom header:
response = redirect(s3_url)
response['File-Name'] = file_name
return response
In chrome dev tools, I don't see any File-Name header in the response headers.

a web server developed by python, how to transfer a image file, and can show in a web browser?

I want to develop a simple web server using python to handle some simple http request. I have learn how to response the request, such as transferring html pages or transferring some other file. When I transfer a image file, a client use a browser to get the file, the url is like below:
http://114.212.82.104:8080/1.png
I set 'Content-Type = application/x-png'. But the browser directly download the file, and can not display in the browser. Not like the image below
https://www.baidu.com/img/bd_logo1.png
it can display in the browser. How to display the image in the browser?
Can someone help me?
and i know i can encode the image file into html page to fix it. code like below:
class RequestHandler(BaseHTTPServer.BaseHTTPRequestHandler):
def do_GET(self):
path = os.getcwd()+ self.path
if os.path.isfile(path):
with open(path,'rb') as fileTrans:
content = fileTrans.read().encode('base64').replace('\n','')
#self.sendContent(200, content)
self.send_response(200)
page = "<p>\"fef\"</p><img src=\"data:image/jpg;base64,{0}\"/>"
contentPage = page.format(content)
self.send_header('Content-Type', 'text/html')
self.send_header("Content-Length", str(len(contentPage)))
self.end_headers()
self.wfile.write(contentPage)
else:
self.sendContent(404,"file do not exists")
But I know there must be another way, i see the source code of URL(https://www.baidu.com/)
it just use
<img hidefocus="true" src="//www.baidu.com/img/bd_logo1.png" width="270" height="129"></div><a href="/" id="result_logo" onmousedown="return c({'fm':'tab','tab':'logo'})">
different from my page:
<p>"fef"</p><img src="data:image/jpg;base64,iVBORw0KGgoAAAANSUhEUgAAAcIAAAJYCAIAAAB+b3GqAAAAGXRFWHRTb2Z0d2FyZQBBZG9iZSBJbWFnZVJlYWR5ccllPAAAAvppVFh0WE1MOmNvbS5hZG9iZS54bXAAAAAAADw/eHBhY2tldCBiZWdpbj0i77u/IiBpZD0iVzVNME1wQ2VoaUh6cmVTek5UY3prYzlkIj8+IDx4OnhtcG1ldGEgeG1sbnM6eD0iYWRvYmU6bnM6bWV0YS8iIHg6eG1wdGs9IkFkb2JlIFhNUCBDb3JlIDUuNi1jMTMyIDc5LjE1OTI4NCwgMjAxNi8wNC8xOS0xMzoxMzo0MCAgICAgICAgIj4gPHJkZjpSREYgeG1sbnM6cmRmPSJodHRwOi8vd3d3LnczLm9yZy8xOTk5LzAyLzIyLXJkZi1zeW50YXgtbnMjIj4gPHJkZjpEZXNjcmlwdGlvbiByZGY6YWJvdXQ9IiIgeG1sbnM6eG1wTU09Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC9tbS8iIHhtbG5zOnN0UmVmPSJodHRwOi8vbnMuYWRvYmUuY29tL3hhcC8xLjAvc1R5cGUvUmVzb3VyY2VSZWYjIiB4bWxuczp4bXA9Imh0dHA6Ly9ucy5hZG9iZS5jb20veGFwLzEuMC8iIHhtcE1NOkRvY3VtZW50SUQ9InhtcC5kaWQ6ODg3NkM1Njg1MzVFMTFFNkE0NkJFNTEzMUFCNzc4RTMiIHhtcE1NOkluc3RhbmNlSUQ9InhtcC5paWQ6ODg3NkM1Njc1MzVFMTFFNkE0NkJFNTEzMUFCNzc4RTMiIHhtcDpDcmVhdG9yVG9vbD0iOC4xLjMiPiA8eG1wTU06RG......
OK,I think I have solved this problem.
Just set the attribute - 'Content-Type' of header as 'image/png' instead of 'application/x-png'.

custom HTTP headers for static files with Django

I'm writing an image bank with Django, and I want to add a button to get a hi-res version of an image (the low-res is shown in the details page). If I put just an <a> link, the browser will open the image instead of downloading it. Adding an HTTP header like:
Content-Disposition: attachment; filename="beach008.jpg"
works, but since it's an static file, I don't want to handle the request with Django. Currently, I'm using NGINX to serve static files, and dynamic pages are redirected via FastCGI to the Django process. I'm thinking about using NGINX add-header command, but could it set the filename="xx" part?. Or maybe there's some way to handle the request in Django, but make NGINX serve the content?
If your django app is proxied by nginx you can use x-accell-redirect. You need to pass a special header in your response, nginx will intercepet this and start serving the file, you can also pass Content-Disposition in the same response to force a download.
That solution is good if you want to control which users acess these files.
You can also use a configuration like this:
#files which need to be forced downloads
location /static/high_res/ {
root /project_root;
#don't ever send $request_filename in your response, it will expose your dir struct, use a quick regex hack to find just the filename
if ($request_filename ~* ^.*?/([^/]*?)$) {
set $filename $1;
}
#match images
if ($filename ~* ^.*?\.((jpg)|(png)|(gif))$) {
add_header Content-Disposition "attachment; filename=$filename";
}
}
location /static {
root /project_root;
}
This will force download on all images in some high_res folder (MEDIAROOT/high_rest). And for the other static files it will behave like normal. Please note that this is a modified quick hack that works for me. It may have security implications, so use it with precaution.
I wrote a simple decorator, for django.views.static.serve view
Which works for me perfectly.
def serve_download(view_func):
def _wrapped_view_func(request, *args, **kwargs):
response = view_func(request, *args, **kwargs)
response['Content-Type'] = 'application/octet-stream';
import os.path
response['Content-Disposition'] = 'attachment; filename="%s"' % os.path.basename(kwargs['path'])
return response
return _wrapped_view_func
Also you can play with nginx mime-types
http://wiki.codemongers.com/NginxHttpCoreModule#types
This solution didn't work for me, because I wanted to have both direct link for the file (so user can view images, for example), and download link.
What i'm doing now is to use a different URL for download than for 'views', and add the filename as an URL arg:
usual media link: http://xx.com/media/images/lores/f_123123.jpg
download link: http://xx.com/downs/hires/f_12323?beach008.jpg
and nginx has a config like this:
location /downs/ {
root /var/www/nginx-attachment;
add_header Content-Disposition 'attachment; filename="$args"';
}
but i really don't like the smell of it.