OneDriveHelpers.psm1
<#
.SYNOPSIS Helper functions for OneDrive file attributes and logging. .DESCRIPTION This module provides functions to: - Write log messages to console and log file - Resolve full paths - Check OneDrive files/folders for online-only or local availability - Force files/folders to be locally available or online-only - Wait for OneDrive file/folder attribute state changes #> #region Logging <# .SYNOPSIS Writes a log message to console and optionally to a log file. .PARAMETER message The log message text. .PARAMETER level Log level. Acceptable values: Info, Warning, Error, Success, Debug. .PARAMETER NoNewLine If set, does not output a newline at the end. .PARAMETER StartNewLine If set, starts with a new line before the log entry. .EXAMPLE Write-Log -message "Process started" -level "Info" #> function Write-Log { param( [Parameter(Mandatory=$true)] [string]$message, [ValidateSet("Info","Warning","Error","Success","Debug")] [string]$level = "Info", [switch]$NoNewLine, [switch]$StartNewLine ) $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $log_entry = "[$timestamp] [$level] $message" if ($StartNewLine) { $log_entry = "`n$log_entry" Write-Host "" } # Write to log file if $script:log_file is set if ($script:log_file) { try { Add-Content -Path $script:log_file -Value $log_entry -ErrorAction Stop } catch { Write-Host "Failed to write to log file: $($_.Exception.Message)" -ForegroundColor Red } } # Write to console with color $foreground_color = switch ($level.ToLower()) { "info" {"White"} "warning" {"Yellow"} "error" {"Red"} "success" {"Green"} "debug" {"Cyan"} default {"White"} } Write-Host -NoNewline "[$timestamp]" -ForegroundColor "DarkGray" Write-Host " [$level] $message" -ForegroundColor $foreground_color -NoNewline if (-not $NoNewLine) { Write-Host "" } } #endregion #region Path Helpers <# .SYNOPSIS Resolves a full, absolute path from a given string. .PARAMETER Path The input path string. .EXAMPLE Resolve-FullPath -Path ".\file.txt" #> function Resolve-FullPath { param([Parameter(Mandatory=$true)][string]$Path) try { return (Resolve-Path -LiteralPath $Path -ErrorAction Stop).ProviderPath } catch { throw "Resolve-FullPath: cannot resolve '$Path' - $($_.Exception.Message)" } } #endregion #region OneDrive Attribute Helpers <# .SYNOPSIS Tests if a OneDrive file or folder is online-only. .PARAMETER Path The path to the file or folder. .RETURNS $true if online-only (Offline attribute set), $false if locally available. .EXAMPLE Test-OneDriveOnlineOnly -Path "C:\Users\User\OneDrive\file.txt" #> function Test-OneDriveOnlineOnly { param([Parameter(Mandatory=$true)][string]$Path) $full = Resolve-FullPath -Path $Path $item = Get-Item -LiteralPath $full -ErrorAction Stop $attrs = $item.Attributes return ([int]($attrs -band [System.IO.FileAttributes]::Offline) -ne 0) } <# .SYNOPSIS Internal helper to invoke attrib.exe with arguments. .PARAMETER Args Array of attrib.exe command line arguments. .EXAMPLE Invoke-AttribInternal -Args @("+p", "C:\file.txt") #> function Invoke-AttribInternal { 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 ' ')" } } } <# .SYNOPSIS Requests a OneDrive file/folder to become locally available. .PARAMETER Path The path to the file or folder. .PARAMETER RetryAttrib Number of times to retry the attrib command (default 1). .EXAMPLE Set-OneDriveLocalAvailable -Path "C:\Users\User\OneDrive\file.txt" #> function Set-OneDriveLocalAvailable { [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$Path, [int]$RetryAttrib=1) try { $full = Resolve-FullPath -Path $Path } catch { Write-Warning "Resolve-FullPath failed: $($_.Exception.Message)"; return $false } try { if (-not (Test-OneDriveOnlineOnly -Path $full)) { return $true } } catch { Write-Warning "Test-OneDriveOnlineOnly failed: $($_.Exception.Message)"; 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-Warning "Set-OneDriveLocalAvailable: attrib +p failed for '$full'. Last exit: $($last.ExitCode)." return $false } <# .SYNOPSIS Requests a OneDrive file/folder to become online-only. .PARAMETER Path The path to the file or folder. .PARAMETER RetryAttrib Number of times to retry the attrib command (default 1). .EXAMPLE Clear-OneDriveLocalSpace -Path "C:\Users\User\OneDrive\file.txt" #> function Clear-OneDriveLocalSpace { [CmdletBinding()] param([Parameter(Mandatory=$true)][string]$Path, [int]$RetryAttrib=1) try { $full = Resolve-FullPath -Path $Path } catch { Write-Warning "Resolve-FullPath failed: $($_.Exception.Message)"; return $false } try { if (Test-OneDriveOnlineOnly -Path $full) { return $true } } catch { Write-Warning "Test-OneDriveOnlineOnly failed: $($_.Exception.Message)"; 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-Warning "Clear-OneDriveLocalSpace: attrib -p failed for '$full'. Last exit: $($last.ExitCode)." return $false } #endregion #region Wait Helpers <# .SYNOPSIS Waits for OneDrive file/folder attribute state (local or online-only). .PARAMETER Path The path to the file or folder. .PARAMETER Mode Target mode: 'BecomeLocal' or 'BecomeOnlineOnly'. .PARAMETER TimeoutSeconds Timeout in seconds (0 = wait forever). .PARAMETER CheckIntervalSeconds Interval between checks (seconds). .PARAMETER RetryAttribOnLoop If set, re-issues attrib command on each loop. .PARAMETER RetryAttrib Number of attempts for each attrib execution. .EXAMPLE Wait-ForAttributeState_AttribOnly -Path "C:\file.txt" -Mode "BecomeLocal" #> function Wait-ForAttributeState_AttribOnly { [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-Error "Resolve-FullPath failed: $($_.Exception.Message)"; return $false } $action = if ($Mode -eq 'BecomeLocal') { '+p' } else { '-p' } 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' -and -not $curOnline) -or ($Mode -eq 'BecomeOnlineOnly' -and $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-Warning "Wait-ForAttributeState_AttribOnly timed out after $TimeoutSeconds s for '$full' (Mode=$Mode)." return $false } } } } <# .SYNOPSIS Waits until a OneDrive file/folder becomes locally available. #> 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 } <# .SYNOPSIS Waits until a OneDrive file/folder becomes online-only. #> 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 |