【実務・中級編】【実務中級者向け】フォルダ内のバックアップファイルを「最新N世代のみ保存」し古いファイルを自動計算して安全削除する世代管理VBS – VBScript (Visual Basic Scripting Edition)解析バイブル

スポンサーリンク
スポンサーリンク

破壊的変更を許さない設計:FSOによる「世代管理」の極致

業務自動化の現場において、最も「劇薬」となるコードは何か? それは、ファイル削除を伴うスクリプトだ。

「古いバックアップを消して、最新の5世代だけ残す」
一見、単純なロジックに見えるだろう。しかし、現場で「昨日まで動いていたスクリプトが、なぜか全ファイルを消し去った」という悲劇が起きるのは、設計思想が「正常系」のみに偏っているからだ。

本稿では、単なるコードの書き方を教えるのではない。ファイルシステムオブジェクト(FSO)を操り、不測の事態(ファイルロック、アクセス権限、予期せぬファイル名の混入)を回避しながら、堅牢に世代管理を実現するためのアーキテクトの思考プロセスを伝授する。

1. 初級者が陥る「3つの致命的な設計ミス」

まず、なぜ君たちの書いたスクリプトが信頼に値しないのか、その理由を明確にしよう。

① 文字列ソートへの依存

多くの人間は、ファイル名に含まれる日付(例:`backup_20231001.zip`)を文字列としてソートしようとする。これは極めて危険だ。ファイル命名規則がわずかに変更された瞬間、ソート順は崩壊し、最新のファイルが「古いもの」として判定され、削除の対象となる。
正解は、ファイルシステムの「更新日時(DateLastModified)」を真実のソース(Single Source of Truth)とすることだ。

② 例外処理の欠如と「全消去」のリスク

ループの中で `On Error Resume Next` を乱用し、エラーを握り潰してそのまま削除プロセスを進める行為は、プロフェッショナルとして失格だ。ファイルが使用中(ロック状態)であった場合、エラーを無視して次の処理へ進む設計は、ログの不整合や、不完全なバックアップ状態を招く。

③ 「ドライラン」概念の欠落

本番環境へ投入するコードに、「実際には削除せず、何が削除されるかを表示するだけ」のモードが実装されていない。これは、開発者の怠慢である。

2. アーキテクチャ設計:メタデータ抽出型アプローチ

堅牢な世代管理スクリプトを構築するには、以下の3ステップに分離して設計すべきだ。

1. フェッチ(Fetch): 対象フォルダ内のファイルを走査し、「パス」と「更新日時」のペアをメモリ上の構造体に格納する。
2. ソート(Sort): 格納した構造体を「更新日時」の降順(新しい順)で並び替える。
3. 判定・実行(Execute): 指定した世代数 $N$ を超えるインデックスの要素に対し、安全確認を経て削除を実行する。

この設計の利点は、「ファイル操作」と「ロジック(判定)」を完全に分離できる点にある。これにより、テストが容易になり、バグの混入を防ぐことができる。

3. プロダクション・グレードのコード実装

以下に、私が実務で推奨する、保守性と堅牢性を極限まで高めたVBScriptのテンプレートを示す。これをそのままコピペして使うのではない。なぜこのように書いているのか、その意図を読み解け。

'===============================================================================
' Script Name: RobustGenerationManager.vbs
' Description: 指定フォルダ内のファイルを更新日時順に管理し、最新N世代を残して削除する。
' Author: Chief Architect
'===============================================================================
Option Explicit

' --- 設定セクション ---
Const TARGET_DIR     = "C:\Backup\Data\"      ' 対象フォルダパス
Const KEEP_GENERATIONS = 5                   ' 残す世代数
Const FILE_PATTERN   = "backup_*.bak"        ' 対象ファイル名のパターン(簡易判定用)
Const IS_DRY_RUN     = True                  ' True: 削除シミュレーション, False: 実際に削除
Const LOG_FILE       = "C:\Backup\log.txt"   ' 実行ログ出力先

