Merge-EvidenceLocks.ps1

function Write-Info {
    Param (
        [Parameter()]
        [string]
        $Message
    )

    process {
        Write-Information "$(Get-Date -Format 'yyyy-MM-dd HH:mm:ss') - $Message"
    }
}

<#
    .SYNOPSIS
        Merges all duplicate evidence locks into a single record
 
    .DESCRIPTION
        Duplicate evidence locks are identified by creating a key out of the Header, StartTime and EndTime values.
     
        If the -CreateNew flag is not provided, then the information about the unique, deduplicated evidence locks
        will be logged and the cmdlet will return.
     
        If -CreateNew is provided, then for each group of evidence locks, this cmdlet creates a list of all devices
        associated with the group, identifies the oldest duplicate record in the group, as well as the record with
        the evidence lock expiration furthest into the future. And a new evidence lock entry is created with these
        properties.
 
        If -DeleteOnSuccess is provided, then if there were NO ERRORS when creating the new evidence lock entry, the
        old evidence lock entries will be deleted, leaving only the new consolidated entry.
 
        Note: If errors occur, it's possible the new evidence lock could be partially created. If it wasn't 100%
        successful, then it is assumed to be a complete failure and deletions will not occur, and the failure will be
        logged to the Failures*.csv file.
 
    .PARAMETER LogDirectory
        Specifies the path where up to three CSV files will be saved. These CSV files will be named with a timestamp
        to avoid overwriting the results of prior executions.
 
        UniqueEvidenceLocks*.csv - After grouping duplicate evidence locks into sets, each unique set will be documented here
 
        Successes*.csv - If -CreateNew is provided, any 100% successfully created evidence locks will be documented here
 
        Failures*.csv - If -CreateNew is provided, any evidence locks that were not 100% successfully created will be documented here
 
    .PARAMETER CreateNew
        Attempt to create a new merged evidence lock for each set of duplicates.
 
    .PARAMETER DeleteOnSuccess
        Delete original evidence lock entries if the new consolidated evidence lock was created without errors.
 
    .PARAMETER RebuildSingleEntries
        Create new evidence lock entries even for evidence locks with no duplicates.
         
        This may be useful if it is believed the Recording Servers no longer posess a record of the original evidence lock
        definition. Using this parameter, a new evidence lock will be created, and successfully creating the evidence lock
        means the Recording Server has acknowledged that is has received and added the "sticker" to the media database.
 
    .EXAMPLE
        Connect-ManagementServer -Server hostname
        Merge-EvidenceLocks
 
        Generates a report of unique evidence locks including the largest "Size" property from each unique set for reference
 
    .EXAMPLE
        Connect-ManagementServer -Server hostname
        Merge-EvidenceLocks -CreateNew
 
        Attempt to create new consolidated evidence locks, but do not remove any existing evidence locks even when the new lock is created successfully
 
    .EXAMPLE
        Connect-ManagementServer -Server hostname
        Merge-EvidenceLocks -CreateNew -DeleteOnSuccess -RebuildSingleEntries
 
        Attempt a full de-duplication of evidence locks, as well as rebuilding evidence lock entries that do not currently have any duplicates.
#>

