OneDriveHelpers.psm1

# ===============================
# OneDriveHelpers Module
# Depends on InitHelpers
# ===============================

# 自动导入 InitHelpers
if (-not (Get-Module -Name InitHelpers)) {
    Import-Module InitHelpers -ErrorAction Stop
}

#region Helper Functions

function Resolve-FullPath {
<#
.SYNOPSIS
Resolves a full path from a given input path.
.DESCRIPTION
Uses Resolve-Path and raises an error if path cannot be resolved.
.PARAMETER Path
Path to resolve (mandatory).
.EXAMPLE
Resolve-FullPath -Path "C:\Temp\file.txt"
#>

    param([Parameter(Mandatory=$true)][string]$Path)
    try {
        return (Resolve-Path -LiteralPath $Path -ErrorAction Stop).ProviderPath
    } catch {
        throw "Resolve-FullPath: cannot resolve '$Path' - $($_.Exception.Message)"
    }
}

function Test-OneDriveOnlineOnly {
<#
.SYNOPSIS
Checks if a file/folder is an online-only OneDrive placeholder.
.PARAMETER Path
Path to check.
.EXAMPLE
Test-OneDriveOnlineOnly -Path "C:\Users\User\OneDrive\File.txt"
#>

    param([Parameter(Mandatory=$true)][string]$Path)
    $full = Resolve-FullPath -Path $Path
    $item = Get-Item -LiteralPath $full -ErrorAction Stop
    return ([int]($item.Attributes -band [System.IO.FileAttributes]::Offline) -ne 0)
}

function Invoke-AttribInternal {
<#
.SYNOPSIS
Invokes the attrib command and captures its output.
.PARAMETER Args
Arguments to pass to attrib.exe
.EXAMPLE
Invoke-AttribInternal -Args @('+p','C:\Temp\File.txt')
#>

    param([Parameter(Mandatory=$true)][string[]]$Args)

    $attribExe = Join-Path $env:SystemRoot 'System32\attrib.exe'
    if (-not (Test-Path $attribExe)) { $attribExe = 'attrib.exe' }

    try {
        $out = & $attribExe @Args 2>&1
        $exit = $LASTEXITCODE
        $outputText = if ($out -is [array]) { $out -join "`n" } else { [string]$out }
        return @{ ExitCode = $exit; Output = $outputText; Command = "$attribExe $($Args -join ' ')" }
    } catch {
        return @{ ExitCode = -1; Output = $_.Exception.Message; Command = "$attribExe $($Args -join ' ')" }
    }
}

#endregion

#region OneDrive Attribute Management

function Set-OneDriveLocalAvailable {
<#
.SYNOPSIS
Marks a OneDrive file/folder as locally available (+p).
.PARAMETER Path
Path to set locally available.
.PARAMETER RetryAttrib
Number of retries for attrib command (default: 1).
.EXAMPLE
Set-OneDriveLocalAvailable -Path "C:\Users\User\OneDrive\File.txt"
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string]$Path,
        [int]$RetryAttrib = 1
    )

    try {
        $full = Resolve-FullPath -Path $Path
    } catch {
        Write-Log -Message "Resolve-FullPath failed: $($_.Exception.Message)" -Level Warning
        return $false
    }

    try {
        if (-not (Test-OneDriveOnlineOnly -Path $full)) { return $true }  # already local
    } catch {
        Write-Log -Message "Test-OneDriveOnlineOnly failed: $($_.Exception.Message)" -Level Warning
        return $false
    }

    $last = $null
    for ($i = 1; $i -le [Math]::Max(1,$RetryAttrib); $i++) {
        $last = Invoke-AttribInternal -Args @('+p', $full)
        if ($last.ExitCode -eq 0) { return $true }
        Start-Sleep -Milliseconds 200
    }

    Write-Log -Message "Set-OneDriveLocalAvailable: attrib +p failed for '$full'. Last exit: $($last.ExitCode)." -Level Warning
    return $false
}

function Clear-OneDriveLocalSpace {
<#
.SYNOPSIS
Marks a OneDrive file/folder as online-only (-p).
.PARAMETER Path
Path to set online-only.
.PARAMETER RetryAttrib
Number of retries for attrib command (default: 1).
.EXAMPLE
Clear-OneDriveLocalSpace -Path "C:\Users\User\OneDrive\File.txt"
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string]$Path,
        [int]$RetryAttrib = 1
    )

    try {
        $full = Resolve-FullPath -Path $Path
    } catch {
        Write-Log -Message "Resolve-FullPath failed: $($_.Exception.Message)" -Level Warning
        return $false
    }

    try {
        if (Test-OneDriveOnlineOnly -Path $full) { return $true }  # already online-only
    } catch {
        Write-Log -Message "Test-OneDriveOnlineOnly failed: $($_.Exception.Message)" -Level Warning
        return $false
    }

    $last = $null
    for ($i = 1; $i -le [Math]::Max(1,$RetryAttrib); $i++) {
        $last = Invoke-AttribInternal -Args @('-p', $full)
        if ($last.ExitCode -eq 0) { return $true }
        Start-Sleep -Milliseconds 200
    }

    Write-Log -Message "Clear-OneDriveLocalSpace: attrib -p failed for '$full'. Last exit: $($last.ExitCode)." -Level Warning
    return $false
}

