PowerShellの[Copy-Item]コマンドで効率的なファイルコピーを実現!5つの具体的な利用例

Copy-Itemで業務ファイルを複製するときは、曖昧な相対パスや既存宛先を使いません。source leafの完全パス・長さ・SHA256を固定し、run固有の新規directory内に一度だけcopyします。preview後にもsource同一性と宛先不在を読み直し、検証とcleanupを別工程にします。

目次

source leafと一意な宛先をbindする

source rootとdestination rootをResolve-Pathで完全パス化し、source leafが通常fileであることを確認します。destinationはGUIDを含む未存在run directoryと同じleaf名です。sourceのFullName・Length・SHA256を承認対象として保持します。

$ErrorActionPreference='Stop'
$sourceRoot=(Resolve-Path -LiteralPath 'C:\Lab\CopySource' -ErrorAction Stop).Path
$destinationRoot=(Resolve-Path -LiteralPath 'C:\Lab\CopyDestination' -ErrorAction Stop).Path
$sourceLeaf='monthly-report.csv'
$sourcePath=Join-Path $sourceRoot $sourceLeaf
if(-not(Test-Path -LiteralPath $sourcePath -PathType Leaf)){throw "source leaf not found: $sourcePath"}
$runId=[guid]::NewGuid().ToString('N')
$runDirectory=Join-Path $destinationRoot "ittrip-copy-$runId"
$destinationPath=Join-Path $runDirectory $sourceLeaf
if(Test-Path -LiteralPath $runDirectory){throw "run directory already exists: $runDirectory"}
$sourceItem=Get-Item -LiteralPath $sourcePath -Force -ErrorAction Stop
$sourceHash=(Get-FileHash -LiteralPath $sourcePath -Algorithm SHA256 -ErrorAction Stop).Hash
$state=[ordered]@{RunId=$runId;SourceRoot=$sourceRoot;SourceLeaf=$sourceLeaf;SourcePath=$sourceItem.FullName;SourceLength=[long]$sourceItem.Length;SourceSha256=$sourceHash;DestinationRoot=$destinationRoot;RunDirectory=$runDirectory;DestinationPath=$destinationPath;Stage='Planned'}
[pscustomobject]$state

previewと承認後に一度だけcopyする

WhatIf表示と完全なsource/destination/hashを含むtokenを確認します。承認後にsource identityとrun directory不在を再確認し、新規directoryを作った直後にdestination leaf不在を確認して、Forceなしのterminating Copy-Itemを一度だけ実行します。

Copy-Item -LiteralPath $state.SourcePath -Destination $state.DestinationPath -WhatIf
$token="COPY OP=$($state.RunId) SOURCE=$($state.SourcePath) DEST=$($state.DestinationPath) SHA256=$($state.SourceSha256)"
if((Read-Host "previewを確認し、実行する場合だけ $token を入力") -ne $token){throw 'copy was not approved'}
$sourceNow=Get-Item -LiteralPath $state.SourcePath -Force -ErrorAction Stop
$hashNow=(Get-FileHash -LiteralPath $state.SourcePath -Algorithm SHA256 -ErrorAction Stop).Hash
if($sourceNow.FullName -cne $state.SourcePath -or [long]$sourceNow.Length -ne [long]$state.SourceLength -or $hashNow -cne $state.SourceSha256){throw 'source identity changed after preview'}
if(Test-Path -LiteralPath $state.RunDirectory){throw 'destination run directory appeared before copy'}
$created=New-Item -ItemType Directory -Path $state.RunDirectory -ErrorAction Stop
try{
  if(Test-Path -LiteralPath $state.DestinationPath){throw 'destination leaf exists immediately before Copy-Item'}
  Copy-Item -LiteralPath $state.SourcePath -Destination $state.DestinationPath -ErrorAction Stop
}catch{
  if((Get-ChildItem -LiteralPath $state.RunDirectory -Force -ErrorAction SilentlyContinue|Measure-Object).Count -eq 0){Remove-Item -LiteralPath $state.RunDirectory -ErrorAction SilentlyContinue}
  throw
}
$state.Stage='Copied'

