Guten Tag,
mit Package Customizing → Extra Files steht eine integrierte Funktion zur Verfügung, um zusätzliche Dateien in PSADT-Pakete einzubinden. Das bisher eingesetzte Custom-Skript auf Basis von General\CustomFiles kann dadurch abgelöst werden. Es ist also ratsam, bestehende Pakete umzustellen.
Für die Bestandsaufnahme steht das Skript Get-CustomFilesReport.ps1 zur Verfügung
Funktionsumfang
Das Skript ermittelt alle Pakete, deren Verzeichnisse Files oder SupportFiles unterhalb von CustomFiles Inhalte enthalten. Die Ergebnisse werden in der Konsole ausgegeben und zusätzlich als Übersicht und als Detailliste im CSV-Format exportiert.
Hinweise
- Das Skript greift ausschließlich lesend zu. Es werden keine Dateien verändert oder gelöscht.
- Das Standardverzeichnis
SupportFiles\Userwird nicht als Inhalt gewertet. Dateien darin werden jedoch erfasst. - Standardmäßig werden nur befüllte Verzeichnisse ausgegeben. Mit
-IncludeEmptywerden auch die leeren Verzeichnisse aufgeführt. Mit-Recurseenthält die Detailliste den vollständigen Inhalt aller Unterverzeichnisse. - Die Verzeichnisse
LogosundAppDeployToolkitsind nicht Bestandteil der Prüfung.
Vorteile der Umstellung
- Die zusätzlichen Dateien sind direkt am Paket dokumentiert und nachvollziehbar.
- Es gibt keine Abhängigkeit mehr von einem individuell gepflegten Pipeline-Skript.
- Das Risiko von Inkompatibilitäten bei zukünftigen Updates sinkt.
Empfohlenes Vorgehen
- Bestandsaufnahme mit dem Skript durchführen.
- Die ermittelten Pakete auf „Extra Files“ migrieren.
- Das Custom-Skript aus der Pipeline entfernen.
Skript
<#
.SYNOPSIS
Report which packages contain custom content in their Files or SupportFiles directories
.DESCRIPTION
Script to scan the neo42 CustomFiles directory ("<GlobalGeneralDirectory>\CustomFiles\<AppVendor> <AppName>")
and report every package whose Files or SupportFiles directory contains anything
(files of any type, archives, executables, subdirectories, hidden items).
The placeholder directory SupportFiles\User exists in every package and is ignored itself.
Only content inside of it is reported.
Results are shown in the console and exported to two CSV files:
- Summary: one row per Files / SupportFiles directory
- Details: one row per item found
.PARAMETER GlobalGeneralDirectory
The directory where package assets are stored. Example Global.GeneralDirectory = C:\neo42\General
The script scans the subdirectory 'CustomFiles'
.PARAMETER OutputDirectory
The directory where the CSV reports are written. Default: current directory
.PARAMETER Recurse
List the complete content of Files / SupportFiles including all subdirectories in the details report.
Without this switch only the top level items are listed. File count and size are always calculated recursively.
.PARAMETER IncludeEmpty
Show and export empty directories as well. By default only directories which contain content are reported
.PARAMETER PlaceholderDirectories
Directories directly below SupportFiles which exist in every package and are ignored themselves. Default: User
.OUTPUTS
CSV files: CustomFilesReport_Summary_<timestamp>.csv and CustomFilesReport_Details_<timestamp>.csv
.EXAMPLE
.\Get-CustomFilesReport.ps1 -GlobalGeneralDirectory "E:\neo42\General"
.EXAMPLE
.\Get-CustomFilesReport.ps1 -GlobalGeneralDirectory "E:\neo42\General" -IncludeEmpty -Recurse -OutputDirectory "C:\Temp"
.NOTES
Author: DBT - neo42 GmbH
Creation Date: 29.09.2026
Tested on: 4.9.3.0
#>
[CmdletBinding()]
Param (
[Parameter(Mandatory = $true)]
[ValidateScript({$true -eq $_.Exists})]
[System.IO.DirectoryInfo]
$GlobalGeneralDirectory,
[Parameter(Mandatory = $false)]
[System.IO.DirectoryInfo]
$OutputDirectory = (Get-Location).Path,
[Parameter(Mandatory = $false)]
[switch]
$Recurse,
[Parameter(Mandatory = $false)]
[switch]
$IncludeEmpty,
[Parameter(Mandatory = $false)]
[string[]]
$PlaceholderDirectories = @('User')
)
# Set path variables
$customFiles = Join-Path $GlobalGeneralDirectory.FullName "CustomFiles"
$directoryNames = @('Files', 'SupportFiles')
if ($false -eq (Test-Path -Path $customFiles -PathType Container)) {
throw "The directory '$customFiles' does not exist"
}
# Returns the path of an item relative to a base path
function Get-RelativePath {
Param (
[string]$BasePath,
[string]$Path
)
$BasePath = $BasePath.TrimEnd('\', '/')
if ($true -eq $Path.StartsWith($BasePath, [System.StringComparison]::OrdinalIgnoreCase)) {
return $Path.Substring($BasePath.Length).TrimStart('\', '/')
}
return $Path
}
Write-Host "Scanning '$customFiles' (ignored placeholder directories: $($PlaceholderDirectories -join ', ')) ..." -ForegroundColor Cyan
$summary = New-Object System.Collections.Generic.List[object]
$details = New-Object System.Collections.Generic.List[object]
# Scan every package directory ("<AppVendor> <AppName>")
$packages = Get-ChildItem -Path $customFiles -Directory -Force -ErrorAction SilentlyContinue | Sort-Object Name
foreach ($package in $packages) {
foreach ($directoryName in $directoryNames) {
$directory = Join-Path $package.FullName $directoryName
if ($false -eq (Test-Path -Path $directory -PathType Container)) {
continue
}
# Determine placeholder directories (only directly below SupportFiles)
$placeholderPaths = @()
if ('SupportFiles' -eq $directoryName) {
$placeholderPaths = @(Get-ChildItem -LiteralPath $directory -Directory -Force -ErrorAction SilentlyContinue |
Where-Object { $PlaceholderDirectories -contains $_.Name } |
ForEach-Object { $_.FullName })
}
# Collect items, placeholder directories themselves are excluded
$items = @(Get-ChildItem -LiteralPath $directory -Recurse:$Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { $placeholderPaths -notcontains $_.FullName })
if ($false -eq $Recurse.IsPresent) {
# Without -Recurse the direct content of placeholder directories is listed as well
foreach ($placeholderPath in $placeholderPaths) {
$items += @(Get-ChildItem -LiteralPath $placeholderPath -Force -ErrorAction SilentlyContinue)
}
}
# Top level items (including direct content of placeholder directories)
$topLevelItems = @($items | Where-Object {
$parentPath = Split-Path -Path $_.FullName -Parent
($parentPath -eq $directory) -or ($placeholderPaths -contains $parentPath)
})
# File count and size are always calculated recursively
$allFiles = @(Get-ChildItem -LiteralPath $directory -File -Recurse -Force -ErrorAction SilentlyContinue)
$allDirectories = @(Get-ChildItem -LiteralPath $directory -Directory -Recurse -Force -ErrorAction SilentlyContinue |
Where-Object { $placeholderPaths -notcontains $_.FullName })
$size = ($allFiles | Measure-Object -Property Length -Sum).Sum
if ($null -eq $size) {
$size = 0
}
$summary.Add([PSCustomObject]@{
Package = $package.Name
Directory = $directoryName
Status = if ($items.Count -gt 0) { 'HasContent' } else { 'Empty' }
FileCount = $allFiles.Count
DirectoryCount = $allDirectories.Count
SizeMB = [math]::Round($size / 1MB, 2)
FileTypes = (($allFiles | ForEach-Object { if ($_.Extension) { $_.Extension.ToLower() } else { '(none)' } } |
Sort-Object -Unique) -join ', ')
TopLevelItems = (($topLevelItems | ForEach-Object {
$relativePath = Get-RelativePath -BasePath $directory -Path $_.FullName
if ($true -eq $_.PSIsContainer) { "[$relativePath]" } else { $relativePath }
}) -join ' | ')
FullPath = $directory
})
foreach ($item in $items) {
$details.Add([PSCustomObject]@{
Package = $package.Name
Directory = $directoryName
Item = Get-RelativePath -BasePath $directory -Path $item.FullName
Type = if ($true -eq $item.PSIsContainer) { 'Directory' } else { 'File' }
Extension = if ($true -eq $item.PSIsContainer) { '' } else { $item.Extension.ToLower() }
SizeMB = if ($true -eq $item.PSIsContainer) { '' } else { [math]::Round($item.Length / 1MB, 3) }
LastModified = $item.LastWriteTime.ToString('yyyy-MM-dd HH:mm')
FullPath = $item.FullName
})
}
}
}
if (0 -eq $summary.Count) {
Write-Warning "No 'Files' or 'SupportFiles' directories found below '$customFiles'"
return
}
# Console output
$result = if ($true -eq $IncludeEmpty.IsPresent) { $summary } else { @($summary | Where-Object { 'HasContent' -eq $_.Status }) }
Write-Host ""
Write-Host "=== Summary ===" -ForegroundColor Cyan
foreach ($group in ($result | Group-Object Package)) {
Write-Host ""
Write-Host $group.Name -ForegroundColor White
foreach ($entry in $group.Group) {
if ('HasContent' -eq $entry.Status) {
Write-Host (" {0,-13} {1} file(s), {2} directory(s), {3} MB -> {4}" -f `
$entry.Directory, $entry.FileCount, $entry.DirectoryCount, $entry.SizeMB, $entry.TopLevelItems) -ForegroundColor Yellow
}
else {
Write-Host (" {0,-13} empty" -f $entry.Directory) -ForegroundColor DarkGray
}
}
}
$withContent = @($summary | Where-Object { 'HasContent' -eq $_.Status })
Write-Host ""
Write-Host ("Packages: {0} | Directories checked: {1} | with content: {2} | empty: {3}" -f `
$packages.Count, $summary.Count, $withContent.Count, ($summary.Count - $withContent.Count)) -ForegroundColor Cyan
# Export CSV reports (-UseCulture uses the list separator of the current culture, e.g. ';' for de-DE)
if ($false -eq (Test-Path -Path $OutputDirectory.FullName -PathType Container)) {
New-Item -Path $OutputDirectory.FullName -ItemType Directory | Out-Null
}
$timestamp = Get-Date -Format 'yyyyMMdd_HHmmss'
$csvSummary = Join-Path $OutputDirectory.FullName "CustomFilesReport_Summary_$timestamp.csv"
$csvDetails = Join-Path $OutputDirectory.FullName "CustomFilesReport_Details_$timestamp.csv"
$result | Export-Csv -LiteralPath $csvSummary -UseCulture -NoTypeInformation -Encoding UTF8
Write-Host ""
Write-Host "Summary: $csvSummary" -ForegroundColor Green
if ($details.Count -gt 0) {
$details | Export-Csv -LiteralPath $csvDetails -UseCulture -NoTypeInformation -Encoding UTF8
Write-Host "Details: $csvDetails" -ForegroundColor Green
}
Ausgeführt werden kann es wie folgt:
.\Get-CustomFilesReport.ps1 -GlobalGeneralDirectory "<Pfad>\neo42\General"
Erwartetes Ergebnis:
Rückfragen und Anmerkungen gerne in diesem Thread.
