# This script uses code of Fido by Pete Batard: # # Copyright © 2019-2025 Pete Batard # Command line support: Copyright © 2021 flx5 # ConvertTo-ImageSource: Copyright © 2016 Chris Carter # # License of original Fido: GNU General Public License #region Parameters param( # (Optional) The title to display on the application window. [string]$AppTitle = "FidoArch - Archive.org ISO Downloader", # (Optional) '|' separated UI localization strings. [string]$LocData, # (Optional) Forced locale [string]$Locale = "en-US", # (Optional) Path to a file that should be used for the UI icon. [string]$Icon, # (Optional) Name of a pipe the download URL should be sent to. # If not provided, a browser window is opened instead. [string]$PipeName, # (Optional) Only display the download URL [Toggles commandline mode] [switch]$GetUrl = $false, # (Optional) Download with PowerShell [Toggles commandline mode] [switch]$InternalDownload = $false, # (Optional) Increase verbosity [switch]$Verbose = $false, # (Optional) Produce debugging information [switch]$Debug = $false ) #endregion try { [Console]::OutputEncoding = [System.Text.Encoding]::UTF8 } catch {} $Cmd = $false if ($InternalDownload -or $GetUrl -or $Debug) { $Cmd = $true } # Return a decimal Windows version that we can then check for platform support. # Note that because we don't want to have to support this script on anything # other than Windows, this call returns 0.0 for PowerShell running on Linux/Mac. function Get-Platform-Version() { $version = 0.0 $platform = [string][System.Environment]::OSVersion.Platform # This will filter out non Windows platforms if ($platform.StartsWith("Win")) { # Craft a decimal numeric version of Windows $version = [System.Environment]::OSVersion.Version.Major * 1.0 + [System.Environment]::OSVersion.Version.Minor * 0.1 } return $version } $winver = Get-Platform-Version # The default TLS for Windows 8.x doesn't work with Microsoft's servers so we must force it if ($winver -lt 10.0) { [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls -bor [Net.SecurityProtocolType]::Tls11 -bor [Net.SecurityProtocolType]::Tls12 } #region Assembly Types $Drawing_Assembly = "System.Drawing" # PowerShell 7 altered the name of the Drawing assembly... if ($host.version -ge "7.0") { $Drawing_Assembly += ".Common" } $Signature = @{ Namespace = "WinAPI" Name = "Utils" Language = "CSharp" UsingNamespace = "System.Runtime", "System.IO", "System.Text", "System.Drawing", "System.Globalization" ReferencedAssemblies = $Drawing_Assembly ErrorAction = "Stop" WarningAction = "Ignore" IgnoreWarnings = $true MemberDefinition = @" [DllImport("shell32.dll", CharSet = CharSet.Auto, SetLastError = true, BestFitMapping = false, ThrowOnUnmappableChar = true)] internal static extern int ExtractIconEx(string sFile, int iIndex, out IntPtr piLargeVersion, out IntPtr piSmallVersion, int amountIcons); [DllImport("user32.dll")] public static extern bool ShowWindow(IntPtr handle, int state); // Extract an icon from a DLL public static Icon ExtractIcon(string file, int number, bool largeIcon) { IntPtr large, small; ExtractIconEx(file, number, out large, out small, 1); try { return Icon.FromHandle(largeIcon ? large : small); } catch { return null; } } "@ } Write-Host Please Wait... if (!("WinAPI.Utils" -as [type])) { Add-Type @Signature } Add-Type -AssemblyName PresentationFramework if (!$Cmd) { # Hide the powershell window: https://stackoverflow.com/a/27992426/1069307 [WinAPI.Utils]::ShowWindow(([System.Diagnostics.Process]::GetCurrentProcess() | Get-Process).MainWindowHandle, 0) | Out-Null } #endregion #region Data $WindowsVersions = @( @( @("Windows 11"), @("25H2"), @("24H2"), @("23H2"), @("22H2"), @("21H2") ), @( @("Windows 10"), @("22H2"), @("21H2"), @("21H1"), @("20H2"), @("2004"), @("1909"), @("1903"), @("1809"), @("1803"), @("1709"), @("1703"), @("1607"), @("1511"), @("1507") ), @( @("Windows 8.1"), @("-"), @("Enterprise"), @("Pro"), @("Pro with Media Center") ), @( @("Windows 8"), @("-"), @("Enterprise"), @("Pro"), @("Pro with Media Center") ), @( @("Windows 7"), @("-"), @("Ultimate"), @("Enterprise"), @("Pro"), @("Home Premium"), @("Home Basic"), @("Starter"), @("SP1") ), @( @("Windows Vista"), @("-"), @("Ultimate"), @("Enterprise"), @("Pro"), @("Home Premium"), @("Home Basic"), @("SP1"), @("SP2") ), @( @("Windows XP"), @("-"), @("Pro"), @("Home"), @("Media Center"), @("Tablet PC"), @("SP1"), @("SP2"), @("SP3") ), @( @("Windows 2000"), @("-"), @("Pro"), @("Server"), @("Advanced Server") ), @( @("Windows NT"), @("4.0"), @("3.51"), @("3.5"), @("3.1") ), @( @("Windows ME"), @("-") ), @( @("Windows 98"), @("-") ), @( @("Windows 95"), @("-") ) ) #endregion #region Globals $ErrorActionPreference = "Stop" $DefaultTimeout = 30 $dh = 58 $Stage = 0 $SelectedIndex = 0 $ExitCode = 100 $Locale = $Locale $Verbosity = 1 if ($Debug) { $Verbosity = 5 } elseif ($Verbose) { $Verbosity = 2 } #endregion #region Localization $EnglishMessages = "en-US|Version|Release|Edition|Language|Architecture|Download|Continue|Back|Close|Cancel|Error|Please wait...|" + "Download using a browser|Download of Windows ISOs is unavailable due to Microsoft having altered their website to prevent it.|" + "PowerShell 3.0 or later is required to run this script.|Do you want to go online and download it?|" + "This feature is not available on this platform." [string[]]$English = $EnglishMessages.Split('|') [string[]]$Localized = $null if ($LocData -and !$LocData.StartsWith("en-US")) { $Localized = $LocData.Split('|') # Adjust the $Localized array if we have more or fewer strings than in $EnglishMessages if ($Localized.Length -lt $English.Length) { while ($Localized.Length -ne $English.Length) { $Localized += $English[$Localized.Length] } } elseif ($Localized.Length -gt $English.Length) { $Localized = $LocData.Split('|')[0..($English.Length - 1)] } $Locale = $Localized[0] } $QueryLocale = $Locale #endregion #region Functions # Convert a size in bytes to a human readable string function Size-To-Human-Readable([uint64]$size) { $suffix = "bytes", "KB", "MB", "GB", "TB", "PB", "EB", "ZB", "YB" $i = 0 while ($size -gt 1kb) { $size = $size / 1kb $i++ } "{0:N1} {1}" -f $size, $suffix[$i] } function Add-Entry([int]$pos, [string]$Name, [array]$Items, [string]$DisplayName) { $Title = New-Object System.Windows.Controls.TextBlock $Title.FontSize = $WindowsVersionTitle.FontSize $Title.Height = $WindowsVersionTitle.Height; $Title.Width = $WindowsVersionTitle.Width; $Title.HorizontalAlignment = "Left" $Title.VerticalAlignment = "Top" $Margin = $WindowsVersionTitle.Margin $Margin.Top += $pos * $dh $Title.Margin = $Margin $Title.Text = Get-Translation($Name) $XMLGrid.Children.Insert(2 * $Stage + 2, $Title) $Combo = New-Object System.Windows.Controls.ComboBox $Combo.FontSize = $WindowsVersion.FontSize $Combo.Height = $WindowsVersion.Height; $Combo.Width = $WindowsVersion.Width; $Combo.HorizontalAlignment = "Left" $Combo.VerticalAlignment = "Top" $Margin = $WindowsVersion.Margin $Margin.Top += $pos * $script:dh $Combo.Margin = $Margin $Combo.SelectedIndex = 0 if ($Items) { $Combo.ItemsSource = $Items if ($DisplayName) { $Combo.DisplayMemberPath = $DisplayName } else { $Combo.DisplayMemberPath = $Name } } $XMLGrid.Children.Insert(2 * $Stage + 3, $Combo) $XMLForm.Height += $dh; $Margin = $Continue.Margin $Margin.Top += $dh $Continue.Margin = $Margin $Margin = $Back.Margin $Margin.Top += $dh $Back.Margin = $Margin return $Combo } function Refresh-Control([object]$Control) { $Control.Dispatcher.Invoke("Render", [Windows.Input.InputEventHandler] { $Continue.UpdateLayout() }, $null, $null) | Out-Null } function Send-Message([string]$PipeName, [string]$Message) { [System.Text.Encoding]$Encoding = [System.Text.Encoding]::UTF8 $Pipe = New-Object -TypeName System.IO.Pipes.NamedPipeClientStream -ArgumentList ".", $PipeName, ([System.IO.Pipes.PipeDirection]::Out), ([System.IO.Pipes.PipeOptions]::None), ([System.Security.Principal.TokenImpersonationLevel]::Impersonation) try { $Pipe.Connect(1000) } catch { Write-Host $_.Exception.Message } $bRequest = $Encoding.GetBytes($Message) $cbRequest = $bRequest.Length; $Pipe.Write($bRequest, 0, $cbRequest); $Pipe.Dispose() } # From https://www.powershellgallery.com/packages/IconForGUI/1.5.2 # Copyright © 2016 Chris Carter. All rights reserved. # License: https://creativecommons.org/licenses/by-sa/4.0/ function ConvertTo-ImageSource { [CmdletBinding()] Param( [Parameter(Mandatory=$true,ValueFromPipeline=$true,ValueFromPipelineByPropertyName=$true)] [System.Drawing.Icon]$Icon ) Process { foreach ($i in $Icon) { [System.Windows.Interop.Imaging]::CreateBitmapSourceFromHIcon( $i.Handle, (New-Object System.Windows.Int32Rect -Args 0,0,$i.Width, $i.Height), [System.Windows.Media.Imaging.BitmapSizeOptions]::FromEmptyOptions() ) } } } # Translate a message string function Get-Translation([string]$Text) { if (!($English -contains $Text)) { Write-Host "Error: '$Text' is not a translatable string" return "(Untranslated)" } if ($Localized) { if ($Localized.Length -ne $English.Length) { Write-Host "Error: '$Text' is not a translatable string" } for ($i = 0; $i -lt $English.Length; $i++) { if ($English[$i] -eq $Text) { if ($Localized[$i]) { return $Localized[$i] } else { return $Text } } } } return $Text } # Return an array of releases (e.g. 20H2, 21H1, ...) for the selected Windows version function Get-Windows-Releases([int]$SelectedVersion) { $i = 0 $releases = @() foreach ($version in $WindowsVersions[$SelectedVersion]) { if (($i -ne 0) -and ($version -is [array])) { $releases += @(New-Object PsObject -Property @{ Release = $version[0]; Index = $i }) } $i++ } return $releases } # Return an array of editions (e.g. Home, Pro, etc) for the selected Windows release function Get-Windows-Editions([string]$SelectedVersion, [string]$SelectedRelease) { $editions = @() if ($SelectedRelease -like "-") { $win_flavor = $SelectedVersion } else { $win_flavor = "$($SelectedVersion) $($SelectedRelease)" } $win_flavor_esc = [URI]::EscapeUriString($win_flavor) $response = Invoke-WebRequest -Uri "https://archive.org/services/search/v1/scrape?q=%28%22$($win_flavor_esc)%22%29%20AND%20collection%3A%28cdromimages%20OR%20cdrom_contributions%29&fields=title" -UseBasicParsing $res = $response | ConvertFrom-Json foreach ($item in $res.items ) { if ($item.title.StartsWith($win_flavor)) { $editions += @(New-Object PsObject -Property @{ Edition = $item.title; Id = $item.identifier }) } elseif ($item.title.StartsWith("Microsoft $($win_flavor)")) { $editions += @(New-Object PsObject -Property @{ Edition = $item.title.Substring(10); Id = $item.identifier }) } } return $editions } # Download files.xml, extract .iso URL function Get-Iso-Url([string]$id) { # parsing content bytes in memory often fails for whatever reason, so we download to TMP instead $tmp_file = "$($env:TEMP)\fido-arch-tmp.xml" $response = Invoke-WebRequest -Uri "https://archive.org/download/$($id)/$($id)_files.xml" -OutFile $tmp_file [xml]$xml = Get-Content $tmp_file Remove-Item -Path $tmp_file foreach ($file in $xml.files.file) { if ($file.format -like "ISO Image") { $file_esc = [URI]::EscapeUriString($file.name) return "https://archive.org/download/$($id)/$($file_esc)" } } } # Process the download URL by either sending it through the pipe or by opening the browser function Process-Download-Link([string]$Url) { try { if ($PipeName -and !$Check.IsChecked) { Send-Message -PipeName $PipeName -Message $Url } else { if ($InternalDownload) { $pattern = '.*\/(.*\.iso).*' $File = [regex]::Match($Url, $pattern).Groups[1].Value # PowerShell implicit conversions are iffy, so we need to force them... $str_size = (Invoke-WebRequest -UseBasicParsing -TimeoutSec $DefaultTimeout -Uri $Url -Method Head).Headers.'Content-Length' $tmp_size = [uint64]::Parse($str_size) $Size = Size-To-Human-Readable $tmp_size Write-Host "Downloading '$File' ($Size)..." Start-BitsTransfer -Source $Url -Destination $File } else { Write-Host Download Link: $Url Start-Process -FilePath $Url } } } catch { Error($_.Exception.Message) return 404 } return 0 } #endregion #region Form [xml]$XAML = @"