講到網站或程式部署,已有不少現成檔案同步工具,過去有介紹過:

要忽略沒有異動的檔案,標準做法是檢查檔案更新時間跟大小,但在某些狀況下不管用,例如:有些發佈程序會刪除舊檔重新複製、或是更新檔案時間,而 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

Post a comment


99 - 45 =