Working with Attachments

Steven M. Mortimer

2020-06-07

Attachments in Salesforce

Almost all records in Salesforce support attachments. Attachments are just blob data storage for an associated ParentId. A ParentId is the 18-character Salesforcer Id of the record that the attachment belongs to. To get started creating and updating attachments, first, load the {salesforcer} and {dplyr} packages and login, if needed.

library(dplyr)
library(here)
library(salesforcer)
sf_auth()

The Attachment data, for example, the attachment’s Id, ParentId, Name, Body, ModifiedDate, and other attributes are stored in the Attachment object, a Standard Object in Salesforce.

Creating (Uploading) Attachments

Below we will cover 2 different methods of creating attachments:

  1. Uploading local files as Attachments one at a time using the SOAP and REST API (most common usage)
  2. Uploading large batches of files using the Bulk API, which zips the files before upload

Uploading local files as Attachments (SOAP and REST)

When uploading an attachment stored locally (i.e. on your computer), you can provide an absolute or relative path to the current working directory. If the Name column is omitted, then the name of the attachment as it appears in Salesforce will be the same as the base file name and extension. For example, if the path provided is “/Documents/attachments/doc1.pdf”, then the Name field will be set to “doc1.pdf”. In the sample code below we are leveraging the here package to consistently create relative paths. Although, this is not required, it is encouraged to increase the portability of your code.

In the example below, we upload three attachments to two different parent records. Note: Make sure to replace the paths and ParentIds in the example below with file paths that exist on your local machine and Ids of records in your Salesforce org.

# define the ParentIds where the attachments will be shown in Salesforce
parent_record_id1 <- "0016A0000035mJ4"
parent_record_id2 <- "0016A0000035mJ5"

# provide the file paths of where the attachments exist locally on your machine
# in this case we are referencing images included within the salesforcer package, 
# but any absolute or relative path locally will work
file_path1 <- system.file("extdata", "cloud.png", package="salesforcer")
file_path2 <- system.file("extdata", "logo.png", package="salesforcer")
file_path3 <- system.file("extdata", "old-logo.png", package="salesforcer")

# create a data.frame or tbl_df out of this information
attachment_details <- tibble(Body = rep(c(file_path1, 
                                          file_path2, 
                                          file_path3), 
                                        times=2),
                             ParentId = rep(c(parent_record_id1, 
                                              parent_record_id2), 
                                            each=3))

# create the attachments!
result <- sf_create_attachment(attachment_details)
result
#> # A tibble: 6 x 2
#>   id                 success
#>   <chr>              <lgl>  
#> 1 00P3s00000ZKqUcEAL TRUE   
#> 2 00P3s00000ZKqUhEAL TRUE   
#> 3 00P3s00000ZKqUmEAL TRUE   
#> 4 00P3s00000ZKqUrEAL TRUE   
#> 5 00P3s00000ZKqUwEAL TRUE   
#> # … with 1 more row

Downloading Attachments

After uploading attachments to Salesforce you can download them by first querying the metadata in the Attachment object. This metadata provides the Id for the blob data attachment for download. A convenience function, sf_download_attachment(), has been created to download attachments quickly. The example below shows how to query the metadata of attachments belonging to particular ParentId.

# pull down all attachments associated with a particular record
queried_attachments <- sf_query(sprintf("SELECT Id, Body, Name, ParentId
                                         FROM Attachment
                                         WHERE ParentId IN ('%s', '%s')", 
                                         parent_record_id1, parent_record_id2))
queried_attachments
#> # A tibble: 6 x 4
#>   Id            Body                                     Name      ParentId     
#>   <chr>         <chr>                                    <chr>     <chr>        
#> 1 00P3s00000ZK… /services/data/v48.0/sobjects/Attachmen… cloud.png 0016A0000035…
#> 2 00P3s00000ZK… /services/data/v48.0/sobjects/Attachmen… logo.png  0016A0000035…
#> 3 00P3s00000ZK… /services/data/v48.0/sobjects/Attachmen… old-logo… 0016A0000035…
#> 4 00P3s00000ZK… /services/data/v48.0/sobjects/Attachmen… cloud.png 0016A0000035…
#> 5 00P3s00000ZK… /services/data/v48.0/sobjects/Attachmen… old-logo… 0016A0000035…
#> # … with 1 more row

Before downloading the attachments using the Body it is important to consider whether the attachment names are repeated or duplicates. If so, then the attachments with the same exact name will be overwritten on the local filesystem as they are downloaded. To avoid this problem there are two common strategies:

  1. Create a new column (e.g. unique_name) that is the concatenation of the Attachment Id and the Attachment’s name which is guaranteed to be unique.
  2. Save the attachments in separate folders for each ParentId record.

As long as the same ParentId record doesn’t name attachments with the same name, then Strategy #2 above will work. In addition, it may help better organize the documents if you are planning to download many and then upload again to Salesforce using the Bulk API as demonstrated later in this script.