' --- メインプロセス ---
Main

Sub Main()
    Dim fso, targetFolder, fileList, i
    Set fso = CreateObject("Scripting.FileSystemObject")

    ' 1. フォルダ存在チェック
    If Not fso.FolderExists(TARGET_DIR) Then
        LogError "Target directory does not exist: " & TARGET_DIR
        WScript.Quit 1
    End If

    LogInfo "Starting Generation Management. [DryRun=" & IS_DRY_RUN & "]"

    ' 2. ファイル情報の収集 (メタデータ抽出)
    Set targetFolder = fso.GetFolder(TARGET_DIR)
    Set fileList = GetSortedFileList(targetFolder, FILE_PATTERN)

    If fileList.Count = 0 Then
        LogInfo "No matching files found."
        WScript.Quit 0
    End If

    ' 3. 世代管理ロジック
    ' fileListは更新日時の降順(新しい順)で格納されている。
    ' インデックス 0 から (KEEP_GENERATIONS - 1) までは保持。
    ' それ以降のインデックスを削除対象とする。
    
    Dim deleteCount : deleteCount = 0
    For i = KEEP_GENERATIONS To fileList.Count - 1
        Dim targetFile : targetFile = fileList.Item(i)
        
        If IS_DRY_RUN Then
            LogInfo "[DRY-RUN] Would delete: " & targetFile
            deleteCount = deleteCount + 1
        Else
            If SafeDelete(fso, targetFile) Then
                LogInfo "Deleted: " & targetFile
                deleteCount = deleteCount + 1
            Else
                LogError "Failed to delete: " & targetFile & " (File might be locked)"
            End If
        End If
    Next

    LogInfo "Process completed. Files processed: " & fileList.Count & ", Deleted: " & deleteCount
    LogInfo "-------------------------------------------------------------------------------"
End Sub

'-------------------------------------------------------------------------------
' 関数: ファイルリストを更新日時順(降順)に取得する
'-------------------------------------------------------------------------------
Function GetSortedFileList(folder, pattern)
    Dim fso, file, dict, arrFiles, i, j, temp
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set dict = CreateObject("Scripting.Dictionary") ' Key: Path, Value: DateLastModified

    ' 1. 収集
    For Each file In folder.Files
        ' 簡易的なパターンマッチング (Like演算子の代用)
        If InStr(file.Name, Replace(pattern, "*", "")) > 0 Then
            ' Dictionaryに「パス | 更新日時」の形式で格納
            ' ※VBScriptには構造体がないため、結合文字列で管理するか、
            ' 今回はソート用にカスタムクラスを使うのが理想だが、軽量化のため配列で実装
            dict.Add file.Path, file.DateLastModified
        End If
    Next

    ' 2. ソート用の配列作成 (Path, Date)
    ' dictの内容を2次元配列に展開
    Dim keys : keys = dict.Keys
    Dim vals : vals = dict.Items
    Dim sortedArr : Set sortedArr = CreateObject("System.Collections.ArrayList")
    
    ' ArrayListに「Date|Path」の形式で追加(文字列ソートを利用するため)
    ' ただし、日付は ISO 8601 形式(YYYY-MM-DD HH:MM:SS)にする必要がある
    For i = 0 To dict.Count - 1
        ' 日付をソート可能な文字列に変換して結合
        ' [Date] + [Separator] + [Path]
        sortedArr.Add FormatDateTimeISO(vals(i)) & "|" & keys(i)
    Next

    ' 3. ソート (ArrayListのSortメソッドを利用)
    sortedArr.Sort ' 昇順になる

    ' 4. 降順(新しい順)にするために逆転させる、または抽出ロジックを調整
    ' ここでは「新しい順」として扱いたいので、後ろから取り出すか、
    ' 今回は簡単のため、ArrayListをそのまま使い、インデックスを計算で逆転させる
    
    ' 結果を返すためのDictionary(実際にはソート済みのPathリストを返す)
    Dim resultDict : Set resultDict = CreateObject("Scripting.Dictionary")
    ' 降順にするため、後ろから順にDictionaryに格納
    For i = sortedArr.Count - 1 To 0 Step -1
        Dim entry : entry = Split(sortedArr(i), "|")
        ' entry(0) は日付文字列、entry(1) はパス
        resultDict.Add resultDict.Count, entry(1)
    Next

    Set GetSortedFileList = resultDict
