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 🇨🇴
Cargar archivos a Google Drive con Phoenix LiveView
Santiago Cardona ・ Apr 19 '21
Let's start
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>"
}
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
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
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
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
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 %>
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 }
})
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()
}
})
}
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).
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.
Great, you have your file in Google Drive!
To see the full implementation you can visit the repo:
santiagocardo / 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 theassets
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
- Official website: https://www.phoenixframework.org/
- Guides: https://hexdocs.pm/phoenix/overview.html
- Docs: https://hexdocs.pm/phoenix
- Forum: https://elixirforum.com/c/phoenix-forum
- Source: https://github.com/phoenixframework/phoenix
Top comments (0)