0

Here is my code for testing exporting files from google drive on server side.

import logging

from flask import Flask, render_template, request

from googleapiclient.discovery import build
from googleapiclient.http import MediaIoBaseDownload
from oauth2client.client import AccessTokenCredentials

import httplib2
import io


app = Flask(__name__)


@app.route('/gdrive/selectcallback')
def userselectioncallback():
    print "In user selection callback... "

    code = request.args.get('user_token')
    fileId = request.args.get('fileId')

    credentials = AccessTokenCredentials(code,
                                         'my-user-agent/1.0')


    http = httplib2.Http()
    http_auth = credentials.authorize(http)

    drive_service = build('drive', 'v3', http=http_auth)

    drive_request = drive_service.files().export(
        fileId=fileId,
        mimeType='application/pdf')
    fh = io.FileIO('test.pdf', 'wb')
    downloader = MediaIoBaseDownload(fh, drive_request)
    done = False
    while done is False:
        status, done = downloader.next_chunk()
        print "Download %d%%." % int(status.progress() * 100)

    return code


if __name__ == '__main__':
    # This is used when running locally. Gunicorn is used to run the
    # application on Google App Engine. See entrypoint in app.yaml.
    app.run(host='127.0.0.1', port=8090, debug=True)

On the web client side, once a user selects a file from the file picker, the javascript front end will call the /gdrive/selectcallback in the above python code with the token and file id.

As an example, the token looks something like this: ya29.Glu5BG-LQJFqZ-e4uImMSxz-14iS41jVLfXk6rVKvAPjylCwhUh98ZJk1iIC5Eb49pTfflGnU6qE7uzK44AYr0Wn79QMUkF368WFaYrhidrvpVjcsJSZ9P1M8VU6 and file id looks something like this 1ON9kGyb02TFCygy8jeIYyo2BKj5SzKgAP0xi5Rm08D4

Here is the relevant front end code (in coffeescript):

  pickerCallback = () ->
    view   = new google.picker.View(google.picker.ViewId.PRESENTATIONS)
    picker = new google.picker.PickerBuilder()
      .enableFeature(google.picker.Feature.NAV_HIDDEN)
      .setAppId('zeetings')
      .setOAuthToken(oauthToken)
      .addView(view)
      .setDeveloperKey(env['googleapi-client'].apiKey)
      .setCallback(selectCallback)  # The callback calls the python backend
      .build()
    picker.setVisible true

  selectCallback = (data) ->
    if data.action is google.picker.Action.PICKED
      fileId = data.docs[0].id
      fileSelectedCallback(fileId, oauthToken) if fileSelectedCallback

Based on the debug information, my python code emits these two https calls:

2017-09-01 11:32:38,810 pid 260 tid 140546358265600 INFO
discovery URL being requested: GET https://www.googleapis.com/discovery/v1/apis/drive/v3/rest

2017-09-01 11:32:39,009 pid 260 tid 140546358265600 INFO discovery URL being requested: GET https://www.googleapis.com/drive/v3/files/1ON9kGyb02TFCygy8jeIYyo2BKj5SzKgAP0xi4Rm08D4/export?mimeType=application%2Fpdf

If I use the second url directly in a browser, I get the following error:

{
 "error": {
  "errors": [
   {
    "domain": "usageLimits",
    "reason": "dailyLimitExceededUnreg",
    "message": "Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup.",
    "extendedHelp": "https://code.google.com/apis/console"
   }
  ],
  "code": 403,
  "message": "Daily Limit for Unauthenticated Use Exceeded. Continued use requires signup."
 }
}

