實用小工具 - 比對檔案內容差異同步資料夾
0 |
講到網站或程式部署,已有不少現成檔案同步工具,過去有介紹過:
- 檔案部署指令實戰技巧整理
- 在TFS 2012 Build Service使用Robocopy實現自動部署
- 如果考慮商業軟體,BeyondCompare 更是其中的王者
自動產生專案部署相關文件(使用BeyondCompare)
要忽略沒有異動的檔案,標準做法是檢查檔案更新時間跟大小,但在某些狀況下不管用,例如:有些發佈程序會刪除舊檔重新複製、或是更新檔案時間,而 Git 在切換 Branch 或 Reset 過程,相同檔案在還原後日期會改變。BeyondCompare 具有忽略檔案日期直接比對檔案內容的功能,能因應這類情境找出真正異動的檔案,而我想在 PowerShell 實現相同功能。
實作原理其實很簡單,用 Get-ChildItem 可以列舉目錄包含子目錄的所有檔案,與目標資料夾比對,若檔案不存在可直接複製;若存在,先比對檔案大小,大小相同再用 Get-FileHash 計算 MD5 雜湊(此類應用無防破解需求,就不浪費資源跑 SHA1、SHA256 了),若檔案一致就不複製。至於目標資料有,但來源資料夾沒有的檔案,為求保險我沒採取直接刪除的策略,而是 Write-Output 產生 DEL 檔名指令,可人工確認後再執行。另外,我還加了一個功能,比對差異不覆寫檔案,而是將要新增及覆寫檔案依原有目錄結構整理成資料夾,方便後續應用。
Sync-FilesByHash.ps1 完整程式碼如下:
param(
[Parameter(Mandatory = $true)][string]$srcPath,
[Parameter(Mandatory = $true)][string]$dstPath,
[string]$diffPath
)
$ErrorActionPreference = 'STOP'
function GetAbsPath([string]$path) {
if ($path.Contains(":") -or $path.StartsWith('\\')) {
return $path
}
return (Resolve-Path $path)
}
$dstPath = GetAbsPath $dstPath
$cmpPath = $dstPath
$srcPath = GetAbsPath $srcPath
if (![string]::IsNullOrEmpty($diffPath)) {
$dstPath = GetAbsPath $diffPath
}
Push-Location
Set-Location $srcPath
try {
$list = Get-ChildItem -Recurse -File $srcPath
$total = $list.Length
$idx = 0
$srcRelPaths = @{}
$list | ForEach-Object {
$srcFilePath = $_.FullName
$relPath = Resolve-Path -Relative $srcFilePath
$srcRelPaths[$relPath] = $true
$idx++
Write-Progress -Activity "Syncing files..." -Status "$relPath ( $idx / $total )" -PercentComplete ($idx * 100 / $total)
$cmpFilePath = Join-Path $cmpPath $relPath
$dstFilePath = Join-Path $dstPath $relPath
if (!(Test-Path -PathType Leaf $cmpFilePath)) {
Write-Host "A $relPath"
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($dstFilePath)) | Out-Null
Copy-Item $srcFilePath $dstFilePath
}
else {
# compare file size first, then MD5 hash (enough for this scenario)
if ($_.Length -ne (Get-Item $cmpFilePath).Length -or `
(Get-FileHash $srcFilePath -Algorithm MD5).Hash -ne (Get-FileHash $cmpFilePath -Algorithm MD5).Hash) {
Write-Host "M $relPath"
[System.IO.Directory]::CreateDirectory([System.IO.Path]::GetDirectoryName($dstFilePath)) | Out-Null
Copy-Item $srcFilePath $dstFilePath
}
}
$total++
}
}
finally {
Pop-Location
}
Write-Progress -Activity "Syncing files..." -Completed
Write-Host "-- $total files processed. --"
# check files not existing in source
Push-Location
try {
Set-Location $cmpPath
Get-ChildItem -Recurse -File $cmpPath | ForEach-Object {
$relPath = Resolve-Path -Relative $_.FullName
if (!$srcRelPaths.ContainsKey($relPath)) {
Write-Output "DEL $($_.FullName)"
}
}
}
finally {
Pop-Location
}
為驗證程式功能,我用以下批次檔準備 src4test 及 dst4test 兩個資料夾,刻意模擬出檔案模擬新增、內容更改、內容相同但檔案時間不同、待刪除... 等情況:
rmdir /s /q src4test
rmdir /s /q dst4test
mkdir src4test
mkdir src4test\sub
echo SUB > \src4test\sub\file.txt
echo UNCHG > \src4test\unchg.txt
echo UPDTIME > \src4test\filetime-chg.txt
echo MDFY > \src4test\mdfy.txt
echo NEW > \src4test\new.txt
xcopy src4test dst4test\ /s /y
rem make some difference
del \dst4test\new.txt
echo APPEND >> \dst4test\mdfy.txt
echo UPD_SUB >> \dst4test\sub\file.txt
echo DEL > \dst4test\sub\to-del.txt
rem delay 5s
ping -n 6 127.0.0.1
rem same content with new file update time
echo UPDTIME > \dst4test\filetime-chg.txt
做完先用 BeyondCompare 驗證無誤:
進行實測,Sync-FilesByHash.ps1 修改了 mdfy.txt、sub\file.txt,新增了 new.txt,至於內容沒變只有檔案時間不用的 filetime-chg.txt 則被忽略。而執行後針對 dst4test 有但 src4test 沒有的 to-del.txt 輸出 DEL D:\dst4test\sub\to-del.txt。
如要刪除檔案,可在後方加上 > del.bat 取得指令檔,確認後執行,或是接上 PowerShell Pipeline 用 Remove-Item 刪除:
.\Sync-FilesByHash D:\src4test D:\dst4test > del.bat
(.\Sync-FilesByHash D:\src4test D:\dst4test) | ForEach-Object { Remove-Item $_.Substring(4) }
最後示範加上第三個參數,將差異檔案匯出到 D:\diff 資料夾:
這樣就完成一個改善生活品質的好用工具囉,開心。
A handy tool to sync files between folders by file hash.
Comments
Be the first to post a comment