Chapter 8 Uploads and downloads

Transferring files to and from the user is a common feature of apps. It’s most commonly used to upload data for analysis, or download the results either as a dataset or as a report. This chapter shows the UI and server components you’ll need to files in and out of your app.

8.1 Upload

8.1.1 UI

The UI needed to support file uploads is simple. Like most other UI components, you just need to supply the id and label arguments: fileInput(id, label). The width, buttonLabel and placeholder arguments allow you to tweak the appearance in other ways. I won’t discuss them further here, but you can read more about them in ?fileInput.

8.1.2 Server

Most of the complication arises on the server side because unlike other inputs which return short vectors, input$file returns a data frame with four columns:

  • name: the original file name on the uploader’s computer.

  • size: the file size in bytes. By default, you can only upload files up to 5 MB is file. You can increase this limit to (e.g.) 10 MB with options(shiny.maxRequestSize = 10 * 1024^2)

  • type: the “mime type” of the file. This is a formal specification of the file type, which is usually derived from the extension. It is rarely needed.

  • datapath: the path to where the path has been uploaded to the server. The file is always saved to a temporary directory and given a temporary number. Treat this path as ephemeral: if the user uploads more files, it will go away.

I think the easiest way to get 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.

Note my use of the label and buttonLabel arguments to mildly customise the appearance, and use of multiple = TRUE to allow the user to upload multiple files.

8.1.3 Uploading data

If the user is uploading a dataset, there are three details that you need to be aware of:

  • input$file is initialised to NULL on page load, and so you’ll need req(input$file) to make sure your reactive 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". (You can also use mime types, see ?fileInput for more details.)

  • The accept argument is only a suggestion to the browser, and is not always enforced, so you should also use validate() to check the extension18 in your server function.

Putting all these ideas together gives us the following app where you can upload a .csv file and see the first few rows:

Note that since multiple = FALSE (the default), input$file is a single row data frame, and input$file$filename is a length-1 character vector.

8.2 Download

8.2.1 UI

Again, the UI is straightforward: use either downloadButton(id) or downloadLink(id) to give the user something to click to download a file. They do have o ther arguments, but you’ll hardly ever need them; see ?downloadButton for the details.


8.2.2 Server

The server side of downloads is different to the other ouputs as you don’t use a render function. Instead, you use downloadHandler(), which takes two arguments:

  • filename is a function with no arguments that returns a single string giving the name of the file. This is the string that will appear in the download dialog box. If the file name is constant (i.e. it doesn’t depend on anything else in the app), you can skip the function and just set it to a string.

  • content is a function with a single argument, file, which is the path where your should place the file to be downloaded. The return argument of this function is ignored; it’s called purely for its side-effect of creating a file at the given path.

For example, if you wanted to provide a .csv file, your server function might contain a snippet like this:

8.2.4 Downloading reports

Another common use of downloads is to prepare a report that summarises the result of interactive exploration in the Shiny app. One powerful way to generate such a report is with a parameterised RMarkdown document, A parameterised RMarkdown file has a params field in the YAML metadata:

Inside the document you can refer to those values with params$year, params$region etc.

What makes this technique powerful is you can also set the parameters using code, by calling rmarkdown::render() with the params argument. This makes it possible to generate a range of RMarkdown reports from a single app.

Here’s a simple example taken from, which describes this technique in more detail.

If you want to produce other output formats, just change the output format in the .Rmd, and make sure to update the extension.

There are a few other tricks worth knowing about:

You can see all these pieces put together in rmarkdown-report/.

Later, in Chapter XYZ, we’ll come back to generating a complete report of all the code that your app has executed.

8.3 Case study

We’ll put all the pieces together in 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:

  1. Uploading and parsing the file.
  2. Cleaning the file.
  3. Downloading the file.

And this same organisation makes it easier to understand the app:

8.3.1 Exercises

  1. Note that the browser defintion of an extension is different to the definiton used by the browser - the browser uses .csv where file_ext() returns .csv.

  2. .tsv file is to be preferred over a .csv because many European countries use commas to separate the whole and fractional parts of a number (e.g. 1,23 not 1.23). This means they can’t use commas to separate fields and instead use semi-colons. It’s best to just side-step this issue together, and instead just use tab separatea files.