Upload files to Google Drive with Phoenix LiveView
Tags: elixir, phoenix, liveview, javascript
Published: 2021-04-21 00:34:44

Phoenix LiveView has a great abstraction when it comes to uploading files. It makes our life much easier both from the client side (user interface) and from the server side (our server or even to third party services such as cloud services).

This post is focused on helping you with uploading files to the Google Drive service since many times Google services are difficult to understand and it takes time to figure out how to make successful integration, as was my case. That's why I would like to share with the community how I managed to do it after a long time of searching and testing.

You can also see this post in Spanish 🇨🇴 
{% link https://dev.to/santiagocardo/cargar-archivos-a-google-drive-con-phoenix-liveview-5581 %}

### Let's start

![File upload form](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5n7n9ykkr4oxnq37mv7e.png)

This guide is not intended to show the detail of how the file upload process works in Phoenix LiveView. Rather, it is intended to show the integration of Google Drive with what the Phoenix LiveView [External Uploads](https://hexdocs.pm/phoenix_live_view/uploads-external.html) and [Phoenix LiveView Uploads Deep Dive](https://www.phoenixframework.org/blog/phoenix-live-view-upload-deep-dive) guides by [Chris McCord] (https://twitter.com/chris_mccord) already show in a super clear and easy to understand way.

For details on how the file upload process works in Phoenix LiveView, you can refer to the guides mentioned above.

#### Prerequisites
 
The first thing we must take into account is that we must enable access to the Google Drive API, this can be achieved by visiting the [Google Drive API](https://developers.google.com/drive/api/v3/about-sdk) documentation.

Make sure you create a [Google Cloud Service Account] (https://console.cloud.google.com/iam-admin/serviceaccounts) and finally have the **.json** file with your Google Cloud Service Account credentials. This file should contain something like this:

```json
{
  "type": "service_account",
  "project_id": "<your google cloud project>",
  "private_key_id": "<your private key id>",
  "private_key": "<your private key>",
  "client_email": "<your client email>",
  "client_id": "<your client id>",
  "auth_uri": "https://accounts.google.com/o/oauth2/auth",
  "token_uri": "https://oauth2.googleapis.com/token",
  "auth_provider_x509_cert_url": "https://www.googleapis.com/oauth2/v1/certs",
  "client_x509_cert_url": "<your client x509 cert url>"
}
```

#### Dependencies configuration
In order to authenticate with Google services, we will use the [Goth](https://hexdocs.pm/goth/api-reference.html) library (Google + Auth).

Add Goth to your dependencies in the file **mix.exs**
```elixir
  defp deps do
    [
      ...
      {:goth, "~> 1.2.0"}
    ]
  end
```

Run the following line in the console to download the dependencies:
> $ mix deps.get

And finally, we must expose an environmental variable called **GOOGLE_APPLICATION_CREDENTIALS** in order to let Goth obtain an *access token*:
> $ export GOOGLE_APPLICATION_CREDENTIALS=\<your_service_account_file>\.json

Where \<your_service_account_file> is the path to the **.json** file containing your Google Cloud Service Account credentials.

(This file has sensitive info, you shouldn't add it to your code repository)

### Let's go to the code...
#### On the LiveView side
In our LiveView file we must enable file uploads. To do this in the **mount** function, we modify:

```elixir
  def mount(socket) do
    {:ok,
     allow_upload(
       socket,
       :photos,
       accept: ~w(.png .jpeg .jpg),
       max_entries: 2,
       external: &presign_entry/2
     )}
  end
```

The *accept* property enables file uploads by accepting only **.png**, **.jpeg** or **.jpg** formats, in this case. The *max_entries* property allows the upload of a maximum of two (2) files.

The *external* property must be a callback function with two parameters. This function will actually be in charge of calling the function that uploads the files to Google Drive, passing it the necessary data to perform the upload. When we use *external* it is because the function that will be in charge of uploading the files will be a JavaScript function (on the client side). We will do it with JavaScript, since later we want to know the upload progress of each of the files, to know if there is an error in this upload process, or if there is an error in the validation of the files. All this using JavaScript's *XMLHttpRequest*.

Add the **presign_entry** function in your LiveView file:

```elixir
  @google_drive_url "https://www.googleapis.com/upload/drive/v3/files?uploadType=multipart"
  @google_drive_scope "https://www.googleapis.com/auth/drive.file"
  defp presign_entry(entry, socket) do
    {:ok, %{token: token}} = Goth.Token.for_scope(@google_drive_scope)

    fields = %{
      name: "#{entry.uuid}.#{ext(entry)}",
      content_type: entry.client_type,
      token: token
    }

    {:ok, %{uploader: "GoogleDriveMultipart", url: @google_drive_url, fields: fields}, socket}
  end

  defp ext(entry) do
    [ext | _] = MIME.extensions(entry.client_type)
    ext
  end
```

Add the **handle_event** callback function to handle the event of canceling the upload of one of the files:

```elixir
def handle_event("cancel-entry", %{"ref" => ref}, socket) do
  {:noreply, cancel_upload(socket, :photos, ref)}
end
```

Add the Phoenix HTML components for files uploading:

```elixir
    ...
    <%= live_file_input @uploads.photos %>

    <%= for {_ref, msg} <- @uploads.photos.errors do %>
      <p class="alert alert-danger">
        <%= Phoenix.Naming.humanize(msg) %>
      </p>
    <% end %>

    <%= for entry <- @uploads.photos.entries do %>
      <%= live_img_preview(entry) %>
      <progress max="100" value="<%= entry.progress %>" />
      <a
        href="#"
        phx-click="cancel-entry"
        phx-value-ref="<%= entry.ref %>"
      >
        Cancel
      </a>
    <% end %>
```

#### On the JavaScript side

In the **app.js** file enable external file uploads by adding the following:

```javascript
import { uploadPhotosToGoogleDrive } from "./uploaders/google-drive"

const Uploaders = {
  GoogleDriveMultipart: uploadPhotosToGoogleDrive
}

let liveSocket = new LiveSocket("/live", Socket, {
  uploaders: Uploaders,
  params: { _csrf_token: csrfToken }
})
```

In the *uploaders* folder create the **google-drive.js** file adding the following:

```javascript
const createRequestPayload = (fields, photo) => {
  const boundary = 'uploading photos'
  const multipartRequestHeaders = [
    ['Content-Type', `multipart/related; boundary="${boundary}"`],
    ['Authorization', `Bearer ${fields.token}`]
  ]
  const delimiter = "\r\n--" + boundary + "\r\n"
  const close_delim = "\r\n--" + boundary + "--"
  const contentType = fields.content_type
  const metadata = {
    'name': fields.name,
    'mimeType': contentType,
    'parents': [fields.parent]
  }

  const base64Data = btoa(photo)
  const multipartRequestBody =
    delimiter +
    'Content-Type: application/json; charset=UTF-8\r\n\r\n' +
    JSON.stringify(metadata) +
    delimiter +
    'Content-Type: ' + contentType + '\r\n' +
    'Content-Transfer-Encoding: base64\r\n' +
    '\r\n' +
    base64Data +
    close_delim

  return {
    multipartRequestHeaders,
    multipartRequestBody
  }
}

export const uploadPhotosToGoogleDrive = (entries, onViewError) => {
  entries.forEach(entry => {
    const { file, meta: { url, fields } } = entry

    const reader = new FileReader()

    reader.readAsBinaryString(file)
    reader.onload = () => {
      const {
        multipartRequestHeaders,
        multipartRequestBody
      } = createRequestPayload(fields, reader.result)

      const xhr = new XMLHttpRequest()
      onViewError(() => xhr.abort())

      xhr.onprogress = event => {
        if (event.lengthComputable) {
          const percent = Math.round((event.loaded / event.total) * 100)
          entry.progress(percent)
        }
      }

      xhr.open("POST", url, true)
      multipartRequestHeaders.forEach(([key, value]) => {
        xhr.setRequestHeader(key, value)
      })

      xhr.send(multipartRequestBody)

      xhr.onload = () => {
        if (xhr.status !== 200) {
          return entry.error()
        }
      }
      xhr.onerror = () => entry.error()
    }
  })
}
```

### That's it! Let's try...

When testing the file upload we can see how the upload progress shows the bar at 100% complete (in green color).
![File successfully uploaded](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/sdbumwf3mkzvcg9z1hzh.png)

And finally, in the DevTools we can see a successful response obtained by Google Drive API from which we can know the kind of upload, the ID of the file on Google Drive, the name and format of the file.
![Response de Google Drive](https://dev-to-uploads.s3.amazonaws.com/uploads/articles/5kv594f7hbl4dhd06om4.png)

## Great, you have your file in Google Drive!

---
To see the full implementation you can visit the repo:
{% github santiagocardo/car-workshop %}