#目的
よく更新するファイルを、更新日時を名前に付して自動的にコピー(バックアップ)し、世代管理します。
#環境
OS : Windows 7 以降
PowerShell: Version 3 以降
#使用方法
タスクスケジューラやスタートアップに登録して常時実行したままの状態にします。
#コード
PowerShellスクリプトですが、Windows バッチファイルとして実行できる特殊な構造になっています。PowerShellスクリプトとして実行するには、1行目をコメントアウトまたは削除して拡張子を.ps1としてファイルに保存してください。
- 特定の1ファイルのみを対象とするスクリプト
@PowerShell -NoP -C "&([ScriptBlock]::Create((Get-Content '%~f0'|?{$_.ReadCount -gt 1}|Out-String)))" & exit
# by earthdiver1
###################################################################################################################################
$src_file = "C:\:somedir\important_file.doc"
$dst_dir = "D:\backup"
$num_copy = 20
$interval_1 = 180
$interval_2 = 15
###################################################################################################################################
$ErrorActionPreference = "Stop"
while ( -not (Test-Path $dst_dir) ) { Write-Output "$(Get-Date) Waiting..."; Start-Sleep 15 }
$old_mtime = ""
$old_hash = ""
while ( $true ) {
try {
$file = Get-ChildItem -LiteralPath $src_file
$new_mtime = $file.LastWriteTime.ToString('yyyyMMddHHmm')
if ( $new_mtime -ne $old_mtime ) {
$new_hash = $file.GetHashCode()
if ( $new_hash -ne $old_hash ) {
$dst_file = Join-Path $dst_dir ($file.BaseName + "_" + $new_mtime + $file.Extension)
if ( -not ( Test-Path $dst_file ) ) {
Copy-Item -LiteralPath $src_file -Destination $dst_file -Force
$filter = $file.BaseName + "_????????????" + $file.Extension
Get-ChildItem -LiteralPath $dst_dir -Filter $filter | Sort-Object | Select-Object -SkipLast $num_copy | Remove-Item -Force
}
$old_hash = $new_hash
}
$old_mtime = $new_mtime
}
$interval = $interval_1
} catch [System.Exception] {
Write-Output $Error[0].ToString() $Error[0].InvocationInfo.PositionMessage
$interval = $interval_2
}
Start-Sleep $interval
}
- 指定したフォルダに含まれる全ファイルを対象とするスクリプト
@PowerShell -NoP -C "&([ScriptBlock]::Create((Get-Content '%~f0'|?{$_.ReadCount -gt 1}|Out-String)))" & exit
# by earthdiver1
###################################################################################################################################
$src_root = "C:\somedir_containing_important_files"
$dst_root = "D:\backup"
$num_copy = 20
$interval_1 = 180
$interval_2 = 15
###################################################################################################################################
$ErrorActionPreference = "Stop"
while (-not (Test-Path $dst_root)) { Write-Output "$(Get-Date) Waiting..."; Start-Sleep 15 }
$MD5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider
$src_root = Convert-Path -LiteralPath $src_root
$dst_root = Convert-Path -LiteralPath $dst_root
$old_mtime = New-Object 'System.Collections.Generic.Dictionary[String,String]'
$old_hash = New-Object 'System.Collections.Generic.Dictionary[String,String]'
$old_name = New-Object 'System.Collections.Generic.Dictionary[String,String]'
# Restore $old_hash of the last session from the backup.hash file.
if ( Test-Path -LiteralPath "$dst_root\backup.hash" ) {
try{
$sr = [IO.StreamReader]::new( "$dst_root\backup.hash", [Text.Encoding]::Default )
while ( -not $sr.EndOfStream ) {
$key,$value,$src_file = $sr.ReadLine() -split ","
$old_hash.$key = $value
$old_name.$key = $src_file
}
} catch {
throw
} finally {
$sr.Close()
}
# Regenerate backup.hash for filtering out the updated records.
try {
$sw = [IO.StreamWriter]::new( "$dst_root\backup.hash_", $false, [Text.Encoding]::Default )
$old_hash.GetEnumerator() | & { process{ $sw.WriteLine( "$($_.Key),$($_.Value),$($old_name.($_.Key))" ) } }
} catch {
throw
} finally {
$sw.Close()
}
if ( Test-Path "$dst_root\backup.hash_" ) {
Move-Item -LiteralPath "$dst_root\backup.hash_" -Destination "$dst_root\backup.hash" -Force
}
}
while ( $true ) {
try {
$sw = [IO.StreamWriter]::new( "$dst_root\backup.hash", $true, [Text.Encoding]::Default )
Get-ChildItem -LiteralPath $src_root -File -Recurse | ForEach-Object {
$file = $_
$src_file = $file.FullName
if ( $src_file -Like "$dst_root\*" ) { return }
# if ( $file.Length -gt 100MB ) { return }
$new_mtime = $file.LastWriteTime.ToString('yyyyMMddHHmm')
$idx = [Bitconverter]::ToString($MD5.ComputeHash([Text.Encoding]::Default.GetBytes( $src_file ))).Replace("-","")
# First, pick up candidates by file modification time.
if ( $new_mtime -ne $old_mtime.$idx ) {
$stream = try {
[IO.FileStream]::new( $src_file, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read )
} catch {
[IO.FileStream]::new( $src_file, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite )
}
if ( $stream ) {
try {
$new_hash = ( Get-FileHash -InputStream $stream -Algorithm MD5 ).Hash
} catch {
throw
} finally {
$stream.Close()
}
} else { throw }
# Then, use the hash value to determine the target.
if ( $new_hash -ne $old_hash.$idx ) {
$dst_dir = $dst_root + $file.DirectoryName.Substring($src_root.Length, $file.DirectoryName.Length - $src_root.Length)
if ( -not ( Test-Path -LiteralPath $dst_dir ) ) {
New-Item -ItemType Directory -Force -Path $dst_dir | Out-Null
}
$dst_file = Join-Path $dst_dir ($file.BaseName + "_" + $new_mtime + $file.Extension)
if ( -not ( Test-Path $dst_file ) ) {
Copy-Item -LiteralPath $src_file -Destination $dst_file -Force
$filter = $file.BaseName + "_????????????" + $file.Extension
Get-ChildItem -LiteralPath $dst_dir -File -Filter $filter | Sort-Object | Select-Object -SkipLast $num_copy | Remove-Item -Force
}
$old_hash.$idx = $new_hash
$sw.WriteLine( "$idx,$new_hash,$src_file" )
}
$old_mtime.$idx = $new_mtime
}
}
$sw.Close()
$interval = $interval_1
} catch [System.Exception] {
Write-Output $Error[0].ToString() $Error[0].InvocationInfo.PositionMessage
$interval = $interval_2
} finally {
if ( $sw -and $sw.BaseStream ) { $sw.Close() }
}
Start-Sleep $interval
}
#備考
- 3分毎に更新をチェックします(異常発生時には15秒後)。
- ファイル毎のバックアップの最大数(保存する世代の最大数)は20です。
-
$dst_root
に、$src_root
と同一のフォルダは指定できません。 - 2番目の例で53行目(
$new_mtime =
の行の前)の後にif ($file.Extension -eq ".tmp") {return}
などを挿入することで監視対象から一部のファイルを除外することが可能です。 - ファイルの同一性をチェックするためのハッシュ値のアルゴリズムに MD5 を使用しています(SHA1やSHA256などに容易に変更可能)。
- バックアップ済みの
$src_root
上のファイルのハッシュ値を$dst_root\backup.hash
ファイル に保管します(スクリプトの再実行時に読み込んで前回のセッションの状態を復元するため)。$dst_root
上にコピーされたファイルを移動/削除しても、再度のバックアップは実行されません。リセットするにはbackup.hash
ファイルを削除してください。 - タスクトレイ常駐版1は
こちらをクリック
backup_files_on_update.bat@PowerShell -NoP -W Hidden -C "$PSCP='%~f0';$PSSR='%~dp0'.TrimEnd('\');&([ScriptBlock]::Create((gc '%~f0'|?{$_.ReadCount -gt 1}|Out-String)))" & exit/b # by earthdiver1 if ($PSCommandPath) { $PSCP = $PSCommandPath $PSSR = $PSScriptRoot $code = '[DllImport("user32.dll")]public static extern bool ShowWindowAsync(IntPtr hWnd,int nCmdShow);' $type = Add-Type -MemberDefinition $code -Name Win32ShowWindowAsync -PassThru [void]$type::ShowWindowAsync((Get-Process -PID $PID).MainWindowHandle,0) } Add-Type -AssemblyName System.Windows.Forms, System.Drawing $menuItem = New-Object System.Windows.Forms.MenuItem "Exit" $menuItem.add_Click({$notifyIcon.Visible=$False;while(-not $status.IsCompleted){Start-Sleep 1};$appContext.ExitThread()}) $contextMenu = New-Object System.Windows.Forms.ContextMenu $contextMenu.MenuItems.AddRange($menuItem) $notifyIcon = New-Object System.Windows.Forms.NotifyIcon $notifyIcon.ContextMenu = $contextMenu $notifyIcon.Icon = [System.Drawing.Icon]::ExtractAssociatedIcon($PSCP) $notifyIcon.Text = (Get-ChildItem $PSCP).BaseName $notifyIcon.Visible = $True $_syncHash = [hashtable]::Synchronized(@{}) $_syncHash.NI = $notifyIcon $_syncHash.PSCP = $PSCP $_syncHash.PSSR = $PSSR $runspace = [RunspaceFactory]::CreateRunspace() $runspace.ApartmentState = "STA" $runspace.ThreadOptions = "ReuseThread" $runspace.Open() $runspace.SessionStateProxy.SetVariable("_syncHash",$_syncHash) $scriptBlock = Get-Content $PSCP | ?{ $on -or $_[1] -eq "!" }| %{ $on=1; $_ } | Out-String $action=[ScriptBlock]::Create(@' # param($Param1, $Param2) Start-Transcript -LiteralPath ($_syncHash.PSCP -Replace '\..*?$',".log") -Append Function Start-Sleep { [CmdletBinding(DefaultParameterSetName="S")] param([parameter(Position=0,ParameterSetName="M")][Int]$Milliseconds, [parameter(Position=0,ParameterSetName="S")][Int]$Seconds,[Switch]$NoExit) if ($PsCmdlet.ParameterSetName -eq "S") { $int = 5 for ($i = 0; $i -lt $Seconds; $i += $int) { if (-not($NoExit -or $_syncHash.NI.Visible)) { exit } Microsoft.PowerShell.Utility\Start-Sleep -Seconds $int } } else { $int = 100 for ($i = 0; $i -lt $Milliseconds; $i += $int) { if (-not($NoExit -or $_syncHash.NI.Visible)) { exit } Microsoft.PowerShell.Utility\Start-Sleep -Milliseconds $int }}} $script:PSCommandPath = $_syncHash.PSCP $script:PSScriptRoot = $_syncHash.PSSR '@ + $scriptBlock) $PS = [PowerShell]::Create().AddScript($action) #.AddArgument($Param1).AddArgument($Param2) $PS.Runspace = $runspace $status = $PS.BeginInvoke() $appContext = New-Object System.Windows.Forms.ApplicationContext [void][System.Windows.Forms.Application]::Run($appContext) exit #! ---------- ScriptBlock (Line No. 28) begins here ---------- DO NOT REMOVE THIS LINE ################################################################################################################################### $src_root = "C:\somedir_containing_important_files" $dst_root = "D:\backup" $num_copy = 20 $interval_1 = 180 $interval_2 = 15 ################################################################################################################################### $ErrorActionPreference = "Stop" while (-not (Test-Path $dst_root)) { Write-Output "$(Get-Date) Waiting..."; Start-Sleep 15 } $MD5 = New-Object System.Security.Cryptography.MD5CryptoServiceProvider $src_root = Convert-Path -LiteralPath $src_root $dst_root = Convert-Path -LiteralPath $dst_root $old_mtime = New-Object 'System.Collections.Generic.Dictionary[String,String]' $old_hash = New-Object 'System.Collections.Generic.Dictionary[String,String]' $old_name = New-Object 'System.Collections.Generic.Dictionary[String,String]' # Restore $old_hash of the last session from the backup.hash file. if ( Test-Path -LiteralPath "$dst_root\backup.hash" ) { try{ $sr = [IO.StreamReader]::new( "$dst_root\backup.hash", [Text.Encoding]::Default ) while ( -not $sr.EndOfStream ) { $key,$value,$src_file = $sr.ReadLine() -split "," $old_hash.$key = $value $old_name.$key = $src_file } } catch { throw } finally { $sr.Close() } # Regenerate backup.hash for filtering out the updated records. try { $sw = [IO.StreamWriter]::new( "$dst_root\backup.hash_", $false, [Text.Encoding]::Default ) $old_hash.GetEnumerator() | & { process{ $sw.WriteLine( "$($_.Key),$($_.Value),$($old_name.($_.Key))" ) } } } catch { throw } finally { $sw.Close() } if ( Test-Path "$dst_root\backup.hash_" ) { Move-Item -LiteralPath "$dst_root\backup.hash_" -Destination "$dst_root\backup.hash" -Force } } while ( $true ) { try { $sw = [IO.StreamWriter]::new( "$dst_root\backup.hash", $true, [Text.Encoding]::Default ) Get-ChildItem -LiteralPath $src_root -File -Recurse | ForEach-Object { $file = $_ $src_file = $file.FullName if ( $src_file -Like "$dst_root\*" ) { return } # if ( $file.Length -gt 100MB ) { return } $new_mtime = $file.LastWriteTime.ToString('yyyyMMddHHmm') $idx = [Bitconverter]::ToString($MD5.ComputeHash([Text.Encoding]::Default.GetBytes( $src_file ))).Replace("-","") # First, pick up candidates by file modification time. if ( $new_mtime -ne $old_mtime.$idx ) { $stream = try { [IO.FileStream]::new( $src_file, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::Read ) } catch { [IO.FileStream]::new( $src_file, [IO.FileMode]::Open, [IO.FileAccess]::Read, [IO.FileShare]::ReadWrite ) } if ( $stream ) { try { $new_hash = ( Get-FileHash -InputStream $stream -Algorithm MD5 ).Hash } catch { throw } finally { $stream.Close() } } else { throw } # Then, use the hash value to determine the target. if ( $new_hash -ne $old_hash.$idx ) { $dst_dir = $dst_root + $file.DirectoryName.Substring($src_root.Length, $file.DirectoryName.Length - $src_root.Length) if ( -not ( Test-Path -LiteralPath $dst_dir ) ) { New-Item -ItemType Directory -Force -Path $dst_dir | Out-Null } $dst_file = Join-Path $dst_dir ($file.BaseName + "_" + $new_mtime + $file.Extension) if ( -not ( Test-Path $dst_file ) ) { Copy-Item -LiteralPath $src_file -Destination $dst_file -Force $filter = $file.BaseName + "_????????????" + $file.Extension Get-ChildItem -LiteralPath $dst_dir -File -Filter $filter | Sort-Object | Select-Object -SkipLast $num_copy | Remove-Item -Force } $old_hash.$idx = $new_hash $sw.WriteLine( "$idx,$new_hash,$src_file" ) } $old_mtime.$idx = $new_mtime } } $sw.Close() $interval = $interval_1 } catch [System.Exception] { Write-Output $Error[0].ToString() $Error[0].InvocationInfo.PositionMessage $interval = $interval_2 } finally { if ( $sw -and $sw.BaseStream ) { $sw.Close() } } Start-Sleep $interval }
おまけ(2018.8.26追記)
Remove-Item コマンドレットが含まれる行を
Get-ChildItem -LiteralPath $dst_dir -File -Filter $filter | Sort-Object -Descending | Retention3221 | Remove-Item -Force
に置き換えることにより、下記のバックアップファイルを保持するリテンションポリシーを適用できます。
- 過去72時間(3日間)の全更新ファイル(上限
$num_copy
個) - 過去2週間の各日の最終更新ファイル
- 過去2ヶ月間の各週の最終更新ファイル
- 過去1年間の各月の最終更新ファイル
- 各年の最終更新ファイル
Retention3221 関数の定義は以下の通り(
while ( $true )
の前に挿入します)。
注1:パイプライン内でフィルターとして動作します。
注2:入力されるオブジェクトの並びが更新日時で降順にソートされている必要があります。Function Retention3221 { begin { $now = Get-Date $b3d = $now.AddDays(-3) # 3 days $b2w = $now.AddDays(-7*2) # 2 weeks $b2m = $now.AddMonths(-2) # 2 months $b1y = $now.AddYears(-1) # 1 year $last_year = $now.Year $last_month = $now.Month $last_day = $now.Day $jan1st2000 = [datetime]::ParseExact("2000/01/01", "yyyy/MM/dd", $Null) $last_week = [Math]::Ceiling(($now-$jan1st2000).Days/7) $n3d = 0 } process { $LastWriteTime = $_.BaseName.SubString($_.BaseName.Length-12,12) $lwt = [datetime]::ParseExact($LastWriteTime, "yyyyMMddHHmm", $Null) # $_.LastWriteTime $year = $lwt.Year $month = $lwt.Month $day = $lwt.Day $week = [Math]::Ceiling(($lwt-$jan1st2000).Days/7) $keep = $False if ($lwt -gt $b3d -and $n3d -lt $num_copy) { $n3d++ $keep = $True } elseif ($lwt -gt $b2w) { if ($day -ne $last_day) { $keep = $True } } elseif ($lwt -gt $b2m) { if ($week -ne $last_week) { $keep = $True } } elseif ($lwt -gt $b1y) { if ($month -ne $last_month) { $keep = $True } } else { if ($year -ne $last_year) { $keep = $True } } $last_year = $year $last_month = $month $last_day = $day $last_week = $week if (-not $keep) { return $_ } } }
おまけ2(2021.4.17追記)
コピー先(バックアップ先)のフォルダに古いファイルが大量に溜まってきた場合に、ファイル作成日付が90日前よりも古いファイルを一括削除するスクリプトの例です。コピー先フォルダに置いてダブルクリックで実行します。
一括削除ツールは結構危険ですので無保証・自己責任にてご利用願います(全ての環境での正常動作は保証できません。例えばJunctionが含まれる場合はもう少し凝ったロジックが必要です。)。
@@@DeleteOldFiles.bat@PowerShell -NoP -C "$PSCP='%~f0';&([ScriptBlock]::Create((gc '%~f0'|?{$_.ReadCount -gt 1}|Out-String)))" & pause & exit/b #---------------------------------------------------- $retentionPeriod = 90 #---------------------------------------------------- $limit = (Get-Date).AddDays(-$retentionPeriod) if ($PSCP) { $thisFile = (Get-Item -LiteralPath $PSCP).FullName } else { $thisFile = (Get-Item -LiteralPath $MyInvocation.MyCommand.Path).FullName } if (-not (Test-Path $thisFile)) { exit } $path = Split-Path $thisFile -Parent Write-Host "ファイルを削除しています..." Get-ChildItem -LiteralPath $path -Recurse -File -Force -EA 0 ` | ? { $_.CreationTime -lt $limit } | ? { $_.FullName -ne $thisFile } ` | % { Write-Host $_.FullName; Remove-Item -LiteralPath $_.FullName -Force } Write-Host "空のフォルダを削除しています..." Get-ChildItem -LiteralPath $path -Recurse -Directory -Force -EA 0 ` | ? { (Get-ChildItem -LiteralPath $_.FullName -File -Recurse) -eq $null } ` | Sort-Object -Property @{ Expression={ $_.FullName.Split([IO.Path]::DirectorySeparatorChar).Count }; Descending=$true } ` | % { Write-Host $_.FullName; Remove-Item -LiteralPath $_.FullName -Force }
クリエイティブ・コモンズ 表示 - 継承 4.0 国際
- 過去72時間(3日間)の全更新ファイル(上限