function Merge-EvidenceLocks {
    Param (
        [Parameter()]
        [string]
        $LogDirectory = '.\',
        [Parameter()]
        [switch]
        $CreateNew,
        [Parameter()]
        [switch]
        $DeleteOnSuccess,
        [Parameter()]
        [switch]
        $RebuildSingleEntries
    )

    begin {
        try {
            # Verify already connected to Management Server and LogDirectory path is writable.
            Get-ManagementServer | Out-Null
            $tmpFile = "tmp$(Get-Date -Format 'yyyyMMddHHmmss').txt"
            New-Item -Path ( Join-Path -Path $LogDirectory -ChildPath $tmpFile ) -ErrorAction Stop | Remove-Item
        } catch {
            throw
        }
    }

    process {
        Write-Info "Retrieving all evidence locks for later sorting. . ."
        $allRecords = Get-EvidenceLock
        Write-Info "Found $($allRecords.Count) evidence locks"
        if ($allRecords.Count -eq 0) {
            return
        }

        Write-Info "Sorting evidence locks into groups by Header, Start and End times. . ."
        $groups = $allRecords | Group-Object -Property { [string]::Join(',', @($_.Header, $_.StartTime.Ticks.ToString(), $_.EndTime.Ticks.ToString())) }
        
        $uniqueEvidenceLocks = @()
        $successes = @()
        $failures = @()

        foreach ($group in $groups) {
            Write-Info "Processing '$($group.Name)'"
            $deviceIds = New-Object System.Collections.ArrayList
            $originalLock = $group.Group[0]
            $expire = $group.Group[0].RetentionExpire
            $size = $group.Group[0].RetentionSize

            foreach ($lock in $group.Group) {
                # Build a list of all unique device IDs found in all duplicate locks in the group
                foreach ($deviceId in $lock.DeviceIds) {
                    if (!$deviceIds.Contains($deviceId)) {
                        $deviceIds.Add($deviceId) | Out-Null;
                    }
                }

                # Ensure we grab the first lock of the group ever created
                # And if some of the expire dates are different lets play
                # it safe and use the expiration date furthest in the future
                if ($lock.Created -lt $originalLock.Created) {
                    $originalLock = $lock
                }

                if ($lock.RetentionExpire -gt $expire) {
                    $expire = $lock.RetentionExpire
                }

                if ($lock.RetentionSize -gt $size) {
                    $size = $lock.RetentionSize
                }
            }
            
            # Save information about this deduplicated lock to pscustomobject
            $row = New-Object psobject
            $row | Add-Member -MemberType NoteProperty -Name Header -Value ([System.Security.SecurityElement]::Escape($originalLock.Header))
            $row | Add-Member -MemberType NoteProperty -Name Created -Value $originalLock.Created.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
            $row | Add-Member -MemberType NoteProperty -Name StartTime -Value $originalLock.StartTime.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
            $row | Add-Member -MemberType NoteProperty -Name EndTime -Value $originalLock.EndTime.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
            $row | Add-Member -MemberType NoteProperty -Name Expires -Value $expire.ToString('yyyy-MM-ddTHH:mm:ss.fffZ')
            $row | Add-Member -MemberType NoteProperty -Name DeviceIds -Value ([string]::Join(',', $deviceIds.ToArray()))
            $row | Add-Member -MemberType NoteProperty -Name Size -Value $size
            $uniqueEvidenceLocks += $row

            if (!$CreateNew) { 
                Write-Info 'The -CreateNew flag was not supplied, so deduplication will not proceed'
                continue
            }
            
            if ($group.Count -eq 1 -and !$RebuildSingleEntries) {
                Write-Info 'This evidence lock has no duplicates and the -RebuildSingleEntries flag is not present. Skipping to the next group.'
                continue
            }
            
            # Begin deduplication
            $magicDate = get-date -Year 2084
            try {
                $originalLock.Description = "OriginalUser=$($originalLock.User)`r`n$($originalLock.Description)"
                if ((Get-Date) -gt $expire) {
                    $originalLock.Description = "OriginalExpiration=$($expire)`r`n$($originalLock.Description)"
                    $expire = $magicDate
                }
                Write-Info "Creating new Evidence Lock for '$($originalLock.Header)'"
                $result = Add-EvidenceLock -Header $originalLock.Header -Description $originalLock.Description -DeviceIds $deviceIds -FootageFrom $originalLock.StartTime -FootageTo $originalLock.EndTime -ExpireDate $expire -RetentionType UserDefined -ErrorAction SilentlyContinue
                if ($result.Status -ne [VideoOS.Common.Proxy.Server.WCF.ResultStatus]::Success) {
                    foreach ($fault in $result.FaultDevices) {
                        Write-Warning "$($result.Status): $($fault.Message.Trim()), Device: $($fault.DeviceId)"
                    }
                    Write-Error "Add-EvidenceLock result for '$($originalLock.Header)': $($result.Status)" -ErrorAction Stop
                }
                $successes += $row
                Write-Info "Successfully created Evidence Lock with ID $($result.MarkedData.Id)"
            
                # No errors creating lock, delete duplicates
                if (!$DeleteOnSuccess) {
                    Write-Info 'The -DeleteOnSuccess flag was not supplied, so the new lock is created but the old locks will remain.'
                    continue
                }

                foreach ($lock in $group.Group) {
                    Write-Info "Deleting duplicate originally created on $($lock.Created)"
                    $lock | Remove-EvidenceLock
                }
            }
            catch {
                $ErrorMessage = $_.Exception.Message
                $failures += $row
                Write-Error $ErrorMessage
            }
        }

        $timestamp = Get-Date -Format 'yyyy-MM-dd_HH-mm-ss'
        $filePath = Join-Path -Path $LogDirectory -ChildPath "UniqueEvidenceLocks_$timestamp.csv"
        Write-Info "Saving deduplicated information to $filePath. . ."
        $uniqueEvidenceLocks | Export-Csv -Path $filePath -NoTypeInformation
        
        if ($CreateNew) {

            if ($successes.Count -gt 0) {
                $filePath = Join-Path -Path $LogDirectory -ChildPath "Successes_$timestamp.csv"
                Write-Info "Saving successfully created evidence lock information to $filePath. . ."
                $successes | Export-Csv -Path $filePath -NoTypeInformation
            }

            if ($failures.Count -gt 0) {
                $filePath = Join-Path -Path $LogDirectory -ChildPath "Failures_$timestamp.csv"
                Write-Info "Saving failed deduplications to $filePath. . ."
                $failures | Export-Csv -Path $filePath -NoTypeInformation
            }
        }
    }
}