Functions/Public/Test-FileLock.ps1

function Test-FileLock {
    <#
    .SYNOPSIS
        Tests whether a file is currently locked by another process.

    .DESCRIPTION
        The Test-FileLock function checks if a file can be opened with read/write access.
        If the file is locked, it can either return a boolean, wait for user input, or
        wait with a timeout for the file to become available.

    .PARAMETER FilePath
        The path to the file to check.

    .PARAMETER Silent
        If specified, returns $true if the file is locked, $false if available.
        Does not display any messages or wait for input.

    .PARAMETER Wait
        If specified, waits for user to press Enter when file is locked.
        This is the default interactive behavior.

    .PARAMETER TimeoutSeconds
        If specified, waits up to this many seconds for the file to become available.
        Returns $true if file becomes available, $false if timeout expires.

    .PARAMETER RetryIntervalSeconds
        When using TimeoutSeconds, how often to retry. Default is 2 seconds.

    .INPUTS
        None. You cannot pipe objects to Test-FileLock.

    .OUTPUTS
        System.Boolean when using -Silent or -TimeoutSeconds.
        None when using interactive mode (default).

    .EXAMPLE
        Test-FileLock -FilePath "C:\Data\report.xlsx"

        Checks if the file is locked. If locked, prompts user to close it and press Enter.

    .EXAMPLE
        Test-FileLock -FilePath "C:\Data\report.xlsx" -Silent

        Returns $true if file is locked, $false if available. No user interaction.

    .EXAMPLE
        if (Test-FileLock -FilePath $file -Silent) {
            Write-Warning "File is in use, skipping..."
        }

        Use in scripts to check lock status without interaction.

    .EXAMPLE
        Test-FileLock -FilePath "C:\Data\report.xlsx" -TimeoutSeconds 30

        Waits up to 30 seconds for the file to become available.

    .NOTES
        Author: Sune Alexandersen Narud
        Version: 1.0.0
        Date: February 2026

    .LINK
        Remove-StaleFile
    #>


    [CmdletBinding(DefaultParameterSetName = 'Interactive')]
    [OutputType([bool], ParameterSetName = 'Silent')]
    [OutputType([bool], ParameterSetName = 'Timeout')]
    param (
        [Parameter(Mandatory = $true, Position = 0, ValueFromPipeline = $true)]
        [ValidateNotNullOrEmpty()]
        [string]$FilePath,

        [Parameter(ParameterSetName = 'Silent')]
        [switch]$Silent,

        [Parameter(ParameterSetName = 'Interactive')]
        [switch]$Wait,

        [Parameter(ParameterSetName = 'Timeout')]
        [ValidateRange(1, 3600)]
        [int]$TimeoutSeconds,

        [Parameter(ParameterSetName = 'Timeout')]
        [ValidateRange(1, 60)]
        [int]$RetryIntervalSeconds = 2
    )

    process {
        # Check if file exists
        if (-not (Test-Path $FilePath)) {
            if ($Silent -or $TimeoutSeconds) {
                return $false  # File doesn't exist, so it's not locked
            }
            return  # Nothing to do in interactive mode
        }

        # Function to check if file is locked
        $checkLock = {
            param($path)
            try {
                $stream = [System.IO.File]::Open($path, [System.IO.FileMode]::Open, [System.IO.FileAccess]::ReadWrite, [System.IO.FileShare]::None)
                $stream.Close()
                return $false  # Not locked
            }
            catch {
                return $true  # Locked
            }
        }

        switch ($PSCmdlet.ParameterSetName) {
            'Silent' {
                return (& $checkLock $FilePath)
            }

            'Timeout' {
                $elapsed = 0
                while ($elapsed -lt $TimeoutSeconds) {
                    if (-not (& $checkLock $FilePath)) {
                        return $true  # File is available
                    }
                    Write-Verbose "File is locked, retrying in $RetryIntervalSeconds seconds... ($elapsed/$TimeoutSeconds)"
                    Start-Sleep -Seconds $RetryIntervalSeconds
                    $elapsed += $RetryIntervalSeconds
                }
                return $false  # Timeout expired, file still locked
            }

            'Interactive' {
                $fileName = [System.IO.Path]::GetFileName($FilePath)
                $folderName = [System.IO.Path]::GetDirectoryName($FilePath)
                while (& $checkLock $FilePath) {
                    Write-Host
                    Write-Host "The file '" -NoNewline
                    Write-Host "$fileName" -ForegroundColor Cyan -NoNewline
                    Write-Host "' in folder '" -NoNewline
                    Write-Host "$folderName" -ForegroundColor Cyan -NoNewline
                    Write-Host "' is currently locked or open in another program."
                    Write-Host "Please close it and press Enter to continue..."
                    Read-Host
                }
            }
        }
    }
}