(I don't think the above error message actually reflects the root cause. It is more likely due to the fact that the call is not authenticated in my browser.)

I suspect I have to use the google-auth library (https://google-auth.readthedocs.io/en/latest/user-guide.html#making-authenticated-requests), but I am not sure how to marry google-auth to the python code I have above. I suppose I can obtain a credential via

from google.oauth2 import service_account

credentials = service_account.Credentials.from_service_account_file(
    '/path/to/key.json')

But what should I do with the credentials after this? Do I use it to replace credentials = AccessTokenCredentials(code,'my-user-agent/1.0') entirely?

P.S.

As per @Tanaike's suggestion, I tried to use the API url directly. It is the result I got:

{
 "error": {
  "errors": [
   {
    "domain": "global",
    "reason": "fileNotDownloadable",
    "message": "Only files with binary content can be downloaded. Use Export with Google Docs files.",
    "locationType": "parameter",
    "location": "alt"
   }
  ],
  "code": 403,
  "message": "Only files with binary content can be downloaded. Use Export with Google Docs files."
 }
}

It seems to be a v3 API problem. If I switch to v2 and use a downloadUrl link, I can download the file in pdf format.

Anthony Kong
  • 37,791
  • 46
  • 172
  • 304
  • what error message do you get from the export API? Exporting via the API explorer appears to work fine – Maurice Codik Aug 02 '17 at 18:04
  • It happens to the particular slide as mentioned in the question (a link to which is provided too). "Download 0%" were printed out many time until at end a 500 is caught – Anthony Kong Aug 02 '17 at 23:40
  • You might want to check this reported as [issue](https://issuetracker.google.com/issues/64294118) which Drive API returns error 500. It wasn't clear if it is now working for v3 since the reported only confirmed it is working in v2. You might want to follow the issue, then comment/create a new one. Hope this help – Mr.Rebot Aug 03 '17 at 17:06
  • When it finds the refresh token and file ID, you can use Drive API. In your script, file ID can be found. I would like to confirm the refresh token. What is ``code = request.args.get('user_token')``? It seems that that may be the refresh token. if that is refresh token, can I ask you about the scope included in the refresh token? And, have you already enabled Drive API at API console? – Tanaike Sep 04 '17 at 02:28
  • @Tanaike I have added some front end code to the question, and just spot a problem with my coffeescript code: I might have passed the oauth2 token instead of the file access code to the backend. I will check if it is the root cause. – Anthony Kong Sep 04 '17 at 02:32
  • If you have an usable access token, you can confirm the scopes using this curl command. ``curl -L --data "access_token=### access token ###" https://www.googleapis.com/oauth2/v2/tokeninfo`` – Tanaike Sep 04 '17 at 02:36
  • @Tanaike The weird thing is the token works sometimes. It works on some of my drive files but not other. Thanks for the very useful tip and i will try it out – Anthony Kong Sep 04 '17 at 02:37
  • The token info seems Ok, although I think it is wrong because it comes from `gapi.auth`, not `picker`: `{ "issued_to": "534888811371-fegdtjm9ak4a6nhq2f4ukrjfkgdnv0q6.apps.googleusercontent.com", "audience": "534888811371-fegdtjm9ak4a6nhq2f4ukrjfkgdnv0q6.apps.googleusercontent.com", "scope": "https://www.googleapis.com/auth/drive https://www.googleapis.com/auth/drive.file", "expires_in": 3309, "access_type": "online" }` – Anthony Kong Sep 04 '17 at 02:40
  • Thank you for confirming it. That access token is fine. So, for example, how about the use Drive API on ``picker`` using the refresh token which retrieved the access token? – Tanaike Sep 04 '17 at 02:50
  • I got this ""Only files with binary content can be downloaded." error. Looks like a google drive v3 api issue to me. (I have updated the question with the error message) – Anthony Kong Sep 04 '17 at 05:55
  • The error indicates that you might try to download files except for Google Docs using Export. When you want to download files except for Google Docs files, they have to be downloaded by files.get using the query of ``alt=media``. https://developers.google.com/drive/v3/reference/files/get – Tanaike Sep 04 '17 at 06:09
  • I want to export a google slide. I think the API doc suggests I need to use `export` (https://developers.google.com/drive/v3/reference/files/export). Here is a related youtube video from google drive team: https://youtu.be/-7YH6rdR-tk?t=10m33s – Anthony Kong Sep 04 '17 at 06:16

1 Answers1

0

User @Tanaike has given me many good advise to debug this problem. I were able to test the REST API directly to verify that 1) I have the correct access code and, 2) the drive v3 file export API works as expected

Turns out the issue is with the MediaIoBaseDownload class. If I remove it from the code and just receive the data directly:

data = drive_service.files().export(
        fileId=fileId,
        mimeType='application/pdf').execute()
f = open('test.pdf)
f.write(data)
f.close()

then it works as expected

Anthony Kong
  • 37,791
  • 46
  • 172
  • 304