PowerShell Touch

PowerShell Touch

Ich stand vor demselben Problem und habe mir ein PowerShell CmdLet zusammengestellt, dass als würdiger touch-Ersatz in der Kommandozeile genutzt werden kann.

Im Unternehmen räumen wir alte Files knallhart auf: alles, was älter als 10 Jahre ist, wird vom Fileserver gelöscht. Schon alleine, als weitere Sicherungslinie, um die DSGVO umzusetzen, falls andere Prüfverfahren Dokumente übersehen haben. Nach 10 Jahren sind alle relevanten Verjährungsfristen soweit abgelaufen. Das klappt ganz gut, da die Mitarbeiter angehalten sind, relevante Dokumente im DMS abzulegen. Also kann sich, zumindest per Definition, nichts wichtiges auf den Fileservern befinden.

Um alte Dateien dennoch vor dem Shredder zu bewahren, muss das letzte Schreibdatum auf einen Wert jünger als 10 Jahre zurückgesetzt werden. Leider hat Windows kein vernünftiges touch-Tool um die verschiedenen Zeitstempel von Dateien einfach anzupassen, wie beispielsweise Linux. Das lässt sich zwar alles nachrüsten, aber spätestens bei PowerShell Skripten gibt es in der Anwendung einen Stilbruch.

Um die Zeitstempel anzupassen müssen Sie statt eines CmdLet das Tool Ihrer Wahl aufrufen. Das funktioniert zwar auch und bringt Sie auch zum Ziel, ist aber irgendwie unschön.

Alternativ können Sie natürlich das Datum über das FileInfo-Objekt selber setzen, aber auch das erfindet jedes mal das Rad neu, weil doch mehr als nur ein bequemer Befehl notwendig ist.

Der Weg zur Lösung

PowerShell kann das selbstverständlich mit Bordmitteln erledigen, aber es gibt kein CmdLet, dass die gesamte Arbeit abnimmt. Ich habe im Web zwar einige CmdLets gefunden, die den Job übernehmen können, aber die haben mir alle nicht so ganz gefallen.

Vor allem habe ich kein CmdLet gefunden, dass Pipelining unterstützt. Das wiederum war mir wichtig, weil ich damit zur Auswahl der zu verändernden Dateien Get-ChildItem bemühen kann und das Rad nicht nochmals erfinden muss.

Umsetzung

Das PowerShell CmdLet kann jetzt also nicht nur mit Dateien umgehen, die ihm als Parameter übergeben werden, sondern auch mit der Pipe:

PS C:\> Update-FileInfo *.txt
PS C:\> Get-ChildItem *.txt | Update-FileInfo

Bei den Parametern habe ich den Datentyp von $Path offen gelassen, um diesen Parameter sowohl direkt als String, wie auch über die Pipeline, verwenden zu können. Get-ChildItem liefert eine Reihe von Objekttypen, abhängig vom Provider, zurück. Eine entsprechende Unterscheidung findet dann später im CmdLet statt.

Im Skript habe ich mich für eine Quick&Dirty Lösung entschieden: es kann nur mit Objekten vom Typ String und FileInfo umgehen, bei allen anderen Objekten wird eine Fehlermeldung ausgegeben. Ich wüßte aber im Moment auch nicht, ob es wirklich noch weitere sinnvolle Objekttypen an dieser Stelle gibt.

# (c) Michael Grabowski
# Version 1.0.0 vom 01.11.2019

Function Update-FileTime {
Param (
      [Parameter(Mandatory = $true, ValueFromPipeLine = $true)]
      $Path,

      [Parameter(Mandatory = $false)]
      [string] $DesiredTime = (Get-Date),
      
      [Parameter(Mandatory = $false)]
      [switch] $SetCreationTime = $false,

      [Parameter(Mandatory = $false)]
      [switch] $SetLastAccessTime = $false,

      [Parameter(Mandatory = $false)]
      [switch] $SetLastWriteTime = $false,

      [Parameter(Mandatory = $false)]
      [switch] $AlterDirectory = $false
   )

   Begin {
      If (!$SetCreationTime -and !$SetLastAccessTime -and !$SetLastWriteTime) {
         $SetLastAccessTime = $true
      }
      
      Try {
         [datetime] $_desiredTime = Get-Date -Date $DesiredTime
      }
      Catch {
         throw ("Update-FileTime : {0}" -f $PSItem.Exception.Message)
      }
   }

   Process {
      Switch ($Path.GetType().Name) {
         "String" { $fileList = Get-ChildItem -Path $Path }
         "FileInfo" { $fileList = $Path }
         "DirectoryInfo" { If ($AlterDirectory) { $fileList = $Path } Else { $fileList = @() } }
         default { Write-Warning ("Type {0} is not supported." -f $Path.GetType().Name); $fileList = @() }
      }

      ForEach($fileItem in $fileList) {
         If ($SetCreationTime) { $fileItem.CreationTime = $_desiredTime }
         If ($SetLastAccessTime) { $fileItem.LastAccessTime = $_desiredTime }
         If ($SetLastWriteTime) { $fileItem.LastWriteTime = $_desiredTime }

         return $fileItem
      }
   }

   End {
      
   }
}

Demo

Die Anwendung des CmdLets ist, alleine schon mangels vieler Optionen, simpel. Ich habe das bewusst so gehalten, da die Dateiauswahl mit Hilfe von Get-ChildItem viel flexibler gestaltet werden kann und damit ja auch schon ein sehr gutes Werkzeug dafür zur Verfügung steht.