ventoy-extras/organise-isos.ps1

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
}