src/Private/Core/Copy-FotonCappedHostFileList.ps1
|
function Copy-FotonCappedHostFileList { <# Copy an explicit list of live files (newest first) with count and byte caps. DestRelative may include subfolders so user hives and LNKs do not collide on leaf name. Status is Copied / Missing / CopyError / CappedSkip. #> param( [Parameter(Mandatory)]$Items, [Parameter(Mandatory)][string]$DestinationDirectory, [string]$PackageRelativeDirectory, [int]$MaxFiles = 400, [long]$MaxTotalBytes = 83886080 ) $rows = New-Object System.Collections.ArrayList $list = @($Items | Where-Object { $_ }) if ($list.Count -eq 0) { $null = $rows.Add([PSCustomObject]@{ SourcePath = '' DestRelative = '' SizeBytes = 0 SHA256 = '' Status = 'NoData' Note = 'No source paths were queued. Visibility gap, not proof the artifact class was empty.' }) return @($rows) } $sorted = @($list | Sort-Object { $p = [string]$_.SourcePath # Do not Test-Path UNC during sort (would auth to remote). if ($p -and ($p.StartsWith('\\') -or $p.StartsWith('//'))) { [datetime]::MinValue } elseif ($p -and (Test-Path -LiteralPath $p -PathType Leaf)) { try { (Get-Item -LiteralPath $p -Force).LastWriteTimeUtc } catch { [datetime]::MinValue } } else { [datetime]::MinValue } } -Descending) $copied = 0 $used = [int64]0 $destRootFull = $null try { $destRootFull = [IO.Path]::GetFullPath($DestinationDirectory) } catch { $destRootFull = $DestinationDirectory } foreach ($it in $sorted) { $src = [string]$it.SourcePath $relLeaf = [string]$it.DestRelative if (-not $relLeaf) { $relLeaf = [string]$it.DestRelative } if (-not $relLeaf) { $relLeaf = [IO.Path]::GetFileName($src) } # Never probe UNC: Test-Path / open would auth to remote shares (NTLM leak). if ($src -and ($src.StartsWith('\\') -or $src.StartsWith('//'))) { $null = $rows.Add([PSCustomObject]@{ SourcePath = $src DestRelative = $relLeaf SizeBytes = 0 SHA256 = '' Status = 'SkippedUnc' Note = 'UNC source refused. Deep Collect does not follow remote paths (NTLM / network footprint).' }) continue } # DestRelative must stay under DestinationDirectory (no .. escape). if ($relLeaf -match '(^|[\\/])\.\.([\\/]|$)') { $null = $rows.Add([PSCustomObject]@{ SourcePath = $src DestRelative = $relLeaf SizeBytes = 0 SHA256 = '' Status = 'CopyError' Note = 'DestRelative contained .. segments; refused to prevent package-directory escape.' }) continue } if (-not $src -or -not (Test-Path -LiteralPath $src -PathType Leaf)) { $null = $rows.Add([PSCustomObject]@{ SourcePath = $src DestRelative = $relLeaf SizeBytes = 0 SHA256 = '' Status = 'Missing' Note = 'Source leaf not found. Visibility gap, not proof it never existed on the host.' }) continue } $len = [int64]0 try { $len = (Get-Item -LiteralPath $src -Force).Length } catch { } $skipNote = $null if ($copied -ge $MaxFiles) { $skipNote = "Capped at MaxFiles=$MaxFiles. Inventory CSVs still list names that were not copied." } elseif (($used + $len) -gt [int64]$MaxTotalBytes) { $skipNote = "Capped at MaxTotalBytes=$MaxTotalBytes. Inventory CSVs still list names that were not copied." } if ($skipNote) { $null = $rows.Add([PSCustomObject]@{ SourcePath = $src DestRelative = $relLeaf SizeBytes = $len SHA256 = '' Status = 'CappedSkip' Note = $skipNote }) continue } $dest = Join-Path $DestinationDirectory $relLeaf $destFull = $null try { $destFull = [IO.Path]::GetFullPath($dest) } catch { $destFull = $dest } $rootPrefix = $destRootFull.TrimEnd('\') + '\' if ($destRootFull -and $destFull -and -not ($destFull.Equals($destRootFull, [StringComparison]::OrdinalIgnoreCase)) -and -not $destFull.StartsWith($rootPrefix, [StringComparison]::OrdinalIgnoreCase)) { $null = $rows.Add([PSCustomObject]@{ SourcePath = $src DestRelative = $relLeaf SizeBytes = $len SHA256 = '' Status = 'CopyError' Note = 'Resolved destination left DestinationDirectory; copy refused.' }) continue } $copy = Copy-FotonHostFile -SourcePath $src -DestinationPath $dest $rel = $relLeaf if ($PackageRelativeDirectory) { $rel = Join-Path $PackageRelativeDirectory $relLeaf } $null = $rows.Add([PSCustomObject]@{ SourcePath = $copy.SourcePath DestRelative = $rel SizeBytes = $copy.SizeBytes SHA256 = $copy.SHA256 Status = $copy.Status Note = $copy.Note }) if ($copy.Status -eq 'Copied') { $copied++ $used = $used + [int64]$copy.SizeBytes } } return @($rows) } function Get-FotonSafePathSegment { param([string]$Name) if ([string]::IsNullOrWhiteSpace($Name)) { return 'unknown' } $s = [regex]::Replace($Name, '[\\/:*?"<>|]', '_') $s = $s.Trim() if (-not $s) { return 'unknown' } return $s } |