4

There are a number of different Q/A's regarding this topic on SO, but none that I have been able to find that fit my use-case. I am also very surprised that RStudio / the Shiny developers themselves have not come out with some documentation on how to do this. Regardless, take this example application:

library(shiny)
library(glue)
library(tidyverse)

# Define UI for application 
ui <- fluidPage(
    # Application title
    titlePanel("Test Multi-File Download"),
    p("I hope this works!"),
    downloadButton(
        outputId = "download_btn",
        label = "Download",
        icon = icon("file-download")
    )
)

# Define server logic 
server <- function(input, output) {
    
    #datasets stored in reactiveValues list
    to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
    blahblah <- iris
    
    output$download_btn <- downloadHandler(
        filename = function(){
            paste("my_data_", Sys.Date(), ".csv", sep = "")
        },
        content = function(file){
            
           #works
           #readr::write_csv(blahblah, file)
 
                
            #Attempt 1
           #  #create some temp directory
           #  temp_directory <- tempdir()
           # browser()
           #   reactiveValuesToList(to_download) %>%
           #       #x is data, y is name
           #      imap(function(x,y){
           #          #browser()
           #          #check if data is not null
           #          if(!is.null(x)){
           #              #create file name based on name of dataset
           #              file_name <- glue("{y}_data.csv")
           #              #write file to temp directory
           #              readr::write_csv(x, file_name)
           #          }
           #      })
            
            # zip::zip(
            #     zipfile = file,
            #     files = ls(temp_directory),
            #     root = temp_directory
            # )

        }
        
    )

    
}

# Run the application 
shinyApp(ui = ui, server = server)

I have some datasets that are stored in a reactiveValues list, and I would like the user to be able to download them all. Ideally, I'd like for them just to be able to download multiple files all at once, rather than having to zip them up, and then download a .zip file. Another option I would be okay with is to add each dataset to an Excel sheet, then download the multi-sheet Excel file. My general thought process (on the former) is as follows:

  1. Download button gets pressed
  2. create some temporary directory
  3. write (the not NULL) datasets contained in to_download reactiveValues list to this directory
  4. zip the temp directory and download

I feel like I am very close, however I have not been able to successfully get this work yet. Any ideas?

Edit 1: I am aware of the proposed answer here, but would like to avoid using setwd() because I believe it is bad practice to mess with working directories from within a Shiny application.

Kyle Weise
  • 869
  • 1
  • 8
  • 29
  • Does this answer your question? [Download multiple csv files with one button (downloadhandler) with R Shiny](https://stackoverflow.com/questions/43916535/download-multiple-csv-files-with-one-button-downloadhandler-with-r-shiny) – NelsonGon Dec 16 '21 at 22:04
  • 2
    @NelsonGon I have read through this question, and it seems to me (very) bad practice to use `setwd()` within a Shiny app. I would like to avoid this if at all possible. – Kyle Weise Dec 16 '21 at 22:06
  • For the multi_excel (ah, I am stuck in tidyverse_syntax) sheet, I think you can loop through reactiveVals on button click and write to sheet with openxl(sx) or some other excel writer. – NelsonGon Dec 16 '21 at 22:06
  • @NelsonGon Yes, I had tried that (using `writexl::write_xlsx()`) in a similar way to how I have commented out, inside an `imap()`, but was unsuccessful with then providing some conditional to test whether data is `NULL` or not. – Kyle Weise Dec 16 '21 at 22:13

1 Answers1

5

A few things edited and it's working:

  • using dir instead of ls inside the zip::zip call to show the contents of the temp directory (ls lists R environment rather than directory contents)
  • as a further suggestion: making a new, unique folder inside tempdir() to ensure only relevant files are added.
library(shiny)
library(glue)
library(tidyverse)

# Define UI for application 
ui <- fluidPage(
  # Application title
  titlePanel("Test Multi-File Download"),
  p("I hope this works!"),
  downloadButton(
    outputId = "download_btn",
    label = "Download",
    icon = icon("file-download")
  )
)

# Define server logic 
server <- function(input, output) {
  
  #datasets stored in reactiveValues list
  to_download <- reactiveValues(dataset1 = iris, dataset2 = airquality, dataset3 = mtcars, dataset4 = NULL)
  blahblah <- iris
  
  output$download_btn <- downloadHandler(
    filename = function(){
      paste("my_data_", Sys.Date(), ".zip", sep = "")
    },
    content = function(file){
      
       temp_directory <- file.path(tempdir(), as.integer(Sys.time()))
       dir.create(temp_directory)
       
        reactiveValuesToList(to_download) %>%
           imap(function(x,y){
               if(!is.null(x)){
                   file_name <- glue("{y}_data.csv")
                   readr::write_csv(x, file.path(temp_directory, file_name))
               }
           })
        
      
      zip::zip(
          zipfile = file,
          files = dir(temp_directory),
          root = temp_directory
      )
      
      

    },
    contentType = "application/zip"
    
  )
  
  
}

shinyApp(ui = ui, server = server)

In my own Shiny app I had used a multi-worksheet approach as you suggested above. An alternative setup which works to produce a multi-sheet xlsx workbook using openxlsx could be:

...

output$download_btn <- downloadHandler(
    filename = function(){
      paste("my_data_", Sys.Date(), ".xlsx", sep = "")
    },
    content = function(file){
      
       wb <- createWorkbook()
       
        reactiveValuesToList(to_download) %>%
           imap(function(x,y){
               if(!is.null(x)){
                 addWorksheet(wb, sheetName = y)
                 writeData(wb, x, sheet = y)
               }
           })
        
        saveWorkbook(wb, file = file)
    },
    contentType = "file/xlsx"
    
  )

...

Created on 2021-12-16 by the reprex package (v2.0.1)

Andy Baxter
  • 5,833
  • 1
  • 8
  • 22
  • 1
    Thanks for the help, Andy! I appreciate it. – Kyle Weise Dec 17 '21 at 02:50
  • 1
    Just out of curiosity Andy, do you have any insight on how this could be refactored to not need to `zip` ? i.e., just download 3 files at once? – Kyle Weise Dec 17 '21 at 13:29
  • 1
    hmm that's a good question, and I think outwith the bounds of normal shiny tools to do ([download handler takes one file](https://github.com/rstudio/shiny/blob/main/R/shiny.R#L2001)). You could perhaps make a button manually to prompt it via some [javascript](https://stackoverflow.com/questions/18451856/how-can-i-let-a-user-download-multiple-files-when-a-button-is-clicked)? I wonder if it's a little rude in internet etiquette to do so though :P – Andy Baxter Dec 17 '21 at 15:11
  • FWIW - I quite like your `zip` method here, might make a mental note of that as a tool for future apps! – Andy Baxter Dec 17 '21 at 15:12
  • 1
    Yeah, agreed. `zip` it is then! Thanks again for the help, and appreciate the compliment. – Kyle Weise Dec 17 '21 at 15:49