Migration von CustomFiles zu „Package Customizing → Extra Files“ – Analyseskript

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\User wird nicht als Inhalt gewertet. Dateien darin werden jedoch erfasst.
  • Standardmäßig werden nur befüllte Verzeichnisse ausgegeben. Mit -IncludeEmpty werden auch die leeren Verzeichnisse aufgeführt. Mit -Recurse enthält die Detailliste den vollständigen Inhalt aller Unterverzeichnisse.
  • Die Verzeichnisse Logos und AppDeployToolkit sind 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

  1. Bestandsaufnahme mit dem Skript durchführen.
  2. Die ermittelten Pakete auf „Extra Files“ migrieren.
  3. 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.

2 „Gefällt mir“