first commit

This commit is contained in:
dritter 2025-10-15 12:48:44 +02:00
commit b0fe0ba006
27 changed files with 872 additions and 0 deletions

4
.gitignore vendored Normal file
View file

@ -0,0 +1,4 @@
cache/
downloads/
logs/
temp/

31
Downloader/Script.ps1 Normal file
View 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
View 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"
}
}
]

View 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 = @{}

View file

@ -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
}

View 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
}

View file

@ -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
}

View 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
}

View 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
}
}

View 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()
}
}

View 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
}

View 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()
}
}

View 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
}

View 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
}

View 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
}

View 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))
}

View 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;
}
}

View 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;
}
}

View 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
}

View 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
}

View 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;
}
}

View 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;
}
}

View 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
}

View 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
}

View file

@ -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
}

View 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
View 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
}