End Function

'-------------------------------------------------------------------------------
' 関数: ISO形式の日付文字列変換 (ソート精度を担保するため)
'-------------------------------------------------------------------------------
Function FormatDateTimeISO(dt)
    ' YYYYMMDDHHMMSS 形式で返す(文字列ソートで日付順になるようにする)
    FormatDateTimeISO = Year(dt) & _
                        Right("0" & Month(dt), 2) & _
                        Right("0" & Day(dt), 2) & _
                        Right("0" & Hour(dt), 2) & _
                        Right("0" & Minute(dt), 2) & _
                        Right("0" & Second(dt), 2)
End Function

'-------------------------------------------------------------------------------
' 関数: 安全な削除処理
'-------------------------------------------------------------------------------
Function SafeDelete(fso, filePath)
    On Error Resume Next
    Dim res : res = False
    If fso.FileExists(filePath) Then
        fso.DeleteFile filePath, True
        If Err.Number = 0 Then
            res = True
        Else
            ' エラー内容はログに記録される
            Err.Clear
        End If
    End If
    On Error GoTo 0
    SafeDelete = res
End Function

'-------------------------------------------------------------------------------
' ユーティリティ: ログ出力
'-------------------------------------------------------------------------------
Sub LogInfo(msg)
    WriteLog "[INFO] " & msg
End Sub

Sub LogError(msg)
    WriteLog "[ERROR] " & msg
End Sub

Sub WriteLog(msg)
    Dim fso, logFile
    Dim timestamp : timestamp = Now()
    Dim fullMsg : fullMsg = "[" & timestamp & "] " & msg
    
    ' コンソール出力
    WScript.Echo fullMsg
    
    ' ファイル出力
    On Error Resume Next
    Set fso = CreateObject("Scripting.FileSystemObject")
    Set logFile = fso.OpenTextFile(LOG_FILE, 8, True) ' 8 = ForAppending
    logFile.WriteLine fullMsg
    logFile.Close
    On Error GoTo 0
End Sub

4. エンジニアとしての「極限の知見」

このコードを理解した君に、最後に3つのアドバイスを送る。

1. `System.Collections.ArrayList` の活用

VBScript単体では配列のソートは非常に面倒だ。しかし、Windows環境であれば `System.Collections.ArrayList`(.NET Frameworkのコンポーネント)を呼び出すことで、極めて強力なソート機能を、わずか数行で手に入れることができる。これは「車輪の再発明」を避ける賢明な選択だ。

2. 日付の文字列化における罠

ソートのために日付を文字列にする場合、必ず `YYYYMMDDHHMMSS` のような、「桁数が固定され、かつ大きな単位から並ぶ形式」にせよ。`M/D/YYYY` のような形式でソートを行うと、10月と2月が正しく並ばない。これは初学者が必ず踏む地雷だ。

3. ログは「証拠」である

自動化スクリプトが動いた後、君の仕事は終わらない。「何が、いつ、なぜ消されたのか(あるいは消せなかったのか)」が、ログとして残っていなければならない。エラーが発生した際に、単に無視するのではなく、`SafeDelete` 関数の中でエラーを検知し、ログに刻む。これこそが、運用フェーズで君の身を守る唯一の手段となる。

「コードは書くものではない。設計し、検証し、管理するものだ。」

この意識を持って、次なる自動化の壁に挑んでほしい。

タイトルとURLをコピーしました