Mastering file download in shiny

Author : Vincent Guyader
Categories : shiny
Tags : shiny
Date :

But why does downloadHandler now return an empty file!?

DownloadHandler context

When we start working with {shiny}, we often reach a point where it is necessary to offer the user the option to download a document generated by the application.
This document can be a PDF report, a PNG or JPEG image, or anything else.

The appropriate function for this purpose is shiny::downloadHandler(), but beyond the (relatively) simple case shown in the documentation’s example, when it comes to using the function in a more complex scenario, it is not uncommon to encounter difficulties, even for an experienced developer.

Therefore, I would like to share with you an approach that we use at ThinkR, which handles both straightforward and more complex cases that we sometimes encounter.

TL;DR:

We will consistently use file.copy in the downloadHandler

Exemple 1: Use downloadHandler with only file.copy

Let’s look at a simple scenario together by starting with the example from the R documentation and making it more “robust”.

Before :

ui <- fluidPage(
  downloadButton(outputId = "downloadData",label =  "Download")
)
server <- function(input, output) {
  # Our dataset
  data <- mtcars
  output$downloadData <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      write.csv(data, file)
    }
  )
}
shinyApp(ui, server)

After :

ui <- fluidPage(
  downloadButton(outputId = "downloadData",label =  "Download")
)
server <- function(input, output) {
  local <- reactiveValues(data = mtcars,
                          export_file = NULL
                          )
  observeEvent(local$data,{
    out <- tempfile(fileext = ".csv")
    write.csv(x = local$data,file = out)
    local$export_file <- out
  })
  output$downloadData <- downloadHandler(
    filename = function() {
      paste("data-", Sys.Date(), ".csv", sep="")
    },
    content = function(file) {
      file.copy(
           from = local$export_file,
           to = file
         )
    }
  )
}
shinyApp(ui, server)

Do you see the difference? We regain control over the file that will be placed behind the button. It is created here within an observeEvent, making it easy to debug and understand its content.

Example 2: Manage file names independently

This approach makes complex scenarios more readable :

library(tidyverse)
library(shiny)
library(flextable)
ui <- fluidPage(
  downloadButton(outputId = "downloadData",label =  "Download")
)
server <- function(input, output) {
  local <- reactiveValues(data = mtcars,
                          export_file = NULL
  )
  observeEvent(local$data,{
    out <- tempfile(fileext = ".png",pattern = "table_")
    # write.csv(x = local$data,file = out)
    local$data %>% 
    flextable::flextable() %>% 
      flextable::save_as_image(path = out)
    if (file.exists(out)){
    local$export_file <- out
    } else{
    local$export_file <- NULL
    }
  })
  output$downloadData <- downloadHandler(
    filename = function() {
      basename(local$export_file)
    },
    content = function(file) {
      file.copy(
        from = local$export_file,
        to = file
      )
    }
  )
}
shinyApp(ui, server)

It’s your turn


About the author

Vincent Guyader

Vincent Guyader

Codeur fou, formateur et expert logiciel R


Comments


Also read