手邊有個需求,要對放進某個資料夾進行特別處理。為了方便展示及說明,我們假想一個簡單的檔案分類功能,會自動將放進資料夾中的圖檔搬去 image 資料夾,.txt/.html/.xml 搬去 text 資料夾。

這類需求,無腦做法是設個排程定期輪詢(Polling),掃瞄該目錄是否出現新檔案,若有就依附檔名搬檔,簡單粗暴但有效。但輪詢的缺點是執行間隔難以決定,太頻繁浪費 CPU/IO 資源,拖太久即時性不佳。若使用 .NET 開發,有個較優雅的高級解法 - FileSystemWatcher,可以用事件的概念監聽特定資料夾,當資料夾內的檔案出現異動(例如:新增、刪除、更新、更名)時呼叫你的程式碼,對檔案進行即時處理。FileSytemWatcher 從 .NET 1.1 就有了,不是什麼新鮮事,但這回我想用 PowerShell 來實現,就有些新東西可以學了。

先看程式碼:

$ErrorActionPreference = 'Stop'
$targetPath = Join-Path $PSScriptRoot 'src'
if (-not (Test-Path $targetPath)) {
    New-Item -Path $targetPath -ItemType Directory
}
$watcher = New-Object System.IO.FileSystemWatcher -ArgumentList $targetPath, $filter -Property @{
    Filter = '*.*'
    IncludeSubdirectories = $false
    EnableRaisingEvents = $true
}
$global:mapper = @{
    '.txt' = Join-Path $PSScriptRoot 'text'
    '.html' = Join-Path $PSScriptRoot 'text'
    '.xml' = Join-Path $PSScriptRoot 'text'
    '.jpg' = Join-Path $PSScriptRoot 'image'
    '.png' = Join-Path $PSScriptRoot 'image'
    '.gif' = Join-Path $PSScriptRoot 'image'
}
$srcId = 'FileChanged'
Register-ObjectEvent $watcher Created -SourceIdentifier $srcId -Action {
    # TODO: 設計機制確保檔案寫入完畢(例如:先寫 .unfinish 檔案,寫完後再改名)
    $filePath = $Event.SourceEventArgs.FullPath
    $extension = [System.IO.Path]::GetExtension($filePath)
    if ($global:mapper.ContainsKey($extension)) {
        try {
            Move-Item -Path $filePath -Destination $global:mapper[$extension] -Force
            Write-Host "$filePath moved to $($global:mapper[$extension])"
        }
        catch {
            Write-Host $_.Exception.Message
        }
    } else {
        Write-Host "Unknown file type: $filePath"
    }
} | Out-Null
try {
    while ($true) {
        Wait-Event -SourceIdentifier $srcId
    }
} finally {
    Unregister-Event -SourceIdentifier $srcId 
    $watcher.Dispose()
}

在 PowerShell 裡我們以建立 System.IO.FileSystemWatcher 物件監聽檔案異動,但 PowerShell 註冊 .NET 物件事件的做法比較特殊,要用 Register-ObjectEvent 建立事件並定義事件處理程序,要注意一點,-Action 指定的事件腳本區塊(Script Block)會在另一個新的 Scope 執行,無法存取上一層的變數,故我把副檔名與資料夾對映表設定 $global:mapper,事件腳本區塊才能存得到。定義完事件後,用 while ($true) 無窮迴圈跑 Wait-Event -SourceIdenfier $srcId 等待我們註冊的檔案新增事件發生。如果在等待過程還要處理其他邏輯,則可加上 -Timeout 秒數設定每次等待上限,逾時先決定做其他事,再繼續 while 的下一輪。

由於 FileSystemWatcher 會佔用 Unmanaged 資源,故要用 try finally 確保取消事件註冊及 Dispose 物件。

操作展示如下,先執行 file-dispatcher.ps1,我拖了六個不同型別的檔案到 src 資料夾,分類機制生效,.txt/.xml/.html 被搬到 text,.jpg/.png/.gif 被搬到 image,成功!

這裡留下一個問題,Created 事件會在開始寫檔時觸發,要是檔案很大或寫入速度很慢,我們有可能搬到還在寫入中的檔案,有可能因檔案鎖定無法搬移,也有可能搬到不完整的內容。不過,問題也不算難解,在寫檔時先將檔案取成 .in-process 之類的特殊副檔名,等寫入完成再將檔名換成正式名稱,這招在瀏覽器或 P2P 下載軟體還蠻常用的,這回我們也用到了。

A simple file sorting script using PowerShell and FileSystemWatcher monitors a directory for new files and moves them based on their extensions. Text files (.txt, .html, .xml) go to a ‘text’ folder, and image files (.jpg, .png, .gif) go to an ‘image’ folder. The script uses Register-ObjectEvent for event handling and ensures disposal of resources properly.


Comments

Be the first to post a comment

Post a comment