【テクニカル・上級編】【上級者向け】VSDXファイル破損時の自動復旧を試みる、バイナリ構造を考慮したバックアップ・リストア戦略 – Visio VBA解析バイブル

スポンサーリンク

Visio VBAを掌握する極限の知見:VSDX破損からの自動復旧、バイナリ構造を考慮した堅牢なバックアップ・リストア戦略

Visioは、企業の業務プロセス、システムアーキテクチャ、ネットワーク構成、施設レイアウトといった多岐にわたる重要な情報を視覚化する、極めて重要なツールです。しかし、その成果物であるVSDXファイルが一度破損すれば、プロジェクトの遅延、意思決定の停滞、最悪の場合、ビジネスの継続性そのものに深刻な影響を及ぼしかねません。

本稿では、単なるファイルのコピーに留まらない、VSDXファイルのバイナリ構造を深く理解した上で、破損からの自動復旧を可能にするバックアップ・リストア戦略を解説します。長年、VBAシステムとレガシーアーキテクチャの最前線に立ってきたチーフアーキテクトとしての経験に基づき、Windows APIの直接的な活用、メモリ最適化、そしてシステム間連携の極限の知見をもって、堅牢な運用フローの構築を支援します。

VSDXファイルの真の姿と破損メカニズム

Visio 2013以降で採用されたVSDX形式は、Microsoftが提唱するOpen Packaging Conventions (OPC) に準拠しており、実体は単なるZIPアーカイブです。このZIPファイル内には、XML形式で記述された図形データ、ページ設定、スタイルシート、画像などのリソースが格納されています。

この構造が意味するのは、VSDXファイルが破損する際のメカニズムが、一般的なバイナリファイルとは異なることです。
VSDXの破損は、主に以下の要因によって引き起こされます。

1. 不完全な書き込み: Visioアプリケーションがクラッシュしたり、システムが予期せぬシャットダウンに見舞われたり、ネットワーク接続が途絶したりした際に、ファイルの書き込み処理が完了せず、ZIPアーカイブのメタデータ(セントラルディレクトリ、ローカルファイルヘッダなど)が壊れる。
2. ネットワークI/Oエラー: ネットワークドライブ上のファイルに対して直接操作を行う際、転送エラー、競合、あるいはストレージの物理的障害によってファイルの一部が破損する。特に、大規模なVSDXファイルを頻繁に保存する環境ではリスクが高まります。
3. ディスクの物理的損傷: ハードウェアの故障により、ファイルが保存されているセクタが物理的に読み取れなくなる。
4. サードパーティ製ツールやアンチウイルスソフトウェアの干渉: ファイルのオープン中や保存中に、これらのツールがロックをかけたり、誤ってファイルを変更したりすることが稀にあります。

これらの要因により、ZIP構造の整合性が失われると、Visioアプリケーションはそのファイルを正しく解釈できなくなり、「ファイルが破損しています」といったエラーメッセージが表示され、開くことができなくなります。一般的なファイルコピーだけでは、この「破損した状態」をそのままコピーしてしまうリスクがあるため、より洗練された戦略が必要となるのです。

堅牢なバックアップ戦略の原則:整合性検証と世代管理

単にファイルを別ディレクトリにコピーするだけでは、真に堅牢なバックアップとは言えません。私たちが目指すのは、「確実に復旧可能な状態のファイルを、必要な時に、迅速に提供できる」システムです。

1. タイミングとイベント駆動

バックアップは、ユーザーの意識下にないところで、かつデータが変更されたことを検知した直後に行われるべきです。

  • `Document.Dirty`イベントの活用: Visioの`Document`オブジェクトには`Dirty`プロパティがあり、図面が変更されると`True`になります。このプロパティの状態変化を定期的にポーリングするか、あるいは`Application.DocumentSaved`イベントの直後に、バックアップ処理をトリガーします。
  • タイマーイベント: `Application.OnTime`メソッドを利用し、一定間隔(例:5分、10分)でバックアップ処理をスケジューリングします。ユーザーが保存を忘れた場合でも、直近の状態を保護できます。ただし、大規模なファイルの場合、処理負荷を考慮する必要があります。

2. 世代管理とストレージ効率

