Download multiple csv files with one button (downloadhandler) with R Shiny - r

*Hi, I'm trying to download multiple csv file from a unique excel file. I want to download (using only one downloadbutton) the differents sheets from the excel file.
I don't understand why a for() loop doesn't work, and I can't see how can I do?
If anyone knows..
The point is to download differents csv files, which are in the "wb" list (wb[1],wb[2]...)
Thanks.
Here is my code who works with the third sheet for instance (and sorry for my bad english) :
ui :
library(readxl)
library(shiny)
library(XLConnect)
fluidPage(
titlePanel("Export onglets en CSV"),
sidebarLayout(
sidebarPanel(
fileInput('fichier1','Choisissez votre fichier excel :',
accept = ".xlsx"),
fluidPage(
fluidRow(
column(width = 12,
numericInput("sheet","Indiquez l'onglet à afficher :",min = 1, value = 1),
tags$hr(),
textInput('text',"Indiquez le nom des fichiers :"),
tags$hr(),
h4("Pour télécharger les fichiers .csv :"),
downloadButton("download","Télécharger")
)
)
)),
mainPanel(
tabsetPanel(
tabPanel('Importation',
h4("Fichier de base:"),
dataTableOutput("contents"))
)
)
)
)
Server :
function(input,output){
#Création data :
data <- reactive({
inFile<- input$fichier1
if (is.null(inFile)){
return(NULL)
}else{
file.rename(inFile$datapath,
paste(inFile$datapath,".xlsx", sep =""))
wb = loadWorkbook(paste(inFile$datapath,".xlsx",sep=""))
lst = readWorksheet(wb,sheet = getSheets(wb))
list(wb = wb, lst = lst)
}
})
#Sortie de la table :
output$contents <- renderDataTable({
data()$wb[input$sheet]
},options = list(pageLength = 10))
#Téléchargement :
output$download <- downloadHandler(
#for (i in 1:input$sheet){
filename = function(){
paste(input$text,"_0",3,".csv",sep = "")
},
content = function(file){
write.table(data()$wb[3],file,
sep = ';', row.names = F, col.names = T)
}
#}
)
}

As #BigDataScientist pointed out, you could zip all of your csv file and download the zipped file. Your downloadHandler could look like:
output$download <- downloadHandler(
filename = function(){
paste0(input$text,".zip")
},
content = function(file){
#go to a temp dir to avoid permission issues
owd <- setwd(tempdir())
on.exit(setwd(owd))
files <- NULL;
#loop through the sheets
for (i in 1:input$sheet){
#write each sheet to a csv file, save the name
fileName <- paste(input$text,"_0",i,".csv",sep = "")
write.table(data()$wb[i],fileName,sep = ';', row.names = F, col.names = T)
files <- c(fileName,files)
}
#create the zip file
zip(file,files)
}
)
This does not download all the sheets from the excel file but the sheets ranging from 1 to whatever the user has as input in input$sheet.
You could also disable the download button if the user has not added an excel file/name.

Hope you've solved this MBnn, but in case anyone else is having similar problems, this case is down to RTools not being installed correctly on windows.
Currently you need to play close attention while running through the install process, and make sure to hit the checkbox to edit the system path.
Based on your code, this is likely to be the same issue preventing you from saving XLSX workbooks too.

I know this is an old thread but I had the same issue and the top answer did not work for me. However a simple tweak and using the archive package worked.
Reproductible example below:
library(shiny)
library(archive)
shinyApp(
# ui
ui = fluidPage(downloadButton("dl")),
# server
server = function(input, output, session) {
# download handler
output$dl <- downloadHandler(
filename = function() {"myzipfile.zip"},
# content: iris and mtcars
content = function(file) {
# definition of content to download
to_dl <- list(
# names to use in file names
names = list(a = "iris",
b = "mtcars"),
# data
data = list(a = iris,
b = mtcars)
)
# temp dir for the csv's as we can only create
# an archive from existent files and not data from R
twd <- setwd(tempdir())
on.exit(setwd(twd))
files <- NULL
# loop on data to download and write individual csv's
for (i in c("a", "b")) {
fileName <- paste0(to_dl[["names"]][[i]], ".csv") # csv file name
write.csv(to_dl[["data"]][[i]], fileName) # write csv in temp dir
files <- c(files, fileName) # store written file name
}
# create archive from written files
archive_write_files(file, files)
}
)
}
)
This will create the zip file myzipfile.zip which will contain iris.csv and mtcars.csv.

