Salesforce stores files in two places depending on how old your org is and how records were created: Attachments (the legacy system) and Salesforce Files (the modern ContentDocument/ContentVersion system). Bulk exporting these — especially when you're doing a data migration or org cleanup — can be tricky if you don't know the right approach.
In this guide, I'll walk you through every practical method to bulk export Salesforce Files and Attachments, including Apex code you can run today.
Understanding the Salesforce File System
Before exporting anything, it's important to understand the two file objects:
- Attachment — Legacy object. Files are attached directly to a record (ParentId). Not shareable across records.
- ContentDocument — Modern file system. Files are stored as ContentVersion records and linked to records via ContentDocumentLink.
Most orgs created after 2014 use ContentDocument/ContentVersion for new files, but may have old Attachment records from earlier in the org's history.
Method 1: Export with Data Loader (No Code)
For exporting metadata about files (names, sizes, dates, parent IDs), the Salesforce Data Loader is the simplest option.
For Salesforce Files (ContentVersion)
Use this SOQL query in Data Loader:
SELECT Id, Title, FileType, FileExtension, ContentSize,
ContentDocumentId, CreatedDate, LastModifiedDate,
FirstPublishLocationId, VersionNumber
FROM ContentVersion
WHERE IsLatest = TRUE
ORDER BY CreatedDate DESC
For Legacy Attachments
SELECT Id, Name, ContentType, BodyLength,
ParentId, CreatedDate, LastModifiedDate,
OwnerId
FROM Attachment
ORDER BY CreatedDate DESC
Limitation: Data Loader exports file metadata only — not the actual file binaries. For the actual files, use Method 2 or 3.
Method 2: Apex Batch Export with REST API URLs
To export the actual file contents, you need to generate download URLs and fetch them. Here's an Apex class that builds export-ready REST URLs for all ContentVersion records:
// ContentFileExporter.cls
public class ContentFileExporter {
public static List<Map<String,String>> getFileDownloadUrls(String parentId) {
List<Map<String,String>> result = new List<Map<String,String>>();
// Query all files linked to the record
List<ContentDocumentLink> links = [
SELECT ContentDocumentId
FROM ContentDocumentLink
WHERE LinkedEntityId = :parentId
];
Set<Id> docIds = new Set<Id>();
for (ContentDocumentLink link : links) {
docIds.add(link.ContentDocumentId);
}
// Get latest versions
List<ContentVersion> versions = [
SELECT Id, Title, FileExtension, ContentDocumentId, FileType
FROM ContentVersion
WHERE ContentDocumentId IN :docIds
AND IsLatest = TRUE
];
String baseUrl = URL.getOrgDomainUrl().toExternalForm();
for (ContentVersion cv : versions) {
Map<String,String> fileInfo = new Map<String,String>{
'Id' => cv.Id,
'Title' => cv.Title,
'Extension'=> cv.FileExtension,
'DownloadUrl' => baseUrl + '/sfc/servlet.shepherd/version/download/' + cv.Id
};
result.add(fileInfo);
}
return result;
}
}
Method 3: Bulk Export All Files via Apex Batch
For org-wide exports of all files, use an Apex Batch class that chunks ContentVersion records and either generates a CSV log or calls an external endpoint for each file:
// ContentVersionBatchExporter.cls
global class ContentVersionBatchExporter
implements Database.Batchable<sObject> {
global Database.QueryLocator start(Database.BatchableContext bc) {
return Database.getQueryLocator(
'SELECT Id, Title, FileExtension, ContentSize, ' +
'ContentDocumentId, VersionData ' +
'FROM ContentVersion ' +
'WHERE IsLatest = TRUE'
);
}
global void execute(Database.BatchableContext bc,
List<ContentVersion> scope) {
// Process each file — write metadata to custom object
// or send to external endpoint via callout
List<Export_Log__c> logs = new List<Export_Log__c>();
for (ContentVersion cv : scope) {
Export_Log__c log = new Export_Log__c(
File_Id__c = cv.Id,
File_Name__c = cv.Title + '.' + cv.FileExtension,
File_Size__c = cv.ContentSize,
Document_Id__c = cv.ContentDocumentId
);
logs.add(log);
}
insert logs;
}
global void finish(Database.BatchableContext bc) {
System.debug('File export batch complete.');
}
}
// Run from Developer Console:
// Database.executeBatch(new ContentVersionBatchExporter(), 50);
Method 4: Export Attachments (Legacy)
For old Attachment records, you can fetch the binary body using Apex's
Attachment.Body field (Blob type):
// Query attachments for a specific parent
List<Attachment> attachments = [
SELECT Id, Name, ContentType, Body, BodyLength, ParentId
FROM Attachment
WHERE ParentId = :someRecordId
LIMIT 200
];
for (Attachment att : attachments) {
// att.Body is a Blob — can be base64 encoded for API transfer
String base64Body = EncodingUtil.base64Encode(att.Body);
System.debug('File: ' + att.Name + ' | Size: ' + att.BodyLength);
}
Comparing All Methods
| Method | Gets Binary Files | Bulk-Safe | No Code | Best For |
|---|---|---|---|---|
| Data Loader (SOQL) | ❌ | ✅ | ✅ | Metadata only |
| Apex + REST URLs | ✅ | ✅ | ❌ | Per-record downloads |
| Apex Batch | ✅ | ✅ | ❌ | Full org export log |
| Attachment.Body | ✅ | ⚠️ (heap limits) | ❌ | Small attachment sets |
Important Governor Limit Considerations
- Querying
VersionDatain bulk hits heap size limits — use batch sizes of 10-20 when fetching blob data. - ContentVersion queries with
IsLatest = TRUEare always safer than querying all versions. - For orgs with 100,000+ files, always use a
Database.Batchableapproach — never a single SOQL loop. - The
ContentDocumentLinkobject requires appropriate access permissions — verify sharing settings before bulk queries.
Frequently Asked Questions
Attachment object) are directly
linked to one record. Salesforce Files (ContentDocument/ContentVersion) are the
modern system and can be shared across multiple records. Most orgs migrated to
Files around 2017-2019.
Wrapping Up
Exporting Salesforce Files in bulk doesn't have to be painful. For metadata, Data Loader is your quickest option. For actual file binaries, Apex with REST download URLs is the most reliable approach at scale.
If you're doing a full org migration, I'd recommend combining the batch Apex logger with an external script (Python, Node.js) that iterates over the generated URL list and downloads each file — keeping all file processing outside Salesforce governor limits.
Got questions or edge cases I haven't covered? Drop me a message — I respond to every genuine inquiry.