hf2ollama-python-cli-tool 1.0.0__py3-none-any.whl

This diff represents the content of publicly available package versions that have been released to one of the supported registries. The information contained in this diff is provided for informational purposes only and reflects changes between package versions as they appear in their respective public registries.
@@ -0,0 +1,221 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Downloads a GGUF model file from HuggingFace using direct IP connection.
4
+
5
+ .DESCRIPTION
6
+ Alternative download method that connects to HuggingFace CDN servers
7
+ directly via IP address using PowerShell 7's .NET SslStream.
8
+ Useful when standard HTTPS downloads fail due to SSL or routing issues.
9
+
10
+ Requires PowerShell 7 (pwsh) and an elevated terminal.
11
+
12
+ .PARAMETER Repo
13
+ HuggingFace repo (e.g., "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF").
14
+
15
+ .PARAMETER File
16
+ Exact GGUF filename. If omitted, lists available files.
17
+
18
+ .PARAMETER OutDir
19
+ Output directory. Defaults to script's directory.
20
+
21
+ .EXAMPLE
22
+ pwsh -ExecutionPolicy Bypass -File HfDownload.ps1 "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF" "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"
23
+ #>
24
+ param(
25
+ [Parameter(Mandatory, Position=0)]
26
+ [string]$Repo,
27
+
28
+ [Parameter(Position=1)]
29
+ [string]$File,
30
+
31
+ [string]$OutDir
32
+ )
33
+
34
+ $ErrorActionPreference = 'Stop'
35
+ if (-not $OutDir) { $OutDir = $PSScriptRoot }
36
+
37
+ $US_EAST_IPS = @(
38
+ "35.173.17.142", "34.231.87.187", "44.217.206.136",
39
+ "3.216.102.62", "34.230.200.86", "100.50.185.240",
40
+ "100.57.83.12", "98.90.123.60", "100.31.16.245", "100.29.213.216"
41
+ )
42
+ $RouteExe = "$env:SystemRoot\System32\route.exe"
43
+
44
+ # --- Find Wi-Fi gateway ---
45
+
46
+ function Find-WifiGateway {
47
+ $output = & $RouteExe print -4 0.0.0.0 2>&1 | Out-String
48
+ $routes = @()
49
+ foreach ($line in $output -split "`n") {
50
+ $parts = $line.Trim() -split '\s+'
51
+ if ($parts.Count -ge 5 -and $parts[0] -eq '0.0.0.0' -and $parts[1] -eq '0.0.0.0' -and $parts[2] -ne 'On-link') {
52
+ $routes += @{ Gateway = $parts[2]; Metric = [int]$parts[4] }
53
+ }
54
+ }
55
+ if ($routes.Count -eq 0) { return $null }
56
+ $routes | Sort-Object { $_.Metric } -Descending | Select-Object -First 1 -ExpandProperty Gateway
57
+ }
58
+
59
+ function Find-WifiIfIndex {
60
+ $ipconfig = & "$env:SystemRoot\System32\ipconfig.exe" 2>&1 | Out-String
61
+ $inWifi = $false
62
+ foreach ($line in $ipconfig -split "`n") {
63
+ if ($line -match 'Wi-Fi|Wireless') { $inWifi = $true }
64
+ elseif ($inWifi -and $line -match '%(\d+)') { return [int]$Matches[1] }
65
+ elseif ($inWifi -and $line.Trim() -and -not $line.StartsWith(' ')) { $inWifi = $false }
66
+ }
67
+ return 0
68
+ }
69
+
70
+ # --- Route management ---
71
+
72
+ $script:routes = @()
73
+
74
+ function Add-Route([string]$IP, [string]$Gateway, [int]$IfIdx) {
75
+ $args2 = "add $IP mask 255.255.255.255 $Gateway"
76
+ if ($IfIdx -gt 0) { $args2 += " IF $IfIdx" }
77
+ $args2 += " metric 1"
78
+ & $RouteExe $args2.Split(' ') 2>&1 | Out-Null
79
+ $script:routes += $IP
80
+ }
81
+
82
+ function Remove-AllRoutes {
83
+ foreach ($ip in $script:routes) { & $RouteExe delete $ip 2>&1 | Out-Null }
84
+ $script:routes = @()
85
+ }
86
+
87
+ # --- Main ---
88
+
89
+ Write-Host "`n[1/3] Setup..." -ForegroundColor Cyan
90
+ $gateway = Find-WifiGateway
91
+ if (-not $gateway) { Write-Error "Cannot find Wi-Fi gateway. Ensure Wi-Fi is connected." }
92
+ $ifIdx = Find-WifiIfIndex
93
+ Write-Host " Gateway: $gateway (IF $ifIdx)"
94
+
95
+ # List files if not specified
96
+ if (-not $File) {
97
+ Write-Host "`n Fetching files from huggingface.co/$Repo..." -ForegroundColor DarkGray
98
+ $response = Invoke-RestMethod -Uri "https://huggingface.co/api/models/$Repo/tree/main" -TimeoutSec 30
99
+ $ggufFiles = @($response | Where-Object { $_.path -like '*.gguf' -and $_.type -eq 'file' } |
100
+ Select-Object @{N='Name';E={$_.path}}, @{N='SizeGB';E={[math]::Round($_.size/1GB,2)}} | Sort-Object SizeGB)
101
+ if ($ggufFiles.Count -eq 0) { Write-Error "No GGUF files in $Repo" }
102
+ Write-Host "`n Available:" -ForegroundColor Yellow
103
+ for ($i=0; $i -lt $ggufFiles.Count; $i++) {
104
+ $f = $ggufFiles[$i]
105
+ $lbl = if ($f.Name -match 'Q4_K_M') {' (recommended)'} else {''}
106
+ Write-Host " [$($i+1)] $($f.Name) ($($f.SizeGB) GB)$lbl"
107
+ }
108
+ $choice = Read-Host "`n Select [1-$($ggufFiles.Count)]"
109
+ $File = $ggufFiles[[int]$choice - 1].Name
110
+ }
111
+ Write-Host " File: $File" -ForegroundColor Green
112
+
113
+ # Get redirect URL
114
+ Write-Host "`n[2/3] Getting download URL..." -ForegroundColor Cyan
115
+ $hfUrl = "https://huggingface.co/$Repo/resolve/main/$File"
116
+ $redirectUrl = $null
117
+ try { Invoke-WebRequest -Uri $hfUrl -Method HEAD -MaximumRedirection 0 -ErrorAction Stop } catch {
118
+ $redirectUrl = $_.Exception.Response.Headers.Location.ToString()
119
+ }
120
+ if (-not $redirectUrl) { Write-Error "Failed to get redirect URL from HuggingFace." }
121
+ $cdnUri = [System.Uri]$redirectUrl
122
+ Write-Host " CDN: $($cdnUri.Host)"
123
+
124
+ # Build candidate IPs
125
+ $candidates = @()
126
+ if ($cdnUri.PathAndQuery -match 'xet-bridge-us') {
127
+ $candidates += $US_EAST_IPS
128
+ Write-Host " US bridge detected; using US East IPs" -ForegroundColor DarkGray
129
+ }
130
+ $dnsIPs = @((Resolve-DnsName $cdnUri.Host -Type A -ErrorAction SilentlyContinue).IPAddress)
131
+ foreach ($ip in $dnsIPs) { if ($ip -notin $candidates) { $candidates += $ip } }
132
+
133
+ # Add routes
134
+ foreach ($ip in $candidates) { Add-Route $ip $gateway $ifIdx }
135
+
136
+ # Download
137
+ Write-Host "`n[3/3] Downloading..." -ForegroundColor Cyan
138
+ $destPath = Join-Path $OutDir $File
139
+ $success = $false
140
+
141
+ try {
142
+ foreach ($ip in $candidates) {
143
+ try {
144
+ Write-Host " Trying $ip... " -NoNewline
145
+
146
+ $tcp = New-Object System.Net.Sockets.TcpClient
147
+ $tcp.ReceiveBufferSize = 8 * 1024 * 1024
148
+ $tcp.Connect($ip, 443)
149
+
150
+ $ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, ({$true}))
151
+ $ssl.AuthenticateAsClient($ip)
152
+
153
+ # Verify the cert is from the expected CDN, not a proxy
154
+ $issuer = $ssl.RemoteCertificate.Issuer
155
+ if ($issuer -notmatch 'Amazon|DigiCert|Let''s Encrypt|Google Trust|Cloudflare') {
156
+ Write-Host "unexpected cert issuer" -ForegroundColor Yellow
157
+ $ssl.Close(); $tcp.Close(); continue
158
+ }
159
+
160
+ # Send GET
161
+ $req = "GET $($cdnUri.PathAndQuery) HTTP/1.1`r`nHost: $($cdnUri.Host)`r`nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)`r`nAccept: */*`r`nConnection: keep-alive`r`n`r`n"
162
+ $ssl.Write([System.Text.Encoding]::ASCII.GetBytes($req)); $ssl.Flush()
163
+
164
+ # Read headers
165
+ $headerBytes = New-Object System.Collections.Generic.List[byte]
166
+ $prev = [byte[]]::new(4)
167
+ while ($true) {
168
+ $b = $ssl.ReadByte(); if ($b -eq -1) { throw "Connection closed" }
169
+ $headerBytes.Add([byte]$b)
170
+ $prev[0]=$prev[1];$prev[1]=$prev[2];$prev[2]=$prev[3];$prev[3]=[byte]$b
171
+ if ($prev[0]-eq13 -and $prev[1]-eq10 -and $prev[2]-eq13 -and $prev[3]-eq10) { break }
172
+ }
173
+ $hdr = [System.Text.Encoding]::ASCII.GetString($headerBytes.ToArray())
174
+ $status = ($hdr -split "`r`n")[0]
175
+
176
+ if ($status -notmatch '200') {
177
+ Write-Host $status -ForegroundColor Yellow
178
+ $ssl.Close(); $tcp.Close(); continue
179
+ }
180
+
181
+ $contentLength = [long]0
182
+ if ($hdr -match 'content-length:\s*(\d+)') { $contentLength = [long]$Matches[1] }
183
+ Write-Host "200 OK ($([math]::Round($contentLength/1GB,2)) GB)" -ForegroundColor Green
184
+ Write-Host ""
185
+
186
+ # Stream to file
187
+ $fs = [System.IO.File]::Create($destPath)
188
+ $buf = New-Object byte[] (4*1024*1024)
189
+ $total = [long]0; $sw = [System.Diagnostics.Stopwatch]::StartNew(); $lr = [long]0
190
+
191
+ try {
192
+ while ($total -lt $contentLength) {
193
+ $n = [int][math]::Min([long]$buf.Length, [long]($contentLength - $total))
194
+ $read = $ssl.Read($buf, 0, $n); if ($read -eq 0) { break }
195
+ $fs.Write($buf, 0, $read); $total += $read
196
+ if ($total - $lr -gt 100MB) {
197
+ $pct = [math]::Round($total*100/$contentLength,1)
198
+ $spd = if($sw.Elapsed.TotalSeconds -gt 0){[math]::Round($total/$sw.Elapsed.TotalSeconds/1MB,1)}else{0}
199
+ $eta = if($spd -gt 0){$r=($contentLength-$total)/($spd*1MB);if($r -gt 60){"$([math]::Round($r/60,1))m"}else{"$([math]::Round($r))s"}}else{'?'}
200
+ Write-Host " [$pct%] $([math]::Round($total/1GB,2))GB @ ${spd}MB/s ETA:$eta"
201
+ $lr = $total
202
+ }
203
+ }
204
+ Write-Host " [100%] Done! $([math]::Round($total/1GB,2)) GB in $([math]::Round($sw.Elapsed.TotalMinutes,1)) min" -ForegroundColor Green
205
+ } finally { $fs.Close(); $ssl.Close(); $tcp.Close() }
206
+
207
+ if ($total -ge $contentLength * 0.99) { $success = $true }
208
+ else { Write-Warning "Incomplete: $total / $contentLength bytes" }
209
+ break
210
+ }
211
+ catch {
212
+ Write-Host "FAILED ($($_.Exception.Message))" -ForegroundColor Red
213
+ continue
214
+ }
215
+ }
216
+ } finally {
217
+ Remove-AllRoutes
218
+ }
219
+
220
+ if (-not $success) { Write-Error "All $($candidates.Count) CDN IPs failed." }
221
+ Write-Host "`n Saved: $destPath" -ForegroundColor Green
@@ -0,0 +1 @@
1
+ """Utility modules - VRAM detection and display formatting."""
@@ -0,0 +1,65 @@
1
+ """VRAM detection and quantization recommendation."""
2
+
3
+ from __future__ import annotations
4
+
5
+ import subprocess
6
+ from dataclasses import dataclass
7
+
8
+
9
+ QUANT_MULTIPLIERS = {
10
+ "Q2_K": 0.31,
11
+ "Q3_K_M": 0.44,
12
+ "Q4_K_M": 0.56,
13
+ "Q5_K_M": 0.68,
14
+ "Q6_K": 0.81,
15
+ "Q8_0": 1.0,
16
+ }
17
+
18
+
19
+ @dataclass
20
+ class VRAMInfo:
21
+ """GPU VRAM information from nvidia-smi."""
22
+
23
+ available_gb: float
24
+ gpu_name: str = "Unknown"
25
+
26
+
27
+ def detect_vram() -> VRAMInfo | None:
28
+ """Detect available GPU VRAM using nvidia-smi."""
29
+ try:
30
+ result = subprocess.run(
31
+ ["nvidia-smi", "--query-gpu=memory.total,name", "--format=csv,noheader,nounits"],
32
+ capture_output=True,
33
+ text=True,
34
+ timeout=10,
35
+ )
36
+ if result.returncode == 0:
37
+ line = result.stdout.strip().splitlines()[0]
38
+ parts = line.split(",")
39
+ vram_mb = float(parts[0].strip())
40
+ name = parts[1].strip() if len(parts) > 1 else "NVIDIA GPU"
41
+ return VRAMInfo(available_gb=vram_mb / 1024, gpu_name=name)
42
+ except (FileNotFoundError, subprocess.TimeoutExpired):
43
+ pass
44
+
45
+ return None
46
+
47
+
48
+ def recommend_quant(model_params_b: float, vram_gb: float) -> list[str]:
49
+ """Recommend quantizations that fit in the given VRAM.
50
+
51
+ Args:
52
+ model_params_b: Model parameter count in billions (e.g., 7.0 for 7B).
53
+ vram_gb: Available VRAM in GB.
54
+
55
+ Returns:
56
+ List of quantization names that should fit, from smallest to largest.
57
+
58
+ """
59
+ fits = []
60
+ for quant, multiplier in sorted(QUANT_MULTIPLIERS.items(), key=lambda x: x[1]):
61
+ estimated_gb = model_params_b * multiplier * 1.1
62
+ if estimated_gb <= vram_gb:
63
+ fits.append(quant)
64
+
65
+ return fits
@@ -0,0 +1,221 @@
1
+ <#
2
+ .SYNOPSIS
3
+ Downloads a GGUF model file from HuggingFace using direct IP connection.
4
+
5
+ .DESCRIPTION
6
+ Alternative download method that connects to HuggingFace CDN servers
7
+ directly via IP address using PowerShell 7's .NET SslStream.
8
+ Useful when standard HTTPS downloads fail due to SSL or routing issues.
9
+
10
+ Requires PowerShell 7 (pwsh) and an elevated terminal.
11
+
12
+ .PARAMETER Repo
13
+ HuggingFace repo (e.g., "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF").
14
+
15
+ .PARAMETER File
16
+ Exact GGUF filename. If omitted, lists available files.
17
+
18
+ .PARAMETER OutDir
19
+ Output directory. Defaults to script's directory.
20
+
21
+ .EXAMPLE
22
+ pwsh -ExecutionPolicy Bypass -File HfDownload.ps1 "bartowski/Qwen2.5-Coder-7B-Instruct-GGUF" "Qwen2.5-Coder-7B-Instruct-Q4_K_M.gguf"
23
+ #>
24
+ param(
25
+ [Parameter(Mandatory, Position=0)]
26
+ [string]$Repo,
27
+
28
+ [Parameter(Position=1)]
29
+ [string]$File,
30
+
31
+ [string]$OutDir
32
+ )
33
+
34
+ $ErrorActionPreference = 'Stop'
35
+ if (-not $OutDir) { $OutDir = $PSScriptRoot }
36
+
37
+ $US_EAST_IPS = @(
38
+ "35.173.17.142", "34.231.87.187", "44.217.206.136",
39
+ "3.216.102.62", "34.230.200.86", "100.50.185.240",
40
+ "100.57.83.12", "98.90.123.60", "100.31.16.245", "100.29.213.216"
41
+ )
42
+ $RouteExe = "$env:SystemRoot\System32\route.exe"
43
+
44
+ # --- Find Wi-Fi gateway ---
45
+
46
+ function Find-WifiGateway {
47
+ $output = & $RouteExe print -4 0.0.0.0 2>&1 | Out-String
48
+ $routes = @()
49
+ foreach ($line in $output -split "`n") {
50
+ $parts = $line.Trim() -split '\s+'
51
+ if ($parts.Count -ge 5 -and $parts[0] -eq '0.0.0.0' -and $parts[1] -eq '0.0.0.0' -and $parts[2] -ne 'On-link') {
52
+ $routes += @{ Gateway = $parts[2]; Metric = [int]$parts[4] }
53
+ }
54
+ }
55
+ if ($routes.Count -eq 0) { return $null }
56
+ $routes | Sort-Object { $_.Metric } -Descending | Select-Object -First 1 -ExpandProperty Gateway
57
+ }
58
+
59
+ function Find-WifiIfIndex {
60
+ $ipconfig = & "$env:SystemRoot\System32\ipconfig.exe" 2>&1 | Out-String
61
+ $inWifi = $false
62
+ foreach ($line in $ipconfig -split "`n") {
63
+ if ($line -match 'Wi-Fi|Wireless') { $inWifi = $true }
64
+ elseif ($inWifi -and $line -match '%(\d+)') { return [int]$Matches[1] }
65
+ elseif ($inWifi -and $line.Trim() -and -not $line.StartsWith(' ')) { $inWifi = $false }
66
+ }
67
+ return 0
68
+ }
69
+
70
+ # --- Route management ---
71
+
72
+ $script:routes = @()
73
+
74
+ function Add-Route([string]$IP, [string]$Gateway, [int]$IfIdx) {
75
+ $args2 = "add $IP mask 255.255.255.255 $Gateway"
76
+ if ($IfIdx -gt 0) { $args2 += " IF $IfIdx" }
77
+ $args2 += " metric 1"
78
+ & $RouteExe $args2.Split(' ') 2>&1 | Out-Null
79
+ $script:routes += $IP
80
+ }
81
+
82
+ function Remove-AllRoutes {
83
+ foreach ($ip in $script:routes) { & $RouteExe delete $ip 2>&1 | Out-Null }
84
+ $script:routes = @()
85
+ }
86
+
87
+ # --- Main ---
88
+
89
+ Write-Host "`n[1/3] Setup..." -ForegroundColor Cyan
90
+ $gateway = Find-WifiGateway
91
+ if (-not $gateway) { Write-Error "Cannot find Wi-Fi gateway. Ensure Wi-Fi is connected." }
92
+ $ifIdx = Find-WifiIfIndex
93
+ Write-Host " Gateway: $gateway (IF $ifIdx)"
94
+
95
+ # List files if not specified
96
+ if (-not $File) {
97
+ Write-Host "`n Fetching files from huggingface.co/$Repo..." -ForegroundColor DarkGray
98
+ $response = Invoke-RestMethod -Uri "https://huggingface.co/api/models/$Repo/tree/main" -TimeoutSec 30
99
+ $ggufFiles = @($response | Where-Object { $_.path -like '*.gguf' -and $_.type -eq 'file' } |
100
+ Select-Object @{N='Name';E={$_.path}}, @{N='SizeGB';E={[math]::Round($_.size/1GB,2)}} | Sort-Object SizeGB)
101
+ if ($ggufFiles.Count -eq 0) { Write-Error "No GGUF files in $Repo" }
102
+ Write-Host "`n Available:" -ForegroundColor Yellow
103
+ for ($i=0; $i -lt $ggufFiles.Count; $i++) {
104
+ $f = $ggufFiles[$i]
105
+ $lbl = if ($f.Name -match 'Q4_K_M') {' (recommended)'} else {''}
106
+ Write-Host " [$($i+1)] $($f.Name) ($($f.SizeGB) GB)$lbl"
107
+ }
108
+ $choice = Read-Host "`n Select [1-$($ggufFiles.Count)]"
109
+ $File = $ggufFiles[[int]$choice - 1].Name
110
+ }
111
+ Write-Host " File: $File" -ForegroundColor Green
112
+
113
+ # Get redirect URL
114
+ Write-Host "`n[2/3] Getting download URL..." -ForegroundColor Cyan
115
+ $hfUrl = "https://huggingface.co/$Repo/resolve/main/$File"
116
+ $redirectUrl = $null
117
+ try { Invoke-WebRequest -Uri $hfUrl -Method HEAD -MaximumRedirection 0 -ErrorAction Stop } catch {
118
+ $redirectUrl = $_.Exception.Response.Headers.Location.ToString()
119
+ }
120
+ if (-not $redirectUrl) { Write-Error "Failed to get redirect URL from HuggingFace." }
121
+ $cdnUri = [System.Uri]$redirectUrl
122
+ Write-Host " CDN: $($cdnUri.Host)"
123
+
124
+ # Build candidate IPs
125
+ $candidates = @()
126
+ if ($cdnUri.PathAndQuery -match 'xet-bridge-us') {
127
+ $candidates += $US_EAST_IPS
128
+ Write-Host " US bridge detected; using US East IPs" -ForegroundColor DarkGray
129
+ }
130
+ $dnsIPs = @((Resolve-DnsName $cdnUri.Host -Type A -ErrorAction SilentlyContinue).IPAddress)
131
+ foreach ($ip in $dnsIPs) { if ($ip -notin $candidates) { $candidates += $ip } }
132
+
133
+ # Add routes
134
+ foreach ($ip in $candidates) { Add-Route $ip $gateway $ifIdx }
135
+
136
+ # Download
137
+ Write-Host "`n[3/3] Downloading..." -ForegroundColor Cyan
138
+ $destPath = Join-Path $OutDir $File
139
+ $success = $false
140
+
141
+ try {
142
+ foreach ($ip in $candidates) {
143
+ try {
144
+ Write-Host " Trying $ip... " -NoNewline
145
+
146
+ $tcp = New-Object System.Net.Sockets.TcpClient
147
+ $tcp.ReceiveBufferSize = 8 * 1024 * 1024
148
+ $tcp.Connect($ip, 443)
149
+
150
+ $ssl = New-Object System.Net.Security.SslStream($tcp.GetStream(), $false, ({$true}))
151
+ $ssl.AuthenticateAsClient($ip)
152
+
153
+ # Verify the cert is from the expected CDN, not a proxy
154
+ $issuer = $ssl.RemoteCertificate.Issuer
155
+ if ($issuer -notmatch 'Amazon|DigiCert|Let''s Encrypt|Google Trust|Cloudflare') {
156
+ Write-Host "unexpected cert issuer" -ForegroundColor Yellow
157
+ $ssl.Close(); $tcp.Close(); continue
158
+ }
159
+
160
+ # Send GET
161
+ $req = "GET $($cdnUri.PathAndQuery) HTTP/1.1`r`nHost: $($cdnUri.Host)`r`nUser-Agent: Mozilla/5.0 (Windows NT 10.0; Win64; x64)`r`nAccept: */*`r`nConnection: keep-alive`r`n`r`n"
162
+ $ssl.Write([System.Text.Encoding]::ASCII.GetBytes($req)); $ssl.Flush()
163
+
164
+ # Read headers
165
+ $headerBytes = New-Object System.Collections.Generic.List[byte]
166
+ $prev = [byte[]]::new(4)
167
+ while ($true) {
168
+ $b = $ssl.ReadByte(); if ($b -eq -1) { throw "Connection closed" }
169
+ $headerBytes.Add([byte]$b)
170
+ $prev[0]=$prev[1];$prev[1]=$prev[2];$prev[2]=$prev[3];$prev[3]=[byte]$b
171
+ if ($prev[0]-eq13 -and $prev[1]-eq10 -and $prev[2]-eq13 -and $prev[3]-eq10) { break }
172
+ }
173
+ $hdr = [System.Text.Encoding]::ASCII.GetString($headerBytes.ToArray())
174
+ $status = ($hdr -split "`r`n")[0]
175
+
176
+ if ($status -notmatch '200') {
177
+ Write-Host $status -ForegroundColor Yellow
178
+ $ssl.Close(); $tcp.Close(); continue
179
+ }
180
+
181
+ $contentLength = [long]0
182
+ if ($hdr -match 'content-length:\s*(\d+)') { $contentLength = [long]$Matches[1] }
183
+ Write-Host "200 OK ($([math]::Round($contentLength/1GB,2)) GB)" -ForegroundColor Green
184
+ Write-Host ""
185
+
186
+ # Stream to file
187
+ $fs = [System.IO.File]::Create($destPath)
188
+ $buf = New-Object byte[] (4*1024*1024)
189
+ $total = [long]0; $sw = [System.Diagnostics.Stopwatch]::StartNew(); $lr = [long]0
190
+
191
+ try {
192
+ while ($total -lt $contentLength) {
193
+ $n = [int][math]::Min([long]$buf.Length, [long]($contentLength - $total))
194
+ $read = $ssl.Read($buf, 0, $n); if ($read -eq 0) { break }
195
+ $fs.Write($buf, 0, $read); $total += $read
196
+ if ($total - $lr -gt 100MB) {
197
+ $pct = [math]::Round($total*100/$contentLength,1)
198
+ $spd = if($sw.Elapsed.TotalSeconds -gt 0){[math]::Round($total/$sw.Elapsed.TotalSeconds/1MB,1)}else{0}
199
+ $eta = if($spd -gt 0){$r=($contentLength-$total)/($spd*1MB);if($r -gt 60){"$([math]::Round($r/60,1))m"}else{"$([math]::Round($r))s"}}else{'?'}
200
+ Write-Host " [$pct%] $([math]::Round($total/1GB,2))GB @ ${spd}MB/s ETA:$eta"
201
+ $lr = $total
202
+ }
203
+ }
204
+ Write-Host " [100%] Done! $([math]::Round($total/1GB,2)) GB in $([math]::Round($sw.Elapsed.TotalMinutes,1)) min" -ForegroundColor Green
205
+ } finally { $fs.Close(); $ssl.Close(); $tcp.Close() }
206
+
207
+ if ($total -ge $contentLength * 0.99) { $success = $true }
208
+ else { Write-Warning "Incomplete: $total / $contentLength bytes" }
209
+ break
210
+ }
211
+ catch {
212
+ Write-Host "FAILED ($($_.Exception.Message))" -ForegroundColor Red
213
+ continue
214
+ }
215
+ }
216
+ } finally {
217
+ Remove-AllRoutes
218
+ }
219
+
220
+ if (-not $success) { Write-Error "All $($candidates.Count) CDN IPs failed." }
221
+ Write-Host "`n Saved: $destPath" -ForegroundColor Green