常に最新のファイルだけをバックアップするのでは不十分です。最新のファイルが破損していた場合、復旧の機会を失います。複数世代のバックアップを保持する「ローリングバックアップ」が不可欠です。

  • 命名規則: `[オリジナルファイル名]_YYYYMMDD_HHMMSS.vsdx` のように、タイムスタンプを付与することで、世代を明確に区別します。
  • 世代数: ストレージ容量と復旧要件に応じて、N世代(例: 5〜10世代)を保持し、最も古い世代を自動的に削除します。
  • 圧縮: VSDX自体がZIPですが、バックアップファイルをさらにZIP圧縮することで、ストレージの消費を抑え、ネットワーク転送時間を短縮できます。VBAから`Shell.Application`を使う簡易的な方法や、外部のDLL (`zlib`など) を利用する高度な方法があります。

3. バックアップファイルの整合性検証

ここが最も重要です。単にコピーしただけでは、破損したファイルをバックアップしてしまう可能性があります。バックアップ後に、そのファイルが正常なVSDXファイルとして開けるかどうかを検証する必要があります。

  • Visioアプリケーションでの試行: バックアップファイルを実際にVisioで開いてみて、エラーが発生しないことを確認します。ただし、これは時間がかかり、Visioインスタンスを複数起動するなどのオーバーヘッドを伴います。
  • ZIP構造の検証: VSDXがZIPファイルであるという特性を利用し、ZIPライブラリ(`Shell.Application`、または`zlib`などの外部DLL)を用いて、アーカイブの整合性チェック(CRCチェックなど)を行います。これにより、Visioアプリケーションを起動することなく、ファイルの破損を早期に検知できます。

堅牢なバックアップ実装(VBA + Windows API)

VBAの標準機能である`FileCopy`ステートメントは手軽ですが、大規模なファイルや不安定なネットワーク環境下では、エラーハンドリングや進捗監視の点で限界があります。ここでは、Windows APIを直接呼び出すことで、より堅牢なファイル操作を実現します。

Windows API `CopyFileEx`の活用

`Kernel32.dll`が提供する`CopyFileEx`関数は、ファイルコピーの進捗状況をコールバック関数で監視したり、コピー処理を途中でキャンセルしたり、より詳細なエラー情報を取得したりすることが可能です。これにより、大規模なVSDXファイルのバックアップにおいても、信頼性の高い操作を実現します。

‘==============================================================================
‘ モジュールレベル宣言 (標準モジュールに記述)
‘==============================================================================
‘ Kernel32.dllのCopyFileEx関数を宣言
‘ 詳細なファイルコピー機能を提供し、進捗状況のコールバックやエラーハンドリングを強化
Private Declare PtrSafe Function CopyFileEx Lib “kernel32” ( _
ByVal lpExistingFileName As String, _
ByVal lpNewFileName As String, _
ByVal lpProgressRoutine As LongPtr, _
ByVal lpData As LongPtr, _
ByVal lpByteCancel As Long, _
ByVal dwCopyFlags As Long _
) As Long

‘ CopyFileExのコールバック関数プロトタイプ
‘ 進捗情報を取得し、コピーの継続/中断を制御するために使用
Private Declare PtrSafe Function CopyProgressRoutine Lib “kernel32” ( _
ByVal TotalFileSize As LargeInteger, _
ByVal TotalBytesTransferred As LargeInteger, _
ByVal StreamSize As LargeInteger, _
ByVal StreamBytesTransferred As LargeInteger, _
ByVal dwStreamNumber As Long, _
ByVal dwCallbackReason As Long, _
ByVal hSourceFile As LongPtr, _
ByVal hDestinationFile As LongPtr, _
ByVal lpData As LongPtr _
) As Long

‘ CopyFileExのフラグ定数
Private Const COPY_FILE_FAIL_IF_EXISTS As Long = &H1 ‘ 宛先ファイルが存在する場合にコピーを失敗させる
Private Const COPY_FILE_RESTARTABLE As Long = &H2 ‘ コピーを再開可能にする(ネットワーク切断時など)
Private Const COPY_FILE_ALLOW_DECRYPTED_DESTINATION As Long = &H8 ‘ 暗号化されたファイルを復号してコピー

‘ コールバック関数の戻り値定数
Private Const PROGRESS_CONTINUE As Long = 0 ‘ コピーを継続
Private Const PROGRESS_CANCEL As Long = 1 ‘ コピーをキャンセル
Private Const PROGRESS_STOP As Long = 2 ‘ コピーを停止(一時停止)
Private Const PROGRESS_QUIET As Long = 3 ‘ 進捗表示を抑制

