DEV Community

loading...
Cover image for Upload files to Google Drive with Phoenix LiveView

Upload files to Google Drive with Phoenix LiveView

Santiago Cardona
Hello! I'm Santiago, Software Engineer and Web Enthusiast with experience in backend using Elixir/Phoenix and Node.js. I also enjoy doing some frontend with React.js
Updated on ・5 min read

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 🇨🇴

Let's start

File upload form

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 and Phoenix LiveView Uploads Deep Dive guides by 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 documentation.

Make sure you create a Google Cloud Service Account and finally have the .json file with your Google Cloud Service Account credentials. This file should contain something like this:

{
  "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>"
}
Enter fullscreen mode Exit fullscreen mode

Dependencies configuration

In order to authenticate with Google services, we will use the Goth library (Google + Auth).

Add Goth to your dependencies in the file mix.exs

  defp deps do
    [
      ...
      {:goth, "~> 1.2.0"}
    ]
  end
Enter fullscreen mode Exit fullscreen mode

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:

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

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:

  @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
Enter fullscreen mode Exit fullscreen mode

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

def handle_event("cancel-entry", %{"ref" => ref}, socket) do
  {:noreply, cancel_upload(socket, :photos, ref)}
end
Enter fullscreen mode Exit fullscreen mode

Add the Phoenix HTML components for files uploading:

    ...
    <%= 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 %>
Enter fullscreen mode Exit fullscreen mode

On the JavaScript side

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

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

const Uploaders = {
  GoogleDriveMultipart: uploadPhotosToGoogleDrive
}

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

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

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()
    }
  })
}
Enter fullscreen mode Exit fullscreen mode

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

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

Great, you have your file in Google Drive!


To see the full implementation you can visit the repo:

GitHub logo santiagocardo80 / car-workshop

Car Workshop Managment Web App

CarWorkshop

To start your Phoenix server:

  • Install dependencies with mix deps.get
  • Create and migrate your database with mix ecto.setup
  • Install Node.js dependencies with npm install inside the assets directory
  • Start Phoenix endpoint with mix phx.server

Now you can visit localhost:4000 from your browser.

Ready to run in production? Please check our deployment guides.

Learn more

Discussion (0)