#endregion

#region Wait Helpers

function Wait-ForAttributeState_AttribOnly {
<#
.SYNOPSIS
Waits for OneDrive file/folder to reach a desired attribute state.
.PARAMETER Path
Path to wait for.
.PARAMETER Mode
'BecomeLocal' or 'BecomeOnlineOnly'.
.PARAMETER TimeoutSeconds
Timeout in seconds (0 = infinite).
.PARAMETER CheckIntervalSeconds
Interval between checks.
.PARAMETER RetryAttribOnLoop
Retry attrib each loop.
.PARAMETER RetryAttrib
Number of retries per attrib command.
.EXAMPLE
Wait-ForAttributeState_AttribOnly -Path "C:\Users\User\OneDrive\File.txt" -Mode "BecomeLocal"
#>

    [CmdletBinding()]
    param(
        [Parameter(Mandatory=$true)][string]$Path,
        [ValidateSet('BecomeLocal','BecomeOnlineOnly')][string]$Mode,
        [int]$TimeoutSeconds = 0,
        [int]$CheckIntervalSeconds = 3,
        [switch]$RetryAttribOnLoop,
        [int]$RetryAttrib = 1
    )

    try { $full = Resolve-FullPath -Path $Path } catch { Write-Log -Message "Resolve-FullPath failed: $($_.Exception.Message)" -Level Error; return $false }

    $action = if ($Mode -eq 'BecomeLocal') { '+p' } else { '-p' }

    # initial attrib attempt
    for ($r=1; $r -le [Math]::Max(1,$RetryAttrib); $r++) {
        $startRes = Invoke-AttribInternal -Args @($action, $full)
        if ($startRes.ExitCode -eq 0) { break }
        Start-Sleep -Milliseconds 150
    }

    $start = [DateTime]::UtcNow
    while ($true) {
        Start-Sleep -Seconds $CheckIntervalSeconds

        try {
            $curOnline = Test-OneDriveOnlineOnly -Path $full
            if ($Mode -eq 'BecomeLocal') {
                if (-not $curOnline) { return $true }
            } else {
                if ($curOnline) { return $true }
            }
        } catch { }

        if ($RetryAttribOnLoop) {
            for ($r=1; $r -le [Math]::Max(1,$RetryAttrib); $r++) {
                Invoke-AttribInternal -Args @($action, $full) | Out-Null
                Start-Sleep -Milliseconds 150
            }
        }

        if ($TimeoutSeconds -gt 0) {
            $elapsed = ([DateTime]::UtcNow - $start).TotalSeconds
            if ($elapsed -ge $TimeoutSeconds) {
                Write-Log -Message "Wait-ForAttributeState_AttribOnly timed out after $TimeoutSeconds s for '$full' (Mode=$Mode)." -Level Warning
                return $false
            }
        }
    }
}

function Wait-OneDriveLocalAvailable {
    param(
        [Parameter(Mandatory=$true)][string]$Path,
        [int]$TimeoutSeconds = 0,
        [int]$CheckIntervalSeconds = 3,
        [switch]$RetryAttribOnLoop,
        [int]$RetryAttrib = 1
    )
    return Wait-ForAttributeState_AttribOnly -Path $Path -Mode 'BecomeLocal' -TimeoutSeconds $TimeoutSeconds -CheckIntervalSeconds $CheckIntervalSeconds -RetryAttribOnLoop:$RetryAttribOnLoop -RetryAttrib $RetryAttrib
}

function Wait-ClearOneDriveLocalSpace {
    param(
        [Parameter(Mandatory=$true)][string]$Path,
        [int]$TimeoutSeconds = 0,
        [int]$CheckIntervalSeconds = 3,
        [switch]$RetryAttribOnLoop,
        [int]$RetryAttrib = 1
    )
    return Wait-ForAttributeState_AttribOnly -Path $Path -Mode 'BecomeOnlineOnly' -TimeoutSeconds $TimeoutSeconds -CheckIntervalSeconds $CheckIntervalSeconds -RetryAttribOnLoop:$RetryAttribOnLoop -RetryAttrib $RetryAttrib
}

#endregion