#Requires -Version 7.2 [CmdletBinding()] param ( [Parameter(Mandatory, Position=0, ValueFromPipeline)] [ValidateNotNullOrEmpty()] [string]$Inp, [Parameter(Position=1)] [string]$Out = $PWD.Path ) function Write-Log { param ( [ValidateSet("INF", "OK", "WRN", "ERR")] [string]$Level, [string]$Msg ) $time = [datetime]::Now.ToString("HH:mm:ss") $lbl = switch ($Level) { "INF" { "[INF]" } "OK" { "$($PSStyle.Foreground.Green)[OK] $($PSStyle.Reset)" } "WRN" { "$($PSStyle.Foreground.Yellow)[WRN]$($PSStyle.Reset)" } "ERR" { "$($PSStyle.Foreground.Red)[ERR]$($PSStyle.Reset)" } } Write-Host "$time $lbl $Msg" } $src = (Resolve-Path -LiteralPath $Inp -ErrorAction Stop).Path $dest = [System.IO.Path]::GetFullPath($Out) if (-not (Test-Path -LiteralPath $dest)) { $null = New-Item -ItemType Directory -Path $dest -Force } function Get-FileOutPath { param ([Uri]$Uri, [string]$Dir) $raw = [System.IO.Path]::GetFileName($Uri.LocalPath) $name = [string]::IsNullOrWhiteSpace($raw) ? [System.IO.Path]::GetRandomFileName() : $raw $invalid = [System.IO.Path]::GetInvalidFileNameChars() $safe = -join ($name.ToCharArray() | ForEach-Object { $_ -in $invalid ? "_" : $_ }) $base = [System.IO.Path]::GetFileNameWithoutExtension($safe) $ext = [System.IO.Path]::GetExtension($safe) $path = Join-Path $Dir $safe $i = 1 while (Test-Path -LiteralPath $path) { $path = Join-Path $Dir "$base ($i)$ext" $i++ } return $path } function Save-FileList { param ([string]$Path, [System.Collections.Generic.List[string]]$Data) [System.IO.File]::WriteAllLines($Path, $Data) } $lines = [System.Collections.Generic.List[string]]::new([System.IO.File]::ReadAllLines($src)) $stats = @{ Succ = 0; Fail = 0; Skip = 0 } $tmp = $null $abort = $false Write-Log INF "Reading $src" Write-Log INF "Downloading to $dest" try { $i = 0 while ($i -lt $lines.Count) { $line = $lines[$i].Trim() if ([string]::IsNullOrWhiteSpace($line) -or $line.StartsWith("#")) { $stats.Skip++ $i++ continue } [Uri]$uri = $null $valid = [Uri]::TryCreate($line, [System.UriKind]::Absolute, [ref]$uri) -and ($uri.Scheme -in "http", "https") if (-not $valid) { Write-Log ERR "Malformed URL on line $($i + 1): $line" $stats.Fail++ $i++ continue } $target = Get-FileOutPath -Uri $uri -Dir $dest $tmp = "$target.tmp" Write-Log INF "Downloading: $line" try { Invoke-WebRequest -Uri $uri.AbsoluteUri ` -OutFile $tmp ` -TimeoutSec 60 ` -ErrorAction Stop Move-Item -LiteralPath $tmp -Destination $target $tmp = $null $lines[$i] = "# $line" Save-FileList -Path $src -Data $lines Write-Log OK "Saved: $([System.IO.Path]::GetFileName($target))" $stats.Succ++ $i++ } catch { if ($tmp -and (Test-Path -LiteralPath $tmp)) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue $tmp = $null } $msg = ($_.Exception.Message -replace '\s+', " ").Trim() Write-Log ERR "'$line': $msg" Save-FileList -Path $src -Data $lines $stats.Fail++ $i++ } } } catch [System.Management.Automation.PipelineStoppedException], [System.Threading.ThreadAbortException] { $abort = $true } finally { if ($tmp -and (Test-Path -LiteralPath $tmp)) { Remove-Item -LiteralPath $tmp -Force -ErrorAction SilentlyContinue $tmp = $null } if ($abort) { Write-Log WRN "Download aborted (direct interrupt)" } Write-Log OK "" Write-Log OK "Summary" Write-Log OK "Success: $($stats.Succ)" Write-Log OK "Skipped: $($stats.Skip)" Write-Log OK "Failed: $($stats.Fail)" }