Related

Bug with ShinyFeedback not displaying correct label with File Input

I'm building an app that takes excel files as an input. To protect against errors, I first check if the file is an .xls or .xslx, and then I check if it contains the right content based on the sheet names.
I'm encountering that the first time I upload the right file, the app works as expected:
But when I hit 'browse' and try to upload again, I get this:
It only shows the right label on the first time.
This doesn't happen if I upload an excel file with the wrong info multiple times
If I then try to upload a different type of file, I get this:
Here's the simple code
library(shiny)
library(readxl)
library(tools)
library(shinyFeedback)
ui <- fluidPage(
useShinyFeedback(),
fileInput("file", "upload file", multiple = FALSE),
tableOutput("data")
)
server <- function(input, output, session) {
data <- reactive({
req(input$file)
path <- input$file$datapath
good_file <- file_ext(path) == "xls" || file_ext(path) == "xlsx"
feedbackDanger(inputId = "file",
show=!good_file,
color = "orange",
text = "Upload only .xls or .xlsx files")
req(good_file)
good_sheet <- "Special Sheet" %in% excel_sheets(path)
feedbackDanger(inputId = "file",
show=!good_sheet,
color = "#E6007E",
text = "File must contain the special sheet'")
req(good_sheet)
read_excel(path)
})
output$data = renderTable(data())
}
shinyApp(ui, server)
Not sure, but perhaps this is due to the fact that the two feedbacks are the same.
Here is a solution:
server <- function(input, output, session) {
data <- reactive({
req(input$file)
hideFeedback("file") # hide feedback, if any
path <- input$file$datapath
good_file <- file_ext(path) == "xls" || file_ext(path) == "xlsx"
if(!good_file){
showFeedbackDanger(inputId = "file",
color = "orange",
text = "Upload only .xls or .xlsx files")
}
req(good_file)
good_sheet <- "Special Sheet" %in% excel_sheets(path)
if(!good_sheet){
showFeedbackDanger(inputId = "file",
color = "#E6007E",
text = "File must contain the special sheet'")
}
req(good_sheet)
read_excel(path)
})
output$data = renderTable(data())
}

How to download multiple files from R/Shiny app?

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:
Download button gets pressed
create some temporary directory
write (the not NULL) datasets contained in to_download reactiveValues list to this directory
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.
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)

Use R shiny to add a sheet to a pre-existing excel file with action button

I have an excel file called testfile.xlsx. the first sheet of this file is called sheet1.
I have written appended a new sheet called New_Sheet using xlsx package as follows
library(xlsx)
setwd()##set the file path to where testfile.xlsx is located
write.xlsx('new_data', "testfile.xlsx", sheetName="New_Sheet", append=TRUE)
This adds the required sheet.
I have created the following shiny app to write the sheet to the file
library(shiny)
library(xlsx)
library(openxlsx)
library(readxl)
ui <- fluidPage(
titlePanel("Writer App"),
sidebarLayout(sidebarPanel(fileInput(inputId = "file", label = "Read File Here", accept =
c(".xlsx")),actionButton(inputId = "Run", label = "Write Data to Table")),
mainPanel(dataTableOutput(outputId = "table1"))))
server <- function(input, output) {
datasetInput <- reactive({
infile<- input$file
if (is.null(infile))
return(NULL)
#READ .XLSX AND .CSV FILES
if(grepl(infile, pattern = ".xlsx" )==T){
data=read_excel(infile$datapath)
} else if(grepl(infile , pattern = ".csv" )==T)
{data=read.csv(infile$datapath )}
#RENAME DATAFRAME WITH UNDERSCORES
names(data)<-gsub(pattern = " ", replacement = "_", x = names(data))
return(data) })
output$table1 <- renderDataTable({
datasetInput()})
observeEvent(input$Run,{
infile<-input$file
testfile<-(infile[1])
filepath<-(input$file)
filepath<-gsub(pattern = "0.xlsx", replacement ="" , x = filepath)
# print(infile$datapath[1])
print(filepath)
print(testfile)
setwd(dir = filepath)
write.xlsx('new_data', testfile, sheetName="New_Sheet3", append=TRUE)})
}
shinyApp(ui = ui, server = server)
The app renders the data in the excel sheet as a table without any problems
.When we push the run app button, the print commands generate the name of the file and the filepath. The write excel function doesnt work. Is there a way to insert the new_data sheet using the action button. I request someone to guide me here.
I recommend using downloadHandler instead. See here for an example.

