389 lines
No EOL
25 KiB
PowerShell
389 lines
No EOL
25 KiB
PowerShell
# organise-isos.ps1
|
|
# Usage: .\organise-isos.ps1 [-SourceDir <path>] [-DestDir <path>] [-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 '(?<![a-zA-Z])(\d+\.\d+(\.\d+)?)') {
|
|
$parts = $matches[1] -split '\.'
|
|
$version = if ($parts.Count -ge 3) { "$($parts[0]).$($parts[1])" } else { $matches[1] }
|
|
}
|
|
# Bare integer
|
|
elseif ($clean -match '(?<![a-zA-Z])(\d{1,3})(?![a-zA-Z0-9\._])') {
|
|
$version = $matches[1]
|
|
}
|
|
|
|
return [PSCustomObject]@{ OS = $os; Version = $version; Arch = $arch }
|
|
}
|
|
|
|
# ── Main ──────────────────────────────────────────────────────────────────────
|
|
$isos = Get-ChildItem -Path $SourceDir -Filter "*.iso"
|
|
|
|
if ($isos.Count -eq 0) { Write-Error "No ISO files found in $SourceDir"; exit 1 }
|
|
if ($DryRun) { Write-Host "[DRY RUN] No files will be moved`n" -ForegroundColor Yellow }
|
|
|
|
$results = @()
|
|
foreach ($iso in $isos) {
|
|
$meta = Get-IsoMeta $iso.Name
|
|
$target = Join-Path $DestDir "$($meta.OS)\$($meta.Version)\$($meta.Arch)"
|
|
|
|
Write-Host $iso.Name -NoNewline
|
|
Write-Host " -> " -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
|
|
} |