Transfer S3 Contents Between Buckets with PowerShell
Moving files between Amazon S3 buckets is usually simple when the same AWS account owns everything. It gets a little more annoying when the source and destination buckets use different credentials, different accounts, or different paths inside the buckets.
This PowerShell script wraps the AWS CLI so you can copy or move objects from one S3 bucket path to another. It can use one AWS profile for the source bucket and another profile for the destination bucket.
The example values in this article are placeholders. Replace the bucket names, paths, regions, and AWS keys with your own values before running the script.
What the Script Does
The Transfer-S3Contents.ps1 script:
- Builds source and destination S3 URIs from bucket and path settings.
- Installs AWS CLI v2 if
awsis not found. - Creates or updates AWS CLI profiles for the source and destination credentials.
- Tests access to the source S3 path.
- Tests access to the destination S3 path.
- Downloads the source objects into a local temp folder.
- Uploads the temp folder contents to the destination path.
- Compares source and destination object counts.
- Leaves the source objects in place when
$Mode = "copy". - Deletes source objects only when
$Mode = "move"and the object counts match.
Copy Mode vs Move Mode
Use copy mode when you want the source bucket to stay unchanged:
$Mode = "copy"
Use move mode only when you want the source objects removed after a successful transfer:
$Mode = "move"
Move mode is intentionally cautious. The script copies the objects, counts the source and destination objects, and only deletes the source when the counts match.
Configure the Buckets and Paths
At the top of the script, set the source and destination bucket names:
$SourceBucket = "source-bucket-name" $DestinationBucket = "destination-bucket-name"
Use / when you want the root of the bucket:
$SourcePath = "/" $DestinationPath = "/"
Use folder-style paths when you only want to transfer a specific prefix:
$SourcePath = "/images" $DestinationPath = "/archive/images"
The script turns those values into S3 URIs such as:
s3://source-bucket-name/images s3://destination-bucket-name/archive/images
Configure AWS Credentials
The script writes AWS CLI profiles for the source and destination. That lets the source bucket and destination bucket use different credentials when needed.
Set the profile names:
$SourceProfile = "s3-source" $DestinationProfile = "s3-destination"
Then set the access keys:
$SourceAccessKeyId = "SOURCE-ACCESS-KEY-ID" $SourceSecretAccessKey = "SOURCE-SECRET-ACCESS-KEY" $DestinationAccessKeyId = "DESTINATION-ACCESS-KEY-ID" $DestinationSecretAccessKey = "DESTINATION-SECRET-ACCESS-KEY"
If one AWS key can access both buckets, you can use the same key values for the source and destination settings. If the buckets are in different AWS accounts, use credentials from each account.
The credentials are stored by the AWS CLI under the current Windows user profile:
C:\Users\YourUserName\.aws\credentials C:\Users\YourUserName\.aws\config
How to Run the Script
Put Transfer-S3Contents.ps1 in a working folder where it can create its local temp directory.
Generic command:
.\script_location\scriptname.ps1
Concrete example:
Set-Location C:\Scripts\S3Transfer .\Transfer-S3Contents.ps1
You can also ask the script for its built-in help:
.\Transfer-S3Contents.ps1 --help
If PowerShell blocks the script, see:
Make Sure PowerShell Scripts Can Run on Windows
Check the Transfer
After the script finishes, you can inspect both locations with the AWS CLI:
aws s3 ls "s3://source-bucket-name/images" --recursive --profile s3-source aws s3 ls "s3://destination-bucket-name/archive/images" --recursive --profile s3-destination
For a large transfer, use counts:
aws s3 ls "s3://source-bucket-name/images" --recursive --profile s3-source | Measure-Object aws s3 ls "s3://destination-bucket-name/archive/images" --recursive --profile s3-destination | Measure-Object
Important Notes
- Start with
$Mode = "copy"until you confirm the transfer works. - Use least-privilege AWS keys when possible.
- Do not publish real AWS access keys in documentation, tickets, screenshots, or chat.
- Delete or rotate exposed AWS keys immediately if they were ever shared by mistake.
- The script stores credentials in the normal AWS CLI profile files for the current Windows user.
- The local temp folder is created beside the script as
S3-Move-Temp. - For very large buckets, make sure the machine running the script has enough local disk space for the temporary copy.
The full script is below. Save it as Transfer-S3Contents.ps1.
PowerShell Script
Clear-Host
Push-Location $PSScriptRoot
$ProgressPreference = 'SilentlyContinue'
#region Documentation
<#
PURPOSE
Copy or move objects from one Amazon S3 bucket/path to another Amazon S3 bucket/path.
MODE OPTIONS
copy
Copies objects from the source path to the destination path.
Source objects are NOT deleted.
move
Copies objects from the source path to the destination path.
Confirms the source and destination object counts match.
Deletes the source objects only after successful verification.
PATH OPTIONS
Use "/" for the root of the bucket.
Examples:
$SourcePath = "/"
$DestinationPath = "/"
$SourcePath = "/images"
$DestinationPath = "/archive/images"
$SourcePath = "/folder/subfolder"
$DestinationPath = "/new-folder"
AWS PROFILE NOTES
AWS CLI uses named profiles to store credentials.
This script lets you put the Access Key ID and Secret Access Key as variables at the top.
The script then writes those values into AWS CLI profiles automatically.
Source profile:
Used to read/list/delete from the source bucket.
Destination profile:
Used to write/list to the destination bucket.
If the same AWS key has access to both buckets, you can use the same values for:
$SourceAccessKeyId
$SourceSecretAccessKey
$DestinationAccessKeyId
$DestinationSecretAccessKey
If the buckets are in different AWS accounts, use the source account key for the source variables
and the destination account key for the destination variables.
CREDENTIAL FILE LOCATION
AWS CLI stores credentials here:
C:\Users\<YourUserName>\.aws\credentials
C:\Users\<YourUserName>\.aws\config
REFERENCES
AWS CLI Install:
https://docs.aws.amazon.com/cli/latest/userguide/getting-started-install.html
AWS CLI Configure Files:
https://docs.aws.amazon.com/cli/latest/userguide/cli-configure-files.html
AWS S3 Sync:
https://docs.aws.amazon.com/cli/latest/reference/s3/sync.html
AWS S3 RM:
https://docs.aws.amazon.com/cli/latest/reference/s3/rm.html
#>
#endregion
if ($args | Where-Object { $_ -in @("-h", "--help", "-Help", "--usage") }) {
@'
Transfer-S3Contents.ps1
Copy or move objects from one Amazon S3 bucket/path to another Amazon S3 bucket/path.
Usage:
powershell -ExecutionPolicy Bypass -File .\Transfer-S3Contents.ps1
powershell -ExecutionPolicy Bypass -File .\Transfer-S3Contents.ps1 --help
Configure these values in the Settings region before running:
$SourceBucket
$DestinationBucket
$SourcePath
$DestinationPath
$Mode = "copy" or "move"
$SourceAccessKeyId / $SourceSecretAccessKey
$DestinationAccessKeyId / $DestinationSecretAccessKey
Examples:
$SourcePath = "/"
$DestinationPath = "/"
$SourcePath = "/images"
$DestinationPath = "/archive/images"
$SourcePath = "/folder/subfolder"
$DestinationPath = "/new-folder"
'@
Pop-Location
exit 0
}
#region Settings
$SourceBucket = "SOURCE-BUCKET-NAME"
$DestinationBucket = "DESTINATION-BUCKET-NAME"
$SourcePath = "/"
$DestinationPath = "/"
$Mode = "copy"
$SourceRegion = "us-east-1"
$DestinationRegion = "us-east-1"
$SourceProfile = "s3-source"
$DestinationProfile = "s3-destination"
$SourceAccessKeyId = "SOURCE-ACCESS-KEY-ID"
$SourceSecretAccessKey = "SOURCE-SECRET-ACCESS-KEY"
$DestinationAccessKeyId = "DESTINATION-ACCESS-KEY-ID"
$DestinationSecretAccessKey = "DESTINATION-SECRET-ACCESS-KEY"
$TempFolder = Join-Path $PSScriptRoot "S3-Move-Temp"
#endregion
#region Functions
function ConvertTo-S3Uri {
param (
[Parameter(Mandatory = $true)]
[string]$Bucket,
[Parameter(Mandatory = $true)]
[string]$Path
)
$CleanPath = $Path.Trim()
if ([string]::IsNullOrWhiteSpace($CleanPath) -or $CleanPath -eq "/") {
return "s3://$Bucket"
}
$CleanPath = $CleanPath.TrimStart("/")
$CleanPath = $CleanPath.TrimEnd("/")
return "s3://$Bucket/$CleanPath"
}
#endregion
#region Build S3 Paths
Write-Host "Building S3 paths..."
$SourceS3Uri = ConvertTo-S3Uri -Bucket $SourceBucket -Path $SourcePath
$DestinationS3Uri = ConvertTo-S3Uri -Bucket $DestinationBucket -Path $DestinationPath
Write-Host "Source S3 URI: $SourceS3Uri"
Write-Host "Destination S3 URI: $DestinationS3Uri"
Write-Host "Mode: $Mode"
#endregion
#region Check AWS CLI
Write-Host "Checking AWS CLI..."
$AwsCommand = Get-Command aws -ErrorAction SilentlyContinue
if (-not $AwsCommand) {
Write-Host "AWS CLI not found. Installing AWS CLI v2..."
$InstallerPath = Join-Path $env:TEMP "AWSCLIV2.msi"
Invoke-WebRequest `
-Uri "https://awscli.amazonaws.com/AWSCLIV2.msi" `
-OutFile $InstallerPath
if (-not (Test-Path $InstallerPath)) {
Write-Host "AWS CLI installer download failed."
exit 1
}
Start-Process msiexec.exe `
-ArgumentList "/i `"$InstallerPath`" /qn" `
-Wait
$env:Path = [System.Environment]::GetEnvironmentVariable("Path", "Machine") + ";" + [System.Environment]::GetEnvironmentVariable("Path", "User")
$AwsCommand = Get-Command aws -ErrorAction SilentlyContinue
if (-not $AwsCommand) {
Write-Host "AWS CLI install completed, but aws.exe was not found in PATH. Open a new PowerShell window and run again."
exit 1
}
}
Write-Host "AWS CLI found."
aws --version
if ($LASTEXITCODE -ne 0) {
Write-Host "AWS CLI version check failed."
exit 1
}
#endregion
#region Validate Settings
Write-Host "Validating settings..."
if ([string]::IsNullOrWhiteSpace($SourceBucket) -or $SourceBucket -eq "SOURCE-BUCKET-NAME") {
Write-Host "Source bucket name is not set."
exit 1
}
if ([string]::IsNullOrWhiteSpace($DestinationBucket) -or $DestinationBucket -eq "DESTINATION-BUCKET-NAME") {
Write-Host "Destination bucket name is not set."
exit 1
}
if ($Mode -notin @("copy", "move")) {
Write-Host "Mode must be copy or move."
exit 1
}
if ([string]::IsNullOrWhiteSpace($SourceRegion)) {
Write-Host "Source region is not set."
exit 1
}
if ([string]::IsNullOrWhiteSpace($DestinationRegion)) {
Write-Host "Destination region is not set."
exit 1
}
if ([string]::IsNullOrWhiteSpace($SourceAccessKeyId) -or $SourceAccessKeyId -eq "SOURCE-ACCESS-KEY-ID") {
Write-Host "Source access key ID is not set."
exit 1
}
if ([string]::IsNullOrWhiteSpace($SourceSecretAccessKey) -or $SourceSecretAccessKey -eq "SOURCE-SECRET-ACCESS-KEY") {
Write-Host "Source secret access key is not set."
exit 1
}
if ([string]::IsNullOrWhiteSpace($DestinationAccessKeyId) -or $DestinationAccessKeyId -eq "DESTINATION-ACCESS-KEY-ID") {
Write-Host "Destination access key ID is not set."
exit 1
}
if ([string]::IsNullOrWhiteSpace($DestinationSecretAccessKey) -or $DestinationSecretAccessKey -eq "DESTINATION-SECRET-ACCESS-KEY") {
Write-Host "Destination secret access key is not set."
exit 1
}
Write-Host "Settings validated."
#endregion
#region Configure Source Credentials
Write-Host "Configuring source AWS CLI profile..."
aws configure set aws_access_key_id $SourceAccessKeyId --profile $SourceProfile
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to set source access key."
exit 1
}
aws configure set aws_secret_access_key $SourceSecretAccessKey --profile $SourceProfile
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to set source secret key."
exit 1
}
aws configure set region $SourceRegion --profile $SourceProfile
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to set source region."
exit 1
}
Write-Host "Source AWS CLI profile configured: $SourceProfile"
#endregion
#region Configure Destination Credentials
Write-Host "Configuring destination AWS CLI profile..."
aws configure set aws_access_key_id $DestinationAccessKeyId --profile $DestinationProfile
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to set destination access key."
exit 1
}
aws configure set aws_secret_access_key $DestinationSecretAccessKey --profile $DestinationProfile
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to set destination secret key."
exit 1
}
aws configure set region $DestinationRegion --profile $DestinationProfile
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to set destination region."
exit 1
}
Write-Host "Destination AWS CLI profile configured: $DestinationProfile"
#endregion
#region Create Temp Folder
Write-Host "Creating temp folder..."
if (-not (Test-Path $TempFolder)) {
New-Item -ItemType Directory -Path $TempFolder | Out-Null
}
if (-not (Test-Path $TempFolder)) {
Write-Host "Failed to create temp folder."
exit 1
}
Write-Host "Temp folder ready: $TempFolder"
#endregion
#region Test Source Path Access
Write-Host "Testing source path access..."
aws s3 ls "$SourceS3Uri" --profile $SourceProfile --region $SourceRegion
if ($LASTEXITCODE -ne 0) {
Write-Host "Could not access source path."
exit 1
}
Write-Host "Source path access confirmed."
#endregion
#region Test Destination Path Access
Write-Host "Testing destination path access..."
aws s3 ls "$DestinationS3Uri" --profile $DestinationProfile --region $DestinationRegion
if ($LASTEXITCODE -ne 0) {
Write-Host "Could not access destination path."
exit 1
}
Write-Host "Destination path access confirmed."
#endregion
#region Download From Source
Write-Host "Downloading objects from source path..."
aws s3 sync "$SourceS3Uri" "$TempFolder" --profile $SourceProfile --region $SourceRegion
if ($LASTEXITCODE -ne 0) {
Write-Host "Download from source path failed."
exit 1
}
Write-Host "Download completed."
#endregion
#region Upload To Destination
Write-Host "Uploading objects to destination path..."
aws s3 sync "$TempFolder" "$DestinationS3Uri" --profile $DestinationProfile --region $DestinationRegion
if ($LASTEXITCODE -ne 0) {
Write-Host "Upload to destination path failed."
exit 1
}
Write-Host "Upload completed."
#endregion
#region Verify Object Counts
Write-Host "Checking source object count..."
$SourceCount = aws s3 ls "$SourceS3Uri" --recursive --profile $SourceProfile --region $SourceRegion | Measure-Object | Select-Object -ExpandProperty Count
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to count source path objects."
exit 1
}
Write-Host "Checking destination object count..."
$DestinationCount = aws s3 ls "$DestinationS3Uri" --recursive --profile $DestinationProfile --region $DestinationRegion | Measure-Object | Select-Object -ExpandProperty Count
if ($LASTEXITCODE -ne 0) {
Write-Host "Failed to count destination path objects."
exit 1
}
Write-Host "Source object count: $SourceCount"
Write-Host "Destination object count: $DestinationCount"
if ($SourceCount -ne $DestinationCount) {
Write-Host "Object counts do not match. Source will not be deleted."
exit 1
}
Write-Host "Object counts match."
#endregion
#region Delete Source If Move Mode
if ($Mode -eq "move") {
Write-Host "Move mode selected."
Write-Host "Source objects will now be deleted because copy and verification succeeded."
aws s3 rm "$SourceS3Uri" --recursive --profile $SourceProfile --region $SourceRegion
if ($LASTEXITCODE -ne 0) {
Write-Host "Source delete failed."
exit 1
}
Write-Host "Source objects deleted."
}
else {
Write-Host "Copy mode selected. Source objects were not deleted."
}
#endregion
#region Complete
Write-Host "S3 $Mode completed successfully."
Write-Host "Credentials saved under:"
Write-Host "$env:USERPROFILE\.aws\credentials"
Write-Host "$env:USERPROFILE\.aws\config"
$ProgressPreference = 'Continue'
#endregion