Maintain control of your Microsoft 365 data
jasonede
Service Provider
Posts: 177
Liked: 52 times
Joined: Jan 04, 2018 4:51 pm
Contact:

Re: KB4835

Post by jasonede » 1 person likes this post

I've put this together and trying to work through my repositories. So far it seems to be working. It checks if a job is running then waits for it to finish before doing integrity check and repair. The job is then run and the check at the end carried out. It creates a log file for each job and a transcript in case it crashes.

Code: Select all

#Requires -Version 5.1
<#
.SYNOPSIS
    Identifies and repairs corrupted files in a Veeam Backup for Microsoft 365
    object-storage repository, following the procedure in KB4874.

.PARAMETER JobName
    Name of the Veeam backup job whose repository will be repaired.
    If omitted, the script prompts for it interactively.

.PARAMETER StartFromStep
    Step number to resume from (1-6). Default is 1 (full run).
    Use this to re-run from a specific step after a failure:
      1 - Full run (integrity check through final verification)
      2 - Re-parse integrity check results (re-uses last Test-VBORepository output)
      3 - Skip integrity check; go straight to repair session
      4 - Skip repair session; just re-enable the job
      5 - Skip repair session and re-enable; just run the backup job
      6 - Skip to final verification only
    Note: When starting from Step 3 or lower, the job is disabled first.
    When starting from Step 4 or higher, the job is assumed to be already enabled.

.NOTES
    Applies to: Veeam Backup for Microsoft 365 v8.5
    Must be run from an elevated (Run as Administrator) PowerShell window.
#>

[CmdletBinding()]
param(
    [Parameter(Position = 0)]
    [string]$JobName,

    [Parameter()]
    [ValidateRange(1, 6)]
    [int]$StartFromStep = 1
)

# ==============================================================================
# Configuration -- adjust these as needed
# ==============================================================================
$TopSitesCount          = 10    # How many top-affected sites to display after Step 2
$PollIntervalSeconds    = 30    # Polling interval for repair session and backup job
$PostEnableDelaySeconds = 60    # Seconds to wait after re-enabling the job before starting it
$ModulePath             = "C:\Program Files\Veeam\Backup365\Veeam.Archiver.PowerShell.dll"


# ==============================================================================
# Helper functions
# ==============================================================================

function Test-IsAdmin {
    $id = [Security.Principal.WindowsIdentity]::GetCurrent()
    $p  = New-Object Security.Principal.WindowsPrincipal($id)
    return $p.IsInRole([Security.Principal.WindowsBuiltInRole]::Administrator)
}

function Get-SafeFileName ([string]$Name) {
    foreach ($c in [IO.Path]::GetInvalidFileNameChars()) {
        $Name = $Name.Replace([string]$c, '_')
    }
    return $Name
}

function Write-Log {
    param(
        [string]$Message,
        [ValidateSet("INFO","WARN","ERROR")]
        [string]$Level = "INFO"
    )
    $ts   = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
    $line = "[$ts] [$Level] $Message"
    Write-Host $line
    Add-Content -Path $script:LogFile -Value $line
}

function Write-Section ([string]$Title) {
    $bar   = "=" * 80
    $block = @("", $bar, "  $Title", $bar)
    foreach ($line in $block) {
        Write-Host $line
        Add-Content -Path $script:LogFile -Value $line
    }
}

function Write-TableToScreenAndLog ([object[]]$Data, [string[]]$Properties = @()) {
    $table = if ($Properties.Count -gt 0) {
        $Data | Format-Table $Properties -AutoSize -Wrap | Out-String -Width 600
    } else {
        $Data | Format-Table -AutoSize -Wrap | Out-String -Width 600
    }
    Write-Host $table
    Add-Content -Path $script:LogFile -Value $table
}

