【実務・中級編】【上級者】SharePoint/Teams上の共同編集環境で発生する保存競合(コンフリクト)をVBA側で検知し、データ消失を防ぐためにローカルへタイムスタンプ付き退避コピーを自動生成するリスク回避マクロ – PowerPoint VBA解析バイブル

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

クラウド共編環境の「静かなる破壊」を防げ:SharePoint/Teamsにおける保存競合回避プロテクトの実装

現代のエンタープライズ開発において、ファイルはもはやローカルストレージに孤立して存在するものではない。SharePointやMicrosoft Teamsといったクラウドストレージ上での「共同編集(Co-authoring)」は、業務スピードを劇的に向上させた一方で、VBAエンジニアにとっては「非決定的な動作」という名の地雷原を歩かされているに等しい。

多くの開発者が陥る罠がある。それは、「`ActivePresentation.Save` を呼び出せば、ファイルは保存される」という、ローカル環境時代の素朴な信仰だ。

クラウド環境において、この信仰は時に致命的なデータ消失を招く。他者の編集による同期競合(Sync Conflict)、ネットワークの瞬断、あるいはファイルロックの競合。これらが重なったとき、VBAはエラーを吐いて停止するか、あるいは最悪の場合、「誰の変更も反映されていない、あるいは不完全な状態のファイル」を上書きしてしまう。

本記事では、このような「クラウド特有の不確実性」を、VBAの設計レベルでいかに制御し、ユーザーの資産を守るか。そのための「緊急避難型・自動バックアップ機構」の設計思想と、プロダクションレベルのコードを伝授する。

1. なぜ、標準の `Save` では不十分なのか

設計者としてまず理解すべきは、SharePoint上のファイル操作が「同期エンジン」を介した非同期プロセスであるという事実だ。

1. Race Condition(競合状態): ユーザーがVBAで保存を実行した瞬間、クラウド側で別のユーザーが同期を完了させていた場合、ファイルの状態はVBAが認識しているものと乖離している。
2. Silent Failureの回避: クラウドストレージとの連携時、エラーハンドリングが不十分だと、VBAは「保存に成功した」と誤認したまま処理を続行し、ユーザーが「保存できた」と思い込んだままPCを閉じるという最悪のシナリオを招く。
3. Lock Contention: 共同編集中、特定のセクションがロックされている場合、VBAによる書き込みが拒否される。

我々が実装すべきは、単なる「保存」ではない。「保存の試行」→「失敗の検知」→「即時的なローカルへの退避(Evacuation)」という、一連の防御的ワークフローである。

2. 堅牢な設計指針:防御的プログラミングの要諦

この機能を実装する際、以下の3つの原則を厳守せよ。

① エラー番号による「状態」の峻別

単に `On Error Resume Next` でエラーを無視するのは、プロフェッショルのすることではない。エラー番号(`Err.Number`)を精査し、それが「単なるネットワークの瞬断」なのか、「ファイルロックによる拒否」なのかを判別するロジックが必要だ。

② 冪等性(Idempotency)とタイムスタンプ

退避用ファイル名は、絶対に既存のファイルと衝突してはならない。ISO 8601形式に近い `YYYYMMDD_HHMMSS` 形式のタイムスタンプを付与し、ファイル名がソート可能かつユニークであることを保証する。

③ ローカル領域の選定

退避先は、ユーザーの環境に依存しないよう、`Environ(“USERPROFILE”)` を起点としたローカルパス(デスクトップやドキュメントフォルダ)を動的に取得すること。クラウド同期フォルダ内に退避させては、競合を再生産するだけだ。

3. プロダクション・コード例:Cloud-Protector Module

以下に、実務でそのまま利用可能な、クラスモジュールではなく扱いやすい標準モジュール形式の設計を示す。

Option Explicit

