PowerShell · 手動コピー用

SVN-ProtectFile.ps1

SVN-ProtectFile.ps1
# SVN作業コピー内ファイルの直接上書き検知スクリプト(複数ファイル対応)
# 手順: 指定した複数のファイルを常駐監視し、作業フォルダ内でファイルを直接開いて編集中でない状態
#       (Word/Excel等が作る "~$ファイル名" ロックファイルが無い状態)での変更(=外部からの直接上書きコピー)を
#       検知したら警告ダイアログを表示する。
# 使い方: .\SVN-ProtectFile.ps1 -ListFile "C:\path\to\watchlist.txt"
#     ※ watchlist.txt は1行に1パス。空行や "#" で始まる行はコメントとして無視されます。
#         このウィンドウを開いたまま(最小化可)にしておいてください。終了するには Ctrl+C を押してください。
# 注: 警告を表示するだけで、上書き自体は止めません。

param(
    [Parameter(Mandatory = $true)]
    [string]$ListFile
)

Add-Type -AssemblyName System.Windows.Forms

# --- 一覧ファイルからのパス読込 ---
if (-not (Test-Path $ListFile)) {
    Write-Host "一覧ファイルが見つかりません: $ListFile" -ForegroundColor Red
    exit 1
}

$allPaths = Get-Content $ListFile |
    ForEach-Object { $_.Trim() } |
    Where-Object { $_ -ne '' -and -not $_.StartsWith('#') }

if ($allPaths.Count -eq 0) {
    Write-Host "一覧ファイルに有効なパスが記載されていません: $ListFile" -ForegroundColor Red
    exit 1
}

# --- 監視対象ファイルの検証・正規化 ---
$targets = @()
foreach ($p in $allPaths) {
    if (-not (Test-Path $p)) {
        Write-Host "監視対象のファイルが見つかりません: $p" -ForegroundColor Red
        continue
    }
    $full = (Resolve-Path $p).Path
    $dir = Split-Path $full -Parent
    $name = Split-Path $full -Leaf
    $targets += [PSCustomObject]@{
        FilePath       = $full
        Dir            = $dir
        Name           = $name
        OfficeLockPath = Join-Path $dir ('~$' + $name)
    }
}

if ($targets.Count -eq 0) {
    Write-Host "有効な監視対象がありません。終了します。" -ForegroundColor Red
    exit 1
}

# 同一ファイルが重複指定された場合は除去
$targets = $targets | Sort-Object FilePath -Unique

# デバウンス管理用(ファイルごと)
$global:SvnProtectLastWarn = @{}
foreach ($t in $targets) {
    $global:SvnProtectLastWarn[$t.FilePath] = (Get-Date).AddSeconds(-3600)
}

# フォルダごとにグループ化し、フォルダ単位で FileSystemWatcher を1つ作成
$watchers = @()
$groups = $targets | Group-Object Dir

foreach ($g in $groups) {
    $watcher = New-Object System.IO.FileSystemWatcher
    $watcher.Path = $g.Name
    $watcher.Filter = '*'
    $watcher.NotifyFilter = [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::Size
    $watcher.EnableRaisingEvents = $true

    # このフォルダ内で監視すべきファイル名 -> ターゲット情報 のマップ
    $fileMap = @{}
    foreach ($t in $g.Group) {
        $fileMap[$t.Name] = $t
    }

    $messageData = [PSCustomObject]@{
        FileMap  = $fileMap
        Debounce = 5
    }

    $action = {
        $data = $Event.MessageData
        $changedName = $Event.SourceEventArgs.Name
        if (-not $data.FileMap.ContainsKey($changedName)) {
            # 監視対象外のファイルなので無視
            return
        }
        $target = $data.FileMap[$changedName]

        if (Test-Path $target.OfficeLockPath) {
            # 作業フォルダ内でファイルが直接開かれて編集中(Office系ロックファイルあり)なので警告しない
            return
        }

        $now = Get-Date
        $last = $global:SvnProtectLastWarn[$target.FilePath]
        if (($now - $last).TotalSeconds -lt $data.Debounce) { return }
        $global:SvnProtectLastWarn[$target.FilePath] = $now

        [System.Windows.Forms.MessageBox]::Show(
            "このファイルはSVN作業コピー内のファイルです:`n$($target.FilePath)`n`n他の場所からの直接上書きコピーは行わず、作業フォルダ内でこのファイルを直接開いて編集・保存してください。",
            "SVN管理ファイルへの直接上書きを検知",
            [System.Windows.Forms.MessageBoxButtons]::OK,
            [System.Windows.Forms.MessageBoxIcon]::Warning
        ) | Out-Null
    }

    Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $action -MessageData $messageData | Out-Null
    Register-ObjectEvent -InputObject $watcher -EventName Created -Action $action -MessageData $messageData | Out-Null
    Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $action -MessageData $messageData | Out-Null

    $watchers += $watcher
}

Write-Host "監視を開始しました:" -ForegroundColor Green
foreach ($t in $targets) {
    Write-Host "  - $($t.FilePath)" -ForegroundColor Green
}
Write-Host "このウィンドウを開いたままにしてください。終了するには Ctrl+C を押してください。" -ForegroundColor Yellow

try {
    while ($true) {
        Wait-Event -Timeout 1 | Out-Null
    }
}
finally {
    foreach ($watcher in $watchers) {
        Get-EventSubscriber | Where-Object { $_.SourceObject -eq $watcher } | Unregister-Event
        $watcher.Dispose()
    }
    Write-Host "監視を終了しました。" -ForegroundColor Yellow
}
watchlist.example.txt
# SVN-ProtectFile.ps1 の監視対象ファイル一覧
# 1行に1パスを記述してください。空行や # で始まる行は無視されます。
C:\path\to\file1.xlsx
C:\path\to\file2.docx
C:\other\dir\file3.xlsx