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, warn.conflicts = FALSE)
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.
Below we will cover 2 different methods of creating attachments:
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 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
<- "0016A0000035mJ4"
parent_record_id1 <- "0016A0000035mJ5"
parent_record_id2
# 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
<- system.file("extdata", "cloud.png", package="salesforcer")
file_path1 <- system.file("extdata", "logo.png", package="salesforcer")
file_path2 <- system.file("extdata", "old-logo.png", package="salesforcer")
file_path3
# create a data.frame or tbl_df out of this information
<- tibble(Body = rep(c(file_path1,
attachment_details
file_path2,
file_path3), times=2),
ParentId = rep(c(parent_record_id1,
parent_record_id2), each=3))
# create the attachments!
<- sf_create_attachment(attachment_details)
result
result#> # A tibble: 6 × 2
#> id success
#> <chr> <lgl>
#> 1 00P3s00000gWuEqEAK TRUE
#> 2 00P3s00000gWuEvEAK TRUE
#> 3 00P3s00000gWuF0EAK TRUE
#> 4 00P3s00000gWuF5EAK TRUE
#> 5 00P3s00000gWuFAEA0 TRUE
#> # … with 1 more row
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
<- sf_query(sprintf("SELECT Id, Body, Name, ParentId
queried_attachments FROM Attachment
WHERE ParentId IN ('%s', '%s')",
parent_record_id1, parent_record_id2))
queried_attachments#> # A tibble: 6 × 4
#> Id Body Name ParentId
#> <chr> <chr> <chr> <chr>
#> 1 00P3s00000gWuEvEAK /services/data/v54.0/sobjects/Attachment/00… logo… 0016A00…
#> 2 00P3s00000gWuF0EAK /services/data/v54.0/sobjects/Attachment/00… old-… 0016A00…
#> 3 00P3s00000gWuEqEAK /services/data/v54.0/sobjects/Attachment/00… clou… 0016A00…
#> 4 00P3s00000gWuFFEA0 /services/data/v54.0/sobjects/Attachment/00… old-… 0016A00…
#> 5 00P3s00000gWuF5EAK /services/data/v54.0/sobjects/Attachment/00… clou… 0016A00…
#> # … 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:
unique_name
) that is the concatenation of the Attachment Id and the Attachment’s name which is guaranteed to be unique.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
<- tempdir()
temp_dir 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
<- mapply(sf_download_attachment,
download_result body = queried_attachments$Body,
name = queried_attachments$Name,
path = queried_attachments$Path)
download_result#> /services/data/v54.0/sobjects/Attachment/00P3s00000gWuEvEAK/Body
#> "/var/folders/s2/1mxmzrg52tx7l0pg8lq1320w0000gn/T//RtmpFlHRfy/0016A0000035mJ4QAI/logo.png"
#> /services/data/v54.0/sobjects/Attachment/00P3s00000gWuF0EAK/Body
#> "/var/folders/s2/1mxmzrg52tx7l0pg8lq1320w0000gn/T//RtmpFlHRfy/0016A0000035mJ4QAI/old-logo.png"
#> /services/data/v54.0/sobjects/Attachment/00P3s00000gWuEqEAK/Body
#> "/var/folders/s2/1mxmzrg52tx7l0pg8lq1320w0000gn/T//RtmpFlHRfy/0016A0000035mJ4QAI/cloud.png"
#> /services/data/v54.0/sobjects/Attachment/00P3s00000gWuFFEA0/Body
#> "/var/folders/s2/1mxmzrg52tx7l0pg8lq1320w0000gn/T//RtmpFlHRfy/0016A0000035mJ5QAI/old-logo.png"
#> /services/data/v54.0/sobjects/Attachment/00P3s00000gWuF5EAK/Body
#> "/var/folders/s2/1mxmzrg52tx7l0pg8lq1320w0000gn/T//RtmpFlHRfy/0016A0000035mJ5QAI/cloud.png"
#> /services/data/v54.0/sobjects/Attachment/00P3s00000gWuFAEA0/Body
#> "/var/folders/s2/1mxmzrg52tx7l0pg8lq1320w0000gn/T//RtmpFlHRfy/0016A0000035mJ5QAI/logo.png"
Once an Attachment record is created, you are still able to update the content and metadata (Name
, IsPrivate
, and OwnerId
) for the record. The sf_update_attachment()
function works just like sf_update()
except that you will need to include the Body
column where the content of the attachment is help. NOTE: If you just want to update the metadata and not the Attachment content itself, you can simply use sf_update()
to modify fields other than the Body
field on the record.
The example below demonstrates how to add an attachment to a record, then download it, zip it, re-upload to the record in order to save disk space in your Org.
# upload a PDF to a particular record as an Attachment
<- system.file("extdata",
file_path "data-wrangling-cheatsheet.pdf",
package = "salesforcer")
<- "0036A000002C6MmQAK" # replace with your own ParentId!
parent_record_id <- tibble(Body = file_path, ParentId = parent_record_id)
attachment_details <- sf_create_attachment(attachment_details)
create_result
# download, zip, and re-upload the PDF
<- sf_download_attachment(sf_id = create_result$id[1])
pdf_path <- paste0(pdf_path, ".zip")
zipped_path zip(zipped_path, pdf_path, flags = "-qq") # quiet zipping messages
<- tibble(Id = create_result$id, Body = zipped_path)
attachment_details sf_update_attachment(attachment_details)
#> # A tibble: 1 × 2
#> id success
#> <chr> <lgl>
#> 1 00P3s00000gWuFKEA0 TRUE
There is a function sf_delete_attachment()
, which simply wraps sf_delete()
so feel free to use for clarity in your code that you’re working with attachments use sf_delete()
.
sf_delete_attachment(ids = create_result$id)
#> # A tibble: 1 × 2
#> id success
#> <chr> <lgl>
#> 1 00P3s00000gWuFKEA0 TRUE
# sf_delete(ids = create_result$id) # would also work
Uploading large batches of attachments 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 three attachments each belonging to two different parent records. Assuming that I have run that code and the files are in that my computer I can demonstrate that all of them can be uploaded at once using the Bulk 1.0 API.
# create the attachment metadata required (Name, Body, ParentId)
<- queried_attachments %>%
attachment_details mutate(Body = file.path(Path, Name)) %>%
select(Name, Body, ParentId)
<- sf_create_attachment(attachment_details, api_type="Bulk 1.0")
result
result#> # A tibble: 6 × 4
#> Id Success Created Error
#> <chr> <lgl> <lgl> <lgl>
#> 1 00P3s00000gWuFPEA0 TRUE TRUE NA
#> 2 00P3s00000gWuFQEA0 TRUE TRUE NA
#> 3 00P3s00000gWuFREA0 TRUE TRUE NA
#> 4 00P3s00000gWuFSEA0 TRUE TRUE NA
#> 5 00P3s00000gWuFTEA0 TRUE TRUE NA
#> # … with 1 more row
NOTE: As of v48.0 (Spring ’20), it does not appear that the Bulk 2.0 API supports working with Attachments, so the Bulk 1.0 API must be used for bulk functionality.
Finally, you are able to update Attachments with the Bulk API just as shown above in the REST/SOAP API examples.
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
<- tibble(Name = "Data Wrangling Cheatsheet - Test 1",
document_details Description = "RStudio cheatsheet covering dplyr and tidyr.",
Body = system.file("extdata",
"data-wrangling-cheatsheet.pdf",
package="salesforcer"),
FolderId = "00l6A000001EgIwQAK",
Keywords = "test,cheatsheet,document")
<- sf_create_attachment(document_details, object_name = "Document")
result
result#> # A tibble: 1 × 2
#> id success
#> <chr> <lgl>
#> 1 0153s000002btxkAAA 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.
<- "https://rstudio.com/wp-content/uploads/2015/02/data-wrangling-cheatsheet.pdf"
cheatsheet_url <- tibble(Name = "Data Wrangling Cheatsheet - Test 2",
document_details Description = "RStudio cheatsheet covering dplyr and tidyr.",
Url = cheatsheet_url,
FolderId = "00l6A000001EgIwQAK",
Keywords = "test,cheatsheet,document")
<- sf_create_attachment(document_details, object_name = "Document")
result
result#> # A tibble: 1 × 2
#> id success
#> <chr> <lgl>
#> 1 0153s000002btxpAAA TRUE
Below is a list of links to existing Salesforce documentation that provide more detail into how Attachments, Documents, and other blob data are handled via their APIs. As with many functions in {salesforcer}, we have tried to translate these functions exactly as they are described in the Salesforce documentation so that they are flexible enough to handle most all cases that the APIs were intended to support.
Attachment Object: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_attachment.htm
Document Object: https://developer.salesforce.com/docs/atlas.en-us.api.meta/api/sforce_api_objects_document.htm
REST API Upload Attachment: https://developer.salesforce.com/docs/atlas.en-us.api_rest.meta/api_rest/dome_sobject_insert_update_blob.htm
Bulk API Upload Attachments: https://developer.salesforce.com/docs/atlas.en-us.api_asynch.meta/api_asynch/binary_intro.htm