‘ LargeInteger構造体(64ビット整数を扱うため)
Private Type LargeInteger
LowPart As Long
HighPart As Long
End Type

‘==============================================================================
‘ バックアップ処理の本体
‘==============================================================================
Public Sub PerformVisioBackup(ByVal targetDocument As Visio.Document)
Dim fso As Object
Dim backupDir As String
Dim originalPath As String
Dim fileName As String
Dim baseName As String
Dim ext As String
Dim currentBackupPath As String
Dim i As Long
Dim oldestBackupFile As String
Dim oldestBackupDate As Date
Dim backupFiles As Object ‘ Scripting.Dictionaryを想定

‘ エラーハンドリング
On Error GoTo ErrorHandler

Set fso = CreateObject(“Scripting.FileSystemObject”)
originalPath = targetDocument.FullName
fileName = fso.GetFileName(originalPath)
baseName = fso.GetBaseName(fileName)
ext = fso.GetExtensionName(fileName)

‘ バックアップディレクトリの設定
‘ ここではドキュメントと同じ階層に “Backup” フォルダを作成する例
‘ 実際の運用では共有ストレージや専用のバックアップパスを指定する
backupDir = fso.GetParentFolderName(originalPath) & “\Backup\”

‘ バックアップディレクトリが存在しない場合は作成
If Not fso.FolderExists(backupDir) Then
fso.CreateFolder backupDir
Debug.Print “バックアップディレクトリを作成しました: ” & backupDir
End If

‘ 現在のタイムスタンプに基づくバックアップファイル名
currentBackupPath = backupDir & baseName & “_” & Format(Now, “YYYYMMDD_HHMMSS”) & “.” & ext

Debug.Print “バックアップ開始: ” & originalPath & ” -> ” & currentBackupPath

‘ CopyFileExを使ってファイルをコピー
‘ lpProgressRoutine には、進捗コールバック関数へのポインタを渡す
‘ lpData はコールバック関数に渡す任意のデータ(ここでは0)
‘ dwCopyFlags はコピー動作を指定するフラグ(ここでは再開可能フラグを使用)
If CopyFileEx(StrPtr(originalPath), StrPtr(currentBackupPath), _
AddressOf CopyProgressHandler, 0, 0, COPY_FILE_RESTARTABLE) = 0 Then
‘ CopyFileExは成功すると0以外を返す。0は失敗。
‘ GetLastError()で詳細なエラーコードを取得可能
Dim lastError As Long
lastError = Err.LastDllError
Err.Raise vbObjectError + 1, “PerformVisioBackup”, “ファイルコピーに失敗しました (Error Code: ” & lastError & “)”
End If

Debug.Print “バックアップ完了: ” & currentBackupPath

‘==========================================================================
‘ 世代管理 (N世代保持)
‘==========================================================================
Const MAX_BACKUP_GENERATIONS As Long = 5 ‘ 保持する世代数

Set backupFiles = CreateObject(“Scripting.Dictionary”) ‘ バックアップファイル名をキー、日付時刻を値として保持

‘ バックアップディレクトリ内の関連ファイルを列挙
For Each f In fso.GetFolder(backupDir).Files
If LCase(fso.GetBaseName(f.Name)) Like LCase(baseName) & “_????????_??????” & “.” & LCase(ext) Then
Dim fileDatePart As String
Dim fileTimePart As String
Dim fileDateTimeStr As String
Dim fileDateTime As Date

‘ ファイル名から日付時刻部分を抽出 (例: MyDiagram_20231027_153045.vsdx)
fileDatePart = Mid(fso.GetBaseName(f.Name), Len(baseName) + 2, 8) ‘ YYYYMMDD
fileTimePart = Mid(fso.GetBaseName(f.Name), Len(baseName) + 11, 6) ‘ HHMMSS

‘ 抽出した文字列から日付時刻オブジェクトを作成
fileDateTimeStr = Left(fileDatePart, 4) & “/” & Mid(fileDatePart, 5, 2) & “/” & Right(fileDatePart, 2) & ” ” & _
Left(fileTimePart, 2) & “:” & Mid(fileTimePart, 3, 2) & “:” & Right(fileTimePart, 2)
fileDateTime = CDate(fileDateTimeStr)