# Called on all exit paths -- re-enables the job if this script disabled it
function Exit-Script ([int]$Code = 0) {
    if ($null -ne $script:DisabledJob) {
        Write-Log "Re-enabling job '$($script:DisabledJob.Name)' before exit..."
        try {
            Enable-VBOJob -Job $script:DisabledJob -ErrorAction SilentlyContinue | Out-Null
            Write-Log "Job re-enabled."
        } catch {
            Write-Log "Could not re-enable job automatically. Please re-enable manually in the Veeam console: $($script:DisabledJob.Name)" -Level "WARN"
        }
        $script:DisabledJob = $null
    }
    Write-Log "Script exiting with code $Code."
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit $Code
}

$script:DisabledJob = $null
$script:LogFile     = $null   # set below once we have the job name


# ==============================================================================
# Job name -- parameter or interactive prompt
# ==============================================================================
if ([string]::IsNullOrWhiteSpace($JobName)) {
    $JobName = Read-Host "Enter the Veeam backup job name"
}
if ([string]::IsNullOrWhiteSpace($JobName)) {
    Write-Host "[ERROR] No job name provided. Exiting."
    exit 1
}


# ==============================================================================
# Log and transcript setup
# ==============================================================================
$SafeName        = Get-SafeFileName $JobName
$CurrentDir      = (Get-Location).Path
$script:LogFile  = Join-Path $CurrentDir "${SafeName}.log"
$TranscriptFile  = Join-Path $CurrentDir "Transcript_${SafeName}.log"

# Transcript is overwritten on each run
Start-Transcript -Path $TranscriptFile -Force | Out-Null

# Append a run-start separator to the log
Add-Content -Path $script:LogFile -Value ""
Add-Content -Path $script:LogFile -Value ("*" * 80)
Add-Content -Path $script:LogFile -Value ("*  NEW RUN -- $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')  StartFromStep=$StartFromStep")
Add-Content -Path $script:LogFile -Value ("*" * 80)

Write-Section "Veeam VBO365 Repository Repair (KB4874) -- $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Log "Job name:              $JobName"
Write-Log "Starting from step:    $StartFromStep"
Write-Log "Log file:              $($script:LogFile)"
Write-Log "Transcript:            $TranscriptFile"
Write-Log "Running as admin:      $(Test-IsAdmin)"
Write-Log "Top sites to display:  $TopSitesCount"
Write-Log "Post-enable delay:     ${PostEnableDelaySeconds}s"


# ==============================================================================
# Prerequisites -- module
# ==============================================================================
Write-Section "Checking Prerequisites"