Use multiple reactive datasets to write to an excel workbook under downloadHandler shiny

Good afternoon, thank you in advance for taking the time to read my question. In my Shiny app, I am trying to create a list of the reactive objects to write to a single excel workbook for the user to download. I was able to use parts of responses from other posts to replicate my issue and I get pretty close to a resolution. However, while the example below uses a list of dataframes, such as mtcars, iris, etc, I am trying to use reactive datasets, such as datasetInput1(), datasetInput2(), etc.
shinyApp(
ui = fluidPage(
downloadButton("downloadExcelSheet", "Download Excel Workbook with Multiple Sheets")
),
server = function(input, output) {
#### Write an Excel workbook with one sheet per dataframe ####
output$downloadExcelSheet <- downloadHandler(
filename = function() {
"excelWorkbook.xlsx"
},
content = function(file) {
# write workbook and first sheet
write.xlsx(mtcars, file, sheetName = "mtcars", append = FALSE)
# add other sheets for each dataframe
listOtherFiles <- list(iris = iris,
airquality = airquality,
sleep = sleep)
for(i in 1:length(listOtherFiles)) {
write.xlsx(listOtherFiles[i], file,
sheetName = names(listOtherFiles)[i], append = TRUE)
}
}
)
When I try to use these reactive objects in the example below, I am able to successfully download the data when there is just one dataset in the list. For example, the below works, but once I start adding more to the list listOtherFiles such as listOtherFiles <- list(datasetInput2(), datasetInput3()), I get an error.
shinyApp(
ui = fluidPage(
downloadButton("downloadExcelSheet", "Download Excel Workbook with Multiple Sheets")
),
server = function(input, output) {
datasetInput1 <- reactive({
data %>%
filter(sub_date == input$date, app_type == input$type)
})
datasetInput2 <- reactive({
data2 %>%
filter(sub_date == input$date, app_type == input$type)
})
output$downloadExcelSheet <- downloadHandler(
filename = function() {
"datasetOutput.xlsx"
},
content = function(file) {
# write workbook and first sheet
write.xlsx(datasetInput1(), file, sheetName = "dataset1", append = FALSE)
# add other sheets for each dataframe
listOtherFiles <- list(datasetInput2())
for(i in 1:length(listOtherFiles)) {
write.xlsx(listOtherFiles[i], file,
sheetName = names(listOtherFiles)[i], append = TRUE)
}
}
)
I'm not sure I can reproduce the problem. Here is my example below. This seems to work and uses two reactive expressions. Does it work for you?
If not, please edit your question and describe further. Perhaps include example data and ui with inputs to reproduce. What was your error?
library(xlsx)
library(shiny)
library(tidyverse)
shinyApp(
ui = fluidPage(
downloadButton("downloadExcelSheet", "Download Excel Workbook with Multiple Sheets")
),
server = function(input, output) {
datasetInput1 <- reactive({
iris %>%
filter(Species == "setosa")
})
datasetInput2 <- reactive({
iris %>%
filter(Species == "versicolor")
})
#### Write an Excel workbook with one sheet per dataframe ####
output$downloadExcelSheet <- downloadHandler(
filename = function() {
"excelWorkbook.xlsx"
},
content = function(file) {
# write workbook and first sheet
write.xlsx(mtcars, file, sheetName = "mtcars", append = FALSE)
# add other sheets for each dataframe
listOtherFiles <- list(setosa = datasetInput1(), versicolor = datasetInput2())
for(i in 1:length(listOtherFiles)) {
write.xlsx(listOtherFiles[[i]], file,
sheetName = names(listOtherFiles)[i], append = TRUE)
}
}
)
}
)
datasetInput1() is a reactive value not defined within the server logic. This needs to be assigned a value first or a function created to update values.
The following are some useful articles to understand shiny's reactive elements:
https://shiny.rstudio.com/articles/understanding-reactivity.html
https://shiny.rstudio.com/articles/reactivity-overview.html

Use downloadHandler to edit an existing excel file based on uploaded data?

I am working on a shiny app where the user will upload an excel file, the data will be manipulated, and then a new excel file with this data is exported for the user to examine. I am having issues with the downloadHandler function. I used to create an entirely new excel file every time based on the uploaded data like this:
output$export <- downloadHandler(
filename = "answers.xlsx",
content = function(file){
write.xlsx(exportdata(), file)
})
})
This works fine.
Now I would like to edit an excel file that I will include when I publish the app and allow the user to download this edited version like this:
output$export <- downloadHandler(
filename = "answers.xlsx",
content = function(file){
wb <- loadWorkbook("6rep-charts.xlsx")
writeData(wb, sheet = "Species Match Results", correlInput())
writeData(wb, sheet = "BS1 Data", bs1Input())
writeData(wb, sheet = "BS2 Data", bs2Input())
saveWorkbook(wb, file)
})
However, this results in the error Warning: Error in write_file: Expecting a single string value: [type=character; extent=0]. [No stack trace available]. I am not sure what is going wrong as when I run the content section outside of the shiny app, it works just fine. The problem seems to be in the saveWorkbook command.
The reason I would like to edit an existing excel file rather than create a new one is that the template file I'm including in the app has charts already made that should change when the new data is written into the file. The users would like to be able to edit these charts themselves, rather than just see a picture of a graph. If anyone has a simpler way to accomplish this, that would be great! Thank you in advance for your help!
Reproducible example using this excel file:
library(shiny); library(readxl); library(xlsx)
ui <- shinyUI(fluidPage(
titlePanel("Old Faithful Geyser Data"),
fluidRow(
column(3,
downloadButton(outputId = "export",
label = "Export Results to Excel")
),
column(6,
dataTableOutput("data")
))))
server <- function(input, output) {
adata <- faithful[1:20,]
bdata <- faithful[21:50,]
cdata <- faithful[51:200,]
read_excel_allsheets <- function(filename, tibble = FALSE) {
sheets <- readxl::excel_sheets(filename)
x <- lapply(sheets, function(Y) {readxl::read_excel(filename, sheet = Y)})
if(!tibble) x <- lapply(x, as.data.frame)
names(x) <- sheets
x
}
output$data <- renderDataTable({
adata })
output$export <- downloadHandler(
filename = "answers.xlsx",
content = function(file){
wb <- loadWorkbook("./Data/template.xlsx")
writeData(wb, sheet = "Alpha", adata)
writeData(wb, sheet = "Beta", bdata)
writeData(wb, sheet = "Gamma", cdata)
saveWorkbook(wb, file="./Data/temp.xlsx", overwrite = T)
print("done")
Fin_WB<- read_excel_allsheets("./Data/temp.xlsx")
write.xlsx(Fin_WB, file)
} ) }
shinyApp(ui = ui, server = server)
Simpler solution (I am unable to leave comments due to too low reputation):
output$export <- downloadHandler(
filename = "answers.xlsx",
content = function(file){
wb <- loadWorkbook("./Data/template.xlsx")
writeData(wb, sheet = "Alpha", adata)
writeData(wb, sheet = "Beta", bdata)
writeData(wb, sheet = "Gamma", cdata)
saveWorkbook(wb, file="./Data/temp.xlsx", overwrite = T)
file.copy(from = "./Data/temp.xlsx", to = file)
})
No need for the function read_excel_allsheets.
The saveWorkbook function does not like to operate as a write function for the content function. Confusing yes, but we can still use it within the content function, just not as the final step. We have to use a classic write function such as write.xlsx to satisfy the content function which is expecting the file and file path to write to.
To get around this we create a temp file in the Data folder (this folder will be in the dir with server and ui, and will be published). The program reads in the template file (see special function below), modifies it, and then writes/overwrites the temp file. This temp file is then read in and assigned by read.xlsx and then written to by write.xlsx. This satisfies the content function and allows us to use a template for export.
read_excel_allsheets <- function(filename, tibble = FALSE) {
sheets <- readxl::excel_sheets(filename)
x <- lapply(sheets, function(X) readxl::read_excel(filename, sheet = X))
if(!tibble) x <- lapply(x, as.data.frame)
names(x) <- sheets
x
}
output$export <- downloadHandler(
filename = "answers.xlsx",
content = function(file){
wb <- loadWorkbook("./Data/template.xlsx")
writeData(wb, sheet = "Alpha", adata)
writeData(wb, sheet = "Beta", bdata)
writeData(wb, sheet = "Gamma", cdata)
saveWorkbook(wb, file="./Data/temp.xlsx", overwrite = T)
print("done")
Fin_WB<- read_excel_allsheets("./Data/temp.xlsx")
write.xlsx(Fin_WB, file)
})
}
In order to read in all the sheets, we cannot simply use read.xlsx as it wont look at all the sheets. Using a function created here by Jeromy Anglim, we can read in all the sheets from excel. This is the read_excel_allsheets function.

Resources