|
| 1 | +function Save-MaesterOffline { |
| 2 | + <# |
| 3 | + .SYNOPSIS |
| 4 | + Download local copies of Maester and its dependencies for use on systems that cannot access the PowerShell Gallery. |
| 5 | +
|
| 6 | + .DESCRIPTION |
| 7 | + This function downloads the Maester module and its dependencies to a specified directory, allowing for offline |
| 8 | + installation on systems without access to the PowerShell Gallery. Once downloaded the modules can be copied to |
| 9 | + the target system and installed using Install-Module with the -Path parameter. |
| 10 | +
|
| 11 | + .PARAMETER DestinationPath |
| 12 | + The directory to download and save the required PowerShell modules in. |
| 13 | +
|
| 14 | + .EXAMPLE |
| 15 | + Save-MaesterOffline -DestinationPath ~/Downloads/Maester |
| 16 | +
|
| 17 | + .NOTES |
| 18 | + Author: Sam Erde (@SamErde) |
| 19 | + Company: Sentinel Technologies, Inc |
| 20 | + Version: 1.0.0 |
| 21 | + Date: 2025-09-09 |
| 22 | +
|
| 23 | + #> |
| 24 | + [CmdletBinding()] |
| 25 | + param ( |
| 26 | + # Directory to download and save the required PowerShell modules in. |
| 27 | + [Parameter(HelpMessage = 'The path to an existing directory to download and save the required PowerShell modules in.')] |
| 28 | + [ValidateScript( { Test-Path -Path $_ -PathType Container -IsValid } )] |
| 29 | + [string] $DestinationPath = $PWD.Path, |
| 30 | + |
| 31 | + # Switch to create a ZIP file of the downloaded modules. |
| 32 | + [Parameter(HelpMessage = 'Create a ZIP file of the downloaded modules.')] |
| 33 | + [switch] $CreateZip |
| 34 | + ) |
| 35 | + |
| 36 | + # Ensure the destination path exists, or try to create it, if necessary. |
| 37 | + if (Test-Path -Path $DestinationPath -PathType Container) { |
| 38 | + Write-Verbose "Using existing directory: $DestinationPath" |
| 39 | + } else { |
| 40 | + try { |
| 41 | + New-Item -ItemType Directory -Path $DestinationPath -Force | Out-Null |
| 42 | + Write-Verbose "Created directory: $DestinationPath" |
| 43 | + } catch { |
| 44 | + Write-Error "Failed to create directory: $DestinationPath. Error: $_" |
| 45 | + return |
| 46 | + } |
| 47 | + } |
| 48 | + |
| 49 | + # Check if the Microsoft.PowerShell.PSResourceGet module is available and attempt to install it if necessary. If |
| 50 | + # the PSResourceGet module is not available, fall back to using Save-Module. |
| 51 | + $PSResourceGetInstalled = $false |
| 52 | + if ( (Get-Command -Name Save-PSResource -ErrorAction SilentlyContinue) ) { |
| 53 | + $PSResourceGetInstalled = $true |
| 54 | + } else { |
| 55 | + Write-Host "The 'Microsoft.PowerShell.PSResourceGet' module is not available. Attempting to install it from the PowerShell Gallery..." |
| 56 | + try { |
| 57 | + Install-Module -Name 'Microsoft.PowerShell.PSResourceGet' -Scope CurrentUser -Force -ErrorAction Stop |
| 58 | + Import-Module -Name 'Microsoft.PowerShell.PSResourceGet' -Force -ErrorAction Stop |
| 59 | + Write-Host "Successfully installed and imported the 'Microsoft.PowerShell.PSResourceGet' module." |
| 60 | + $PSResourceGetInstalled = $true |
| 61 | + } catch { |
| 62 | + Write-Error "Failed to install or import the 'Microsoft.PowerShell.PSResourceGet' module. Error: $_" |
| 63 | + return |
| 64 | + } |
| 65 | + } |
| 66 | + |
| 67 | + # List the required module names and versions. |
| 68 | + $RequiredModules = @( |
| 69 | + @{ |
| 70 | + Name = 'Pester' |
| 71 | + Prerelease = $false |
| 72 | + Version = [version]'5.7.1' |
| 73 | + }, |
| 74 | + @{ |
| 75 | + Name = 'Maester' |
| 76 | + Prerelease = $true |
| 77 | + Version = $null # Get the latest |
| 78 | + }, |
| 79 | + @{ |
| 80 | + Name = 'Az.Accounts' |
| 81 | + Prerelease = $false |
| 82 | + Version = $null # Get the latest |
| 83 | + }, |
| 84 | + @{ |
| 85 | + Name = 'ExchangeOnlineManagement' |
| 86 | + Prerelease = $false |
| 87 | + Version = $null # Get the latest |
| 88 | + }, |
| 89 | + @{ |
| 90 | + Name = 'Microsoft.Graph.Authentication' |
| 91 | + Prerelease = $false |
| 92 | + Version = $null # Get the latest. Just don't get 2.26.*! |
| 93 | + }, |
| 94 | + @{ |
| 95 | + Name = 'MicrosoftTeams' |
| 96 | + Prerelease = $false |
| 97 | + Version = $null # Get the latest |
| 98 | + } |
| 99 | + ) |
| 100 | + |
| 101 | + # Track installed modules. |
| 102 | + $InstalledModules = @() |
| 103 | + |
| 104 | + if ($PSResourceGetInstalled) { |
| 105 | + Write-Host "Using 'Save-PSResource' (Microsoft.PowerShell.PSResourceGet) to download modules.`n" -ForegroundColor White |
| 106 | + } else { |
| 107 | + Write-Host "Using 'Save-Module' (PowerShellGet) to download modules.`n" -ForegroundColor White |
| 108 | + } |
| 109 | + |
| 110 | + # Download the required modules into the DestinationPath. |
| 111 | + foreach ($Module in $RequiredModules) { |
| 112 | + $Name = $Module.Name |
| 113 | + $Version = $Module.Version |
| 114 | + $Prerelease = $Module.Prerelease |
| 115 | + |
| 116 | + try { |
| 117 | + Write-Host "Downloading module: $Name, Version: $Version, Prerelease: $Prerelease" -ForegroundColor Cyan |
| 118 | + if ($PSResourceGetInstalled) { |
| 119 | + #try { |
| 120 | + if ($Version) { |
| 121 | + Save-PSResource -Name $Name -Path $DestinationPath -Version $Version -Prerelease:$Prerelease -SkipDependencyCheck |
| 122 | + } else { |
| 123 | + Write-Verbose "Getting latest version" |
| 124 | + Save-PSResource -Name $Name -Path $DestinationPath -Prerelease:$Prerelease -SkipDependencyCheck |
| 125 | + } |
| 126 | + $InstalledModules += $Name |
| 127 | + Write-Host "Successfully downloaded module: $Name" -ForegroundColor Green |
| 128 | + } else { |
| 129 | + if ($Version) { |
| 130 | + Save-Module -Name $Name -Path $DestinationPath -MinimumVersion $Version -AllowPrerelease:$Prerelease |
| 131 | + } else { |
| 132 | + Save-Module -Name $Name -Path $DestinationPath -AllowPrerelease:$Prerelease |
| 133 | + } |
| 134 | + $InstalledModules += $Name |
| 135 | + Write-Host "Successfully downloaded module: $Name" -ForegroundColor Green |
| 136 | + } |
| 137 | + } catch { |
| 138 | + Write-Error "Failed to download module: $Name. Error: $_" |
| 139 | + } |
| 140 | + } |
| 141 | + |
| 142 | + # Summary of downloaded modules. |
| 143 | + if ($InstalledModules.Count -gt 0) { |
| 144 | + Write-Host "`nDownloaded modules to $DestinationPath`n" |
| 145 | + $InstalledModules | ForEach-Object -Process { Write-Host "`t$_" } -End "`n" |
| 146 | + } else { |
| 147 | + Write-Warning 'No modules were downloaded.' |
| 148 | + } |
| 149 | + |
| 150 | + # Create a ZIP file of the downloaded modules if requested. |
| 151 | + if ($CreateZip.IsPresent -and $InstalledModules.Count -gt 0) { |
| 152 | + $ZipPath = Join-Path -Path $DestinationPath -ChildPath 'MaesterModuleWithDependencies.zip' |
| 153 | + try { |
| 154 | + # Remove old ZIP file if it exists already. |
| 155 | + if (Test-Path -Path $ZipPath) { |
| 156 | + Remove-Item -Path $ZipPath -Force |
| 157 | + Write-Verbose "Removed existing ZIP file: $ZipPath" |
| 158 | + } |
| 159 | + Compress-Archive -Path (Join-Path -Path $DestinationPath -ChildPath '*') -DestinationPath $ZipPath -Force |
| 160 | + Write-Host "`nCreated ZIP file: $ZipPath" -ForegroundColor Green |
| 161 | + } catch { |
| 162 | + Write-Error "Failed to create ZIP file: $ZipPath. Error: $_" |
| 163 | + } |
| 164 | + } |
| 165 | +} |
0 commit comments