backupFiles.Add f.Path, fileDateTime
End If
Next f

‘ バックアップファイルがMAX_BACKUP_GENERATIONSを超えている場合、最も古いものを削除
If backupFiles.Count > MAX_BACKUP_GENERATIONS Then
oldestBackupDate = Now
oldestBackupFile = “”

For Each key In backupFiles.Keys
If backupFiles(key) < oldestBackupDate Then oldestBackupDate = backupFiles(key) oldestBackupFile = key End If Next key If oldestBackupFile <> “” Then
fso.DeleteFile oldestBackupFile, True ‘ True: ReadOnlyファイルを強制削除
Debug.Print “古いバックアップファイルを削除しました: ” & oldestBackupFile
End If
End If

‘==========================================================================
‘ バックアップファイルの整合性検証 (簡易的なZIP構造チェック)
‘==========================================================================
If Not IsValidVisioFile(currentBackupPath) Then
Debug.Print “警告: 作成されたバックアップファイルが破損している可能性があります: ” & currentBackupPath
‘ ここでエラーログ記録や管理者への通知を行うべき
End If

Exit Sub

ErrorHandler:
Debug.Print “エラー発生 (PerformVisioBackup): ” & Err.Description
‘ エラーログ記録、管理者への通知などを実装
End Sub

‘==============================================================================
‘ CopyFileExのコールバック関数
‘==============================================================================
Private Function CopyProgressHandler( _
ByVal TotalFileSize As LargeInteger, _
ByVal TotalBytesTransferred As LargeInteger, _
ByVal StreamSize As LargeInteger, _
ByVal StreamBytesTransferred As LargeInteger, _
ByVal dwStreamNumber As Long, _
ByVal dwCallbackReason As Long, _
ByVal hSourceFile As LongPtr, _
ByVal hDestinationFile As LongPtr, _
ByVal lpData As LongPtr _
) As Long
‘ 進捗表示(デバッグウィンドウに出力)
Static lastPercentage As Long
Dim currentPercentage As Long

‘ 64ビット値の計算 (LowPart + HighPart 2^32)
Dim total As Double
Dim transferred As Double

total = CDbl(TotalFileSize.LowPart) + CDbl(TotalFileSize.HighPart) (2 ^ 32)
transferred = CDbl(TotalBytesTransferred.LowPart) + CDbl(TotalBytesTransferred.HighPart) (2 ^ 32)

If total > 0 Then
currentPercentage = Int((transferred / total) 100)
If currentPercentage <> lastPercentage Then
Debug.Print “コピー進捗: ” & currentPercentage & “%”
lastPercentage = currentPercentage
End If
End If

CopyProgressHandler = PROGRESS_CONTINUE ‘ コピーを継続
End Function

‘==============================================================================
‘ VSDX (ZIP) ファイルの整合性検証関数
‘==============================================================================
‘ VSDXファイルが基本的なZIPアーカイブとして開けるかをチェックする
‘ より厳密な検証には、zlibなどの外部DLLをP/Invokeで呼び出す必要があるが、
‘ VBA単体での簡易チェックとしてはShell.Applicationが有用。
Public Function IsValidVisioFile(ByVal filePath As String) As Boolean
Dim objShell As Object ‘ Shell.Application
Dim objFolder As Object ‘ ZIPファイルの中身を表すフォルダ
Dim result As Boolean

Set objShell = CreateObject(“Shell.Application”)
result = False

On Error Resume Next ‘ エラーが発生しても処理を中断しない
Set objFolder = objShell.NameSpace(filePath)

If Not objFolder Is Nothing Then
‘ ZIPファイルとして認識され、中のファイルコレクションにアクセスできるかを確認
‘ これだけでも、ZIPヘッダが致命的に破損している場合はエラーになる
If objFolder.Items.Count > 0 Then
result = True
End If
End If
On Error GoTo 0 ‘ エラーハンドリングを元に戻す

Set objFolder = Nothing
Set objShell = Nothing
IsValidVisioFile = result
End Function