if (-not (Test-Path $ModulePath)) {
    Write-Log "Veeam PowerShell module not found at: $ModulePath" -Level "ERROR"
    Write-Log "Ensure Veeam Backup for Microsoft 365 v8.5 is installed." -Level "ERROR"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

try {
    Import-Module $ModulePath -ErrorAction Stop
    Write-Log "Veeam PowerShell module imported successfully."
} catch {
    Write-Log "Failed to import Veeam module: $($_.Exception.Message)" -Level "ERROR"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}


# ==============================================================================
# Locate the job
# ==============================================================================
Write-Section "Locating Backup Job"

$job = $null
try {
    $job = Get-VBOJob -Name $JobName -ErrorAction Stop
} catch {
    Write-Log "Could not find a job named '$JobName': $($_.Exception.Message)" -Level "ERROR"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}
Write-Log "Found job: $($job.Name)"


# ==============================================================================
# Check job scope -- must include OneDrive, SharePoint, or Teams
# ==============================================================================
Write-Section "Checking Job Scope"

$items = $null
try {
    $items = @(Get-VBOBackupItem -Job $job -ErrorAction Stop)
} catch {
    Write-Log "Could not retrieve backup items for job '$JobName': $($_.Exception.Message)" -Level "ERROR"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}

$hasOneDrive   = ($items | Where-Object { $_.OneDrive } | Measure-Object).Count -gt 0
$hasSharePoint = ($items | Where-Object { $_.Sites }    | Measure-Object).Count -gt 0
$hasTeams      = ($items | Where-Object { $_.Teams }    | Measure-Object).Count -gt 0

Write-Log "Job scope -- OneDrive: $hasOneDrive  |  SharePoint: $hasSharePoint  |  Teams: $hasTeams"

if (-not ($hasOneDrive -or $hasSharePoint -or $hasTeams)) {
    Write-Log "Job '$JobName' does not back up OneDrive, SharePoint, or Teams workloads." -Level "WARN"
    Write-Log "KB4874 remediation applies only to those workloads. No action taken." -Level "WARN"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 0
}
Write-Log "Job covers at least one applicable workload. Proceeding."


# ==============================================================================
# Get repository from job
# ==============================================================================
$repository = $job.Repository
if ($null -eq $repository) {
    Write-Log "Could not determine the repository from the job object. Verify the job has an assigned repository." -Level "ERROR"
    Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
    exit 1
}
Write-Log "Repository: $($repository.Name)"


# ==============================================================================
# Pre-Step: Disable the job (only when running Steps 1-3)
# ==============================================================================
if ($StartFromStep -le 3) {
    Write-Section "Pre-Step: Disabling Backup Job"

    # Wait for any currently-running session to finish before disabling
    $activeSession = Get-VBOJobSession -Job $job -Last
    $jobRunningStatuses = @("Running", "Queued", "Updating")

    if ($null -ne $activeSession -and $activeSession.Status -in $jobRunningStatuses) {
        Write-Log "Job '$($job.Name)' is currently running (Status=$($activeSession.Status)). Waiting for it to finish before disabling..."
        Write-Host ""

        $firstPoll     = $true
        $pollLineCount = 0

        while ($true) {
            $activeSession = Get-VBOJobSession -Job $job -Last
            $timestamp     = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

            $displayLines = @(
                "Timestamp  : $timestamp",
                "Job        : $($activeSession.JobName)",
                "Status     : $($activeSession.Status)",
                "Sync Type  : $($activeSession.JobSessionConfigType)"
            )

            if (-not $firstPoll -and $pollLineCount -gt 0) {
                try {
                    [System.Console]::SetCursorPosition(0, [System.Console]::CursorTop - $pollLineCount)
                } catch { }
            }
            foreach ($line in $displayLines) { Write-Host $line }
            $pollLineCount = $displayLines.Count
            $firstPoll     = $false

            Add-Content -Path $script:LogFile -Value "[$timestamp] [POLL] Waiting for running job -- Status=$($activeSession.Status)"

            if ($activeSession.Status -notin $jobRunningStatuses) {
                Write-Host ""
                Write-Log "Job session finished with status '$($activeSession.Status)'. Proceeding to disable."
                break
            }

            Start-Sleep -Seconds $PollIntervalSeconds
        }
    } else {
        Write-Log "Job '$($job.Name)' is not currently running. Proceeding to disable."
    }

    Write-Log "Disabling job '$($job.Name)' to prevent scheduled runs during repair..."

    try {
        Disable-VBOJob -Job $job -ErrorAction Stop | Out-Null
        $script:DisabledJob = $job
        Write-Log "Job disabled."
    } catch {
        Write-Log "Failed to disable job: $($_.Exception.Message)" -Level "ERROR"
        Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
        exit 1
    }
} else {
    Write-Log "Pre-Step: Job disable skipped (StartFromStep=$StartFromStep -- job assumed already enabled)."
}


# ==============================================================================
# Steps 1 and 2: Integrity check and review results
# ==============================================================================
if ($StartFromStep -le 2) {

    # ------------------------------------------------------------------
    # Step 1: Integrity check
    # ------------------------------------------------------------------
    Write-Section "Step 1: Running Integrity Check (Test-VBORepository)"
    Write-Log "This may take several hours depending on repository size."

    if (-not (Test-IsAdmin)) {
        Write-Log "WARNING: PowerShell is NOT running as administrator. If Test-VBORepository fails, close this session and re-run the script from an elevated (Run as Administrator) PowerShell window." -Level "WARN"
    }

    Write-Log "Running Test-VBORepository against '$($repository.Name)'..."

    $integrityErrors = @()
    try {
        Test-VBORepository -Repository $repository -ErrorVariable integrityErrors -ErrorAction SilentlyContinue | Out-Null
    } catch {
        $integrityErrors += $_
    }

    # Check whether the failure looks privilege-related
    if ($integrityErrors.Count -gt 0) {
        $combinedMessages = ($integrityErrors | ForEach-Object { "$_" }) -join " "
        if ((-not (Test-IsAdmin)) -and ($combinedMessages -match 'access.denied|denied|privilege|administrator|elevation|unauthorized')) {
            Write-Log "Test-VBORepository appears to have failed because the session is not elevated." -Level "ERROR"
            Write-Log "Please close this window, re-open PowerShell using 'Run as Administrator', and run the script again." -Level "ERROR"
            Exit-Script 1
        }
    }

    Write-Log "Integrity check complete. Reviewing results..."

    # ------------------------------------------------------------------
    # Step 2: Parse and review results
    # ------------------------------------------------------------------
    Write-Section "Step 2: Reviewing Integrity Check Results"

    $corruptedSites = [System.Collections.Generic.List[PSCustomObject]]::new()
    $corruptedLists = [System.Collections.Generic.List[PSCustomObject]]::new()

    foreach ($errObj in $integrityErrors) {
        $msg = "$errObj"

        if ($msg -match "The following site has corrupted file data:\s*(.+?)\s*\(.*?count:\s*(\d+)\)") {
            $siteUrl   = $Matches[1].Trim()
            $siteCount = [int]$Matches[2]
            $corruptedSites.Add([PSCustomObject]@{ Type = "Site"; Path = $siteUrl; Count = $siteCount })
        }

        if ($msg -match "The following list contains items referencing corrupted files:\s*(.+?)\s*\(.*?count:\s*(\d+)\)") {
            $listPath  = $Matches[1].Trim()
            $listCount = [int]$Matches[2]
            $corruptedLists.Add([PSCustomObject]@{ Type = "List"; Path = $listPath; Count = $listCount })
        }
    }

    $corruptionFound = ($corruptedSites.Count -gt 0) -or ($corruptedLists.Count -gt 0)

    if (-not $corruptionFound) {
        Write-Log "No corruption detected in repository '$($repository.Name)'."
        Write-Log "No further action is required."
        Exit-Script 0
    }

    $totalSiteFiles = ($corruptedSites | Measure-Object -Property Count -Sum).Sum
    $totalListItems = ($corruptedLists | Measure-Object -Property Count -Sum).Sum
    $totalAffected  = $totalSiteFiles + $totalListItems

    Write-Log "Corruption detected."
    Write-Log "  Corrupted sites:      $($corruptedSites.Count) site(s) -- $totalSiteFiles affected file(s)"
    Write-Log "  Corrupted lists:      $($corruptedLists.Count) list(s) -- $totalListItems affected item(s)"
    Write-Log "  Total affected items: $totalAffected"

    $allAffected = [System.Collections.Generic.List[PSCustomObject]]::new()
    foreach ($s in $corruptedSites) { $allAffected.Add($s) }
    foreach ($l in $corruptedLists) { $allAffected.Add($l) }

    $topAffected = @($allAffected | Sort-Object Count -Descending | Select-Object -First $TopSitesCount)

    $topHeader = "Top $TopSitesCount most-affected sites/locations (by corrupted item count):"
    Write-Host ""
    Write-Host $topHeader
    Add-Content -Path $script:LogFile -Value ""
    Add-Content -Path $script:LogFile -Value $topHeader

    Write-TableToScreenAndLog $topAffected -Properties @("Type","Count","Path")

} else {
    Write-Log "Steps 1-2: Integrity check skipped (StartFromStep=$StartFromStep)."
}


# ==============================================================================
# Step 3: Start repair session
# ==============================================================================
if ($StartFromStep -le 3) {

    Write-Section "Step 3: Scanning Repository and Flagging Affected Items"
    Write-Log "Starting full repair session on repository '$($repository.Name)'..."
    Write-Log "Note: The repository is locked for all backups and restores during this step."
    Write-Log "      Ensure all backup and restore sessions for this repository are closed."

    $repairSession = $null
    try {
        $repairSession = Start-VBORepositoryRepairSession `
            -Repository $repository `
            -Type CleanMissingFilesData `
            -Mode Full `
            -Scope Full `
            -ErrorAction Stop
        Write-Log "Repair session started. Session ID: $($repairSession.Id)"
    } catch {
        Write-Log "Failed to start repair session: $($_.Exception.Message)" -Level "ERROR"
        Exit-Script 1
    }

    Write-Log "Polling repair session every $PollIntervalSeconds seconds. Waiting for completion..."
    Write-Host ""

    # Non-terminal states -- session is still starting up or running
    $repairNonTerminal = @("Running", "Preparing", "Queued")

    $firstPoll     = $true
    $pollLineCount = 0

    while ($true) {
        $sessionState = Get-VBORepositoryRepairSession -Id $repairSession.Id
        $timestamp    = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

        $displayLines = @(
            "Timestamp  : $timestamp",
            "Id         : $($sessionState.Id)",
            "Type       : $($sessionState.Type)",
            "Repository : $($sessionState.RepositoryId)",
            "State      : $($sessionState.State)",
            "Error      : $($sessionState.ErrorMessage)"
        )

        if (-not $firstPoll -and $pollLineCount -gt 0) {
            try {
                [System.Console]::SetCursorPosition(0, [System.Console]::CursorTop - $pollLineCount)
            } catch { }
        }
        foreach ($line in $displayLines) { Write-Host $line }
        $pollLineCount = $displayLines.Count
        $firstPoll     = $false

        Add-Content -Path $script:LogFile -Value "[$timestamp] [POLL] Repair -- State=$($sessionState.State)  Error=$($sessionState.ErrorMessage)"

        if ($sessionState.State -notin $repairNonTerminal) {
            Write-Host ""
            if ($sessionState.State -eq "Finished" -and [string]::IsNullOrEmpty($sessionState.ErrorMessage)) {
                Write-Log "Repair session completed successfully."
            } else {
                Write-Log "Repair session ended with state '$($sessionState.State)'." -Level "WARN"
                if (-not [string]::IsNullOrEmpty($sessionState.ErrorMessage)) {
                    Write-Log "Session error: $($sessionState.ErrorMessage)" -Level "ERROR"
                    Write-Log "Stopping -- repair session reported an error. Review the error above before retrying." -Level "ERROR"
                    Exit-Script 1
                }
            }
            break
        }

        Start-Sleep -Seconds $PollIntervalSeconds
    }

} else {
    Write-Log "Step 3: Repair session skipped (StartFromStep=$StartFromStep)."
}


# ==============================================================================
# Step 4: Re-enable the job
# ==============================================================================
Write-Section "Step 4: Re-enabling Backup Job"

# Decide whether the job needs enabling. Re-enable if this run disabled it, or if
# the job is currently found disabled (e.g. left disabled by an aborted prior run).
$disabledByThisRun = ($null -ne $script:DisabledJob)
$needsEnable       = $disabledByThisRun

if (-not $disabledByThisRun) {
    # Re-query current state -- IsEnabled on the object fetched at start may be stale
    try {
        $currentJobState = Get-VBOJob -Name $JobName -ErrorAction Stop
        if (-not $currentJobState.IsEnabled) {
            $needsEnable = $true
            Write-Log "Job '$($job.Name)' is currently disabled (not by this run). It will be re-enabled."
        }
    } catch {
        Write-Log "Could not re-query job state to check if enabled: $($_.Exception.Message)" -Level "WARN"
    }
}

if ($needsEnable) {
    Write-Log "Re-enabling job '$($job.Name)'..."
    try {
        Enable-VBOJob -Job $job -ErrorAction Stop | Out-Null
        $script:DisabledJob = $null
        Write-Log "Job re-enabled successfully."
    } catch {
        Write-Log "Failed to re-enable job: $($_.Exception.Message)" -Level "ERROR"
        Exit-Script 1
    }

    Write-Log "Waiting $PostEnableDelaySeconds seconds for repository to exit maintenance mode before starting backup..."
    $waitEnd = (Get-Date).AddSeconds($PostEnableDelaySeconds)
    while ((Get-Date) -lt $waitEnd) {
        $remaining = [int]($waitEnd - (Get-Date)).TotalSeconds
        Write-Host "`r  $remaining seconds remaining...    " -NoNewline
        Start-Sleep -Seconds 1
    }
    Write-Host ""
    Write-Log "Wait complete. Proceeding to Step 5."

} else {
    Write-Log "Step 4: Job is already enabled -- nothing to do."
}


# ==============================================================================
# Step 5: Run the backup job
# ==============================================================================
if ($StartFromStep -le 5) {

    Write-Section "Step 5: Running Backup Job"
    Write-Log "Starting backup job '$($job.Name)' to redownload affected files..."

    try {
        Start-VBOJob -Job $job -ErrorAction Stop | Out-Null
        Write-Log "Backup job started."
    } catch {
        Write-Log "Failed to start backup job: $($_.Exception.Message)" -Level "ERROR"
        Exit-Script 1
    }

    # Brief pause to allow the session record to be created before first poll
    Start-Sleep -Seconds 5

    Write-Log "Polling backup job status every $PollIntervalSeconds seconds. Waiting for completion..."
    Write-Host ""

    $firstPoll     = $true
    $pollLineCount = 0

    # Non-terminal: Running, Queued, Updating -- anything else means the job has finished
    $backupTerminal = @("Success", "Warning", "Failed", "Stopped", "NotConfigured")

    while ($true) {
        $currentSession = Get-VBOJobSession -Job $job -Last
        $timestamp      = Get-Date -Format "yyyy-MM-dd HH:mm:ss"

        if ($null -eq $currentSession) {
            Write-Host "[$timestamp] Waiting for backup session to initialise..."
            Start-Sleep -Seconds $PollIntervalSeconds
            continue
        }

        $displayLines = @(
            "Timestamp  : $timestamp",
            "Job        : $($currentSession.JobName)",
            "Status     : $($currentSession.Status)",
            "Sync Type  : $($currentSession.JobSessionConfigType)"
        )

        if (-not $firstPoll -and $pollLineCount -gt 0) {
            try {
                [System.Console]::SetCursorPosition(0, [System.Console]::CursorTop - $pollLineCount)
            } catch { }
        }
        foreach ($line in $displayLines) { Write-Host $line }
        $pollLineCount = $displayLines.Count
        $firstPoll     = $false

        Add-Content -Path $script:LogFile -Value "[$timestamp] [POLL] Backup -- Status=$($currentSession.Status)  SyncType=$($currentSession.JobSessionConfigType)"

        if ($currentSession.Status -in $backupTerminal) {
            Write-Host ""
            Write-Log "Backup job finished. Final status: $($currentSession.Status)"
            if ($currentSession.Status -eq "Failed") {
                Write-Log "Backup job reported a failure. Check the Veeam console for details. Proceeding to Step 6 for verification." -Level "WARN"
            }
            break
        }

        Start-Sleep -Seconds $PollIntervalSeconds
    }

} else {
    Write-Log "Step 5: Backup job run skipped (StartFromStep=$StartFromStep)."
}


# ==============================================================================
# Step 6: Final verification and identify unrecoverable files
# ==============================================================================
Write-Section "Step 6: Final Verification (Test-VBORepository -DetailedOutput)"
Write-Log "Running detailed integrity check. This downloads file metadata from M365 and may take time."

# Record time before running so we can identify the log file this run produces
$verificationStartTime = Get-Date

try {
    Test-VBORepository -Repository $repository -DetailedOutput -ErrorAction SilentlyContinue 2>&1 | Out-Null
    Write-Log "Detailed verification complete."
} catch {
    Write-Log "Test-VBORepository (detailed) noted: $($_.Exception.Message)" -Level "WARN"
    Write-Log "The detailed log file should still have been written to disk." -Level "WARN"
}

# Locate the integrity log written during this run
$integrityLogBase = Join-Path $env:ProgramData "Veeam\Backup365\Logs\IntegrityVerification"
$integrityLogDir  = Join-Path $integrityLogBase $repository.Name

Write-Log "Looking for integrity verification logs in: $integrityLogDir"

if (-not (Test-Path $integrityLogDir)) {
    Write-Log "Integrity log directory not found: $integrityLogDir" -Level "WARN"
    Write-Log "Check manually under: $integrityLogBase" -Level "WARN"
    Exit-Script 0
}

# Only consider log files written after this run's Test-VBORepository call started
$latestLog = Get-ChildItem -Path $integrityLogDir -Filter "IntegrityVerification_$($repository.Name)_*.log" -ErrorAction SilentlyContinue |
    Where-Object { $_.LastWriteTime -ge $verificationStartTime } |
    Sort-Object LastWriteTime -Descending |
    Select-Object -First 1

if ($null -eq $latestLog) {
    Write-Log "No integrity verification log for repository '$($repository.Name)' written since $($verificationStartTime.ToString('yyyy-MM-dd HH:mm:ss')) found in: $integrityLogDir" -Level "WARN"
    Write-Log "The verification may not have produced output, or the log path differs. Check manually under: $integrityLogBase" -Level "WARN"
    Exit-Script 0
}

Write-Log "Scanning log file: $($latestLog.FullName)"

# Stream the log with Select-String (avoids loading the whole file into memory).
# Match on the rarer marker first, then confirm the second on the same line.
$unrecoverableLines = @(
    Select-String -Path $latestLog.FullName -Pattern 'RepairHistory: FileWasReset' -ErrorAction Stop |
        Where-Object { $_.Line -match 'Backed-up file is empty' } |
        ForEach-Object { $_.Line }
)

if ($unrecoverableLines.Count -eq 0) {
    Write-Log "No unrecoverable files found. All affected files were successfully redownloaded."
    Exit-Script 0
}

Write-Section "Unrecoverable Files -- $($unrecoverableLines.Count) file(s) could not be recovered"
Write-Log "$($unrecoverableLines.Count) file(s) remain empty in the backup. These files no longer exist in Microsoft 365 and cannot be restored."

$parsedFiles = foreach ($line in $unrecoverableLines) {
    $orgId    = 'N/A'
    $siteId   = 'N/A'
    $webId    = 'N/A'
    $intWebId = 'N/A'
    $fileId   = 'N/A'
    $version  = 'N/A'

    if ($line -match 'OrganizationId:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})') { $orgId    = $Matches[1] }
    if ($line -match 'SiteId:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})')         { $siteId   = $Matches[1] }
    if ($line -match '(?<!Internal)WebId:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})') { $webId = $Matches[1] }
    if ($line -match 'InternalWebId:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})')  { $intWebId = $Matches[1] }
    if ($line -match 'file: Id:\s*([0-9a-fA-F]{8}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{4}-[0-9a-fA-F]{12})')       { $fileId   = $Matches[1] }
    if ($line -match 'Version:\s*(\d+)')                                                                                    { $version  = $Matches[1] }

    [PSCustomObject]@{
        OrganizationId = $orgId
        SiteId         = $siteId
        WebId          = $webId
        InternalWebId  = $intWebId
        FileId         = $fileId
        Version        = $version
    }
}

Write-TableToScreenAndLog $parsedFiles

Write-Log "For further assistance, raise a Veeam Support case: https://www.veeam.com/kb1771"


# ==============================================================================
# Done
# ==============================================================================
Write-Section "Script Complete -- $(Get-Date -Format 'yyyy-MM-dd HH:mm:ss')"
Write-Log "All steps completed. Review the full log at: $($script:LogFile)"

Stop-Transcript -ErrorAction SilentlyContinue | Out-Null
Post Reply

Who is online

Users browsing this forum: No registered users and 129 guests