first commit
This commit is contained in:
commit
b0fe0ba006
27 changed files with 872 additions and 0 deletions
4
.gitignore
vendored
Normal file
4
.gitignore
vendored
Normal file
|
|
@ -0,0 +1,4 @@
|
|||
cache/
|
||||
downloads/
|
||||
logs/
|
||||
temp/
|
||||
31
Downloader/Script.ps1
Normal file
31
Downloader/Script.ps1
Normal file
|
|
@ -0,0 +1,31 @@
|
|||
# If not running as file, ensure to be in cwd
|
||||
$global:ScriptDirectory = if ([string]::IsNullOrWhiteSpace(($PSScriptRoot))) { Get-Location } else { $PSScriptRoot }
|
||||
|
||||
. $ScriptDirectory\functions\autoload.ps1
|
||||
|
||||
# Getting all items
|
||||
$allItems = Get-Content -Path $ScriptDirectory\config.json | ConvertFrom-Json
|
||||
|
||||
# Main loop
|
||||
foreach ($cfg in $allItems) {
|
||||
|
||||
$software = Start-SoftwareDownload -cfg $cfg
|
||||
if($software.CanContinue){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Test-SoftwareDownloadSignature $software
|
||||
if($software.CanContinue){
|
||||
continue;
|
||||
}
|
||||
|
||||
|
||||
Import-SoftwareDownlaod $software
|
||||
}
|
||||
|
||||
|
||||
Write-Log "Items updated: $($updatedItems.Count)"
|
||||
Write-Log "Items checked: $($checkedItems.Count)"
|
||||
Write-Log "Items failed: $($failedItems.Count)"
|
||||
Write-Log "Items total: $($allItems.Count)"
|
||||
67
Downloader/config.json
Normal file
67
Downloader/config.json
Normal file
|
|
@ -0,0 +1,67 @@
|
|||
[
|
||||
{
|
||||
"Name": "NotepadPlusPlus",
|
||||
"Mode": "GitHubRelease",
|
||||
"Source": "notepad-plus-plus/notepad-plus-plus",
|
||||
"Asset": "npp.(?<Version>.+).Installer.x64.exe$",
|
||||
"Target": "NotepadPlusPlus-Setup.exe",
|
||||
"SigCN": null,
|
||||
"Version": {
|
||||
"Mode": "ReleaseRegex",
|
||||
"Source": null,
|
||||
"Asset": null
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "GoogleChrome",
|
||||
"Mode": "StaticUrl",
|
||||
"Source": "https://dl.google.com/edgedl/chrome/install/GoogleChromeStandaloneEnterprise64.msi",
|
||||
"Asset": null,
|
||||
"Target": "Chrome-Setup.msi",
|
||||
"SigCN": "CN=Google LLC, O=Google LLC, L=Mountain View, S=California, C=US, SERIALNUMBER=3582691, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.2=Delaware, OID.1.3.6.1.4.1.311.60.2.1.3=US",
|
||||
"Version": {
|
||||
"Mode": "RestRequest",
|
||||
"Source": "https://versionhistory.googleapis.com/v1/chrome/platforms/win64/channels/stable/versions",
|
||||
"Asset": "versions[0].version"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "Firefox",
|
||||
"Mode": "StaticUrl",
|
||||
"Source": "https://download.mozilla.org/?product=firefox-esr-latest-ssl&os=win64&lang=de",
|
||||
"Asset": null,
|
||||
"Target": "Firefox-Setup.exe",
|
||||
"SigCN": "CN=Mozilla Corporation, OU=Firefox Engineering Operations, O=Mozilla Corporation, L=San Francisco, S=California, C=US",
|
||||
"Version": {
|
||||
"Mode": "RestRequest",
|
||||
"Source": "https://product-details.mozilla.org/1.0/firefox_versions.json",
|
||||
"Asset": "FIREFOX_ESR"
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "7zip",
|
||||
"Mode": "Regex",
|
||||
"Source": "https://7-zip.de/download.html",
|
||||
"Asset": "https.*\\/a/7z[0-9]+-x64\\.exe",
|
||||
"Target": "7zip-Setup.exe",
|
||||
"SigCN": null,
|
||||
"Version": {
|
||||
"Mode": "Regex",
|
||||
"Source": "https://www.7-zip.org/download.html",
|
||||
"Asset": "Zip\\s(?<Version>[^ ]+)\\s*\\("
|
||||
}
|
||||
},
|
||||
{
|
||||
"Name": "WinSCP",
|
||||
"Mode": "SourceforgeBestRelease",
|
||||
"Source": "https://sourceforge.net/projects/winscp/best_release.json",
|
||||
"Asset": "windows",
|
||||
"Target": "WinSCP-Setup.exe",
|
||||
"SigCN": "CN=Martin Prikryl, O=Martin Prikryl, L=Prague, C=CZ, SERIALNUMBER=87331519, OID.2.5.4.15=Private Organization, OID.1.3.6.1.4.1.311.60.2.1.3=CZ",
|
||||
"Version": {
|
||||
"Mode": "Regex",
|
||||
"Source": "https://winscp.net/eng/downloads.php",
|
||||
"Asset": "/download/WinSCP-(?<Version>(.+))-Setup.exe/download"
|
||||
}
|
||||
}
|
||||
]
|
||||
37
Downloader/functions/autoload.ps1
Normal file
37
Downloader/functions/autoload.ps1
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
. $ScriptDirectory\functions\private\utils\Write-Log.ps1
|
||||
|
||||
# Setting up da logga
|
||||
Initialize-Log -LogDirectory ([IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\logs")).FullName)
|
||||
|
||||
# Loading our functions dot-sourced
|
||||
. $ScriptDirectory\functions\private\converters\ConvertFrom-ResponseHeaders.ps1
|
||||
. $ScriptDirectory\functions\private\http\Invoke-FileDownload.ps1
|
||||
. $ScriptDirectory\functions\private\http\Invoke-GetRequest.ps1
|
||||
. $ScriptDirectory\functions\private\http\Invoke-HeadRequest.ps1
|
||||
. $ScriptDirectory\functions\private\http\Invoke-RestRequest.ps1
|
||||
. $ScriptDirectory\functions\private\features\Get-SoftwareUrl.ps1
|
||||
. $ScriptDirectory\functions\private\features\Get-SoftwareVersion.ps1
|
||||
. $ScriptDirectory\functions\private\features\Test-IfUpdated.ps1
|
||||
. $ScriptDirectory\functions\private\features\Test-Signature.ps1
|
||||
. $ScriptDirectory\functions\private\utils\Add-Items.ps1
|
||||
. $ScriptDirectory\functions\private\utils\Get-JsonPath.ps1
|
||||
|
||||
|
||||
. $ScriptDirectory\functions\public\Get-SoftwareOutputItem.ps1
|
||||
. $ScriptDirectory\functions\public\Find-SoftwareDownloadUrl.ps1
|
||||
. $ScriptDirectory\functions\public\Skip-SoftwareDownload.ps1
|
||||
. $ScriptDirectory\functions\public\Invoke-SoftwareDownload.ps1
|
||||
. $ScriptDirectory\functions\public\Find-SoftwareDownloadVersion.ps1
|
||||
. $ScriptDirectory\functions\public\Set-SoftwareDownloadCache.ps1
|
||||
. $ScriptDirectory\functions\public\Start-SoftwareDownload.ps1
|
||||
|
||||
|
||||
# Create and define necessary directories
|
||||
$global:outputDirectory = [IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\downloads")).FullName
|
||||
$global:cacheDirectory = [IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\cache")).FullName
|
||||
$global:tempDirectory = [IO.Directory]::CreateDirectory([IO.Path]::Combine($ScriptDirectory, "data\temp")).FullName
|
||||
|
||||
# Setting up da logga
|
||||
$global:updatedItems = @{}
|
||||
$global:checkedItems = @{}
|
||||
$global:failedItems = @{}
|
||||
|
|
@ -0,0 +1,11 @@
|
|||
function ConvertFrom-ResponseHeaders {
|
||||
param(
|
||||
$Response
|
||||
)
|
||||
$headers = @{}
|
||||
|
||||
foreach ($h in $response.Headers) { $headers[$h.Key] = $h.Value -join ', ' }
|
||||
foreach ($h in $response.Content.Headers) { $headers[$h.Key] = $h.Value -join ', ' }
|
||||
|
||||
return $headers
|
||||
}
|
||||
61
Downloader/functions/private/features/Get-SoftwareUrl.ps1
Normal file
61
Downloader/functions/private/features/Get-SoftwareUrl.ps1
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
function Get-SoftwareUrl {
|
||||
param(
|
||||
$cfg
|
||||
)
|
||||
|
||||
switch ($cfg.Mode) {
|
||||
'GitHubRelease' { $url = Get-GitHubAssetUrl -Repo $cfg.Source -AssetPattern $cfg.Asset }
|
||||
'Regex' { $url = Get-RegexMatchUrl -Url $cfg.Source -Pattern $cfg.Asset }
|
||||
'SourceforgeBestRelease' { $url = Get-SourceforgeBestReleaseUrl -Url $cfg.Source -Plattform $cfg.Asset }
|
||||
'StaticUrl' { $url = $cfg.Source }
|
||||
default { throw "Unknown mode: $($cfg.Mode) in $($cfg.Name)" }
|
||||
}
|
||||
|
||||
return $url
|
||||
}
|
||||
|
||||
function Get-GithubAssetUrl {
|
||||
param(
|
||||
[string]$Repo,
|
||||
[string]$AssetPattern
|
||||
)
|
||||
|
||||
$result = Invoke-RestRequest -Uri "https://api.github.com/repos/$Repo/releases/latest" `
|
||||
-Headers @{ Accept = 'application/json'
|
||||
"User-Agent" = "PowerShell/Agent"}
|
||||
|
||||
return $result.assets | Where-Object { $_.name -match $AssetPattern } | Select-Object -ExpandProperty browser_download_url
|
||||
}
|
||||
|
||||
function Get-RegexMatchUrl
|
||||
{
|
||||
param(
|
||||
[string]$Url,
|
||||
[string]$Pattern
|
||||
)
|
||||
|
||||
$MatchInfo = Invoke-GetRequest -Uri $Url | Select-String -Pattern $Pattern
|
||||
$Value = $MatchInfo.Matches | Select-Object -First 1 -ExpandProperty Value
|
||||
|
||||
if(!$Value){
|
||||
Write-Warning "Can not find pattern '$Pattern' on $Url"
|
||||
return
|
||||
}
|
||||
|
||||
if($Value.StartsWith("/")){
|
||||
$Uri = [Uri]::new($Url)
|
||||
return ("{0}://{1}{2}" -f $Uri.Scheme, $Uri.Host, $Value)
|
||||
}
|
||||
|
||||
return $Value
|
||||
}
|
||||
|
||||
function Get-SourceforgeBestReleaseUrl {
|
||||
param(
|
||||
[string]$Url,
|
||||
[string]$Plattform
|
||||
)
|
||||
|
||||
$Releases = Invoke-RestRequest -Uri $Url
|
||||
return $Releases.platform_releases.$Plattform.url
|
||||
}
|
||||
|
|
@ -0,0 +1,56 @@
|
|||
function Get-SoftwareVersion {
|
||||
param(
|
||||
$cfg,
|
||||
$releaseUrl = ""
|
||||
)
|
||||
|
||||
switch ($cfg.Version.Mode) {
|
||||
'ReleaseRegex' { $version = Get-SoftwareVersionReleaseRegex -cfg $cfg -ReleaseUrl $releaseUrl }
|
||||
'Regex' { $version = Get-SoftwareVersionRegex -cfg $cfg }
|
||||
'RestRequest' { $version = Get-SoftwareVersionRestRequest -cfg $cfg }
|
||||
default { throw "Unknown mode: $($cfg.Version.Mode) in $($cfg.Name)" }
|
||||
}
|
||||
|
||||
return $version
|
||||
}
|
||||
|
||||
function Get-SoftwareVersionReleaseRegex {
|
||||
param(
|
||||
$cfg,
|
||||
$ReleaseUrl
|
||||
)
|
||||
|
||||
$Result = $ReleaseUrl | Select-String -Pattern $cfg.Asset
|
||||
|
||||
if($Result.Matches){
|
||||
$FirstMatch = $Result.Matches | Where-Object { $_.Groups["Version"] } | Select-Object -First 1
|
||||
return $FirstMatch.Groups["Version"].Value
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SoftwareVersionRegex {
|
||||
param(
|
||||
$cfg
|
||||
)
|
||||
|
||||
$plain = Invoke-GetRequest -Uri $cfg.Version.Source
|
||||
$Result = $plain | Select-String -Pattern $cfg.Version.Asset
|
||||
|
||||
if($Result.Matches){
|
||||
$FirstMatch = $Result.Matches | Where-Object { $_.Groups["Version"] } | Select-Object -First 1
|
||||
return $FirstMatch.Groups["Version"].Value
|
||||
}
|
||||
|
||||
return $null
|
||||
}
|
||||
|
||||
function Get-SoftwareVersionRestRequest {
|
||||
param(
|
||||
$cfg
|
||||
)
|
||||
|
||||
$json = Invoke-RestRequest -Uri $cfg.Version.Source
|
||||
return Get-JsonPath -Json $json -Path $cfg.Version.Asset
|
||||
}
|
||||
27
Downloader/functions/private/features/Test-IfUpdated.ps1
Normal file
27
Downloader/functions/private/features/Test-IfUpdated.ps1
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
function Test-IfUpdated {
|
||||
param(
|
||||
[string]$Url,
|
||||
[string]$CacheFile
|
||||
)
|
||||
|
||||
if(-not (Test-Path -Path $CacheFile)){
|
||||
return $true
|
||||
}
|
||||
|
||||
$Cache = Get-Content $CacheFile | ConvertFrom-Json
|
||||
$Head = Invoke-HeadRequest -Url $Url
|
||||
|
||||
if($Head.Keys -contains "ETag" `
|
||||
-and $Cache.PSObject.Properties.Name -contains "ETag" `
|
||||
-and !($null -eq $Cache.ETag -or [string]::IsNullOrWhiteSpace($Cache.ETag))){
|
||||
return -not ($Head["ETag"] -eq $Cache.ETag)
|
||||
}
|
||||
|
||||
if($Head.Keys -contains "Last-Modified" `
|
||||
-and $Cache.PSObject.Properties.Name -contains "LastModified" `
|
||||
-and !($null -eq $Cache.LastModified -or [string]::IsNullOrWhiteSpace($Cache.LastModified))){
|
||||
return -not ($Head["Last-Modified"] -eq $Cache.LastModified)
|
||||
}
|
||||
|
||||
return $true
|
||||
}
|
||||
27
Downloader/functions/private/features/Test-Signature.ps1
Normal file
27
Downloader/functions/private/features/Test-Signature.ps1
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
function Test-Signature{
|
||||
param(
|
||||
[string]$FilePath,
|
||||
[string]$ExpectedCN
|
||||
)
|
||||
|
||||
try{
|
||||
$signature = Get-AuthenticodeSignature -FilePath $FilePath
|
||||
|
||||
if(-not ($signature.SignerCertificate.Verify())){
|
||||
Write-Warning "Verify of $FilePath failed."
|
||||
return $false
|
||||
}
|
||||
|
||||
$match = $signature.SignerCertificate.Subject -eq $ExpectedCN
|
||||
|
||||
if(-not $match){
|
||||
Write-Warning "Signature verification of $FilePath failed. Signature mismatch. Expected: >$ExpectedCN< Got: $($signature.SignerCertificate.Subject)"
|
||||
}
|
||||
|
||||
return $match
|
||||
}
|
||||
catch{
|
||||
return $false
|
||||
}
|
||||
|
||||
}
|
||||
42
Downloader/functions/private/http/Invoke-FileDownload.ps1
Normal file
42
Downloader/functions/private/http/Invoke-FileDownload.ps1
Normal file
|
|
@ -0,0 +1,42 @@
|
|||
function Invoke-FileDownload {
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Url,
|
||||
|
||||
# Ziel-Ordner, in dem die Datei abgelegt wird
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$FilePath
|
||||
)
|
||||
|
||||
# HttpClient einmalig anlegen
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
|
||||
try {
|
||||
# Nur Header laden, um sofort an Dateinamen zu kommen
|
||||
$response = $client.GetAsync(
|
||||
$Url,
|
||||
[System.Net.Http.HttpCompletionOption]::ResponseHeadersRead
|
||||
).Result
|
||||
|
||||
$response.EnsureSuccessStatusCode() | Out-Null
|
||||
|
||||
# Download-Stream in Datei schreiben
|
||||
$inStream = $response.Content.ReadAsStreamAsync().Result
|
||||
$outStream = [System.IO.File]::Create($FilePath)
|
||||
$inStream.CopyToAsync($outStream).Wait()
|
||||
|
||||
# Aufräumen
|
||||
$outStream.Dispose()
|
||||
$inStream.Dispose()
|
||||
|
||||
Write-Host "Datei erfolgreich heruntergeladen: $FilePath"
|
||||
return $response
|
||||
}
|
||||
catch {
|
||||
Write-Error "Fehler beim Herunterladen: $_"
|
||||
throw
|
||||
}
|
||||
finally {
|
||||
$client.Dispose()
|
||||
}
|
||||
}
|
||||
21
Downloader/functions/private/http/Invoke-GetRequest.ps1
Normal file
21
Downloader/functions/private/http/Invoke-GetRequest.ps1
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
function Invoke-GetRequest {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Uri
|
||||
)
|
||||
|
||||
# .NET-Klasse laden (falls noch nicht im AppDomain)
|
||||
Add-Type -AssemblyName System.Net.Http -ErrorAction SilentlyContinue
|
||||
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
|
||||
# GET ausführen (synchron gewartet)
|
||||
$response = $client.GetAsync($Uri).Result
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
throw "Request failed: $($response.StatusCode) - $($response.ReasonPhrase)"
|
||||
}
|
||||
|
||||
# Rückgabe: reiner String-Content
|
||||
return $response.Content.ReadAsStringAsync().Result
|
||||
}
|
||||
21
Downloader/functions/private/http/Invoke-HeadRequest.ps1
Normal file
21
Downloader/functions/private/http/Invoke-HeadRequest.ps1
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
function Invoke-HeadRequest {
|
||||
param (
|
||||
[Parameter(Mandatory = $true)]
|
||||
[string]$Url
|
||||
)
|
||||
|
||||
# HttpClient in using-Block sorgt für sauberes Dispose
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
$request = [System.Net.Http.HttpRequestMessage]::new(
|
||||
[System.Net.Http.HttpMethod]::Head,
|
||||
$Url)
|
||||
|
||||
# Antwort abholen
|
||||
$response = $client.SendAsync($request).GetAwaiter().GetResult()
|
||||
try {
|
||||
return ConvertFrom-ResponseHeaders -Response $response
|
||||
}
|
||||
finally {
|
||||
$response.Dispose()
|
||||
}
|
||||
}
|
||||
28
Downloader/functions/private/http/Invoke-RestRequest.ps1
Normal file
28
Downloader/functions/private/http/Invoke-RestRequest.ps1
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
function Invoke-RestRequest {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$Uri,
|
||||
|
||||
[hashtable]$Headers = @{}
|
||||
)
|
||||
|
||||
# .NET-Klasse laden und einmaligen HttpClient erzeugen
|
||||
Add-Type -AssemblyName System.Net.Http
|
||||
$client = [System.Net.Http.HttpClient]::new()
|
||||
|
||||
# optionale Header setzen
|
||||
foreach ($key in $Headers.Keys) {
|
||||
$client.DefaultRequestHeaders.Add($key, $Headers[$key])
|
||||
}
|
||||
|
||||
# synchronen GET ausführen (für asynchron siehe *.GetAsync().ConfigureAwait())
|
||||
$response = $client.GetAsync($Uri).Result
|
||||
if (-not $response.IsSuccessStatusCode) {
|
||||
throw "Request failed: $($response.StatusCode) - $($response.ReasonPhrase)"
|
||||
}
|
||||
|
||||
# Inhalt lesen + JSON in PSCustomObject umwandeln
|
||||
$json = $response.Content.ReadAsStringAsync().Result
|
||||
return $json | ConvertFrom-Json
|
||||
}
|
||||
26
Downloader/functions/private/utils/Add-Items.ps1
Normal file
26
Downloader/functions/private/utils/Add-Items.ps1
Normal file
|
|
@ -0,0 +1,26 @@
|
|||
function Add-FailedItems{
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
Write-Log "$($output.InputObject.Name) : $($output.Message)" -Severity "Error";
|
||||
$failedItems[$output.InputObject.Name] = $output
|
||||
}
|
||||
|
||||
function Add-CheckedItems{
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
Write-Log "$($output.InputObject.Name) : $($output.Message)"
|
||||
$checkedItems[$output.InputObject.Name] = $output
|
||||
}
|
||||
|
||||
function Add-UpdatedItems{
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
Write-Log "$($output.InputObject.Name) : $($output.Message)"
|
||||
$updatedItems[$output.InputObject.Name] = $output
|
||||
}
|
||||
17
Downloader/functions/private/utils/Get-JsonPath.ps1
Normal file
17
Downloader/functions/private/utils/Get-JsonPath.ps1
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
function Get-JsonPath {
|
||||
param(
|
||||
[Parameter(Mandatory=$true)][object]$Json,
|
||||
[Parameter(Mandatory=$true)][string]$Path
|
||||
)
|
||||
|
||||
$PathParts = $Path -split '\.'
|
||||
$value = $Json
|
||||
foreach ($part in $PathParts) {
|
||||
if ($part -match '(.+)\[(\d+)\]') {
|
||||
$value = $value.($matches[1])[[int]$matches[2]]
|
||||
} else {
|
||||
$value = $value.$part
|
||||
}
|
||||
}
|
||||
return $value
|
||||
}
|
||||
37
Downloader/functions/private/utils/Write-Log.ps1
Normal file
37
Downloader/functions/private/utils/Write-Log.ps1
Normal file
|
|
@ -0,0 +1,37 @@
|
|||
function Initialize-Log {
|
||||
param(
|
||||
[string]$LogDirectory
|
||||
)
|
||||
$LogFileName = "log_$(Get-Date -Format "yyyy-MM-dd_HHmmss").log"
|
||||
$global:LogFilePath = Join-Path -Path $LogDirectory -ChildPath $LogFileName
|
||||
}
|
||||
|
||||
function Write-Log {
|
||||
param(
|
||||
[string]$Message,
|
||||
[string]$Severity = "Info"
|
||||
)
|
||||
|
||||
$Date = Get-Date -Format "yyyy'-'MM'-'dd'T'HH':'mm':'ss.fffffffK"
|
||||
$Prefix = "[$Date]:`t$($Severity.ToUpper())`t`t"
|
||||
|
||||
Write-Host -Object $Message
|
||||
"$Prefix$Message" | Out-File -FilePath $global:LogFilePath -Encoding utf8 -Append -Force
|
||||
}
|
||||
|
||||
function Set-StringLength {
|
||||
[CmdletBinding()]
|
||||
param (
|
||||
[Parameter(Mandatory)]
|
||||
[string]$InputString,
|
||||
|
||||
[Parameter(Mandatory)]
|
||||
[int]$TotalLength
|
||||
)
|
||||
|
||||
if ($InputString.Length -ge $TotalLength) {
|
||||
return $InputString.Substring(0, $TotalLength)
|
||||
}
|
||||
|
||||
return $InputString + (" " * ($TotalLength - $InputString.Length))
|
||||
}
|
||||
21
Downloader/functions/public/Find-SoftwareDownloadUrl.ps1
Normal file
21
Downloader/functions/public/Find-SoftwareDownloadUrl.ps1
Normal file
|
|
@ -0,0 +1,21 @@
|
|||
function Find-SoftwareDownloadUrl{
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
try {
|
||||
$output.DownloadUrl = Get-SoftwareUrl -cfg $cfg
|
||||
|
||||
$paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22
|
||||
Write-Log "$paddedName Extracted url: $($output.DownloadUrl)"
|
||||
return $output
|
||||
}
|
||||
catch {
|
||||
$output.Message = "Failed to find software url. Exception: $_"
|
||||
$output.Exception = $_
|
||||
|
||||
Add-FailedItems $output
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
24
Downloader/functions/public/Find-SoftwareDownloadVersion.ps1
Normal file
24
Downloader/functions/public/Find-SoftwareDownloadVersion.ps1
Normal file
|
|
@ -0,0 +1,24 @@
|
|||
function Find-SoftwareDownloadVersion {
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
$paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22
|
||||
|
||||
Write-Log "$paddedName Try getting current product version"
|
||||
try {
|
||||
|
||||
$output.CurrentVersion = Get-SoftwareVersion -cfg $output.InputObject -releaseUrl $output.DownloadUrl
|
||||
Write-Log "$paddedName Current Version -> $($output.CurrentVersion)"
|
||||
|
||||
return $output
|
||||
}
|
||||
catch {
|
||||
$output.Message = "Error getting current product version. Exception: $_"
|
||||
$output.Exception = $_
|
||||
|
||||
Add-FailedItems $output
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
28
Downloader/functions/public/Get-SoftwareOutputItem.ps1
Normal file
28
Downloader/functions/public/Get-SoftwareOutputItem.ps1
Normal file
|
|
@ -0,0 +1,28 @@
|
|||
function Get-SoftwareOutputItem{
|
||||
param(
|
||||
$cfg
|
||||
)
|
||||
|
||||
$output = [PSCustomObject] @{
|
||||
"InputObject" = $cfg
|
||||
"CanContinue" = $true
|
||||
"DownloadChanged" = $false
|
||||
"SignatureVerified" = $false
|
||||
"DownloadUrl" = $null
|
||||
"DownloadHeaders" = $null
|
||||
"DownloadResponse" = $null
|
||||
"CacheFile" = $null
|
||||
"TargetFile" = $null
|
||||
"TempFile" = $null
|
||||
"Exception" = $null
|
||||
"Message" = $null
|
||||
"CurrentVersion" = $null
|
||||
"HasSignatureCheck" = (($null -ne $cfg.SigCN) -and ![string]::IsNullOrWhiteSpace($cfg.SigCN))
|
||||
}
|
||||
|
||||
$output.CacheFile = Join-Path -Path $cacheDirectory -ChildPath "$($cfg.Name).cache.json"
|
||||
$output.TargetFile = Join-Path -Path $outputDirectory -ChildPath $cfg.Target
|
||||
$output.TempFile = Join-Path -Path $tempDirectory -ChildPath $cfg.Target
|
||||
|
||||
return $output
|
||||
}
|
||||
17
Downloader/functions/public/Import-SoftwareDownload.ps1
Normal file
17
Downloader/functions/public/Import-SoftwareDownload.ps1
Normal file
|
|
@ -0,0 +1,17 @@
|
|||
function Import-SoftwareDownlaod{
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
$paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22
|
||||
|
||||
if ([IO.File]::Exists($output.TargetFile)) {
|
||||
Write-Log "$paddedName Target already exists. Deleting: $($output.TargetFile)"
|
||||
Remove-Item -Path $output.TargetFile -Force -Confirm:$false
|
||||
}
|
||||
|
||||
Write-Log "$paddedName Moving item $($output.TempFile) -> $($output.TargetFile)"
|
||||
Move-Item -Path $output.TempFile -Destination $output.TargetFile -Force -Confirm:$false
|
||||
|
||||
$updatedItems[$cfg.Name] = $target
|
||||
}
|
||||
27
Downloader/functions/public/Invoke-SoftwareDownload.ps1
Normal file
27
Downloader/functions/public/Invoke-SoftwareDownload.ps1
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
function Invoke-SoftwareDownload{
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
$paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22
|
||||
Write-Log "$paddedName Remote file changed or local doesn't exist."
|
||||
|
||||
try {
|
||||
Write-Log "$paddedName Starting download $($output.DownloadUrl) -> $($output.TempFile)"
|
||||
|
||||
$output.DownloadResponse = Invoke-FileDownload -Url $output.DownloadUrl -FilePath $output.TempFile
|
||||
$output.DownloadHeaders = ConvertFrom-ResponseHeaders -Response $output.DownloadResponse
|
||||
|
||||
Write-Log "$paddedName Download finished $($output.DownloadUrl) -> $($output.TempFile)"
|
||||
|
||||
return $output
|
||||
}
|
||||
catch {
|
||||
$output.Message = "Download failed $($output.DownloadUrl) -> $($output.TempFile). Exception: $_"
|
||||
$output.Exception = $_
|
||||
|
||||
Add-FailedItems $output
|
||||
|
||||
return $output;
|
||||
}
|
||||
}
|
||||
29
Downloader/functions/public/Set-SoftwareDownloadCache.ps1
Normal file
29
Downloader/functions/public/Set-SoftwareDownloadCache.ps1
Normal file
|
|
@ -0,0 +1,29 @@
|
|||
function Set-SoftwareDownloadCache {
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
$paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22
|
||||
Write-Log "$paddedName Writing cache file to $($output.CacheFile)"
|
||||
|
||||
try {
|
||||
@{
|
||||
"ETag" = $output.DownloadHeaders["ETag"]
|
||||
"LastModified" = $output.DownloadHeaders["Last-Modified"]
|
||||
"CurrentVersion" = $output.CurrentVersion
|
||||
"DownloadUrl" = $output.DownloadUrl
|
||||
"VersionSource" = $output.InputObject.Version
|
||||
} | ConvertTo-Json | Set-Content -Path $output.CacheFile -Force -Confirm:$false
|
||||
|
||||
return $output
|
||||
}
|
||||
catch {
|
||||
$output.Message = "Error writing cache file. Exception: $_"
|
||||
$output.Exception = $_
|
||||
|
||||
Add-FailedItems $output
|
||||
|
||||
return $output;
|
||||
}
|
||||
|
||||
}
|
||||
27
Downloader/functions/public/Skip-SoftwareDownload.ps1
Normal file
27
Downloader/functions/public/Skip-SoftwareDownload.ps1
Normal file
|
|
@ -0,0 +1,27 @@
|
|||
function Skip-SoftwareDownload {
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
try {
|
||||
$downloadChanged = Test-IfUpdated $output.DownloadUrl $output.CacheFile
|
||||
$localExists = Test-Path -Path $output.TargetFile
|
||||
|
||||
$output.DownloadChanged = ($downloadChanged -or !$localExists)
|
||||
}
|
||||
catch {
|
||||
$output.Message = "Failed when check if remote has changed. Exception: $_"
|
||||
$output.Exception = $_
|
||||
|
||||
$output.DownloadChanged = $false
|
||||
|
||||
Add-FailedItems $output
|
||||
}
|
||||
|
||||
if (-not $output.DownloadChanged) {
|
||||
$output.Message = "Remote file hasn't changed."
|
||||
Add-CheckedItems $output
|
||||
}
|
||||
|
||||
return $output
|
||||
}
|
||||
43
Downloader/functions/public/Start-SoftwareDownload.ps1
Normal file
43
Downloader/functions/public/Start-SoftwareDownload.ps1
Normal file
|
|
@ -0,0 +1,43 @@
|
|||
function Start-SoftwareDownload {
|
||||
param(
|
||||
$cfg
|
||||
)
|
||||
|
||||
$output = Get-SoftwareOutputItem -cfg $cfg
|
||||
$paddedName = Set-StringLength -InputString "$($output.InputObject.Name):" -TotalLength 22
|
||||
|
||||
Write-Log "$paddedName Start processing item."
|
||||
Write-Log "$paddedName Cache File='$($output.CacheFile)', Target File='$($output.TargetFile)', Temp File='$($output.TempFile)'"
|
||||
|
||||
## -- STEP 1:
|
||||
# Find the softwares download url
|
||||
Find-SoftwareDownloadUrl $output
|
||||
if($output.Exception){ return $output }
|
||||
|
||||
|
||||
## -- STEP 2:
|
||||
# Skip if remote not changed and local exists
|
||||
Skip-SoftwareDownload $output
|
||||
if($output.Exception){ return $output }
|
||||
|
||||
## -- STEP 3:
|
||||
# Invoke download to temp folder
|
||||
Invoke-SoftwareDownload $output
|
||||
if($output.Exception -and !$output.DownloadChanged){ return $output }
|
||||
|
||||
|
||||
## -- STEP 4:
|
||||
## Find current version
|
||||
Find-SoftwareDownloadVersion $output
|
||||
if($output.Exception){ return $output }
|
||||
|
||||
|
||||
## -- STEP 5:
|
||||
# Set cache file
|
||||
Set-SoftwareDownloadCache $output
|
||||
if($output.Exception){ return $output }
|
||||
|
||||
$output.CanContinue = $false
|
||||
|
||||
return $output
|
||||
}
|
||||
|
|
@ -0,0 +1,19 @@
|
|||
function Test-SoftwareDownloadSignature {
|
||||
param(
|
||||
$output
|
||||
)
|
||||
|
||||
if($output.HasSignatureCheck){
|
||||
|
||||
$output.SignatureVerified = Test-Signature -FilePath $output.TempFile -ExpectedCN $output.InputObject.SigCN
|
||||
|
||||
if(-not $output.SignatureVerified){
|
||||
Add-FailedItems $output
|
||||
$output.CanContinue = $true
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
return $output
|
||||
|
||||
}
|
||||
65
Downloader/template/Untitled-2.html
Normal file
65
Downloader/template/Untitled-2.html
Normal file
|
|
@ -0,0 +1,65 @@
|
|||
<html>
|
||||
<head>
|
||||
<meta http-equiv='Content-Type' content='text/html; charset=utf-8'>
|
||||
<style>
|
||||
body { font-family: Segoe UI, Arial, sans-serif; font-size: 13px; color:#333; }
|
||||
h2 { color:#004a80; border-bottom:1px solid #ccc; padding-bottom:3px; }
|
||||
table { border-collapse: collapse; width: 100%; margin-bottom:15px; }
|
||||
th, td { border:1px solid #ddd; padding:6px 8px; vertical-align:top; }
|
||||
th { background:#f5f5f5; text-align:left; white-space:nowrap; }
|
||||
.badge-ok { color:#1b7f1b; font-weight:bold; }
|
||||
.badge-warn { color:#e67e00; font-weight:bold; }
|
||||
.badge-err { color:#b22222; font-weight:bold; }
|
||||
.small { color:#777; font-size:11px; }
|
||||
</style>
|
||||
</head>
|
||||
<body>
|
||||
<h2>Check for Common Software Report</h2>
|
||||
<p><b>Kunde:</b> $customerPrefix<br>
|
||||
<b>Server:</b> $computerName<br>
|
||||
<b>Datum:</b> $(Get-Date -Format 'dd.MM.yyyy HH:mm')</p>
|
||||
|
||||
<h2>Zusammenfassung</h2>
|
||||
<table>
|
||||
<tr><th>Geprüfte Software-Pakete</th><td>$($SmartResults.TotalChecked)</td></tr>
|
||||
<tr><th>Durchgeführte Updates</th><td>$($SmartResults.TotalUpdates)</td></tr>
|
||||
<tr><th>Gesamtgröße Downloads</th><td>$TotalSizeMB MB</td></tr>
|
||||
</table>
|
||||
|
||||
<h2>Aktualisierte Software</h2><table><tr><th>Produkt</th><th>Vorherige Version</th><th>Neue Version</th><th>Datei</th><th>Status</th></tr>
|
||||
<tr>
|
||||
<td>$($res.Software)</td>
|
||||
<td>$($res.UpdateCheck.LocalVersion)</td>
|
||||
<td>$($res.UpdateCheck.OnlineVersion)</td>
|
||||
<td>$fileCell</td>
|
||||
<td class='badge-ok'>Update durchgeführt</td>
|
||||
</tr>
|
||||
<tr>
|
||||
<td>$($res.Software)</td>
|
||||
<td>$($res.UpdateCheck.LocalVersion)</td>
|
||||
<td>$($res.UpdateCheck.OnlineVersion)</td>
|
||||
<td>$fileCell</td>
|
||||
<td class='badge-ok'>Update durchgeführt</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2>Aktueller Software-Bestand</h2><table><tr><th>Status</th><th>Produkt</th><th>Version</th><th>Datei</th><th>Letzter Check</th></tr>
|
||||
<tr>
|
||||
<td>$status</td>
|
||||
<td>$($res.Software)</td>
|
||||
<td>$($info.Version)</td>
|
||||
<td>$($info.FileName) ($([math]::Round([double]$info.FileSizeMB,1)) MB)</td>
|
||||
<td>$($info.LastCheck)</td>
|
||||
</tr>
|
||||
</table>
|
||||
<h2>Nicht verfügbare Ordner</h2><p class='badge-warn'>" + ($SkippedFolders -join ', ') + "</p>
|
||||
<h2>Systeminfo</h2>
|
||||
<table>
|
||||
<tr><th>Server</th><td>$computerName</td></tr>
|
||||
<tr><th>Kunde</th><td>$customerPrefix</td></tr>
|
||||
<tr><th>Ausführungszeit</th><td>$(Get-Date -Format 'dd.MM.yyyy HH:mm:ss')</td></tr>
|
||||
<tr><th>Basis-Pfad</th><td>$BasePath</td></tr>
|
||||
</table>
|
||||
<p class='small'>Automatisch generiert von Fremdsoftware Download Script v2.0<br>
|
||||
Schleupen SE – ApplicationService EWW<br>
|
||||
Hinweis: E-Mail wird nur bei tatsächlichen Updates versendet.</p>
|
||||
</body></html>
|
||||
59
install.ps1
Normal file
59
install.ps1
Normal file
|
|
@ -0,0 +1,59 @@
|
|||
$ScriptDirectory = if ([string]::IsNullOrWhiteSpace(($PSScriptRoot))) { Get-Location } else { $PSScriptRoot }
|
||||
|
||||
$DestinationDirectory = [IO.Directory]::CreateDirectory("C:\Scripts").FullName
|
||||
|
||||
Copy-Item -Path ([IO.Path]::Combine($ScriptDirectory, "Downloader")) `
|
||||
-Destination $DestinationDirectory `
|
||||
-Recurse `
|
||||
-Force `
|
||||
-Confirm:$false
|
||||
|
||||
# -------------------- Variablen --------------------
|
||||
$TaskName = "Download latest application software"
|
||||
$ScriptPath = [IO.Path]::Combine($DestinationDirectory, "Downloader\Script.ps1")
|
||||
|
||||
# Optional: Task Beschreibung
|
||||
$TaskDescription = "Startet das PowerShell-Projekt automatisch beim Systemstart."
|
||||
|
||||
# -------------------- Existiert der Task schon? --------------------
|
||||
$existingTask = Get-ScheduledTask -TaskName $TaskName -ErrorAction SilentlyContinue
|
||||
|
||||
if ($existingTask) {
|
||||
Write-Host "Task '$TaskName' existiert bereits. Aktualisiere..." -ForegroundColor Yellow
|
||||
Unregister-ScheduledTask -TaskName $TaskName -Confirm:$false
|
||||
}
|
||||
|
||||
# -------------------- Task Action --------------------
|
||||
# PowerShell-Executable
|
||||
$pwshPath = (Get-Command pwsh -ErrorAction SilentlyContinue).Source
|
||||
if (-not $pwshPath) { $pwshPath = (Get-Command powershell).Source }
|
||||
|
||||
$Action = New-ScheduledTaskAction -Execute $pwshPath -Argument "-NoProfile -ExecutionPolicy Bypass -File `"$ScriptPath`""
|
||||
|
||||
# -------------------- Task Trigger --------------------
|
||||
# Beispiel: Beim Systemstart (kannst du ändern zu -AtLogon, -Daily etc.)
|
||||
$Trigger = New-ScheduledTaskTrigger -Daily -At 20:00
|
||||
|
||||
# -------------------- Task Einstellungen --------------------
|
||||
$Settings = New-ScheduledTaskSettingsSet -AllowStartIfOnBatteries -DontStopIfGoingOnBatteries -StartWhenAvailable
|
||||
|
||||
# -------------------- Task Principal --------------------
|
||||
# Führt mit höchsten Privilegien unter SYSTEM aus
|
||||
$Principal = New-ScheduledTaskPrincipal -UserId "SYSTEM" -RunLevel Highest
|
||||
|
||||
# -------------------- Task Registrierung --------------------
|
||||
try{
|
||||
Register-ScheduledTask `
|
||||
-TaskName $TaskName `
|
||||
-Description $TaskDescription `
|
||||
-Action $Action `
|
||||
-Trigger $Trigger `
|
||||
-Principal $Principal `
|
||||
-Settings $Settings
|
||||
|
||||
Write-Host "Task '$TaskName' wurde erfolgreich registriert!" -ForegroundColor Green
|
||||
Write-Host "Pfad: $ScriptPath"
|
||||
}
|
||||
catch{
|
||||
Write-Host "Fehler beim registrieren von Task '$TaskName'. $($_.Exception.Message)" -ForegroundColor Red
|
||||
}
|
||||
Loading…
Add table
Add a link
Reference in a new issue