解説:

  • `CopyFileEx`は、ファイルコピーをより細かく制御するための強力なAPIです。`PtrSafe`キーワードは64bit環境でのVBAに対応するために必須です。
  • `CopyProgressHandler`コールバック関数では、ファイルのコピー進捗をリアルタイムで監視し、ログ出力やUI更新に利用できます。ここではデバッグウィンドウにパーセンテージを出力しています。
  • `LargeInteger`型は、64ビットのファイルサイズを正確に扱うために定義しています。VBAの`Long`型は32ビットなので、大規模なファイルではオーバーフローします。
  • `PerformVisioBackup`プロシージャは、指定されたVisioドキュメントをバックアップディレクトリにコピーし、タイムスタンプ付きのファイル名を付与します。
  • 世代管理ロジックでは、`Scripting.FileSystemObject`と`Scripting.Dictionary`を組み合わせて、指定された世代数を超える古いバックアップファイルを自動的に削除します。
  • `IsValidVisioFile`関数は、`Shell.Application`オブジェクトを利用して、VSDXファイルがZIPアーカイブとして正しく認識され、その内部構造にアクセスできるかを簡易的にチェックします。これにより、Visioアプリケーションを起動せずとも、ある程度のファイル破損を検知できます。

メモリ最適化とパフォーマンスへの配慮

大規模なVSDXファイルを扱う場合、メモリ管理とパフォーマンスは常に意識すべき課題です。

  • オブジェクトの明示的解放: VBAでは、オブジェクト参照がスコープを外れると自動的に解放されますが、特にVisioオブジェクト (`Application`, `Document`, `Page`, `Shape`など) は、明示的に`Set obj = Nothing`とすることで、参照カウントを即座にゼロにし、メモリとリソースを解放することが極めて重要です。特にイベントハンドラを登録している場合は、解放漏れがメモリリークの原因となることがあります。
  • `Application.ScreenUpdating = False`: Visioの画面更新は、大量の図形操作を行う際にパフォーマンスボトルネックとなります。バックアップ処理中はVisioのGUI操作は行いませんが、もし図面をプログラム的に変更する場合は、処理の開始時に`Application.ScreenUpdating = False`を設定し、終了時に`True`に戻すことで劇的に高速化できます。
  • イベントの無効化: 一時的に`Application.EnableEvents = False`を設定することで、不必要なイベント処理によるオーバーヘッドを削減できます。
  • ファイルI/Oのバッファリング: `CopyFileEx`は内部的にバッファリングを行うため、`FileCopy`よりも効率的なI/Oが期待できます。VBAから直接バッファサイズを制御することは困難ですが、API選択の段階でパフォーマンスを考慮することが重要です。

自動復旧戦略:破損検知とリストア

バックアップは保険に過ぎません。その保険を「自動で適用する」仕組みがあって初めて、堅牢な運用フローが完成します。

破損検知のメカニズム

Visio VBAでファイル破損を検知する主な方法は、以下の2つです。

1. Visioアプリケーションでのファイルオープン失敗:
Visioの`Documents.Open`メソッドは、ファイルが開けない場合にエラーを発生させます。これを`On Error GoTo`で捕捉します。
2. `Application.OnDocumentOpenFailure`イベント:
Visioアプリケーションには、ドキュメントのオープンに失敗した際に発生する専用のイベントがあります。これを利用することで、ユーザーが手動でファイルを開こうとして失敗した場合でも、自動復旧プロセスをトリガーできます。

リストアプロセス

破損を検知した場合、以下の手順で自動復旧を試みます。

1. 最新の有効なバックアップを特定: バックアップディレクトリから、最も新しい日時で作成され、かつ`IsValidVisioFile`関数で正常と判断されたバックアップファイルを探します。
2. 破損ファイルの隔離: 現在の破損ファイルを別の場所(例: `_corrupted`フォルダ)に移動またはリネームし、誤って上書きしないように隔離します。
3. バックアップファイルのコピー: 特定した有効なバックアップファイルを、元のファイル名で元の場所にコピーします。
4. Visioでの再オープン試行: コピーしたバックアップファイルをVisioで開き直します。

‘==============================================================================
‘ クラスモジュール (例: CVisioAutoRecovery.cls)
‘ Application.OnDocumentOpenFailure イベントを捕捉するため、クラスモジュールを使用
‘==============================================================================
‘ このクラスはWithEventsを使用してVisio.Applicationオブジェクトを監視します。
‘ 標準モジュールからインスタンス化し、イベントをフックします。
Public WithEvents App As Visio.Application

Private Sub Class_Initialize()
Set App = Visio.Application
End Sub

