Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Django, Heroku, boto: direct file upload to Google cloud storage

In Django projects deployed on Heroku, I used to upload files to Google cloud storage via boto. However, recently I have to upload large files which will cause Heroku timeout.

I am following Heroku's documentation about direct file upload to S3, and customizing as follows:

Python:

conn = boto.connect_gs(gs_access_key_id=GS_ACCESS_KEY,
                       gs_secret_access_key=GS_SECRET_KEY)
presignedUrl = conn.generate_url(expires_in=3600, method='PUT', bucket=<bucketName>, key=<fileName>, force_http=True)

JS:

url = 'https://<bucketName>.storage.googleapis.com/<fileName>?Signature=...&Expires=1471451569&GoogleAccessId=...'; // "presignUrl"

postData = new FormData();
postData.append(...);
...

$.ajax({
  url: url,
  type: 'PUT',
  data: postData,
  processData: false,
  contentType: false,
});

I got the following error message:

XMLHttpRequest cannot load http:/...  Response to preflight request doesn't pass access control check: No 'Access-Control-Allow-Origin' header is present on the requested resource. Origin 'http://localhost:8000' is therefore not allowed access.

EDIT:

The output of gsutil cors get gs://<bucketName>:

[{"maxAgeSeconds": 3600, "method": ["GET", "POST", "HEAD", "DELETE", "PUT"], "origin": ["*"], "responseHeader": ["Content-Type"]}]

It seems the CORS is OK. So, how do I solve the problem? Thanks.

EDIT 2:

The header of the OPTION request from Firefox:

Accept: text/html,application/xhtml+xml,application/xml;q=0.9,*/*;q=0.8
Accept-Encoding: gzip, deflate
Accept-Language: zh-TW,zh;q=0.8,en-US;q=0.5,en;q=0.3
Access-Control-Request-Method: PUT
Connection: keep-alive
Host: <bucketName>.storage.googleapis.com
Origin: http://localhost:8000
User-Agent: Mozilla/5.0 (X11; Ubuntu; Linux x86_64; rv:48.0) Gecko/20100101 Firefox/48.0

The header of the OPTION request from Chrome:

Accept:*/*
Accept-Encoding:gzip, deflate, sdch
Accept-Language:zh-TW,zh;q=0.8,en;q=0.6,en-US;q=0.4,zh-CN;q=0.2
Access-Control-Request-Headers:
Access-Control-Request-Method:PUT
Connection:keep-alive
Host:directupload.storage.googleapis.com
Origin:http://localhost:8000
Referer:http://localhost:8000/
User-Agent:Mozilla/5.0 (X11; Linux x86_64) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/52.0.2743.116 Safari/537.36
X-Client-Data:CIe2yQEIprbJAQjznMoB
like image 241
Randy Tang Avatar asked Aug 14 '16 00:08

Randy Tang


People also ask

Does Heroku allow file upload?

We use direct uploads. Files are uploaded directly to the cloud from your user's browser, without passing through your application. Adding direct uploads to your app allows you to offload the storage of static files from your app. This is crucial on Heroku, because your app's dynos have an ephemeral filesystem.

How do I upload files to Gcloud?

Drag and drop the desired files from your desktop or file manager to the main pane in the Google Cloud console. Click the Upload Files button, select the files you want to upload in the dialog that appears, and click Open.

Can Heroku store files?

Heroku has an “ephemeral” hard drive, this means that you can write files to disk, but those files will not persist after the application is restarted. By default Active Storage uses a :local storage option, which uses the local file system to store any uploaded files.

Where does Heroku store images?

Amazon Simple Storage Service (S3) is a durable and available store, ideal for storing application content like media files, static assets, and user uploads. Storing static files elsewhere is crucial for Heroku apps since dynos have an ephemeral filesystem.


2 Answers

The header issue is not coming from your app, I think it's coming from the cloud storage bucket. I had the same issue when setting up an api, the resource you are posting to is missing the header.

https://cloud.google.com/storage/docs/cross-origin

While useful for preventing malicious behavior, this security measure also prevents useful and legitimate interactions between known origins. For example, a script on a page hosted from Google App Engine at example.appspot.com might want to use static resources stored in a Cloud Storage bucket at example.storage.googleapis.com. However, because these are two different origins from the perspective of the browser, the browser won't allow a script from example.appspot.com to fetch resources from example.storage.googleapis.com using XMLHttpRequest because the resource being fetched is from a different origin.

So it looks like you need to configure the bucket to allow cors requests. The google documentation shows the following code to be run from the google cli.

https://cloud.google.com/storage/docs/cross-origin#Configuring-CORS-on-a-Bucket

gsutil cors set cors-json-file.json gs://example

[
    {
      "origin": ["http://mysite.heroku.com"],
      "responseHeader": ["Content-Type"],
      "method": ["GET", "HEAD", "DELETE", "PUT"],
      "maxAgeSeconds": 3600
    }
]

Which would allow you get, upload, and delete content. Hope that helps.

like image 148
Jordan Parsons Avatar answered Sep 20 '22 06:09

Jordan Parsons


Based on the information in EDIT 2, something is wrong with the request. The preflight (OPTIONS) request includes the header ACCESS-CONTROL-REQUEST-HEADER. This is not a valid CORS header. The correct header is ACCESS-CONTROL-REQUEST-HEADERS, notice the 'S' at the end.

Even if the header was correct, it should not be requesting authorization for a access-control-allow-origin header. ACCESS-CONTROL-ALLOW-ORIGIN is not a header that is sent from the client. It is a header that will automatically be sent in the response from the server to the client when the server gets a preflight request. The client/browser will not allow a cross-origin PUT request unless it gets a ACCESS-CONTROL-ALLOW-ORIGIN header authorizing the browser document's current origin from the cross-origin server in the preflight request.

The presence of the bad header appears to correlate well with the error response you are receiving. However, it looks like that header was probably not in your original code, it looks like you added it later (based on your comments). Make sure to take that header config out, it is definitely not correct.

So I am a little confused about where that header is coming from, but I think it is the source of your problem.

It looks like you are using jQuery to make the AJAX PUT request. All I can really suggest is to make sure you haven't called $.ajaxSetup() somewhere in your JS code that might be configuring the bad header.

like image 35
Jody Boucher Avatar answered Sep 20 '22 06:09

Jody Boucher