''' <summary>
''' クラウド環境下での保存競合を検知し、安全に保存を試みるプロテクト・モジュール
''' </summary>
Public Sub SecureCloudSave()
    Dim targetPres As Presentation
    Set targetPres = ActivePresentation
    
    Dim currentPath As String
    currentPath = targetPres.FullName
    
    ' 1. クラウドパス(http/https)かどうかの簡易チェック
    ' ※ 本来は詳細なURL解析が必要だが、ここでは論理的な分岐として記述
    Dim isCloud As Boolean
    isCloud = (Left(currentPath, 4) = "http")
    
    On Error GoTo ErrorHandler
    
    ' 2. 保存の試行
    Debug.Print "Attempting secure save to: " & currentPath
    targetPres.Save
    
    MsgBox "保存が正常に完了しました。", vbInformation, "Success"
    Exit Sub

ErrorHandler:
    ' 3. エラー検知:ファイルアクセス拒否やロックに関連するエラー番号を特定
    ' Err.Number 70: Permission denied
    ' Err.Number 75: Path/File access error
    ' Err.Number 5832: Network/Sync error (環境により異なる)
    Select Case Err.Number
        Case 70, 75, 5832
            Debug.Print "Conflict detected! Error: " & Err.Description
            ' 競合が発生したため、緊急退避プロセスへ移行
            Call EmergencyEvacuation(targetPres)
        Case Else
            ' その他の予期せぬエラー
            MsgBox "予期せぬエラーが発生しました: " & Err.Description, vbCritical, "Critical Error"
    End Select
End Sub

''' <summary>
''' 競合発生時に、データをローカル環境へ強制退避させる
''' </summary>
Private Sub EmergencyEvacuation(ByRef targetPres As Presentation)
    Dim fso As Object
    Set fso = CreateObject("Scripting.FileSystemObject")
    
    Dim localDir As String
    ' ユーザーのデスクトップを退避先に指定(確実に書き込み権限がある領域)
    localDir = CreateObject("WScript.Shell").SpecialFolders("Desktop") & "\Emergency_Backups\"
    
    ' 退避用ディレクトリの作成
    If Not fso.FolderExists(localDir) Then
        fso.CreateFolder (localDir)
    End If
    
    ' タイムスタンプ付きファイル名の生成 (YYYYMMDD_HHMMSS)
    Dim timestamp As String
    timestamp = Format(Now, "yyyymmdd_hhmmss")
    
    ' 元のファイル名から拡張子を除いた部分を取得
    Dim originalName As String
    originalName = fso.GetBaseName(targetPres.FullName)
    
    Dim extension As String
    extension = fso.GetExtensionName(targetPres.FullName)
    
    Dim backupPath As String
    backupPath = localDir & originalName & "_" & timestamp & "." & extension
    
    On Error GoTo BackupError
    
    ' 4. 強制的な別名保存(SaveAs)を実行
    ' これにより、現在のセッションの作業内容をローカルに固定する
    targetPres.SaveAs backupPath
    
    MsgBox "【警告】クラウド上での保存に失敗しました。" & vbCrLf & _
           "データの消失を防ぐため、ローカルへ退避コピーを作成しました。" & vbCrLf & vbCrLf & _
           "保存先: " & backupPath, vbExclamation, "Conflict Detected"
    
    Exit Sub

BackupError:
    MsgBox "致命的なエラー: 退避コピーの作成にも失敗しました。" & vbCrLf & _
           "作業内容をコピーしてメモ帳等に控えてください。" & vbCrLf & _
           "Error: " & Err.Description, vbCritical, "Total Failure"
End Sub

4. 実装におけるエンジニアの「こだわり」:なぜこの書き方なのか

`SaveAs` によるセッションの切り替え

コード内で `targetPres.SaveAs backupPath` を使用している点に注目してほしい。単なる `FileCopy`(ファイルコピー)ではない。なぜなら、`FileCopy` は「ディスク上のファイル」をコピーするだけであり、「現在メモリ上で編集中の(まだ保存されていない)データ」を救うことはできないからだ。 `SaveAs` を使うことで、メモリ上の最新状態を強制的にローカルファイルとして書き出す。

`FileSystemObject` の採用

`Dir` 関数や `Name` ステートメントといった組み込み関数ではなく、`Scripting.FileSystemObject` を採用している。これは、ディレクトリ作成やパス操作における堅牢性を確保するためだ。プロダクション環境では、パスの末尾にバックスラッシュがあるかないかといった些細な違いがバグを誘発する。FSOはそれらを抽象化し、安全に処理してくれる。

ユーザーへの「心理的安全性」の提供

エラーが起きた際、単に「保存できませんでした」と出すのは未熟な設計だ。
「なぜ失敗したのか(競合の可能性)」「どこに救済策(退避コピー)を作ったのか」を明示することで、ユーザーのパニックを防ぎ、業務継続性を担保する。これが、ツールを作る者の責任である。

5. まとめ:保守性を高めるために

このコードを導入した後、君は以下のメンテナンスを行う必要がある。

1. エラー番号の継続的なウォッチ: Microsoft 365のアップデートにより、同期エラーの挙動が変わる可能性がある。現場で発生するエラー番号をログに記録し、`Select Case` 文を更新し続けよ。
2. 退避ディレクトリのクリーンアップ: `Emergency_Backups` フォルダが肥大化し、ディスクを圧迫しないよう、一定期間経過したファイルを削除するバッチ、あるいはVBAロジックを追加することを検討せよ。

自動化エンジニアの真価は、成功するケースを増やすことではなく、「失敗するケースにおいて、いかに被害を最小限に抑えるか」という設計思想に宿る。クラウド時代のVBA開発において、この「防御的設計」を忘れてはならない。

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