Private Sub Class_Terminate()
Set App = Nothing
End Sub

‘ ドキュメントオープン失敗イベントハンドラ
Private Sub App_OnDocumentOpenFailure(ByVal Doc As Visio.Document)
Dim originalPath As String
Dim fso As Object
Dim fileName As String
Dim msg As String

‘ DocオブジェクトはOpenに失敗したドキュメントへの参照を持つが、完全に初期化されていない可能性あり
‘ エラー回避のため、可能な限りFullPathを取得
On Error Resume Next
originalPath = Doc.FullName
On Error GoTo 0

If originalPath = “” Then
‘ FullNameが取得できない場合は、処理を中断するか、より汎用的なエラーメッセージを出す
Debug.Print “OnDocumentOpenFailure: ドキュメントパスが取得できませんでした。”
Exit Sub
End If

Set fso = CreateObject(“Scripting.FileSystemObject”)
fileName = fso.GetFileName(originalPath)

msg = “Visioファイル ‘” & fileName & “‘ のオープンに失敗しました。” & vbCrLf & _
“自動復旧を試みますか?” & vbCrLf & _
“(元のファイルは ‘_corrupted’ フォルダに移動されます)”

If MsgBox(msg, vbYesNo + vbExclamation, “Visioファイル破損の可能性”) = vbYes Then
Call AttemptAutoRestore(originalPath)
Else
Debug.Print “ユーザーは自動復旧をキャンセルしました。”
End If

Set fso = Nothing
End Sub

‘==============================================================================
‘ 標準モジュール (例: modRecovery.bas)
‘==============================================================================
‘ CVisioAutoRecoveryクラスのインスタンス
Public g_AutoRecovery As CVisioAutoRecovery

‘ Visio起動時に自動復旧システムを初期化する
Public Sub InitializeAutoRecovery()
If g_AutoRecovery Is Nothing Then
Set g_AutoRecovery = New CVisioAutoRecovery
Debug.Print “Visio自動復旧システムを初期化しました。”
End If
End Sub

‘ 実際に自動復旧を試みるプロシージャ
Public Sub AttemptAutoRestore(ByVal corruptedFilePath As String)
Dim fso As Object
Dim backupDir As String
Dim corruptedFileName As String
Dim baseName As String
Dim ext As String
Dim latestValidBackupPath As String
Dim corruptedFolder As String
Dim restoreSuccess As Boolean

On Error GoTo ErrorHandler

Set fso = CreateObject(“Scripting.FileSystemObject”)
corruptedFileName = fso.GetFileName(corruptedFilePath)
baseName = fso.GetBaseName(corruptedFileName)
ext = fso.GetExtensionName(corruptedFileName)

‘ バックアップディレクトリの設定 (バックアップ時と同じパスを使用)
backupDir = fso.GetParentFolderName(corruptedFilePath) & “\Backup\”

If Not fso.FolderExists(backupDir) Then
MsgBox “バックアップディレクトリが見つかりません: ” & backupDir, vbCritical, “復旧失敗”
Exit Sub
End If

‘==========================================================================
‘ 最新の有効なバックアップファイルを探す
‘==========================================================================
latestValidBackupPath = FindLatestValidBackup(backupDir, baseName, ext)

If latestValidBackupPath = “” Then
MsgBox “復旧可能なバックアップファイルが見つかりませんでした。”, vbCritical, “復旧失敗”
Exit Sub
End If

‘==========================================================================
‘ 破損ファイルを隔離
‘==========================================================================
corruptedFolder = fso.GetParentFolderName(corruptedFilePath) & “\_corrupted\”
If Not fso.FolderExists(corruptedFolder) Then
fso.CreateFolder corruptedFolder
End If

Dim isolatedCorruptedPath As String
isolatedCorruptedPath = corruptedFolder & fso.GetFileName(corruptedFilePath) & “_” & Format(Now, “YYYYMMDD_HHMMSS”) & “.bak”

‘ ファイルがロックされている可能性があるため、一度Visioのインスタンスを閉じる必要がある場合も
‘ ここではユーザーがVisioを閉じることを前提とするか、新規インスタンスで開くことを考慮
‘ 例: App.Quit などはしない (ユーザー操作中の可能性あり)

