【世代別ログアーカイブ】FileSystemObject と Shell.Application を連携させた一定期間経過ログの自動ZIP圧縮
エンタープライズの現場において、システムの肥大化に伴う「ログファイルの圧迫」は避けて通れない課題です。PowerShellやPythonが制限されたレガシーなWindows Server環境、あるいは極限までフットプリントを削りたい軽量なバッチ処理において、いまだにVBScript (WSH)は現役であり、時に最強のソリューションとなります。
しかし、ネット上に転がっているVBScriptのサンプルコードを安易にコピペして本番環境に投入するのは自殺行為です。特に、`FileSystemObject` (FSO) と `Shell.Application` を組み合わせたZIP圧縮処理には、「非同期処理の同期化問題」や「ファイルロックによる排他エラー」といった、一歩間違えればデータ破損や無限ループを引き起こす致命的な地雷が潜んでいます。
本稿では、これらの課題を完全に克服し、実務でそのまま耐えうる「超堅牢な世代別ログアーカイブスクリプト」の設計と実装を徹底的に解説します。
—
1. 露呈する地雷:なぜ一般的なスクリプトは本番で落ちるのか?
VBScriptでログのZIP圧縮と削除を実装する際、多くのエンジニアが以下の2つの罠に嵌まります。
罠①:`Shell.Application` の非同期実行とフリーズ
FSOにはZIP圧縮機能がありません。そのため、Windows標準機能である `Shell.Application` オブジェクトの `CopyHere` メソッドを利用してZIP圧縮を行います。
しかし、`CopyHere` は非同期(バックグラウンド)で実行されます。
呼び出した瞬間にVBScript側へ制御が戻ってしまうため、愚直に以下のようなコードを書くと、「圧縮が完了する前に、元のログファイルをFSOで削除してしまう」という致命的なバグ(ファイル消失、または書き込み中エラーによる処理中断)が発生します。
‘ — 絶対にやってはいけない地雷コードの例 —
shFolder.CopyHere fso.GetFile(logFile)
fso.DeleteFile logFile ‘ ← 圧縮が終わっていないため、エラーになるか中身が壊れる!
罠②:不完全なファイルロックの検知漏れ
アクティブに書き込みが行われている最中のログファイルや、他のプロセスが掴んでいるログファイルを移動・削除しようとすると、スクリプトは容赦なく異常終了します。事前の排他制御チェックは必須です。
—
2. アーキテクチャ設計:堅牢性を担保する3つの鉄則
本モジュールでは、上記の問題を解決するために以下の設計を採用します。
【処理フロー】
[1] ログフォルダのスキャン (FSO)
│
[2] 30日以上更新がないファイルを抽出
│
[3] 保存先ZIPファイル(年月単位: YYYYMM.zip)の有無を確認・自動作成
│
[4] ZIPファイルへコピー実行 (Shell.Application)
│
[5] ★「ポーリング監視」による完全同期化(アイテム数の一致を確認)
│
[6] 安全確認後、元ファイルの削除 (FSO)
鉄則1:空のZIPファイルをバイナリレベルで偽装生成する
`Shell.Application` にZIPファイルを認識させるには、単に拡張子を `.zip` にした空ファイルを作るだけでは不十分です。ZIPのヘッダー情報(22バイトのバイナリ)を正確に書き込んだファイルを用意する必要があります。
鉄則2:ポーリング・ループによる非同期処理の完全同期化
`CopyHere` の実行後、対象のZIPフォルダ内のアイテム数が「コピー前 + 1」になるまで、スクリプトの進行を一定時間(ミリ秒単位)スリープさせながらループ監視(ポーリング)します。さらに、万が一の無限ループを防ぐためのタイムアウト処理も二重に張り巡らせます。
—
3. プロダクションコード:極限の堅牢性を備えたアーカイブスクリプト
以下に、そのままタスクスケジューラに登録して運用できる、エラーハンドリングを極限まで高めたプロダクションコードを示します。コード内のコメントを熟読し、その設計意図を理解してください。
‘ ==============================================================================
‘ SYSTEM : Log Archiver Pro
‘ FILE NAME : LogArchiver.vbs
‘ DESCRIPTION : 指定期間を経過したログファイルを年月別のZIPに圧縮・格納し、元ファイルを削除する。
‘ AUTHOR : Lead Architect
‘ ==============================================================================
Option Explicit
‘ — [設定パラメータ] ———————————————————
Const TARGET_DIR = “C:\Logs” ‘ 監視対象のログフォルダパス(末尾に \ は不要)
Const RETENTION_DAYS = 30 ‘ 保持期間(この日数より古いファイルを圧縮)
Const COMPRESS_TIMEOUT_SEC = 60 ‘ 1ファイルあたりの最大圧縮待機時間(秒)
‘ ——————————————————————————
Dim fso
Set fso = CreateObject(“Scripting.FileSystemObject”)
Main
Sub Main()
On Error Resume Next
Dim targetFolder, file, lastModified, diffDays
Dim archiveZipPath, targetYearMonth
‘ フォルダ存在チェック
If Not fso.FolderExists(TARGET_DIR) Then
WScript.Echo “Error: 対象フォルダが存在しません: ” & TARGET_DIR
WScript.Quit 1
End If
Set targetFolder = fso.GetFolder(TARGET_DIR)
For Each file In targetFolder.Files
‘ 拡張子が .log のファイルのみを対象とする(環境に応じて変更)
If LCase(fso.GetExtensionName(file.Path)) = “log” Then
lastModified = file.DateLastModified
diffDays = DateDiff(“d”, lastModified, Now())
‘ 指定日数以上経過しているか判定
If diffDays >= RETENTION_DAYS Then
‘ ファイルがロックされていないか確認
If IsFileLocked(file.Path) Then
WScript.Echo “Warning: ファイルがロックされているためスキップします: ” & file.Name
Else
‘ 年月(YYYYMM)を取得し、ZIPファイルパスを生成
targetYearMonth = Year(lastModified) & Right(“0” & Month(lastModified), 2)
archiveZipPath = TARGET_DIR & “\” & targetYearMonth & “.zip”
‘ アーカイブ処理の実行
WScript.Echo “Processing: ” & file.Name & ” -> ” & targetYearMonth & “.zip”
If ArchiveFileToZip(file.Path, archiveZipPath) Then
‘ 圧縮成功時のみ安全に削除
SafeDeleteFile file.Path
WScript.Echo “Success: アーカイブ完了、元ファイルを削除しました: ” & file.Name
Else
WScript.Echo “Error: アーカイブに失敗したため、元ファイルを保持します: ” & file.Name
End If
End If
End If
End If
Next
‘ オブジェクトの解放
Set targetFolder = Nothing
Set fso = Nothing
End Sub
‘ ==============================================================================
‘ 機能:指定ファイルをZIPに追加し、完了を同期的に監視する
‘ ==============================================================================
Function ArchiveFileToZip(ByVal sourceFilePath, ByVal zipFilePath)
ArchiveFileToZip = False
On Error Resume Next
‘ 1. ZIPファイルが存在しない場合は新規作成(ZIPヘッダーの書き込み)
If Not fso.FileExists(zipFilePath) Then
If Not CreateEmptyZip(zipFilePath) Then Exit Function
End If
Dim shellApp, zipFolder, sourceItem
Set shellApp = CreateObject(“Shell.Application”)
Set zipFolder = shellApp.NameSpace(zipFilePath)
If zipFolder Is Nothing Then
Set shellApp = Nothing
Exit Function
End If
‘ コピー前のZIP内ファイル数を記録
Dim initialCount
initialCount = zipFolder.Items.Count
‘ 2. 非同期圧縮処理のキック
‘ CopyHere の第2引数「16」は、確認ダイアログを一切表示しない(すべて「はい」)を意味する
zipFolder.CopyHere sourceFilePath, 16
‘ 3. 完全同期化のためのポーリング処理
Dim elapsedSec, isCompleted
elapsedSec = 0
isCompleted = False
Do While elapsedSec < COMPRESS_TIMEOUT_SEC WScript.Sleep 1000 ' 1秒待機 elapsedSec = elapsedSec + 1 ' ZIP内のアイテム数が増加したか確認 If zipFolder.Items.Count = (initialCount + 1) Then isCompleted = True Exit Do End If Loop ' タイムアウト検知 If Not isCompleted Then WScript.Echo "Timeout: 圧縮処理がタイムアウトしました: " & sourceFilePath Set zipFolder = Nothing Set shellApp = Nothing Exit Function End If ' 4. 正常終了フラグの切り替え If Err.Number = 0 Then ArchiveFileToZip = True End If Set zipFolder = Nothing Set shellApp = Nothing End Function ' ============================================================================== ' 機能:ZIP仕様に準拠した22バイトの空ZIPファイルを生成する ' ============================================================================== Function CreateEmptyZip(ByVal zipFilePath) CreateEmptyZip = False On Error Resume Next Dim textStream ' ASCIIで書き込むため、CreateTextFileの第3引数をFalse(ASCII)にする Set textStream = fso.CreateTextFile(zipFilePath, True, False) If Err.Number <> 0 Then Exit Function
‘ 22バイトのZIPヘッダー(End of central directory record)
Dim zipHeader
zipHeader = “PK” & Chr(5) & Chr(6) & String(18, Chr(0))
textStream.Write zipHeader
textStream.Close
If Err.Number = 0 Then
CreateEmptyZip = True
End If
Set textStream = Nothing
End Function
‘ ==============================================================================
‘ 機能:ファイルが他プロセスにロックされているか判定する
‘ ==============================================================================
Function IsFileLocked(ByVal filePath)
IsFileLocked = True
On Error Resume Next
Dim fileNum
fileNum = FreeFile ‘ VBScriptにはFreeFileがないため、代替としてエラーハンドリングを利用
‘ 読み書きモードで排他的に開けるか試行する
Dim testStream
Set testStream = fso.OpenTextFile(filePath, 8, False) ‘ 8 = ForAppending
If Err.Number = 0 Then
testStream.Close
IsFileLocked = False
Set testStream = Nothing
End If
Err.Clear
End Function
‘ ==============================================================================
‘ 機能:安全にファイルを削除する(エラーハンドリング付き)
‘ ==============================================================================
Sub SafeDeleteFile(ByVal filePath)
On Error Resume Next
‘ 読み取り専用属性がついていても強制削除する(第2引数をTrue)
fso.DeleteFile filePath, True
If Err.Number <> 0 Then
WScript.Echo “Warning: 元ファイルの削除に失敗しました: ” & filePath & ” (Error: ” & Err.Description & “)”
End If
End Sub
—
4. コードのディープ解説:プロが施した設計の裏側
① ZIPヘッダー `”PK” & Chr(5) & Chr(6) & String(18, Chr(0))` の本質
VBScriptで空のZIPファイルを作るとき、単に `0バイト` のファイルを作っても `Shell.Application` はそれを「破損した圧縮フォルダ」と見なします。
ZIPファイルフォーマットの末尾には、必ずEOCD(End of Central Directory)という22バイトの固定ブロックが必要です。
- `PK`(`Chr(80)` と `Chr(75)`)はZIPフォーマットを示すシグネチャ。
- `Chr(5) & Chr(6)`(16進数で `05 06`)はEOCDレコードの開始を示します。
- 残りの18バイトをヌル文字(`Chr(0)`)で埋めることで、「格納ファイル数:0」の健全な空ZIPファイルが完成します。
② 同期化ポーリング処理のアルゴリズム
`CopyHere` は非同期のため、スクリプト側で「圧縮完了」を検知する術はイベントハンドラにはありません(WSHではShellイベントのバインドが不安定なため推奨されません)。
本コードでは、「コピー実行前のZIP内ファイル数」と「コピー実行後のファイル数」を比較するという、最もシンプルかつ堅牢なアプローチを採用しています。さらに、処理がスタックして無限ループに陥るのを防ぐため、`COMPRESS_TIMEOUT_SEC` による時間制限を設け、安全弁(フェイルセーフ)としています。
③ 排他制御 `IsFileLocked`
システムログは、ローテーション中やAPサーバ起動中、常に書き込みストリームが開かれている可能性があります。
`ForAppending`(追加書き込みモード)で一時的にファイルを開けるかテストすることで、OSレベルでロックがかかっていないかを安全に確認しています。
—
5. 現場における運用とトラブルシューティング
タスクスケジューラ登録時の「カレントディレクトリの罠」
本スクリプトをWindowsのタスクスケジューラに登録する際、実行ファイルのパスに `wscript.exe` または `cscript.exe` を指定し、引数にスクリプトのフルパスを渡します。
このとき、「開始(オプション)」にスクリプトが格納されているフォルダパスを明記してください。
これを怠ると、カレントディレクトリが `C:\Windows\System32` になり、相対パスで記述された設定が予期せぬ挙動を示す原因になります。(本コードは絶対パス指定のため安全ですが、運用の基本として徹底してください)。
超巨大ログファイル(数GBクラス)への対応
数GBを超える巨大なIISログなどを圧縮する場合、`COMPRESS_TIMEOUT_SEC`(デフォルト60秒)を超過してタイムアウトになる可能性があります。環境のディスクI/O性能に応じて、タイムアウト値は適切に調整(例:300秒など)してください。
—
まとめ:制約の中に美学を宿す
「VBScriptは古い技術だ」と切り捨てるのは容易です。しかし、如何なる最新言語であっても、OSの非同期処理の挙動やファイルシステムのロックといった物理層に近い部分の理解を怠れば、必ず本番障害を引き起こします。
限られた標準機能(FSOとShell API)の制約の中で、いかに「非同期を同期化させ、堅牢なエラーハンドリングを組むか」。このアプローチこそが、業務自動化における真のエンジニアリングです。ぜひ、あなたの現場のサーバー保守にこの極限の知見を役立ててください。
