From 71b1c70596f7a23e2e76bf4d3ab6b1f9fd7c112e Mon Sep 17 00:00:00 2001 From: Laurence Horrocks-Barlow Date: Thu, 21 May 2026 09:54:35 +0100 Subject: [PATCH] Initial commit --- organise-isos.ps1 | 389 ++++++++++++++++++++++++++++++++++++++++++++++ 1 file changed, 389 insertions(+) create mode 100644 organise-isos.ps1 diff --git a/organise-isos.ps1 b/organise-isos.ps1 new file mode 100644 index 0000000..d07e377 --- /dev/null +++ b/organise-isos.ps1 @@ -0,0 +1,389 @@ +# organise-isos.ps1 +# Usage: .\organise-isos.ps1 [-SourceDir ] [-DestDir ] [-DryRun] + +param( + [string]$SourceDir = ".", + [string]$DestDir = ".\iso-library", + [switch]$DryRun +) + +# ── Arch detection ───────────────────────────────────────────────────────────── +$archPatterns = @( + @{ Pattern = 'aarch64'; Canonical = 'aarch64' }, + @{ Pattern = 'arm64'; Canonical = 'aarch64' }, + @{ Pattern = 'arm(?!64)'; Canonical = 'arm' }, + @{ Pattern = 'x86[_-]64'; Canonical = 'x86_64' }, + @{ Pattern = 'amd64'; Canonical = 'x86_64' }, + @{ Pattern = '_x64(?=[_\.\-]|$)'; Canonical = 'x86_64' }, # Win11_x64 + @{ Pattern = '64bit'; Canonical = 'x86_64' }, + @{ Pattern = 'x86(?![_-]64)'; Canonical = 'x86' }, + @{ Pattern = 'i[3-6]86'; Canonical = 'x86' }, + @{ Pattern = '32bit'; Canonical = 'x86' }, + @{ Pattern = 'riscv64'; Canonical = 'riscv64' }, + @{ Pattern = 'riscv32'; Canonical = 'riscv32' }, + @{ Pattern = 'ppc64le'; Canonical = 'ppc64le' }, + @{ Pattern = 'ppc64'; Canonical = 'ppc64' }, + @{ Pattern = 'ppc'; Canonical = 'ppc' }, + @{ Pattern = 's390x'; Canonical = 's390x' }, + @{ Pattern = 'mips64'; Canonical = 'mips64' }, + @{ Pattern = 'mips(?!64)'; Canonical = 'mips' }, + @{ Pattern = 'loong64'; Canonical = 'loong64' }, + @{ Pattern = 'loongarch64'; Canonical = 'loong64' }, + @{ Pattern = 'ia64'; Canonical = 'ia64' }, + @{ Pattern = 'itanium'; Canonical = 'ia64' }, + @{ Pattern = 'sparc64'; Canonical = 'sparc64' }, + @{ Pattern = 'alpha'; Canonical = 'alpha' } +) + +# ── Edition/noise tokens to strip ────────────────────────────────────────────── +# NOTE: deliberately does NOT include 'memtest', 'rescue', 'live' as standalone +# OS names — those are caught by the OS map first, stripping only runs after +# the first-token OS match. +$noisePattern = '(?i)\b(live|server|desktop|dvd|netinstall|netinst|installer|' + + 'everything|boot|minimal|workstation|kde|gnome|xfce|lxde|mate|cinnamon|' + + 'budgie|standard|full|net|bd|cd|usb|dvd1|dvd2|disc\d*|disk\d*|' + + 'multiarch|hybrid|recovery|update|enterprise|community|' + + 'education|educational|studio|gaming|security|nonfree|non-free|' + + 'free|oss|lts|eol|stable|testing|sid|snapshot|nightly|beta|alpha|' + + 'rc\d*|sp\d*|u\d+|patch|hotfix|errata|image|' + + 'english|international|intl|en_us|en_gb)\b' + +# ── OS normalisation map ──────────────────────────────────────────────────────── +$osNormMap = [ordered]@{ + + # ── Windows family ────────────────────────────────────────────────────── + '^win(dows)?(svr|server)(2025|2022|2019|2016|2012|2008|2003)' = 'windows-server' + '^win(dows)?(svr|server)' = 'windows-server' + '^win(dows)?' = 'windows' + '^ms.?dos|^msdos' = 'ms-dos' + '^reactos' = 'reactos' + + # ── RHEL family ───────────────────────────────────────────────────────── + '^rhel|^redhat|^red.hat' = 'rhel' + '^centos.stream' = 'centos-stream' + '^centos' = 'centos' + '^rocky' = 'rocky' + '^alma' = 'almalinux' + '^oracle.linux|^oraclelinux' = 'oraclelinux' + '^scientific' = 'scientific' + '^eurolinux' = 'eurolinux' + '^springdale' = 'springdale' + '^clearos' = 'clearos' + '^navylinux|^navy.linux' = 'navylinux' + + # ── Fedora family ─────────────────────────────────────────────────────── + '^fedora' = 'fedora' + '^qubes' = 'qubes' + '^korora' = 'korora' + + # ── SUSE family ───────────────────────────────────────────────────────── + '^opensuse.leap' = 'opensuse-leap' + '^opensuse.tumbleweed' = 'opensuse-tumbleweed' + '^opensuse.microos' = 'opensuse-microos' + '^opensuse.kubic' = 'opensuse-kubic' + '^opensuse' = 'opensuse' + '^sles|^suse.linux.enterprise' = 'sles' + '^suse' = 'opensuse' + '^geckolinux' = 'geckolinux' + + # ── Debian family ─────────────────────────────────────────────────────── + '^debian' = 'debian' + '^ubuntu.mate' = 'ubuntu-mate' + '^ubuntu.budgie' = 'ubuntu-budgie' + '^ubuntu.kylin' = 'ubuntu-kylin' + '^ubuntu.studio' = 'ubuntu-studio' + '^ubuntu.unity' = 'ubuntu-unity' + '^lubuntu' = 'lubuntu' + '^kubuntu' = 'kubuntu' + '^xubuntu' = 'xubuntu' + '^ubuntu' = 'ubuntu' + '^linuxmint|^linux.mint|^mint' = 'linuxmint' + '^lmde' = 'lmde' + '^pop.os|^popos' = 'pop_os' + '^elementary' = 'elementary' + '^zorin' = 'zorin' + '^kali' = 'kali' + '^parrot' = 'parrot' + '^backbox' = 'backbox' + '^tails' = 'tails' + '^whonix' = 'whonix' + '^raspbian|^raspberry.pi.os' = 'raspios' + '^armbian' = 'armbian' + '^devuan' = 'devuan' + '^mx.linux|^mxlinux' = 'mx' + '^antix' = 'antix' + '^deepin' = 'deepin' + '^pureos' = 'pureos' + '^trisquel' = 'trisquel' + '^siduction' = 'siduction' + '^sparky' = 'sparkylinux' + '^bunsenlabs|^bunsen' = 'bunsenlabs' + '^crunchbang' = 'crunchbangpp' + '^bodhi' = 'bodhi' + '^q4os' = 'q4os' + '^netrunner' = 'netrunner' + '^kaisen' = 'kaisen' + '^grml' = 'grml' + '^libreelec' = 'libreelec' + '^osmc' = 'osmc' + + # ── Arch family ───────────────────────────────────────────────────────── + '^archlinux|^arch.linux' = 'arch' + '^arch(?!ive)' = 'arch' + '^manjaro' = 'manjaro' + '^endeavouros|^endeavour.os' = 'endeavouros' + '^garuda' = 'garuda' + '^artix' = 'artix' + '^parabola' = 'parabola' + '^hyperbola' = 'hyperbola' + '^blackarch' = 'blackarch' + '^archcraft' = 'archcraft' + '^arcolinux' = 'arcolinux' + '^cachyos' = 'cachyos' + '^crystal.linux' = 'crystallinux' + '^blendos' = 'blendos' + + # ── Gentoo family ─────────────────────────────────────────────────────── + '^gentoo' = 'gentoo' + '^calculate' = 'calculate' + '^sabayon' = 'sabayon' + '^funtoo' = 'funtoo' + '^chromeos|^chrome.os' = 'chromeos' + '^chromiumos|^chromium.os' = 'chromiumos' + '^fydeos' = 'fydeos' + + # ── Slackware family ──────────────────────────────────────────────────── + '^slackware' = 'slackware' + '^salix' = 'salix' + '^porteus' = 'porteus' + '^slax' = 'slax' + '^zenwalk' = 'zenwalk' + + # ── Independent / other Linux ──────────────────────────────────────────── + '^void' = 'void' + '^nixos|^nix(?=\d)' = 'nixos' + '^guix' = 'guix' + '^alpine' = 'alpine' + '^tinycore|^tiny.core' = 'tinycore' + '^puppy' = 'puppy' + '^slitaz' = 'slitaz' + '^dsl.linux|^damn.small' = 'dsl' + '^knoppix' = 'knoppix' + '^pclinuxos' = 'pclinuxos' + '^mageia' = 'mageia' + '^mandriva|^mandrake' = 'mandriva' + '^openmandriva' = 'openmandriva' + '^solus' = 'solus' + '^clear.linux' = 'clearlinux' + '^photon' = 'photon' + '^flatcar' = 'flatcar' + '^coreos|^fedora.coreos' = 'coreos' + '^bottlerocket' = 'bottlerocket' + '^talos' = 'talos' + '^kairos' = 'kairos' + '^rancheros' = 'rancheros' + '^rancher' = 'rancher' + '^k3os' = 'k3os' + '^openwrt' = 'openwrt' + '^ddwrt|^dd.wrt' = 'ddwrt' + '^pfsense' = 'pfsense' + '^opnsense' = 'opnsense' + '^ipfire' = 'ipfire' + '^endian' = 'endian' + '^untangle' = 'untangle' + '^smoothwall' = 'smoothwall' + '^zeroshell' = 'zeroshell' + '^vyos' = 'vyos' + '^astralinux|^astr.linux' = 'astralinux' + '^altlinux|^alt.linux' = 'altlinux' + '^redos|^red.os' = 'redos' + '^elbrus' = 'elbrus' + '^rosa' = 'rosa' + + # ── Hypervisor / virtualisation ────────────────────────────────────────── + '^proxmox.ve|^pve' = 'proxmox-ve' + '^proxmox.backup|^pbs' = 'proxmox-bs' + '^proxmox.mail|^pmg' = 'proxmox-mg' + '^proxmox' = 'proxmox-ve' + '^esxi|^vmware.esxi' = 'esxi' + '^vsphere' = 'vsphere' + '^vmware' = 'vmware' + '^xenserver|^citrix.hypervisor' = 'xenserver' + '^xcp.ng' = 'xcp-ng' + '^harvester' = 'harvester' + '^nutanix' = 'nutanix' + + # ── BSD family ────────────────────────────────────────────────────────── + '^freebsd' = 'freebsd' + '^openbsd' = 'openbsd' + '^netbsd' = 'netbsd' + '^dragonfly.bsd' = 'dragonflybsd' + '^truenas.scale' = 'truenas-scale' + '^truenas.core|^freenas' = 'truenas-core' + '^truenas' = 'truenas' + '^ghostbsd' = 'ghostbsd' + '^midnightbsd' = 'midnightbsd' + '^hardenedbsd' = 'hardenedbsd' + '^nomadbsd' = 'nomadbsd' + + # ── macOS / Darwin ─────────────────────────────────────────────────────── + '^macos|^mac.os|^osx' = 'macos' + '^darwin' = 'darwin' + + # ── Solaris / illumos ──────────────────────────────────────────────────── + '^opensolaris' = 'opensolaris' + '^openindiana' = 'openindiana' + '^omnios' = 'omnios' + '^tribblix' = 'tribblix' + '^solaris' = 'solaris' + '^smartos' = 'smartos' + + # ── Other Unix / exotic ────────────────────────────────────────────────── + '^haiku' = 'haiku' + '^freedos' = 'freedos' + '^plan9' = 'plan9' + '^minix' = 'minix' + '^morphos' = 'morphos' + '^amigaos' = 'amigaos' + '^aros' = 'aros' + '^serenityos' = 'serenityos' + '^redox' = 'redox' + '^kolibrios' = 'kolibrios' + '^menuetos' = 'menuetos' + '^syllable' = 'syllable' + '^genode' = 'genode' + '^fuchsia' = 'fuchsia' + + # ── Rescue / tools ─────────────────────────────────────────────────────── + '^clonezilla' = 'clonezilla' + '^systemrescue|^system.rescue' = 'systemrescue' + '^gparted' = 'gparted' + '^partedmagic' = 'partedmagic' + '^hirens|^hiren' = 'hirens' + '^ubcd|^ultimate.boot' = 'ubcd' + '^memtest86' = 'memtest86' + '^memtest' = 'memtest' + '^dban' = 'dban' + '^rescuezilla' = 'rescuezilla' + '^winpe' = 'winpe' + '^ipxe' = 'ipxe' + '^kaspersky' = 'kaspersky-rescue' + '^bitdefender' = 'bitdefender-rescue' + '^drweb' = 'drweb-rescue' + '^eset' = 'eset-rescue' +} + +function Get-IsoMeta { + param([string]$Filename) + + $base = [System.IO.Path]::GetFileNameWithoutExtension($Filename) + + # ── Arch (raw filename before any stripping) ─────────────────────────── + $arch = 'noarch' + foreach ($ap in $archPatterns) { + if ($base -match "(?i)$($ap.Pattern)") { + $arch = $ap.Canonical + break + } + } + + # ── Strip arch tokens and edition noise ──────────────────────────────── + $clean = $base + foreach ($ap in $archPatterns) { + $clean = $clean -replace "(?i)$($ap.Pattern)", '' + } + $clean = $clean -replace $noisePattern, '' + $clean = $clean -replace '[-_\.]{2,}', '-' + $clean = $clean.Trim('-_. ') + + # ── OS: split and take first token, cast to string explicitly ───────── + $segments = @($clean -split '[-_\.]' | Where-Object { $_ -ne '' }) + $firstToken = if ($segments.Count -gt 0) { [string]$segments[0] } else { 'unknown' } + $firstToken = $firstToken.ToLower() + + $os = 'unknown' + foreach ($pattern in $osNormMap.Keys) { + if ($firstToken -match $pattern) { + $os = $osNormMap[$pattern] + break + } + } + # Try first two tokens joined — catches "linux-mint", "pop-os" etc. + if ($os -eq 'unknown' -and $segments.Count -ge 2) { + $twoToken = ([string]$segments[0] + '-' + [string]$segments[1]).ToLower() + foreach ($pattern in $osNormMap.Keys) { + if ($twoToken -match $pattern) { + $os = $osNormMap[$pattern] + break + } + } + } + if ($os -eq 'unknown') { + $os = $firstToken -replace '[^a-z0-9\-]', '' + if (-not $os) { $os = 'unknown' } + } + + # ── Version ─────────────────────────────────────────────────────────── + # Note: version parsing uses $base (raw) for date stamps, $clean for + # everything else, to avoid stripping corrupting the match. + $version = 'unknown' + + # Windows H-series (25H2, 24H2 etc.) — run on raw base + if ($base -match '(?i)\b(\d{2}H[12])\b') { + $version = $matches[1].ToUpper() + } + # Full date stamp YYYYMMDD — extract just the year as version grouping + elseif ($base -match '\b(20\d{2})(\d{2})(\d{2})\b') { + $version = $matches[1] + } + # Semver / partial semver from cleaned string + elseif ($clean -match '(? " -NoNewline -ForegroundColor DarkGray + Write-Host "$($meta.OS)\$($meta.Version)\$($meta.Arch)" -ForegroundColor Cyan + + if (-not $DryRun) { + New-Item -ItemType Directory -Path $target -Force | Out-Null + Move-Item -Path $iso.FullName -Destination (Join-Path $target $iso.Name) + } + + $results += [PSCustomObject]@{ + File = $iso.Name + OS = $meta.OS + Version = $meta.Version + Arch = $meta.Arch + Target = "$($meta.OS)\$($meta.Version)\$($meta.Arch)" + } +} + +Write-Host "" +$results | Format-Table -AutoSize + +if (-not $DryRun) { + Write-Host "`nFinal structure:" + Get-ChildItem -Path $DestDir -Recurse -Filter "*.iso" | + ForEach-Object { $_.FullName.Replace("$((Resolve-Path $DestDir).Path)\", "") } | + Sort-Object +} \ No newline at end of file