‘ 破損ファイルの名前を変更して隔離
‘ FSO.MoveFileはロックされているファイルに対して失敗する可能性が高い
‘ Windows API MoveFileEx を利用する手もあるが、ここではFSOで試行
If fso.FileExists(corruptedFilePath) Then
On Error Resume Next ‘ 移動失敗を許容
fso.MoveFile corruptedFilePath, isolatedCorruptedPath
If Err.Number <> 0 Then
Debug.Print “破損ファイルの隔離に失敗しました (ファイルがロックされている可能性): ” & Err.Description
‘ 強制的にファイルを削除する選択肢もあるが、データロスリスクがあるため注意
Else
Debug.Print “破損ファイルを隔離しました: ” & isolatedCorruptedPath
End If
On Error GoTo 0
End If

‘==========================================================================
‘ バックアップファイルからリストア
‘==========================================================================
Debug.Print “リストア開始: ” & latestValidBackupPath & ” -> ” & corruptedFilePath

‘ CopyFileExを使ってバックアップファイルを元のパスにコピー
If CopyFileEx(StrPtr(latestValidBackupPath), StrPtr(corruptedFilePath), _
AddressOf CopyProgressHandler, 0, 0, COPY_FILE_RESTARTABLE) = 0 Then
Dim lastError As Long
lastError = Err.LastDllError
Err.Raise vbObjectError + 2, “AttemptAutoRestore”, “バックアップファイルからのリストアに失敗しました (Error Code: ” & lastError & “)”
End If

Debug.Print “リストア完了: ” & corruptedFilePath
restoreSuccess = True

‘==========================================================================
‘ リストアされたファイルをVisioで開く
‘==========================================================================
‘ 現在のVisioインスタンスで開く
Visio.Application.Documents.Open corruptedFilePath

MsgBox “ファイルが正常に復旧されました。”, vbInformation, “復旧成功”

Exit Sub

ErrorHandler:
Debug.Print “エラー発生 (AttemptAutoRestore): ” & Err.Description
If Not restoreSuccess Then
MsgBox “自動復旧中にエラーが発生しました。手動で確認してください。”, vbCritical, “復旧失敗”
End If
‘ エラーログ記録、管理者への通知などを実装
End Sub

‘ バックアップディレクトリから最新の有効なバックアップファイルを検索する関数
Private Function FindLatestValidBackup(ByVal backupDir As String, ByVal baseName As String, ByVal ext As String) As String
Dim fso As Object
Dim f As Object
Dim latestDate As Date
Dim latestPath As String
Dim fileDateTimeStr As String
Dim fileDateTime As Date

Set fso = CreateObject(“Scripting.FileSystemObject”)
latestDate = CDate(“1900/01/01”) ‘ 非常に古い日付で初期化
latestPath = “”

If Not fso.FolderExists(backupDir) Then Exit Function

For Each f In fso.GetFolder(backupDir).Files
If LCase(fso.GetBaseName(f.Name)) Like LCase(baseName) & “_????????_??????” & “.” & LCase(ext) Then
‘ ファイル名から日付時刻を抽出
fileDateTimeStr = Mid(fso.GetBaseName(f.Name), Len(baseName) + 2, 8) & ” ” & _
Mid(fso.GetBaseName(f.Name), Len(baseName) + 11, 6)
fileDateTime = CDate(Left(fileDateTimeStr, 4) & “/” & Mid(fileDateTimeStr, 5, 2) & “/” & Mid(fileDateTimeStr, 7, 2) & _
” ” & Mid(fileDateTimeStr, 10, 2) & “:” & Mid(fileDateTimeStr, 12, 2) & “:” & Mid(fileDateTimeStr, 14, 2))

‘ 最新かつ有効なファイルをチェック
If fileDateTime > latestDate Then
‘ バックアップファイル自体の整合性もチェック
If IsValidVisioFile(f.Path) Then
latestDate = fileDateTime
latestPath = f.Path
Else
Debug.Print “無効なバックアップファイルが検出されました (スキップ): ” & f.Path
End If
End If
End If
Next f

FindLatestValidBackup = latestPath
Set fso = Nothing
End Function