# create a new folder for each ParentId in the dataset
temp_dir <- tempdir()
for (pid in unique(queried_attachments$ParentId)){
  dir.create(file.path(temp_dir, pid), showWarnings = FALSE) # ignore if already exists
}

# create a new columns in the queried data so that we can pass this information 
# on to the function `sf_download_attachment()` that will actually perform the download
queried_attachments <- queried_attachments %>% 
  # Strategy 1: Unique file names (ununsed here, but shown as an example)
  mutate(unique_name = paste(Id, Name, sep='___')) %>% 
  # Strategy 2: Separate folders per parent
  mutate(Path = file.path(temp_dir, ParentId))

# download all of the attachments for a single ParentId record to their own folder
download_result <- mapply(sf_download_attachment, 
                          queried_attachments$Body, 
                          queried_attachments$Name, 
                          queried_attachments$Path)
download_result
#> /services/data/v48.0/sobjects/Attachment/00P3s00000ZKqUcEAL/Body 
#>                                                             TRUE 
#> /services/data/v48.0/sobjects/Attachment/00P3s00000ZKqUhEAL/Body 
#>                                                             TRUE 
#> /services/data/v48.0/sobjects/Attachment/00P3s00000ZKqUmEAL/Body 
#>                                                             TRUE 
#> /services/data/v48.0/sobjects/Attachment/00P3s00000ZKqUrEAL/Body 
#>                                                             TRUE 
#> /services/data/v48.0/sobjects/Attachment/00P3s00000ZKqV1EAL/Body 
#>                                                             TRUE 
#> /services/data/v48.0/sobjects/Attachment/00P3s00000ZKqUwEAL/Body 
#>                                                             TRUE

Uploading large batches using the Bulk API

The SOAP and REST APIs are good for working with a few attachments at a time. However, the Bulk API can be invoked using api_type=“Bulk 1.0” to automatically take a data.frame or tbl_df of Attachment field data and create a ZIP file with CSV manifest that is required by that API to upload In the example above we downloaded the 3 attachments each belonging to 2 different parent records. Assuming that I have a directory on my computer

# create the attachment metadata required (Name, Body, ParentId)
attachment_details <- queried_attachments %>% 
  mutate(Body = file.path(Path, Name)) %>% 
  select(Name, Body, ParentId) 
result <- sf_create_attachment(attachment_details, api_type="Bulk 1.0")
result
#> # A tibble: 6 x 4
#>   Id                 Success Created Error
#>   <chr>              <lgl>   <lgl>   <lgl>
#> 1 00P3s00000ZKqV6EAL TRUE    TRUE    NA   
#> 2 00P3s00000ZKqV7EAL TRUE    TRUE    NA   
#> 3 00P3s00000ZKqV8EAL TRUE    TRUE    NA   
#> 4 00P3s00000ZKqV9EAL TRUE    TRUE    NA   
#> 5 00P3s00000ZKqVAEA1 TRUE    TRUE    NA   
#> # … with 1 more row

Extending to Documents and Other Blob Data

The commands for working with Attachments also work for uploading documents and other blob data as well. Documents are just like Attachments in Salesforce except instead of having an associated ParentId they have an associated FolderId where the blob will be associated with upon creation. Here is a brief example of uploading a PDF (“Document”) to a Folder

# the function supports inserting all types of blob content, just update the 
# object_name argument to add the PDF as a Document instead of an Attachment
document_details <- tibble(Name = "Data Wrangling Cheatsheet - Test 1",
                           Description = "RStudio cheatsheet covering dplyr and tidyr.",
                           Body = system.file("extdata", 
                                              "data-wrangling-cheatsheet.pdf",
                                              package="salesforcer"),
                           FolderId = "00l6A000001EgIwQAK",
                           Keywords = "test,cheatsheet,document")
result <- sf_create_attachment(document_details, object_name = "Document")
result
#> # A tibble: 1 x 2
#>   id                 success
#>   <chr>              <lgl>  
#> 1 0153s000001gYuEAAU TRUE

With Documents, users are also able to save storage by specifying a Url instead of a a file path where the Body content is stored locally. Specifying the Url field will reference the URL instead of uploading into the Salesforce org, thereby saving space if limited in your organization.

cheatsheet_url <- "https://rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf"
document_details <- tibble(Name = "Data Wrangling Cheatsheet - Test 2",
                           Description = "RStudio cheatsheet covering dplyr and tidyr.",
                           Url = cheatsheet_url,  
                           FolderId = "00l6A000001EgIwQAK",
                           Keywords = "test,cheatsheet,document")
result <- sf_create_attachment(document_details, object_name = "Document")
result
#> # A tibble: 1 x 2
#>   id                 success
#>   <chr>              <lgl>  
#> 1 0153s000001gYuJAAU TRUE