Public/Move-FileShare.ps1
|
function Move-FileShare { <# .SYNOPSIS Migrates Azure file shares from one storage account to another using azcopy. .DESCRIPTION Enumerates file shares on the source storage account and copies them to the destination using azcopy with SAS token authentication. Supports parallel copy, dry-run mode, and per-share log files. .PARAMETER SourceAccountName Name of the source storage account. .PARAMETER DestinationAccountName Name of the destination storage account. .PARAMETER SasExpiryHours Hours until generated SAS tokens expire. Default: 24. .PARAMETER ShareNames Optional list of specific share names to copy. When omitted, all shares are copied. .PARAMETER ThrottleLimit Maximum parallel share copies. Default: 10. .PARAMETER LogDirectory Base directory for log files. Default: current directory. .EXAMPLE Move-FileShare -SourceAccountName 'sourceaccount' -DestinationAccountName 'destaccount' .EXAMPLE Move-FileShare -SourceAccountName 'sourceaccount' -DestinationAccountName 'destaccount' -ShareNames 'finance','hr' -WhatIf #> [CmdletBinding(SupportsShouldProcess)] [Diagnostics.CodeAnalysis.SuppressMessageAttribute('PSAvoidUsingPositionalParameters', '', Justification = 'Join-Path positional parameters are idiomatic and readable')] param( [Parameter(Mandatory)] [string]$SourceAccountName, [Parameter(Mandatory)] [string]$DestinationAccountName, [Parameter()] [int]$SasExpiryHours = 24, [Parameter()] [string[]]$ShareNames, [Parameter()] [ValidateRange(1, 64)] [int]$ThrottleLimit = 10, [Parameter()] [string]$LogDirectory = (Get-Location).Path ) Set-StrictMode -Version Latest $ErrorActionPreference = 'Stop' # ── Pre-flight ─────────────────────────────────────────────────────────── Assert-AzCopyInstalled Assert-AzCliLogin | Out-Null Test-StorageAccountAccess -AccountName $SourceAccountName Test-StorageAccountAccess -AccountName $DestinationAccountName # ── Generate SAS tokens ────────────────────────────────────────────────── $sourceSas = New-SasToken -AccountName $SourceAccountName -Permissions 'rl' -ExpiryHours $SasExpiryHours $destSas = New-SasToken -AccountName $DestinationAccountName -Permissions 'rwdlc' -ExpiryHours $SasExpiryHours # ── Enumerate source shares ────────────────────────────────────────────── Write-Host "`n[$(Get-Timestamp)] Enumerating file shares on '$SourceAccountName'..." -ForegroundColor Cyan $sharesOutput = az storage share list --account-name $SourceAccountName --query '[].name' -o json 2>&1 if ($LASTEXITCODE -ne 0) { throw "Failed to list file shares: $sharesOutput" } $sharesJson = ($sharesOutput | Where-Object { $_ -isnot [System.Management.Automation.ErrorRecord] }) -join "`n" $shares = @($sharesJson | ConvertFrom-Json) if ($shares.Count -eq 0) { Write-Warning "No file shares found on '$SourceAccountName'. Nothing to do." return } Write-Host " Found $($shares.Count) file share(s) on source." -ForegroundColor Green # Filter to requested shares if ($ShareNames) { $missing = $ShareNames | Where-Object { $_ -notin $shares } if ($missing) { throw "The following share(s) do not exist on '$SourceAccountName': $($missing -join ', ')" } $shares = @($shares | Where-Object { $_ -in $ShareNames }) Write-Host " Targeting $($shares.Count) share(s): $($shares -join ', ')" -ForegroundColor Yellow } else { Write-Host " Targeting all $($shares.Count) share(s): $($shares -join ', ')" -ForegroundColor Green } # ── Prepare ────────────────────────────────────────────────────────────── $dryRunFlag = if ($WhatIfPreference) { '--dry-run' } else { $null } $modeLabel = if ($WhatIfPreference) { 'DRY-RUN' } else { 'COPY' } $failed = [System.Collections.Concurrent.ConcurrentBag[string]]::new() $succeeded = [System.Collections.Concurrent.ConcurrentBag[string]]::new() $logEntries = [System.Collections.Concurrent.ConcurrentBag[string]]::new() $logDir = Join-Path $LogDirectory 'logs' "move-fileshares_$(Get-Date -Format 'yyyyMMdd_HHmmss')" New-Item -ItemType Directory -Path $logDir -Force -Confirm:$false -WhatIf:$false | Out-Null $scriptStart = Get-Date Write-Host "`n[$(Get-Timestamp)] Starting parallel copy of $($shares.Count) share(s) with ThrottleLimit=$ThrottleLimit..." -ForegroundColor Cyan Write-Host "[$(Get-Timestamp)] Log directory: $logDir" -ForegroundColor Cyan # ── Parallel copy ──────────────────────────────────────────────────────── $shares | ForEach-Object -ThrottleLimit $ThrottleLimit -Parallel { $share = $_ $srcAcct = $using:SourceAccountName $dstAcct = $using:DestinationAccountName $srcSas = $using:sourceSas $dstSas = $using:destSas $dryRun = $using:dryRunFlag $mode = $using:modeLabel $failedBag = $using:failed $succeededBag = $using:succeeded $logBag = $using:logEntries $logFile = Join-Path $using:logDir "$share.log" function Write-Log { param([string]$Message, [string]$Color = 'White', [string]$LogPath) $ts = (Get-Date).ToString('yyyy-MM-dd HH:mm:ss') $line = "[$ts] [$share] $Message" Write-Host $line -ForegroundColor $Color $line | Out-File -FilePath $LogPath -Append -Encoding utf8 } $sourceUrl = "https://$srcAcct.file.core.windows.net/$share/?$srcSas" $destUrl = "https://$dstAcct.file.core.windows.net/$share/?$dstSas" $shareStart = Get-Date Write-Log "Starting $mode..." -Color Yellow -LogPath $logFile Write-Log " Source : https://$srcAcct.file.core.windows.net/$share/" -Color Gray -LogPath $logFile Write-Log " Dest : https://$dstAcct.file.core.windows.net/$share/" -Color Gray -LogPath $logFile $azcopyArgs = @( 'copy' $sourceUrl $destUrl '--recursive' '--preserve-smb-permissions=false' '--preserve-smb-info=true' '--overwrite=ifSourceNewer' ) if ($dryRun) { $azcopyArgs += $dryRun } $azcopyOutput = & azcopy @azcopyArgs 2>&1 $exitCode = $LASTEXITCODE $shareEnd = Get-Date $elapsed = $shareEnd - $shareStart foreach ($line in $azcopyOutput) { $ts = $shareEnd.ToString('yyyy-MM-dd HH:mm:ss') $prefixed = "[$ts] [$share] $line" Write-Host $prefixed $prefixed | Out-File -FilePath $logFile -Append -Encoding utf8 } if ($exitCode -ne 0) { Write-Log "FAILED (exit code $exitCode) after $($elapsed.ToString('hh\:mm\:ss'))" -Color Red -LogPath $logFile $failedBag.Add($share) $logBag.Add("FAILED | $share | exit=$exitCode | duration=$($elapsed.ToString('hh\:mm\:ss')) | log=$logFile") } else { Write-Log "SUCCEEDED in $($elapsed.ToString('hh\:mm\:ss'))" -Color Green -LogPath $logFile $succeededBag.Add($share) $logBag.Add("SUCCESS | $share | duration=$($elapsed.ToString('hh\:mm\:ss')) | log=$logFile") } } # ── Summary ────────────────────────────────────────────────────────────── $totalElapsed = (Get-Date) - $scriptStart Write-Host "`n════════════════════════════════════════════════════════════" -ForegroundColor Cyan Write-Host " $modeLabel SUMMARY" -ForegroundColor Cyan Write-Host '════════════════════════════════════════════════════════════' -ForegroundColor Cyan Write-Host " Total time : $($totalElapsed.ToString('hh\:mm\:ss'))" -ForegroundColor White Write-Host " Succeeded : $($succeeded.Count) — $(($succeeded | Sort-Object) -join ', ')" -ForegroundColor Green if ($failed.Count -gt 0) { Write-Host " Failed : $($failed.Count) — $(($failed | Sort-Object) -join ', ')" -ForegroundColor Red } Write-Host '' Write-Host ' Per-share details:' -ForegroundColor Cyan foreach ($entry in ($logEntries | Sort-Object)) { $color = if ($entry.StartsWith('SUCCESS')) { 'Green' } else { 'Red' } Write-Host " $entry" -ForegroundColor $color } Write-Host '' Write-Host " Log directory: $logDir" -ForegroundColor Cyan Write-Host '' if ($failed.Count -gt 0) { throw "$($failed.Count) file share(s) failed to copy." } } |