Efficient Media Migration in Sitecore Using PowerShell and Packages
Transferring large media libraries between Sitecore instances using PowerShell and content packages
Start typing to search...
Transferring large media libraries between Sitecore instances using PowerShell and content packages
Migrating large numbers of media items between Sitecore instances can be challenging due to the size of blobs and the limitations of packages when handling large binary data. This blog post outlines a two-script process to transfer media items efficiently, leveraging Sitecore PowerShell Extensions (SPE) and publicly available media URLs.
Instead of exporting full binary blobs into the package (which can lead to huge files and timeouts), this method separates metadata and structure from the binary blob and transfers them independently:
Before running the scripts:
/media library/test-folder.$removeBackupAtTheEnd = $false to inspect backup content before deletion.This script:
Blob field from the original items.# CONFIGURATION
$mediaLibraryPath = "master:/sitecore/media library/test-folder"
$removeBackupAtTheEnd = $true
$backupFolderSuffix = "-backup"
$timestamp = Get-Date -Format "yyyyMMdd-HHmmss"
$packageName = "MediaLibraryExport-$timestamp"
# --------------
$sourceItem = Get-Item -Path $mediaLibraryPath
if (-not $sourceItem) {
Write-Error "Source item not found: $mediaLibraryPath"
return
}
$backupPath = "$mediaLibraryPath$backupFolderSuffix"
function Get-MediaUrl($mediaitem) {
$siteContext = [Sitecore.Sites.SiteContext]::GetSite("website");
$result = New-UsingBlock(New-Object Sitecore.Sites.SiteContextSwitcher $siteContext){
[Sitecore.Resources.Media.MediaManager]::GetMediaUrl($mediaitem)
}
$result
}
# STEP 1: Copy recursively all items to backup folder
Write-Host "Creating backup at $backupPath"
Copy-Item -Path $mediaLibraryPath -Destination $backupPath -Recurse -Force
# STEP 2: Remove "Blob" field from all items
Write-Host "Removing Blob fields from original media items..."
$mediaItems = Get-ChildItem -Path $mediaLibraryPath -Recurse
foreach ($item in $mediaItems) {
if ($item["Blob"]) {
$item.Editing.BeginEdit()
$item["__Long description"] = Get-MediaUrl($item)
$item["Blob"] = $null
$item.Editing.EndEdit()
}
}
# STEP 3: Generate content package without blobs
Write-Host "Generating content package without blobs..."
$ItemsToBeDeployed = @(
@{ Recursive = $true; Source = $mediaLibraryPath }
)
$Package = New-Package -Name $packageName
$Package.Sources.Clear()
$Package.Metadata.Author = "PowerShell"
$Package.Metadata.Publisher = "Automated Script"
$Package.Metadata.Version = Get-Date -Format FileDateTimeUniversal
$Package.Metadata.Readme = "This package contains media items without blobs."
foreach ($item in $ItemsToBeDeployed) {
if ($item.Recursive) {
$source = Get-Item $item.Source | New-ItemSource -Name "Media Source" -InstallMode Overwrite
} else {
$source = Get-Item $item.Source | New-ExplicitItemSource -Name "Media Source" -InstallMode Overwrite
}
$Package.Sources.Add($source)
}
$packageFileName = "$($Package.Name)-$($Package.Metadata.Version).zip"
$packageFullPath = Join-Path -Path $SitecorePackageFolder -ChildPath $packageFileName
Export-Package -Project $Package -Path $packageFullPath -Zip
Download-File $packageFullPath
Write-Host "✅ Package exported: $packageFileName"
# STEP 4: Restore blobs from backup
Write-Host "Restoring blobs from backup..."
$backupItems = Get-ChildItem -Path $backupPath -Recurse
foreach ($backupItem in $backupItems) {
$relativePath = $backupItem.Paths.FullPath.Replace($backupPath.Replace("master:",""), "")
$originalPath = "$mediaLibraryPath$relativePath"
$originalItem = Get-Item -Path $originalPath
if ($originalItem -and $backupItem.Fields["Blob"]) {
$originalItem.Editing.BeginEdit()
$originalItem["__Long description"] = ""
$originalItem["Blob"] = $backupItem["Blob"]
$originalItem.Editing.EndEdit()
}
}
# STEP 5: Remove or Notify backup folder
if ($removeBackupAtTheEnd) {
if (-not $backupItem) {
Write-Error "Backup path not found: $backupPath"
return
}
# Get all child items recursively, sorted bottom-up
$allItemsToBeRemoved = Get-ChildItem -Path $backupPath -Recurse | Sort-Object { $_.Paths.FullPath } -Descending
Write-Host "Removing back up folder: $backupPath"
Write-Host "⌛This process may take several minutes, you can go ahead and install the downloaded package in the other Sitecore instance."
# Delete all children
foreach ($itemToDelete in $allItemsToBeRemoved) {
try {
Remove-Item -Path $itemToDelete.ItemPath -Permanently -Force
} catch {
Write-Warning "Failed to delete $($item.Paths.FullPath): $_"
}
}
# Delete the root folder last
try {
Remove-Item -Path $backupPath -Permanently -Force
} catch {
Write-Warning "Failed to delete root item: $($backupItem.Paths.FullPath)"
}
} else {
Write-Warning "Backup created at: $backupPath"
Write-Host "⚠️ Remember to manually delete the backup folder once you've verified the package is correct."
}
Write-Host "Finished"
The script processes each item, strips the blob, stores its expected URL in __Long description, and prepares the clean package for export.
Once you've installed the metadata-only content package on the target instance, you run this script.
It will:
__Long description field, which contains the download URL.WebClient.Blob, Mime Type, and Size fields.# CONFIGURATION
# This value should be the same as in the first script
$mediaLibraryPath = "master:/sitecore/media library/test-folder"
# In XMCloud (Exp. Edge) the URL will be like "https://edge.sitecorecloud.io/mycompanyna44a2-proja11e-dev231a-1df8"
# In XP (with a CD server) the URL will be your front end: "https://www.mycompany.com"
$publicDownloadUrl = "https://www.mycompany.com"
# --------------
function Get-MimeType {
param (
[string]$extension
)
$extension = $extension.ToLower()
switch ($extension) {
'pdf' { return 'application/pdf' }
'doc' { return 'application/msword' }
'docx' { return 'application/vnd.openxmlformats-officedocument.wordprocessingml.document' }
'zip' { return 'application/zip' }
'png' { return 'image/png' }
'jpeg' { return 'image/jpeg' }
'jpg' { return 'image/jpeg' }
default { return 'application/octet-stream' }
}
}
function Restore-MediaBlobs {
$publicDownloadUrl = $publicDownloadUrl.TrimEnd('/')
$rootItem = Get-Item -Path $mediaLibraryPath
if (-not $rootItem) {
Write-Error "Could not find item: $mediaLibraryPath"
return
}
# Include root and all descendants
$mediaItems = @($rootItem) + $rootItem.Axes.GetDescendants()
foreach ($item in $mediaItems) {
try {
$relativePath = $item["__Long description"]
if (-not $relativePath) {
Write-Host "Skipping item (no path): $($item.Paths.FullPath) - No path found in __Long description - it may indicate a folder or any other item with no media."
continue
}
# Construct the relative path from the media root
if ($publicDownloadUrl.ToLower().IndexOf("edge.sitecorecloud.io") -ne -1 -AND $relativePath.StartsWith("/-")) {
$relativePath = $relativePath.Substring(2) #remove /-
}
$mediaUrl = "$publicDownloadUrl$relativePath".ToLower()
Write-Host "Downloading: $mediaUrl"
$webClient = New-Object System.Net.WebClient
$fileBytes = $webClient.DownloadData($mediaUrl)
if ($fileBytes -and $fileBytes.Length -gt 0) {
$item.Editing.BeginEdit()
$item.Fields["Blob"].SetBlobStream([System.IO.MemoryStream]::new($fileBytes))
$item["Mime Type"] = Get-MimeType -extension $extension
$item["Size"] = $fileBytes.Length.ToString()
$item["__Long description"] = ""
$item.Editing.EndEdit()
Write-Host "✅ Restored: $($item.Paths.FullPath)"
} else {
Write-Warning "⚠️ Empty file or download failed: $mediaUrl"
}
}
catch {
Write-Warning "❌ Error processing $($item.Paths.FullPath): $_"
}
}
}
# Execute
Restore-MediaBlobs
The script ensures each media item becomes functional and complete again by rehydrating it from the online media source.
After working on several projects and experimenting with different migration methods, I found this approach to be the most reliable and efficient. By separating the metadata from the binary content and leveraging Sitecore's published medias, this method simplifies large media transfers while minimizing complexity.