FileBuddy
A Salesforce CLI (sf) plugin that migrates ContentDocument files — and their record associations — between orgs. Sandbox to production, production to sandbox, or org-to-org.
Installation
$sf plugins install sf-cli-migratorAbout
SF CLI Migrator
A Salesforce CLI (sf) plugin that migrates ContentDocument files — and their record associations — between orgs. Sandbox to production, production to sandbox, or org-to-org.
Built on oclif and @salesforce/sf-plugins-core.
How It Works
- Connect two orgs — uses your existing
sfCLI authenticated sessions (zero additional setup) - Pick an object (Account, Case, Opportunity, Custom_Object__c, etc.)
- Choose match fields to map records between orgs — same field on both, or different fields (e.g., source
Id→ targetLegacy_Id__c) - The plugin automatically:
- Queries all ContentDocumentLinks for your records in the source org
- Downloads the file binaries
- Uploads them as new ContentVersions in the target org
- Creates ContentDocumentLinks to associate files with the correct target records
Prerequisites
- Node.js 18+
- Salesforce CLI (
sf) — Install guide - At least two authenticated orgs:
sf org login web --alias my-source sf org login web --alias my-target
Installation
From Source (Local Development)
git clone https://github.com/Marceswan/sf-cli-migrator.git
cd sf-cli-migrator
npm install
npm run build
sf plugins link .
Verify Installation
sf filebuddy migrate --help
Usage
The plugin supports two modes: interactive (menu-driven) and flag-driven (scriptable).
Interactive Mode
Run the command with no flags to launch the interactive menu:
sf filebuddy migrate
You'll see:
╔══════════════════════════════════════╗
║ SF File Migrator (sf plugin) ║
║ ContentDocument Migration Tool ║
╚══════════════════════════════════════╝
✓ 4 authenticated org(s) available.
Source: ○ Not connected
Target: ○ Not connected
? What would you like to do?
Connect Source Org (migrate FROM)
Connect Target Org (migrate TO)
──────────────
Start Migration
──────────────
Disconnect Source
Disconnect Target
Exit
The menu guides you through connecting orgs, selecting an object, choosing a match field (with autocomplete), setting an optional SOQL filter, and choosing dry-run vs. live mode.
Flag Mode
Provide all four required flags for direct, scriptable execution:
sf filebuddy migrate \
--source-org my-source \
--target-org my-target \
--object Account \
--match-field External_Id__c
Flags
| Flag | Short | Required | Description |
|---|---|---|---|
--source-org |
-s |
For flag mode | Org to migrate files FROM (username or alias) |
--target-org |
-t |
For flag mode | Org to migrate files TO (username or alias) |
--object |
-o |
For flag mode | Source object API name (e.g., Account, Case) |
--match-field |
-m |
For flag mode | Field on the source org to match records (e.g., Id, External_Id__c, Name). Used on both orgs unless --target-match-field is also provided. |
--target-match-field |
No | Field on the target org to match against source values. Defaults to --match-field if omitted. |
|
--where |
-w |
No | SOQL WHERE clause to filter source records |
--dry-run |
-d |
No | Preview what would be migrated without making changes |
--resume |
-r |
No | Resume a previously paused or failed migration |
If any of the four "required" flags are omitted, the plugin falls into interactive mode (pre-filling whichever flags were provided).
Record Matching
By default, --match-field is used on both orgs. If the source and target use different field names to identify the same records, provide both:
# Source org uses standard Id, target org stores it in Legacy_Id__c
sf filebuddy migrate \
--source-org old-prod \
--target-org new-prod \
--object Account \
--match-field Id \
--target-match-field Legacy_Id__c
The plugin queries SELECT Id FROM Account on the source, then SELECT Id, Legacy_Id__c FROM Account WHERE Legacy_Id__c IN (...) on the target to build the record mapping.
In interactive mode, after choosing the source match field you'll be asked: "Use a different field on the target org?" — selecting yes gives you autocomplete for the target org's fields.
Examples
Same field on both orgs (External ID):
sf filebuddy migrate \
--source-org sandbox \
--target-org prod \
--object Account \
--match-field External_Id__c \
--dry-run
Source Id → target Legacy_Id__c (asymmetric match):
sf filebuddy migrate \
--source-org old-prod \
--target-org new-prod \
--object Account \
--match-field Id \
--target-match-field Legacy_Id__c
Filter by SOQL WHERE clause:
sf filebuddy migrate \
--source-org sandbox \
--target-org prod \
--object Case \
--match-field CaseNumber \
--where "CreatedDate >= 2024-01-01T00:00:00Z"
Custom object with owner filter:
sf filebuddy migrate \
--source-org dev \
--target-org staging \
--object Equipment_Rental__c \
--match-field Rental_Number__c \
--where "OwnerId = '005xx000001234'"
Interactive mode with source org pre-filled:
sf filebuddy migrate --source-org my-sandbox
Filtering Records
The --where flag accepts any valid SOQL WHERE clause (without the WHERE keyword):
CreatedDate >= 2024-01-01T00:00:00Z
OwnerId = '005xx000001234'
Status = 'Closed'
Name LIKE 'ACME%'
Omit the flag to migrate files for all records of the selected object.
Pause & Resume
Large migrations can be paused (Ctrl+C), survive crashes, or hit API limits and resume where they left off. Progress is saved automatically after each batch of 200 files.
How It Works
- Files are processed in batches of 200: download → upload → resolve IDs → save progress → clean up local files
- SOQL queries (Steps 1-4) always re-run fresh on resume — they're fast and idempotent
- Completed file uploads are tracked and skipped on resume
- ContentDocumentLink creation (Step 8) deduplicates against existing links in the target org
Flag Mode
# Start a migration (state is automatically tracked)
sf filebuddy migrate \
--source-org my-source \
--target-org my-target \
--object Account \
--match-field External_Id__c
# Press Ctrl+C to pause — progress is saved automatically
# Resume from where you left off:
sf filebuddy migrate \
--source-org my-source \
--target-org my-target \
--object Account \
--match-field External_Id__c \
--resume
Interactive Mode
When saved migration states exist, the main menu shows a Resume Migration option. Selecting it lists saved migrations with their progress, lets you pick one, and resumes from where it left off.
When starting a new migration that matches an existing saved state, you'll be prompted to either resume or start fresh.
SIGINT Handling
- First Ctrl+C: Finishes the current batch, saves progress, and exits gracefully
- Second Ctrl+C: Force quits immediately (may lose up to one batch of progress)
State Files
State files are stored in the OS temp directory ($TMPDIR/sf-filebuddy-migrate/.state/) and are automatically cleaned up when a migration completes successfully. They contain a deterministic ID based on the migration config (object, match fields, orgs, WHERE clause), so the same configuration always maps to the same state file.
Limitations
- 10 MB per file — Files larger than 10 MB are flagged and skipped (Salesforce REST API limit). They appear in the summary as skipped files.
- Latest version only — Only the most recent version of each file is migrated (
IsLatest = true). - API limits — Each file download/upload consumes API calls. Monitor your org's daily API usage for large migrations.
- Session expiry — If an org session has expired, the plugin will tell you. Refresh with
sf org login web. - Record matching — Records in the source org that don't have a matching record (by the chosen field) in the target org are skipped.
How the Migration Pipeline Works
The migration runs in 8 steps:
- Query source records —
SELECT Id, {sourceMatchField} FROM {object}with optional WHERE filter - Find ContentDocumentLinks — Identifies all files attached to the source records
- Fetch ContentVersion metadata — Gets file details (title, size, extension) for the latest version of each document
- Map records to target — Queries the target org using
{targetMatchField}to match against source values and builds a source-to-target ID map - Download files — Downloads file binaries from the source org to a temp directory
- Upload files — Uploads each file as a new ContentVersion (base64) in the target org
- Resolve new ContentDocumentIds — Queries back the newly created ContentVersions to get their ContentDocumentIds
- Create ContentDocumentLinks — Links the new files to the correct target records
SOQL queries are chunked in batches of 200 IDs to stay within SOQL character limits, and queryMore handles pagination for result sets over 2,000 records.
Uninstalling
sf plugins unlink sf-cli-migrator
Development
Build
npm run build # Compile TypeScript → lib/
npm run clean # Remove compiled output
Link for Testing
sf plugins link . # After building, makes `sf filebuddy migrate` available
Project Structure
src/
├── commands/filebuddy/migrate.ts # oclif command class (flag parsing, dual-mode routing)
├── lib/
│ ├── migration.ts # 8-step migration pipeline (batch download/upload)
│ ├── interactive.ts # Inquirer-based interactive menu
│ ├── state.ts # Migration state persistence (pause/resume)
│ └── temp.ts # Temp directory management
└── index.ts # Plugin export
License
MIT
Reviews (0)
No reviews yet. Be the first to leave one!
Details
- Author
- Marc Swan
- Category
- CLI Plugin
- Published
- Feb 11, 2026