Golang : Resumable upload to Google Drive(RESTful) example




This tutorial is a supplement to previous tutorial on how to upload file to Google Drive via REST API with MULTIPART upload method . This time, we will use the resumable upload method.

Before you start, please turn on the Google Drive API if you haven't do so and download the credential file - client_secret.json by following the Step 1: Turn on the Drive API section found in https://developers.google.com/drive/v3/web/quickstart/go#prerequisites

Once you've downloaded the credential file, move it to the same directory as the source code below before running the program.


NOTE:

You will need to change the file to be uploaded - installgoogledrive.dmg to something else that you have.

When prompted for the authorization code for the first time, cut-n-paste the URL from your terminal to your browser, then you will get a string(token), cut-n-paste that string into your terminal where you execute the program.

In case you get Authorization error in the API JSON reply, you will need to authorize the Drive API v3 scope at https://developers.google.com/oauthplayground/

Next, do a go get command

go get google.golang.org/api/drive/v3

Finally, enable the Drive API by going to https://developers.google.com/drive/v3/web/enable-sdk and follow the instruction listed below To enable the Drive API, complete these steps:


Run this code example below and observe the changes to your own Google Drive content at https://drive.google.com/drive/my-drive

Here comes the RESUMABLE upload method!

 package main

 import (
 "encoding/json"
 "fmt"
 "golang.org/x/net/context"
 "golang.org/x/oauth2"
 "golang.org/x/oauth2/google"
 "google.golang.org/api/drive/v3"
 "io/ioutil"
 "log"
 "net/http"
 "net/url"
 "os"
 "os/user"
 "path/filepath"
 "strconv"
 "strings"
 )

 // NOTE : we don't want to visit CSRF URL to get the authorization code
 // and paste into the terminal each time we want to send an email
 // therefore we will retrieve a token for our client, save the token into a file
 // you will be prompted to visit a link in your browser for authorization code only ONCE
 // and subsequent execution of the program will not prompt you for authorization code again
 // until the token expires.

 // getClient uses a Context and Config to retrieve a Token
 // then generate a Client. It returns the generated Client.
 func getClient(ctx context.Context, config *oauth2.Config) *http.Client {
 cacheFile, err := tokenCacheFile()
 if err != nil {
 log.Fatalf("Unable to get path to cached credential file. %v", err)
 }
 tok, err := tokenFromFile(cacheFile)
 if err != nil {
 tok = getTokenFromWeb(config)
 saveToken(cacheFile, tok)
 }
 return config.Client(ctx, tok)
 }

 // getTokenFromWeb uses Config to request a Token.
 // It returns the retrieved Token.
 func getTokenFromWeb(config *oauth2.Config) *oauth2.Token {
 authURL := config.AuthCodeURL("state-token", oauth2.AccessTypeOffline)
 fmt.Printf("Go to the following link in your browser then type the "+
 "authorization code: \n%v\n", authURL)

 var code string
 if _, err := fmt.Scan(&code); err != nil {
 log.Fatalf("Unable to read authorization code %v", err)
 }

 tok, err := config.Exchange(oauth2.NoContext, code)
 if err != nil {
 log.Fatalf("Unable to retrieve token from web %v", err)
 }
 return tok
 }

 // tokenCacheFile generates credential file path/filename.
 // It returns the generated credential path/filename.
 func tokenCacheFile() (string, error) {
 usr, err := user.Current()
 if err != nil {
 return "", err
 }
 tokenCacheDir := filepath.Join(usr.HomeDir, ".credentials")
 os.MkdirAll(tokenCacheDir, 0700)
 return filepath.Join(tokenCacheDir,
 url.QueryEscape("google-drive-golang.json")), err
 }

 // tokenFromFile retrieves a Token from a given file path.
 // It returns the retrieved Token and any read error encountered.
 func tokenFromFile(file string) (*oauth2.Token, error) {
 f, err := os.Open(file)
 if err != nil {
 return nil, err
 }
 t := &oauth2.Token{}
 err = json.NewDecoder(f).Decode(t)
 defer f.Close()
 return t, err
 }

 // saveToken uses a file path to create a file and store the
 // token in it.
 func saveToken(file string, token *oauth2.Token) {
 fmt.Printf("Saving credential file to: %s\n", file)
 f, err := os.Create(file)
 if err != nil {
 log.Fatalf("Unable to cache oauth token: %v", err)
 }
 defer f.Close()
 json.NewEncoder(f).Encode(token)
 }

 func main() {

 ctx := context.Background()

 // process the credential file
 credential, err := ioutil.ReadFile("client_secret.json")
 if err != nil {
 log.Fatalf("Unable to read client secret file: %v", err)
 }

 // In order for POST upload attachment to work
 // You need to authorize the Gmail API v1 scope
 // at https://developers.google.com/oauthplayground/
 // otherwise you will get Authorization error in the API JSON reply

 // Use DriveScope for this example. Because of we want to Manage the files in
 // Google Drive.

 // See the rest at https://godoc.org/google.golang.org/api/drive/v3#pkg-constants

 config, err := google.ConfigFromJSON(credential, drive.DriveScope)
 if err != nil {
 log.Fatalf("Unable to parse client secret file to config: %v", err)
 }

 client := getClient(ctx, config)

 // initiate a new Google Drive service
 //driveClientService, err := drive.New(client)
 //if err != nil {
 // log.Fatalf("Unable to initiate new Drive client: %v", err)
 // }

 // get our token
 cacheFile, err := tokenCacheFile()
 if err != nil {
 log.Fatalf("Unable to get path to cached credential file. %v", err)
 }

 token, err := tokenFromFile(cacheFile)
 if err != nil {
 log.Fatalf("Unable to get token from file. %v", err)
 }

 // here comes the Resumable Upload REST method
 // https://developers.google.com/drive/v3/web/manage-uploads#resumable

 postURL := "https://www.googleapis.com/upload/drive/v3/files?uploadType=resumable"

 // read file for upload purpose
 fileName := "installgoogledrive.dmg" // <------------ CHANGE HERE!
 fileBytes, err := ioutil.ReadFile(fileName)
 if err != nil {
 log.Fatalf("Unable to read file for upload: %v", err)
 }

 fileMIMEType := http.DetectContentType(fileBytes)
 fileSize := len(string(fileBytes))

 // extract auth or access token from Token file
 // see https://godoc.org/golang.org/x/oauth2#Token
 authToken := token.AccessToken

 uploadData := []byte("\n" +
 "{ \n" +
 string('"') + "name" + string('"') + ":" + string('"') + fileName + string('"') + "\n" +
 "} \n\n")

 // STEP 1:  initiate a resumable session for the Drive API.
 request, _ := http.NewRequest("POST", postURL, strings.NewReader(string(uploadData)))
 request.Header.Add("Host", "www.googleapis.com")
 request.Header.Add("Authorization", "Bearer "+authToken)
 request.Header.Add("Content-Length", "38")
 request.Header.Add("Content-Type", "application/json; charset="+string('"')+"UTF-8"+string('"'))

 request.Header.Add("X-Upload-Content-Type", fileMIMEType)

 // optional - if we use chunked transfer encoding
 request.Header.Add("X-Upload-Content-Length", strconv.Itoa(fileSize))

 // debug
 //fmt.Println(request)

 response, err := client.Do(request)
 if err != nil {
 log.Fatalf("Unable to be post to Google API: %v", err)
 }

 defer response.Body.Close()
 _, err = ioutil.ReadAll(response.Body)

 if err != nil {
 log.Fatalf("Unable to read Google API response: %v", err)
 }

 // STEP 2: Save the resumable session URI

 // see https://golang.org/pkg/net/http/#Header
 //fmt.Println("Status : ", response.Status)
 //fmt.Println("Header : ", response.Header)
 //fmt.Println("Location : ", response.Header["Location"])

 session_uri := ""
 if response.Status == "200 OK" {
 session_uri = response.Header["Location"][0]
 } else {
 log.Fatalf("Unable to get session URI and response status is not 200")
 }

 // STEP 3: Upload the file
 putRequest, _ := http.NewRequest("PUT", session_uri, strings.NewReader(string(fileBytes)))
 putRequest.Header.Add("Content-Length", strconv.Itoa(fileSize))
 putRequest.Header.Add("Content-Type", fileMIMEType)

 putResponse, err := client.Do(putRequest)
 if err != nil {
 log.Fatalf("Unable to be post to Google API: %v", err)
 }

 defer response.Body.Close()
 _, err = ioutil.ReadAll(response.Body)

 if err != nil {
 log.Fatalf("Unable to read Google API response: %v", err)
 }

 // STEP 4 -
 // if put response returns 201 Created or 200 OK - file uploaded successfully and everything is ok
 // if put response returns 5xx - follow the procedure outlined in
 // resume an interrupted upload.
 // see https://developers.google.com/drive/v3/web/manage-uploads#resume-upload

 // debug
 //fmt.Println("-----------------------------------------")
 //fmt.Println("put status : ", putResponse.Status) // returns 200 OK instead of 201 Created

 if putResponse.Status == "200 OK" || putResponse.Status == "200 Created" {
 fmt.Println(fileName + " uploaded to Drive via REST API")
 } else {
 fmt.Println("Server responded : ", putResponse.Status)
 log.Fatalf("Try uploading again or implement the exponential backoff strategy -- some complicated stuff that sane people should avoid ")
 }

 // NOTE : While it is NOT recommended to upload file in small chunks
 // you can see
 // https://www.socketloop.com/tutorials/golang-how-to-split-or-chunking-a-file-to-smaller-pieces
 // on how chunk file into smaller pieces to use resumable file upload request

 }

References:

https://developers.google.com/drive/v3/web/manage-uploads#resumable

https://www.socketloop.com/tutorials/golang-how-to-split-or-chunking-a-file-to-smaller-pieces

https://developers.google.com/drive/v3/web/manage-downloads

  See also : Golang : Google Drive API upload and rename example





By Adam Ng

IF you gain some knowledge or the information here solved your programming problem. Please consider donating to the less fortunate or some charities that you like. Apart from donation, planting trees, volunteering or reducing your carbon footprint will be great too.


Advertisement