LiveView file download from RAM

I have a project where users can create a one time key in a LiveView UI. When they do this I want to trigger a download of a small text file with the private one-time key so they can’t accidentally lose it, but I also don’t want to (even temporarily) persist the private key to the filesystem on the server. I’m looking for the best/simplest way to do this with current Phoenix and LiveView.

I’ve already googled how to do file downloads and the results I get are a little confusing, because I think the available options especially from a LiveView seem to have changed over time. My understanding is basically you have to redirect the browser to a controller with an GET action to do the download, and there are different ways to do the redirect which have different impacts to either keeping the LiveView process running or not. I’ve prototyped that and can at least send a text file with send_download/3, but I’m unsure how to get the key I generated in the LiveView process into the controller. I’m considering something with a named ETS table that the LiveView process writes the data into, and the download controller reads from. Even with that I’m not sure how to associate the download to the specific LiveView session it was redirected from (I don’t want User1 to get User2’s key for example). At this point I’m now a little unsure if I might be headed in the wrong direction entirely.

I’m very inexperienced with web development and Elixir so please bear with me if this question seems poorly formulated… I’d really appreciate any suggestions (or examples!) about good approaches to do this. Thanks!

For just a simple text file you could do this in your LiveView.

push_event(socket, "download:txt", %{filename: "...", text: "..."})

In app.js window.addEventListener("download:txt")

Or in a hook with this.handleEvent("download:txt")

Then google “download text file with js”

The way I do this for generated documents is like this:

I have a hook setup like this:

Hooks.Download = {
  mounted() {
    this.handleEvent("download_file", ({ content, filename, content_type }) => {
      // Create blob from content
      const blob = new Blob([content], { type: content_type || 'application/octet-stream' })

      // Create download link
      const url = URL.createObjectURL(blob)
      const a = document.createElement('a')
      a.href = url
      a.download = filename
      document.body.appendChild(a)
      a.click()
      document.body.removeChild(a)
      URL.revokeObjectURL(url)
    })
  }
}

Then in my code once I return this from a handle_event:


        {:noreply,
         socket
         |> push_event("download_file", %{
           filename: filename,
           content: xml_content,
           content_type: "application/xml"
         })}

In my case it’s an XML, but you get the drift.

The javascript creates an achor tag with the file to download and clicks it triggering the download and then removes it again from the DOM.

There’s no reason to append a child to the actual DOM. You can use a fragment.

const fragment = document.createDocumentFragment();
const blob = new Blob([content], { type: content_type || 'application/octet-stream' })

// Create download link
const url = URL.createObjectURL(blob)
const a = document.createElement('a')

a.href = url
a.download = filename

fragment.appendChild(a)
a.click()
URL.revokeObjectURL(url)