Transferring files to and from the user is a common feature of apps. You can use it to upload data for analysis or download the results as a dataset or as a report. This chapter shows the UI and server components that you’ll need to transfer files in and out of your app. We begin by loading shiny:
library
(
shiny
)
We’ll start by discussing file uploads, showing you the basic UI and server components, and then showing how they fit together in a simple app.
The UI needed to support file uploads is simple: just add fileInput()
to your UI:
ui
<-
fluidPage
(
fileInput
(
"upload"
,
"Upload a file"
)
)
Like most other UI components, there are only two required arguments: id
and label
. The width
, buttonLabel
, and placeholder
arguments allow you to tweak the appearance in other ways. I won’t discuss them here, but you can read more about them in ?fileInput
.
Handling fileInput()
on the server is a little more complicated than other inputs. Most inputs return simple vectors, but fileInput()
returns a data frame with four columns:
name
The original filename on the user’s computer.
size
The file size, in bytes. By default, the user can only upload files up to 5 MB. You can increase this limit by setting the shiny.maxRequestSize
option prior to starting Shiny—to allow up to 10 MB run options(shiny.maxRequestSize = 10 * 1024^2)
, for example.
type
The “MIME type” of the file.1 This is a formal specification of the file type that is usually derived from the extension and is rarely needed in Shiny apps.
datapath
The path to where the data has been uploaded on the server. Treat this path as ephemeral: if the user uploads more files, this file may be deleted. The data is always saved to a temporary directory and given a temporary name.
I think the easiest way to understand this data structure is to make a simple app. Run the following code and upload a few files to get a sense of what data Shiny is providing:
ui
<-
fluidPage
(
fileInput
(
"upload"
,
NULL
,
buttonLabel
=
"Upload..."
,
multiple
=
TRUE
),
tableOutput
(
"files"
)
)
server
<-
function
(
input
,
output
,
session
)
{
output
$
files
<-
renderTable
(
input
$
upload
)
}
You can see the results after I uploaded a couple of puppy photos (from “Images”) in Figure 9-1.
Note my use of the label
and buttonLabel
arguments to mildly customize the appearance, and my use of multiple = TRUE
to allow the user to upload multiple files.
If the user is uploading a dataset, there are two details that you need to be aware of:
input$upload
is initialized to NULL
on page load, so you’ll need req(input$file)
to make sure your code waits until the first file is uploaded.
The accept
argument allows you to limit the possible inputs. The easiest way is to supply a character vector of file extensions, like accept = ".csv"
. But the accept
argument is only a suggestion to the browser and is not always enforced, so it’s good practice to also validate it (e.g., “Validation”) yourself. The easiest way to get the file extension in R is tools::file_ext()
, but just be aware that it removes the leading .
from the extension.
Putting all these ideas together gives us the following, where you can upload a .csv
or .tsv
file and see the first n
rows:
ui
<-
fluidPage
(
fileInput
(
"file"
,
NULL
,
accept
=
c
(
".csv"
,
".tsv"
)),
numericInput
(
"n"
,
"Rows"
,
value
=
5
,
min
=
1
,
step
=
1
),
tableOutput
(
"head"
)
)
server
<-
function
(
input
,
output
,
session
)
{
data
<-
reactive
({
req
(
input
$
file
)
ext
<-
tools
::
file_ext
(
input
$
file
$
name
)
switch
(
ext
,
csv
=
vroom
::
vroom
(
input
$
file
$
datapath
,
delim
=
","
),
tsv
=
vroom
::
vroom
(
input
$
file
$
datapath
,
delim
=
" "
),
validate
(
"Invalid file; Please upload a .csv or .tsv file"
)
)
})
output
$
head
<-
renderTable
({
head
(
data
(),
input
$
n
)
})
}
See it in action in the live app.
Note that since multiple = FALSE
(the default), input$file
will be a single-row data frame, and input$file$name
and input$file$datapath
will be a length-1 character vector.
Next, we’ll look at file downloads, showing you the basic UI and server components, then demonstrating how you might use them to allow the user to download data or reports.
Again, the UI is straightforward: use downloadButton(id)
or downloadLink(id)
to give the user something to click to download a file:
ui
<-
fluidPage
(
downloadButton
(
"download1"
),
downloadLink
(
"download2"
)
)
The results are shown in Figure 9-2.
You can customize their appearance using the same class
and icon
arguments as for actionButtons()
, as described in “Action Buttons”.
Unlike other outputs, downloadButton()
is not paired with a render function. Instead, you use downloadHandler()
, which looks something like this:
output
$
download
<-
downloadHandler
(
filename
=
function
()
{
paste0
(
input
$
dataset
,
".csv"
)
},
content
=
function
(
file
)
{
write.csv
(
data
(),
file
)
}
)
downloadHandler()
has two arguments, both functions:
filename
A function with no arguments that returns a filename (as a string). The job of this function is to create the name that will be shown to the user in the download dialog box.
content
A function with one argument, file
, which is the path to save the file. The job of this function is to save the file in a place that Shiny knows about so it can then send it to the user.
This is an unusual interface, but it allows Shiny to control where the file should be saved (so it can be placed in a secure location) while you still control the contents of that file.
Next we’ll put these pieces together to show how to transfer data files or reports to the user.
The following app shows off the basics of data download by allowing you to download any dataset in the datasets package as a tab-separated file, as shown in Figure 9-3. I recommend using .tsv
(tab-separated values) instead of .csv
(comma-separated values) because many European countries use commas to separate the whole and fractional parts of a number (e.g., 1,23
versus 1.23
). This means they can’t use commas to separate fields and instead use semicolons in so-called CSV files! You can avoid this complexity by using tab-separated files, which work the same way everywhere:
ui
<-
fluidPage
(
selectInput
(
"dataset"
,
"Pick a dataset"
,
ls
(
"package:datasets"
)),
tableOutput
(
"preview"
),
downloadButton
(
"download"
,
"Download .tsv"
)
)
server
<-
function
(
input
,
output
,
session
)
{
data
<-
reactive
({
out
<-
get
(
input
$
dataset
,
"package:datasets"
)
if
(
!
is.data.frame
(
out
))
{
validate
(
paste0
(
"'"
,
input
$
dataset
,
"' is not a data frame"
))
}
out
})
output
$
preview
<-
renderTable
({
head
(
data
())
})
output
$
download
<-
downloadHandler
(
filename
=
function
()
{
paste0
(
input
$
dataset
,
".tsv"
)
},
content
=
function
(
file
)
{
vroom
::
vroom_write
(
data
(),
file
)
}
)
}
Note the use of validate()
to only allow the user to download datasets that are data frames. A better approach would be to prefilter the list, but this lets you see another application of validate()
.
As well as downloading data, you may want the users of your app to download a report that summarizes the result of interactive exploration in the Shiny app. This is quite a lot of work, because you also need to display the same information in a different format, but it is very useful for high-stakes apps.
One powerful way to generate such a report is with a parameterized RMarkdown document. A parameterized RMarkdown file has a params
field in the YAML
metadata:
title
:
My Document
output
:
html_document
params
:
year
:
2018
region
:
Europe
printcode
:
TRUE
data
:
file.csv
Inside the document, you can refer to these values using elements of the params
list (e.g., params$year
, params$region
). The values in the YAML metadata are defaults; you’ll generally override them by providing the params
argument in a call to rmarkdown::render()
. This makes it easy to generate many different reports from the same .Rmd.
Here’s a simple example adapted from “Generating downloadable reports”, which describes this technique in more detail. The key idea is to call rmarkdown::render()
from the content
argument of downloadHandler()
:
ui
<-
fluidPage
(
sliderInput
(
"n"
,
"Number of points"
,
1
,
100
,
50
),
downloadButton
(
"report"
,
"Generate report"
)
)
server
<-
function
(
input
,
output
,
session
)
{
output
$
report
<-
downloadHandler
(
filename
=
"report.html"
,
content
=
function
(
file
)
{
params
<-
list
(
n
=
input
$
n
)
id
<-
showNotification
(
"Rendering report..."
,
duration
=
NULL
,
closeButton
=
FALSE
)
on.exit
(
removeNotification
(
id
),
add
=
TRUE
)
rmarkdown
::
render
(
"report.Rmd"
,
output_file
=
file
,
params
=
params
,
envir
=
new.env
(
parent
=
globalenv
())
)
}
)
}
If you want to produce other output formats, just change the output format in the .Rmd, and make sure to update the extension (e.g., to .pdf). See it in action in the live app. It’ll generally take at least a few seconds to render an .Rmd, so this is a good place to use a notification from “Notifications”.
There are a couple of other tricks worth knowing about:
RMarkdown works in the current working directory, which will fail in many deployment scenarios (e.g., on shinyapps.io). You can work around this by copying the report to a temporary directory when your app starts (i.e., outside of the server function):
report_path
<-
tempfile
(
fileext
=
".Rmd"
)
file.copy
(
"report.Rmd"
,
report_path
,
overwrite
=
TRUE
)
Then replace "report.Rmd"
with report_path
in the call to rmarkdown::render()
:
rmarkdown
::
render
(
report_path
,
output_file
=
file
,
params
=
params
,
envir
=
new.env
(
parent
=
globalenv
())
)
By default, RMarkdown will render the report in the current process, which means that it will inherit many settings from the Shiny app (like loaded packages, options, etc.). For greater robustness, I recommend running render()
in a separate R session using the callr package:
render_report
<-
function
(
input
,
output
,
params
)
{
rmarkdown
::
render
(
input
,
output_file
=
output
,
params
=
params
,
envir
=
new.env
(
parent
=
globalenv
())
)
}
server
<-
function
(
input
,
output
)
{
output
$
report
<-
downloadHandler
(
filename
=
"report.html"
,
content
=
function
(
file
)
{
params
<-
list
(
n
=
input
$
slider
)
callr
::
r
(
render_report
,
list
(
input
=
report_path
,
output
=
file
,
params
=
params
)
)
}
)
}
You can see all these pieces put together in rmarkdown-report/
, found inside the Mastering Shiny GitHub repo.
The shinymeta package solves a related problem: sometimes you need to be able to turn the current state of a Shiny app into a reproducible report that can be rerun in the future. Joe Cheng’s useR! 2019 keynote “Shiny’s Holy Grail: Interactivity with Reproducibility” offers more information.
To finish up, we’ll work through a small case study where we upload a file (with user-supplied separator), preview it, perform some optional transformations using the janitor package, by Sam Firke, and then let the user download it as a .tsv
.
To make it easier to understand how to use the app, I’ve used sidebarLayout()
to divide the app into three main steps:
Uploading and parsing the file:
ui_upload
<-
sidebarLayout
(
sidebarPanel
(
fileInput
(
"file"
,
"Data"
,
buttonLabel
=
"Upload..."
),
textInput
(
"delim"
,
"Delimiter (leave blank to guess)"
,
""
),
numericInput
(
"skip"
,
"Rows to skip"
,
0
,
min
=
0
),
numericInput
(
"rows"
,
"Rows to preview"
,
10
,
min
=
1
)
),
mainPanel
(
h3
(
"Raw data"
),
tableOutput
(
"preview1"
)
)
)
Cleaning the file:
ui_clean
<-
sidebarLayout
(
sidebarPanel
(
checkboxInput
(
"snake"
,
"Rename columns to snake case?"
),
checkboxInput
(
"constant"
,
"Remove constant columns?"
),
checkboxInput
(
"empty"
,
"Remove empty cols?"
)
),
mainPanel
(
h3
(
"Cleaner data"
),
tableOutput
(
"preview2"
)
)
)
Downloading the file:
ui_download
<-
fluidRow
(
column
(
width
=
12
,
downloadButton
(
"download"
,
class
=
"btn-block"
))
)
These get assembled into a single fluidPage()
:
ui
<-
fluidPage
(
ui_upload
,
ui_clean
,
ui_download
)
This same organization makes it easier to understand the app:
server
<-
function
(
input
,
output
,
session
)
{
# Upload ---------------------------------------------------------
raw
<-
reactive
({
req
(
input
$
file
)
delim
<-
if
(
input
$
delim
==
""
)
NULL
else
input
$
delim
vroom
::
vroom
(
input
$
file
$
datapath
,
delim
=
delim
,
skip
=
input
$
skip
)
})
output
$
preview1
<-
renderTable
(
head
(
raw
(),
input
$
rows
))
# Clean ----------------------------------------------------------
tidied
<-
reactive
({
out
<-
raw
()
if
(
input
$
snake
)
{
names
(
out
)
<-
janitor
::
make_clean_names
(
names
(
out
))
}
if
(
input
$
empty
)
{
out
<-
janitor
::
remove_empty
(
out
,
"cols"
)
}
if
(
input
$
constant
)
{
out
<-
janitor
::
remove_constant
(
out
)
}
out
})
output
$
preview2
<-
renderTable
(
head
(
tidied
(),
input
$
rows
))
# Download -------------------------------------------------------
output
$
download
<-
downloadHandler
(
filename
=
function
()
{
paste0
(
tools
::
file_path_sans_ext
(
input
$
file
$
name
),
".tsv"
)
},
content
=
function
(
file
)
{
vroom
::
vroom_write
(
tidied
(),
file
)
}
)
}
Figure 9-4 displays the result.
Use the ambient package by Thomas Lin Pedersen to generate worley noise and download a PNG of it.
Create an app that lets you upload a CSV file, select a variable, and then perform a t.test()
on that variable. After the user has uploaded the CSV file, you’ll need to use updateSelectInput()
to fill in the available variables. See “Updating Inputs” for details.
Create an app that lets the user upload a CSV file, select one variable, draw a histogram, and then download the histogram. For an additional challenge, allow the user to select from .png
, .pdf
, and .svg
output formats.
Write an app that allows the user to create a Lego mosaic from any .png
file using Ryan Timpe’s brickr package. Once you’ve completed the basics, add controls to allow the user to select the size of the mosaic (in bricks), and choose whether to use “universal” or “generic” color palettes.
The final app in “Case Study” contains this one large reactive:
tidied
<-
reactive
({
out
<-
raw
()
if
(
input
$
snake
)
{
names
(
out
)
<-
janitor
::
make_clean_names
(
names
(
out
))
}
if
(
input
$
empty
)
{
out
<-
janitor
::
remove_empty
(
out
,
"cols"
)
}
if
(
input
$
constant
)
{
out
<-
janitor
::
remove_constant
(
out
)
}
out
})
Break it up into multiple pieces so that (for example) janitor::make_clean_names()
is not rerun when input$empty
changes.
In this chapter, you’ve learned how to transfer files to and from the user using fileInput()
and downloadButton()
. Most of the challenges arise either handling the uploaded files or generating the files to download, so I showed you how to handle a couple of common cases. If I didn’t cover your specific challenge here, you’ll need to apply your own unique creativity to the problem .
The next chapter will help you handle a common challenge when working with user supplied data: you need to dynamically adapt the user interface to better fit the data. I’ll start with some simple techniques that are easy to understand and can be applied in many situations, and gradually work my way up to fully a dynamic user interface generated by code.
1 MIME type is short for “multipurpose internet mail extensions type.” As you might guess from the name, it was originally designed for email systems, but now it’s used widely across many internet tools. A MIME type looks like type/subtype
. Some common examples are text/csv
, text/html
, image/png
, application/pdf
, application/vnd.ms-excel
(excel file).
3.147.104.120