JT.AppxDownloader.psm1
#Requires -Modules @{ ModuleName='JT.WriteLog'; ModuleVersion='2.0.0.3' } <# JT.AppxDownloader : AxD #> $_setting = @{ 'DebugMode' = $false } $PSDefaultParameterValues['Write-Log:LengthOfFunctionName'] = 24 $PSDefaultParameterValues['Write-Log:LogFile'] = '' $PSDefaultParameterValues['Write-Log:DebugMode'] = $_setting.DebugMode # ------------------------------------------------------------ # MARK: Get appx list from cache # Date: 2025.03.02 # ------------------------------------------------------------ function Get-AxDCachedList { Return $Script:AppxPackageList } If ($_setting.DebugMode) { Export-ModuleMember -Function Get-AxDCachedList } # ------------------------------------------------------------ # MARK: Get Appx offline files list # Date: 2025.03.02 # ------------------------------------------------------------ function Get-AxDOfflineFileList { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Path ) begin { } process { If (Test-Path $Path) { $_appxFileList = @{} Write-Log -ErrorLevel DEBUG -Message "Collect Appx files list from '$($Path)' folder" $packageExtension = @('*.appx*', '*.msix*') foreach ($extension in $packageExtension) { Write-Log -ErrorLevel DEBUG -Message "> Collecting extension files [$($extension)]" foreach ($packageFile in (Get-ChildItem -Path $Path -Filter $extension -File)) { $fileInfo = $packageFile.Name.Split('_') If ($fileInfo.Count -gt 2) { Write-Log -ErrorLevel DEBUG -Message " - $($fileInfo[0]):$($fileInfo[1])" $_appxFileList[$fileInfo[0]] = @{'Version'=[version]$fileInfo[1]; 'Filename'=$packageFile.Name} } Else { Write-Log -ErrorLevel WARNING -Message " - Filename format incorrect: $($packageFile.Name)" } } } Write-Log -ErrorLevel DEBUG -Message "> Total $($_appxFileList.Count) files found." Return $_appxFileList } Else { Write-Log -ErrorLevel ERROR -Message "The network path '$($Path)' does not exist" Return $null } } end { } } If ($_setting.DebugMode) { Export-ModuleMember -Function Get-AxDOfflineFileList} # ------------------------------------------------------------ # MARK: Get the package list from Internet (using Microsoft Apps 'ProductID') # Date: 2025.03.02 # ------------------------------------------------------------ function Get-AxDPackageList { <# .SYNOPSIS Get-AxDPackageList is a tool for get the package list (by ProductID) .PARAMETER ProductID ProductID .EXAMPLE Get-AxDPackageList -ProductID '9MSMLRH6LZF3' Get the package list for Microsoft Notepad (https://www.microsoft.com/store/productId/9MSMLRH6LZF3) #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$ProductID, [Parameter(DontShow = $true)] [string]$_counter ) begin { } process { Write-Log -ErrorLevel INFO -Message "($($_counter)) Get packing file list from ProductID: $($ProductID)".Replace('() ', '') $_PackageURL = "https://www.microsoft.com/store/productId/" + $ProductID Write-Log -ErrorLevel DEBUG -Message "> URL: $($_PackageURL)" $_retryCount = 0 # Download package list by (Microsoft) ProductID Do { Write-Log -ErrorLevel DEBUG -Message "> Download the package list from the Internet (Retry:$($_retryCount)) .....".Replace('(Retry:0) ', '') $WebResponse = Invoke-WebRequest -UseBasicParsing -Method 'POST' -Uri 'https://store.rg-adguard.net/api/GetFiles' -Body "type=url&url=$($_PackageURL)&ring=Retail" -ContentType 'application/x-www-form-urlencoded' -ErrorAction SilentlyContinue -ErrorVariable WebError If ($WebResponse) { #$MatchLinks = @($WebResponse.Links | Where-Object {($_ -like '*.appx*') -or ($_ -like '*.msix*')} | Where-Object {$_ -like '*_neutral_*' -or $_ -like "*_"+$env:PROCESSOR_ARCHITECTURE.Replace("AMD","X").Replace("IA","X")+"_*"}) $MatchLinks = @($WebResponse.Links | Where-Object {($_ -like '*.appx*') -or ($_ -like '*.msix*')} | Where-Object {$_ -like '*_neutral_*' -or $_ -like "*_x64_*"}) } ElseIf ($webError) { Write-Log -ErrorLevel WARNING -Message "$($WebError.Expection.Message)" } Else { Write-Log -ErrorLevel ERROR -Message "Unknow error!!" Return $null } } Until (($MatchLinks.Count -gt 0) -or ($_retryCount -gt 2)) If ($MatchLinks.Count -gt 0) { Write-Log -ErrorLevel DEBUG -Message "> Total ($($MatchLinks.Count)) links found." } Else { Return $null } # Extract link from downloaded package list $_PackageList = @() foreach ($_link in $MatchLinks) { $_PackageInfo = [PSCustomObject]@{ Filename = ($_link | Select-String -Pattern '(?<=rel="noreferrer">).+(?=</a>)').Matches.Value Url = ($_link | Select-String -Pattern '(?<=a href=").+(?=" r)').Matches.Value Name = '' Version = '' } $_PackageInfo.Name = $_PackageInfo.Filename.Split('_')[0] $_PackageInfo.Version = $_PackageInfo.Filename.Split('_')[1] Write-Log -ErrorLevel DEBUG -Message " - Name:$($_PackageInfo.Name); Version:$($_PackageInfo.Version)" $_PackageList += $_PackageInfo } Return $_PackageList } end { } } Export-ModuleMember -Function Get-AxDPackageList # ------------------------------------------------------------ # MARK: Collect Appx files list # Date: 2025.03.02 # ------------------------------------------------------------ function Invoke-AxDFileDownload { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Uri, [Parameter(Mandatory = $true)] [string]$Destination, [Parameter(DontShow = $true)] [string]$_counter = '' ) begin { If (Test-Path (Split-Path -Path $Destination -Parent)) { If (Test-Path $Destination) { Write-Log -ErrorLevel WARNING -Message "The file '$($Destination)' was exist." $_temp = $Destination.Split('.') $_checkCount = 0 If ($_temp.Count -le 2) { $_basename = $Destination $_ext = '' } Else { $_basename = $_temp[0..$($_temp.Count - 2)] $_ext = $($_temp[-1]) } Do { $_checkCount++ $Destination = "$($_basename) ($($_checkCount)).$($_ext)" } Until (-not (Test-Path $Destination)) Write-Log -ErrorLevel WARNING -Message "> The destination file was renamd to '$($Destination)'." } } Else { Write-Log -ErrorLevel ERROR -Message "The destination path '$(Split-Path -Path $Destination -Parent)' does not exist." Return $null } } process { Write-Log -ErrorLevel DEBUG -Message "(0)Downloading file $(Split-Path $Destination -Leaf) .....".Replace('(0)', $_counter) $_downloader = New-Object System.Net.WebClient $_downloader.DownloadFileAsync($Uri, $Destination) $_StartTime = Get-Date $_baseFilename = Split-Path -Path $Destination -Leaf While ($_downloader.IsBusy) { Start-Sleep -Seconds 1 $_timeDiff = (Get-Date) - $_StartTime #If ($_timeDiff.TotalSeconds -gt 5) { Write-Progress -Activity "(0)Downloading file $($_baseFilename) .....".Replace('(0)', $_counter) -Status $_timeDiff.ToString().Substring(0,8) -id 1 #} } Write-Progress -Id 1 -Completed -Activity "(0)Downloading file $($_baseFilename) .....".Replace('(0)', $_counter) If ((Get-Item $Destination).Length -ne 0) { Write-Log -ErrorLevel SUCCESS -Message "(0)Downloaded file: $($_baseFilename)".Replace('(0)', $_counter) Return $Destination } Else { Write-Log -ErrorLevel ERROR -Message "Failure to download file: $($_baseFilename)" Write-Log -ErrorLevel ERROR -Message "- $($Error[0].Exception.Message)" Return $null } } end { } } If ($_setting.DebugMode) { Export-ModuleMember -Function Invoke-AxDFileDownload } # ------------------------------------------------------------ # MARK: Download latest WinGet package # Date: 2025.03.02 # ------------------------------------------------------------ function Invoke-AxDWinGetDownload { [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$Path ) begin { $_Url = 'https://api.github.com/repos/microsoft/winget-cli/releases/latest' } process { $_retryCount = 0 Write-Log -ErrorLevel INFO -Message 'Update WinGet package' Do { Write-Log -ErrorLevel DEBUG -Message "> Checking the latest version of WinGet file ($($_retryCount)) .....".Replace(' (0)', '') $_result = Invoke-WebRequest -Uri $_Url -ErrorAction SilentlyContinue -ErrorVariable WebError If ($_result) { Write-Log -ErrorLevel DEBUG -Message (" Latest version: {0}" -f ($_result.Content | ConvertFrom-Json).tag_name.Replace('v', '')) $_Pkg = ($_result.Content | ConvertFrom-Json).assets | Where-Object { $_.Name -ilike '*.msixbundle'} } Else { $_retryCount++ } } Until ($_result -or ($_retryCount -gt 3)) If ($_Pkg) { $Destination = "$($Path)\$($_Pkg.Name)" $_oldFile = $_Pkg.Name.Replace('.msixbundle', '.old') If (Test-Path $Destination) { If ((Get-Item -Path $Destination).Length -ne $_Pkg.size) { Write-Log -ErrorLevel DEBUG -Message "> The original file will be rename to '$($_oldFile)'." Rename-Item -Path $Destination -NewName $_oldFile -ErrorAction SilentlyContinue -ErrorVariable RenameError | Out-Null If ($RenameError) { Write-Log -ErrorLevel ERROR -Message 'Failure to rename file' Write-Log -ErrorLevel ERROR -Message "> $($RenameError.Exception.Message)" Return $null } } Else { Write-Log -ErrorLevel DEBUG -Message '> File exist and no need to update.' Return $null } } If ((Invoke-AxDFileDownload -Uri $_Pkg.browser_download_url -Destination $Destination -_counter '- ')) { If (Test-Path -Path "$($Path)\$($_oldFile)") { Write-Log -ErrorLevel DEBUG -Message 'Delete old file' Remove-Item -Path "$($Path)\$($_oldFile)" -Force } Return $Destination } Else { Write-Log -ErrorLevel DEBUG -Message 'Rollbak old file' Rename-Item -Path "$($Path)\$($_oldFile)" -NewName $_Pkg.Name Return $null } } } end { } } If ($_setting.DebugMode) { Export-ModuleMember -Function Invoke-AxDWinGetDownload } # ------------------------------------------------------------ # MARK: Update Appx offline files from Internet # Date: 2025.03.02 # ------------------------------------------------------------ function Update-AxDOfflineFile{ <# .SYNOPSIS Update-AxDOfflineFile is download updated appx/misx package from the Internet. .PARAMETER AppxList Appx list (CSV file format). CSV file Header: AppName,PackageIdentifier .PARAMETER Path The downloaded folder location. If the folder contain another appx/misx file, it will compare the file version before download and repalce it. .EXAMPLE Update-AxDOfflineLine -AppxList 'C:\Temp\AppxList.csv' -Path 'C:\Temp\DownloadedPackage' #> [CmdletBinding()] param ( [Parameter(Mandatory = $true)] [string]$AppxList, [string]$Path = $PWD.ToString(), [Parameter(DontShow = $true)] [switch]$EnableLogging, [Parameter(DontShow = $true)] [switch]$DebugMode = $_setting.DebugMode ) begin { Update-WriteLogSetting -EnableLogging:$EnableLogging -DebugMode:$DebugMode } process { # Get the appx list for download Write-Log -ErrorLevel DEBUG -Message 'Import CSV file .....' If (Test-Path $AppxList) { $_targetList = Import-Csv $AppxList Write-Log -ErrorLevel DEBUG -Message "> Total $($_targetList.Count) appx from the list." } Else { Write-Log -ErrorLevel ERROR -Message 'The CSV file not exist.' } If ($_targetList.Count -eq 0) { Write-Log -ErrorLevel WARNING -Message 'No appx was listed from the CSV file.' } # Collect the existing appx file under 'Destination' folder $Script:AppxPackageList = Get-AxDOfflineFileList -Path $Path If (-not $Script:AppxPackageList) { Return $null } # Get the package list from Internet and download package (if need) $_DownloadedList = @() $_Processing = 1 foreach ($_tList in $_targetList) { $_PkgList = Get-AxDPackageList -ProductID $_tList.PackageIdentifier -_counter "$($_Processing)/$($_targetList.Count)" $_UpdateList = @() # Comparing verison between existing package and downloaded list foreach ($_Pkg in $_PkgList) { If (-not $Script:AppxPackageList.ContainsKey($_Pkg.Name)) { $Script:AppxPackageList["$($_Pkg.Name)"] = @{'Version' = [Version]'0.0.0.0'} } If ($_Pkg.Version -gt $Script:AppxPackageList."$($_Pkg.Name)".Version) { $Script:AppxPackageList."$($_Pkg.Name)"['Url'] = $_Pkg.Url $Script:AppxPackageList."$($_Pkg.Name)"['NewFile'] = $_Pkg.Filename $Script:AppxPackageList."$($_Pkg.Name)".Version = $_Pkg.Version $_UpdateList += $_Pkg.Name } } # Download new package from the Internet $_UpdateList = $_UpdateList | Select-Object -Unique $_Downloading = 1 foreach ($_NewPkg in ($_UpdateList)) { # Download file $_result = Invoke-AxDFileDownload -Uri $Script:AppxPackageList.$_NewPkg.Url -Destination "$($Path)\$($Script:AppxPackageList.$_NewPkg.NewFile)" -_counter "- ($($_Downloading)/$($_UpdateList.Count)) " If ($_result) { $_oldFile = "$($Path)\$($Script:AppxPackageList.$_NewPkg.'Filename')" If ((Test-Path $_oldFile -PathType Leaf) -and ($Script:AppxPackageList.$_NewPkg.'Filename')) { Remove-Item -Path $_oldFile -Force -ErrorAction SilentlyContinue -ErrorVariable RemoveError | Out-Null If ($RemoveError) { Write-Log -ErrorLevel WARNING -Message "Failure to remove old package file: $($Script:AppxPackageList.$_NewPkg.'Filename')" Write-Log -ErrorLevel WARNING -Message "> $($RemoveError.Exception.Message)" } Else { Write-Log -ErrorLevel DEBUG -Message "Old package file '$($Script:AppxPackageList.$_NewPkg.'Filename')' was removed." } } $Script:AppxPackageList.$_NewPkg.'Filename' = Split-Path -Path $_result -Leaf $Script:AppxPackageList.$_NewPkg.Remove('Url') $Script:AppxPackageList.$_NewPkg.Remove('NewFile') $_DownloadedList += $_NewPkg } $_Downloading++ } $_Processing++ } $_DownloadedList += (Invoke-AxDWinGetDownload -Path $Path) If ($_DownloadedList) { If ($_DownloadedList.Count -eq 1) { Write-Log -ErrorLevel SUCCESS -Message 'Total 1 file was updated.' } Else { Write-Log -ErrorLevel SUCCESS -Message "Total $($_DownloadedList.Count) files were updated." } } Else { Write-Log -ErrorLevel INFO -Message 'No file was updated.' } } end { } } Export-ModuleMember -Function Update-AxDOfflineFile # ------------------------------------------------------------ # MARK: (Private function) Update 'Write-Log' setting # Date: 2025.02.22 # ------------------------------------------------------------ function Update-WriteLogSetting { [CmdletBinding()] param ( [switch]$EnableLogging, [switch]$DebugMode ) process { If ($EnableLogging) { If ($Global:PSDefaultParameterValues.ContainsKey('Write-Log:logPath')) { $PSDefaultParameterValues['Write-Log:logPath'] = $Global:PSDefaultParameterValues.'Write-Log:logPath' } Else { $PSDefaultParameterValues['Write-Log:logPath'] = './Log' } } Else { $PSDefaultParameterValues['Write-Log:logPath'] = '' } If ($DebugMode) { $PSDefaultParameterValues['Write-Log:debugMode'] = $true } Else { $PSDefaultParameterValues['Write-Log:debugMode'] = $false } } } |