解説:

  • `CVisioAutoRecovery`クラスは、`WithEvents`キーワードを使用して`Visio.Application`オブジェクトのイベントを捕捉します。これにより、Visioアプリケーション全体のイベントを監視し、`OnDocumentOpenFailure`イベントをトリガーとして自動復旧プロセスを開始できます。
  • `InitializeAutoRecovery`プロシージャをVisioの`ThisDocument`オブジェクトの`Document_Open`イベントなどから呼び出すことで、Visio起動時に自動復旧システムを有効化できます。
  • `AttemptAutoRestore`プロシージャは、破損したファイルのパスを受け取り、バックアップディレクトリから最新の有効なバックアップファイルを検索します。
  • 破損ファイルは、データロスを防ぐために直ちに`_corrupted`フォルダに移動して隔離されます。ファイルがロックされている可能性を考慮し、エラーハンドリングを強化しています。
  • `CopyFileEx`を使ってバックアップファイルが元の場所にリストアされ、その後Visioで自動的に開かれます。
  • `FindLatestValidBackup`関数は、ファイル名に含まれるタイムスタンプと`IsValidVisioFile`関数を組み合わせて、最も新しく、かつ整合性の取れたバックアップファイルを探し出します。これにより、破損したバックアップファイルがリストアされるリスクを最小限に抑えます。

システム連携と拡張性:真の堅牢性へ

VBAは強力ですが、単体では限界があります。より高度な要件(クラウドストレージ連携、高度な暗号化、集中ログ管理)には、外部システムとの連携が不可欠です。

ログ記録とエラー通知

  • 詳細なログ記録: バックアップ、復旧の成否、エラー情報、実行日時などをカスタムログファイル、またはWindowsイベントログに記録します。これにより、問題発生時の原因究明とシステムの監視が容易になります。
  • エラー通知: 復旧失敗時や重要な警告が発生した際には、システム管理者へメール、Teams/SlackのWebhook、あるいは社内監視システムへのAPIコールを通じて通知を行います。これは`CreateObject(“CDO.Message”)`を使った簡易メール送信や、`CreateObject(“MSXML2.XMLHTTP”)`を使ったHTTPリクエストで実装できます。

外部スクリプト/アプリケーションとの連携

VBAから直接Windows APIを呼び出すのは強力ですが、例えばPowerShellスクリプトや.NETアプリケーションを呼び出すことで、より高度な機能を実現できます。

  • PowerShell:
  • Volume Shadow Copy Service (VSS) を利用したシステムレベルのバックアップ。
  • より堅牢なZIP圧縮/解凍(パスワード付きZIPなど)。
  • ACL(アクセス制御リスト)を考慮したファイルコピー。
  • クラウドストレージ(Azure Blob, AWS S3)へのファイル転送。
  • `CreateObject(“WScript.Shell”).Run “powershell.exe -File C:\path\to\script.ps1”, 0, True` のようにVBAから呼び出し、同期・非同期実行を制御できます。
  • .NET Executables/DLL:
  • `zlib`や`DotNetZip`などのライブラリを直接利用した、より詳細なZIPアーカイブ操作や整合性検証。
  • OpenPGPなどを用いた高度な暗号化/復号。
  • 専用のGUIを持つツールとして実装し、ユーザーフレンドリーな復旧インターフェースを提供。
  • VBAから`ShellExecute`や`CreateProcess`(または`WScript.Shell.Run`)で`.exe`を起動したり、COM/Automationで`.NET`のDLLを呼び出したりします。

結論:技術の真髄は細部に宿る

本稿で示したVSDXファイルのバックアップ・リストア戦略は、単なる機能要件を満たすだけでなく、その裏にあるファイルのバイナリ構造、Windows OSの機能、VBAの実行特性を深く理解することで初めて実現できる「極限の知見」に基づいています。

Visio VBAはレガシーな技術と見なされがちですが、その深淵にはWindowsの低レベルな機能と密接に連携する可能性が秘められています。オブジェクトのライフサイクルを意識したメモリ管理、パフォーマンスへの絶え間ない配慮、そしてVBAの限界を外部システム連携で補う柔軟なアーキテクチャは、大規模かつミッションクリティカルな環境において、システムの信頼性と持続性を保証する上で不可欠です。

この堅牢な戦略を導入することで、VSDXファイル破損という避けがたいリスクに対し、自動的かつ迅速に対応し、事業継続性を高めることが可能となります。しかし、完璧なシステムは存在しません。継続的なモニタリング、テスト、そして改善を怠らないことが、真に「掌握された」システムを維持する鍵となるでしょう。技術の真髄は、常にその細部に宿っているのです。

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