相対identity・長さ・SHA256を検証する

source rootからのrelative leafとrun directoryからのrelative leafが完全一致することを確認します。destinationは長さとSHA256もsource snapshotと一致し、run directoryに一件だけ存在する場合に限りVerifiedです。

$sourceRelative=[IO.Path]::GetRelativePath($state.SourceRoot,$state.SourcePath)
$destinationRelative=[IO.Path]::GetRelativePath($state.RunDirectory,$state.DestinationPath)
$destinationItem=Get-Item -LiteralPath $state.DestinationPath -Force -ErrorAction Stop
$destinationHash=(Get-FileHash -LiteralPath $state.DestinationPath -Algorithm SHA256 -ErrorAction Stop).Hash
if($sourceRelative -cne $state.SourceLeaf -or $destinationRelative -cne $state.SourceLeaf){throw 'relative leaf identity mismatch'}
if([long]$destinationItem.Length -ne [long]$state.SourceLength -or $destinationHash -cne $state.SourceSha256){throw 'copied bytes do not match source length/SHA256'}
if((Get-ChildItem -LiteralPath $state.RunDirectory -Force -ErrorAction Stop|Measure-Object).Count -ne 1){throw 'run directory contains an unexpected item'}
[pscustomobject]@{Status='Verified';Source=$state.SourcePath;Destination=$destinationItem.FullName;RelativeIdentity=$destinationRelative;Length=$destinationItem.Length;Sha256=$destinationHash}

run-created destinationだけを承認付きで削除する

cleanupはcopyとは別tokenです。directory、leaf、length、hash、directory item countの全ownership fieldを再確認し、今回作成したfileと空directoryだけをLiteralPathで削除します。sourceや既存destinationを削除しません。

$token="REMOVE-COPY OP=$($state.RunId) DEST=$($state.DestinationPath) SHA256=$($state.SourceSha256)"
if((Read-Host "run-created copyだけを削除する場合は $token を入力") -ne $token){return}
$destinationItem=Get-Item -LiteralPath $state.DestinationPath -Force -ErrorAction Stop
$destinationHash=(Get-FileHash -LiteralPath $state.DestinationPath -Algorithm SHA256 -ErrorAction Stop).Hash
if($destinationItem.DirectoryName -cne $state.RunDirectory -or $destinationItem.Name -cne $state.SourceLeaf -or [long]$destinationItem.Length -ne [long]$state.SourceLength -or $destinationHash -cne $state.SourceSha256){throw 'ownership fields changed; cleanup refused'}
if((Get-ChildItem -LiteralPath $state.RunDirectory -Force -ErrorAction Stop|Measure-Object).Count -ne 1){throw 'cleanup refused because the run directory is not exclusively owned'}
Remove-Item -LiteralPath $state.DestinationPath -ErrorAction Stop
Remove-Item -LiteralPath $state.RunDirectory -ErrorAction Stop
if(Test-Path -LiteralPath $state.RunDirectory){throw 'cleanup readback failed'}
[pscustomobject]@{Status='RunCopyRemoved';RunId=$state.RunId}

停止条件

sourceの差替え、宛先衝突、copy error、hash不一致、余分なdirectory item、cleanup前のidentity変化では停止します。Copy-Itemにはatomic no-clobber switchがないため、共有既存directoryの任意名へ直接copyせず、run固有の新規directoryを所有境界にしています。

受入結果

clean fixtureではsource leaf一件だけが同名relative pathで複製され、Length/SHA256が一致します。二回目や衝突fixtureでは実行前に停止し、cleanup拒否時もsourceとdestinationを保持して手動確認へ渡します。

公式情報・参考資料

この記事を書いた人

実務の現場で詰まりがちなポイントを地図にするITブログ「IT trip」を運営。Windows/Office(Teams・Excel)からSQL、サーバ運用、ガジェットまで、再現性のある手順と“なぜそうなるか”を丁寧に解説します。読んだらすぐ試せること、そして迷った人の次の一歩が見えることを大切にしています。

コメント

コメントする

目次