Variable for "Media created"? (Powershell) - powershell

What is the variable for "Media created", using Powershell?
I'm trying to change many .mov files at one time, in a folder, from "IMG-3523" to their respective creation dates.
I was using the following code to do so, using LastWriteTime:
Get-ChildItem | Rename-Item -NewName {$_.LastWriteTime.ToString("yyyy-MM-dd hh.mm.ss ddd") + ($_.Extension)}
However for some reason there are many duplicate entries for that variable, and I run into an error stating so.
Is there any way I can edit this code to use the "Media created" variable? It's the only date without duplicates.
(I've tried editing the code to include microseconds and $_.CreationTime, however there are many duplicates with that variable too (actually all have the same CreationTime - the timestamp of when I copied the files over from an external disk).

For all files within a directory:
$fldPath = "D:\FolderName";
$flExt = ".mov";
$attrName = "media created"
(Get-ChildItem -Path "$fldPath\*" -Include "*$flExt").FullName | % {
$path = $_
$shell = New-Object -COMObject Shell.Application;
$folder = Split-Path $path;
$file = Split-Path $path -Leaf;
$shellfolder = $shell.Namespace($folder);
$shellfile = $shellfolder.ParseName($file);
$a = 0..500 | % { Process { $x = '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_); If ( $x.split("=")[1].Trim() ) { $x } } };
[int]$num = $a | % { Process { If ($_ -like "*$attrName*") { $_.Split("=")[0].trim() } } };
$mCreated = $shellfolder.GetDetailsOf($shellfile, $num);
$mCreated;
};
For one specific File, you can use:
$flPath = "D:\FolderName\FileName.mov";
$attrName = "media created"
$path = $flPath;
$shell = New-Object -COMObject Shell.Application;
$folder = Split-Path $path;
$file = Split-Path $path -Leaf;
$shellfolder = $shell.Namespace($folder);
$shellfile = $shellfolder.ParseName($file);
$a = 0..500 | % { Process { $x = '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_); If ( $x.split("=")[1].Trim() ) { $x } } };
[int]$num = $a | % { Process { If ($_ -like "*$attrName*") { $_.Split("=")[0].trim() } } };
$mCreated = $shellfolder.GetDetailsOf($shellfile, $num);
$mCreated;
This was answered in SuperUser by vomit-it-chunky-mess-style on Getting a Media Created via PS

Using the Shell.Application object you can get this value from the file, but unfortunately, the property names are all Localized, so if you are looking for a property named Media created on a non-english machine, you will not find it..
For .MOV files, the index of this property in numbered 4.
You can use below function to get that value for all the files in the path and then use that to rename them:
function Get-MetaData {
[CmdletBinding()]
[OutputType([Psobject[]])]
Param (
# Path can be the path to a folder or the full path and filename of a single file
[Parameter(Mandatory = $true, ValueFromPipeline = $true, Position = 0)]
[string]$Path,
# Filter is unused if Path is pointing to a single file
[Alias('Pattern')]
[string]$Filter = '*.*',
[Alias('Indices')]
[int[]]$Properties = 1..500,
# Recurse is unused if Path is pointing to a single file
[switch]$Recurse,
[switch]$IncludeEmptyProperties
)
$item = Get-Item -Path $Path -ErrorAction SilentlyContinue
if (!$item) { Write-Error "$Path could not be found."; return }
if (!$item.PSIsContainer) {
# it's a file
$files = #($item)
$Path = $item.DirectoryName
}
else {
# it's a folder
$files = Get-ChildItem -Path $Path -Filter $Filter -File -Recurse:$Recurse
}
$shell = New-Object -ComObject "Shell.Application"
$objDir = $shell.NameSpace($Path)
foreach($file in $files) {
$objFile = $objDir.ParseName($file.Name)
$mediaFile = $objDir.Items()
foreach($index in $Properties) {
$name = $objDir.GetDetailsOf($mediaFile, $index)
if (![string]::IsNullOrWhiteSpace($name)) {
# to be on the safe side, remove any control character (ASCII < 32) that may be in there
$value = $objDir.GetDetailsOf($objFile, $index) -replace '[\x00-\x1F]+'
if (![string]::IsNullOrWhiteSpace($value) -or $IncludeEmptyProperties) {
[PsCustomObject]#{
Path = $file.FullName
Index = $index
Property = $name
Value = $value
}
}
}
}
}
# clean-up Com objects
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objFile)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objDir)
$null = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($shell)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
# property index 3 --> Last Modified
# property index 4 --> Date Created # we'll use this one
# property index 5 --> Last Accessed
$SourceFolder = 'X:\Somewhere'
Get-MetaData -Path $SourceFolder -Filter '*.mov' -Properties 4 |
ForEach-Object {
# HH for 24-hour clock; hh for 12-hour clock
$dateCreated = '{0:yyyy-MM-dd HH.mm.ss ddd}' -f ([datetime]$_.Value)
$directory = [System.IO.Path]::GetDirectoryName($_.Path)
$extension = [System.IO.Path]::GetExtension($_.Path)
# test if the suggested new name is already in use, append a sequence
# number to the basename until we have a unique name
$newName = '{0}{1}' -f $dateCreated, $extension
$index = 1
while (Test-Path -Path (Join-Path -Path $directory -ChildPath $newName) -PathType Leaf) {
$newName = '{0}({1}){2}' -f $dateCreated, $index++, $extension
}
Rename-Item -Path $_.Path -NewName $newName -WhatIf
}
The -WhatIf is a safety measure. It will make the code only show in the console what would happen. No file is actually renamed.
If you are convinced that output is correct, then remove the -WhatIf switch and run the code again

As "Media created" property of a .mov file might also be multiple, or undefined in many cases, use it may cause also file name duplication, even error.
I'd propose another approach, we first check existence of both "Media created" and "Date taken", if nonexistent, we keep using $_.LastWriteTime as basic file name, adding a number to each file name if duplicated, like this:
#
# fnc001: get file media created, date taken or LastWriteTime:
#
function entGetMediaCreatedOrLastWriteTime($objFile) {
$idxMediaCreated = 208
$objShell = New-Object -COMObject Shell.Application
$objShellFolder = $objShell.NameSpace($objFile.DirectoryName)
$iState = [System.Runtime.Interopservices.Marshal]::ReleaseComObject($objShell)
$objShellFile = $objShellFolder.ParseName($objFile.Name)
$mediaCreated = $objShellFolder.GetDetailsOf($objShellFile, $idxMediaCreated)
#
# if media created is empty, we check if we have Date taken:
#
if($mediaCreated -eq "") {
#
# canon cameras set Date taken for photos:
#
$idxDateTaken = 12
$dateTaken = $objShellFolder.GetDetailsOf($objShellFile, $idxDateTaken)
#
# return LastWriteTime if neither media created, nor Date taken:
#
if($dateTaken -eq "") {
return $objFile.LastWriteTime
}
#
# otherwise return Date taken, removing non-ascii before:
#
else
{
return [DateTime]($dateTaken -replace '\P{IsBasicLatin}')
}
}
#
# otherwise return valid media created, removing non-ascii before:
#
else {
return [DateTime]($mediaCreated -replace '\P{IsBasicLatin}')
}
}
#
# fnc001: increment filename if it already exists:
#
function entIncrementIfExistent($filename) {
$fnew = $filename
$ext = Split-Path $filename -Extension
#
# define prefix before file number:
#
$prefix = "-"
$i = 0
#
# save file base length:
#
$lngbase = $fnew.length - $ext.length
#
# here $filename is like
# 2023-02-05 08.33.00 Sun.mov,
#
# if it exists, we try to use first available of:
#
# 2023-02-05 08.33.00 Sun-1.mov
# 2023-02-05 08.33.00 Sun-2.mov
# ...
# 2023-02-05 08.33.00 Sun-9.mov
# ...
# 2023-02-05 08.33.00 Sun-10.mov
# 2023-02-05 08.33.00 Sun-11.mov
# ...
#
while (Test-Path $fnew)
{
$i++
$fnew = $fnew.Substring(0, $lngbase) + $prefix + $i + $ext
}
return $fnew
}
Get-ChildItem *.mov | Rename-Item -NewName {
$xDateTime = entGetMediaCreatedOrLastWriteTime $_
$fnew = $xDateTime.ToString("yyyy-MM-dd hh.mm.ss ddd") + $_.Extension
entIncrementIfExistent $fnew
}
I've tried this code, it gets something like this:

Related

Unable to delete or rename directories or Files with Multiple Square ([[...]] brackets in Path [duplicate]

This question already has answers here:
How can I make PowerShell handle [ or ] in file name well?
(2 answers)
Error "Could not find a part of the path" while setting attributes on an existing file
(2 answers)
Closed 4 months ago.
This post was edited and submitted for review 4 months ago and failed to reopen the post:
Original close reason(s) were not resolved
I have regular Jobs to clean up old User Windows-Profile directories on a central NAS.
Last time I had Directories containing double square Brackets in the path (Import from Macromedia or so). This looks like:
\server.ad.local\HOME1\Username\System\Anwendungsdaten\Macromedia\FlashPlayer\SharedObjects\G5EUZBX2\www.daserste.de\[[IMPORT]]\players.edgesuite.net\flash\plugins\osmf\advanced-streaming-plugin\v3.4\osmf2.0\AkamaiAdvancedStreamingPlugin.swf\HDCore.sol
As this Job should run automatically with Powershell I tired various things:
Tried to replace the brackets an rename the folder - no success
Tried LiteralPath for Remove-Item - no success
Tried to Deleted with Scripting.FileSystemObject - no success
I always get the following error Message:
The Element ... cannot be removed: A part of the Path "HDCore.sol" cannot be found.
Are there any ideas?
Tried to rename the Folder, tried remove-item with -LiteralPath, tried to use FileSystemObject.
All of the actions gave the same result: Error
Just to complete: here are the functions I used last:
Function RemoveChar
{
Param ($Path)
$Pattern = '#|&|%|\[{1,}|\]{1,}|\^|\s|\.{2,}'
if ($Path -imatch $Pattern){
Rename-Item -Path $Path -NewName ($Path -replace $Pattern,'') -ErrorAction SilentlyContinue
return ($Path -replace $Pattern,'')
} else {
return $Path
}
}
Function Truncate
{
Param ($Path)
$total_path_length_threshold = 248 # maximal erlaubte Zeichenzahl (248 für Verzeichnisse, 260 für Dateien)
$characters_to_truncate = 60 # Anzahl der zeichen, um die der name zu kürzen ist. Ein eindeutiger Index wird angehangen
$virtual_drive = "v:" # Für temp. Prozessing muss das Format "v:" haben
$collection = cmd /c dir $Path /s /b |? {$_.length -gt $total_path_length_threshold }
$count_paths = ($collection | measure).count - 1
foreach ($pathlong in $collection) {
$parent_path = Split-path -path $pathlong
subst $virtual_drive $parent_path
$leaf = split-path -leaf $pathlong
$short_virtual_path = join-path $virtual_drive $leaf
$item = Get-Item -LiteralPath $short_virtual_path
if (Test-Path -LiteralPath $item) {
$filename = $item.name
$filename_extension = $item.Extension
$basename = $item.BaseName
$basename_length = $basename.length
$new_index = "X" + $counter + "X"
$adjusted_characters_to_truncate = $characters_to_truncate + $new_index.length
if ( $basename_length -gt $adjusted_characters_to_truncate ) {
$length_to_use = $basename_length - $adjusted_characters_to_truncate
$new_filename = $basename.substring(0, $length_to_use ) + $new_index + $filename_extension
$new_path = $parent_path + $new_filename
$Path = $new_path
Rename-Item -LiteralPath $short_virtual_path -NewName $new_filename
}
}
subst v: /d
}
return $Path
}
Function removeRecursive
{
param([String] $Path)
$fso = new-object -com "Scripting.FileSystemObject"
function proc {
param($folder)
$folder.Files | foreach-object {
RemoveChar $_
}
$folder.Files | foreach-object {
$short = $fso.GetFile($_.FullName).ShortPath
LogWrite "$FullDate : Processing: $short"
$fso.DeleteFile($short,$true)
}
$folder.SubFolders | foreach-object {
proc $_
}
}
proc $fso.GetFolder($Path)
}
The function I call from main code is removeRecursive. And yes I tried Remove-Item -LiteralPath SomePath -Recursive -Force too but no success as well.

Powershell to return last existing folder in path

I need some help because test-path only returns $true or $false.
I need to return the last existing folder from $path.
$path = \\test.pl\power\shell\test\company
But the last one that exists is the \shell folder.
So how to get a return value like this:
$existingpath = \\test.pl\power\shell
$notexisting = \test\company
An attempt of solving this using the pipeline:
$path = '\\test.pl\power\shell\test\company'
# Create an array that consists of the full path and all parents
$pathAndParents = for( $p = $path; $p; $p = Split-Path $p ) { $p }
# Find the first existing path
$existingPath = $pathAndParents | Where-Object { Test-Path $_ } | Select-Object -First 1
# Extract the non-existing part of the path
$nonExistingPath = $path.SubString( $existingPath.Length + 1 )
The for loop creates a temporay variable $p so the original path will not be destroyed. At each iteration it outputs variable $p, which is automatically added to the array, due to PowerShell's implicit output behaviour. Then it sets $p to the parent path of $p (Split-Path with a single unnamed argument returns the parent path). The loop exits when there is no parent anymore.
The Where-Object | Select-Object line may seem inefficent, but because of Select-Object argument -First 1 it actually tests only the necessary number of paths. When the first existing path is found, the pipeline will be exited (like a break statement in a loop).
The above was the accepted answer. The following solution has been added later. It is more efficient, because it calls Split-Path only as many times as necessary and doesn't need Where-Object and Select-Object.
$path = '\\test.pl\power\shell\test\company'
# Find the first existing path
for( $existingPath = $path;
$existingPath -and -not (Test-Path $existingPath);
$existingPath = Split-Path $existingPath ) {}
# Extract the non-existing part of the path
$nonExistingPath = $path.SubString( $existingPath.Length + 1 )
You can check each folder path one by one, using Split-Path, Test-Path and a while loop:
$path = '.\test.pl\power\shell\test\company'
# Check folder at current path
while(-not(Test-Path $path)){
# Move on to the parent folder
$path = $path |Split-Path
}
# $path will now be `'.\test.pl\power\shell'`
$path
Further to the first answer, it is preferable to resolve and test the root of the passed path before processing:
$path = '\\test.pl\power\shell\test\company'
$resolvedPath = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($path)
$root = Split-Path $resolvedPath -Qualifier
if (Test-Path $root -PathType Container) {
while (-not (Test-Path $resolvedPath -PathType Container)) {
$resolvedPath = Split-Path $resolvedPath
}
} else {
$resolvedPath = ''
}
$resolvedPath
Or using .NET methods:
$root = [System.IO.Path]::GetPathRoot($resolvedPath)
if ([System.IO.Directory]::Exists($root)) {
while (-not [System.IO.Directory]::Exists($resolvedPath)) {
$resolvedPath = [System.IO.Path]::GetDirectoryName($resolvedPath)
}
} else {
$resolvedPath = ''
}
$resolvedPath

Powershell - Variable problems

I wrote a script that will pull data from a .properties file (basically a config file). Some of the data from the properties file has environment data (i.e. %UserProfile%), so I run it through a function (Resolve–EnvVariable) that will replace the environment variable with the actual value. The replace works perfectly, but somehow the data seems to be altered.
When I try to use the values that have been run through the function, they no longer work (see results down below).
This is the file contents of c:\work\test.properties
types="*.txt"
in="%UserProfile%\Downloads"
This is my PowerShell Script
Clear-Host
#Read the properties file and replace the parameters when specified
if (Test-Path C:\work\test.properties) {
$propertiesFile = Get-Content C:\work\test.properties
Write-Host "Parameters will be substituded from properties file" -ForegroundColor Yellow
foreach ($line in $propertiesFile) {
Write-Host ("from Properties file $line")
$propSwitch = $line.Split("=")[0]
$propValue = Resolve–EnvVariable($line.Split("=")[1])
switch ($propSwitch) {
"types" { $types = $propValue }
"in" { $in = $propValue }
}
}
}
write-host ("After running through function `n in=" + $in + "<- types=" + $types + "<-")
# This function resolves environment variables
Function Resolve–EnvVariable {
[cmdletbinding()]
Param(
[Parameter(Position = 0, ValueFromPipeline = $True, Mandatory = $True,
HelpMessage = "Enter string with env variable i.e. %APPDATA%")]
[ValidateNotNullOrEmpty()]
[string]$String
)
Begin {
Write-Verbose "Starting $($myinvocation.mycommand)"
} #Begin
Process {
#if string contains a % then process it
if ($string -match "%\S+%") {
Write-Verbose "Resolving environmental variables in $String"
#split string into an array of values
$values = $string.split("%") | Where-Object { $_ }
foreach ($text in $values) {
#find the corresponding value in ENV:
Write-Verbose "Looking for $text"
[string]$replace = (Get-Item env:$text -erroraction "SilentlyContinue").Value
if ($replace) {
#if found append it to the new string
Write-Verbose "Found $replace"
$newstring += $replace
}
else {
#otherwise append the original text
$newstring += $text
}
} #foreach value
Write-Verbose "Writing revised string to the pipeline"
#write the string back to the pipeline
Write-Output $NewString
} #if
else {
#skip the string and write it back to the pipeline
Write-Output $String
}
} #Process
End {
Write-Verbose "Ending $($myinvocation.mycommand)"
} #End
} #end Resolve-EnvVariable
# Hardcoded values work
$test1 = Get-ChildItem -Path "C:\Users\Paul\Downloads" -Recurse -Include "*.txt"
# Values pulled and updated through function do not work
$test2 = Get-ChildItem -Path $in -Recurse -Include $types
# If I manually assign the values, it works
$in = "C:\Users\Paul\Downloads"
$types = "*.txt"
$test3 = Get-ChildItem -Path $in -Recurse -Include $types
foreach ($test in $test1) { write-host "test1 $test" }
foreach ($test in $test2) { write-host "test2 $test" }
foreach ($test in $test3) { write-host "test3 $test" }
Results
Parameters will be substituded from properties file
from Properties file types="*.txt"
from Properties file in="%UserProfile%\Downloads"
After running through function
in="C:\Users\Paul\Downloads"<- types="*.txt"<-
test1 C:\Users\Paul\Downloads\Test\testPaul.txt
test1 C:\Users\Paul\Downloads\Test2\File1.txt
test3 C:\Users\Paul\Downloads\Test\testPaul.txt
test3 C:\Users\Paul\Downloads\Test2\File1.txt
Two alternatives:
1. Use Environment.ExpandEnvironmentVariables()
If you switched to non-qualified string values and escaped your \, it would be as simple as piping the file to ConvertFrom-StringData, at which point you could expand the variable values with Environment.ExpandEnvironmentVariables():
Properties file:
types=*.txt
in=%UserProfile%\\Downloads
Script:
# Convert file to hashtable
$properties = Get-Content file.properties -Raw |ConvertFrom-StringData
# Copy value to new hashtable, but expand env vars first
$expanded = #{}
foreach($entry in $properties.GetEnumerator()){
$expanded[$entry.Key] = [Environment]::ExpandEnvironmentVariables($entry.Value)
}
Should give you the desired values:
PS C:\> $expanded
Name Value
---- -----
in C:\Users\username\Downloads
types *.txt
2. Use and dot-source a PowerShell script for your properties
This is lifted straight out of a page of the original Exchange Server modules - place all configuration variables in separate scripts, which are in turn dot-sourced when initializing a new session:
Properties file:
$types = "*.txt"
$in = Join-Path $env:USERPROFILE Downloads
Script:
# dot source the variables
. (Join-Path $PSScriptRoot properties.ps1)
# do the actual work
Get-ChildItem $in -Include $types

Bulk File Renaming - Extract parent foldername and use in file renaming

PowerShell novice here again with my proof of concept.
The code below successfully extracts attached files from .msg files located in folders and leaves the extracted filename without changing it. What I'm now looking for now is to extract part of the parent folder name, with standard format of...
nnnn+string (e.g. "8322 MyStudy") i.e. 4 digits followed by a space then string.
...to rename the extracted filename from...
ExtractedFilename.pdf to "0nnnn - ExtractedFilename.pdf". e.g. "08322 - ExtractedFilename.pdf"
My main problem is how to extract the numeric part of the parent folder name (from where my module will be run). I'm hoping that my poor PS formatting skills will allow me to do the rest.
Once again, any help appreciated.
##
## Source: https://chris.dziemborowicz.com/blog/2013/05/18/how-to-batch-extract-attachments-from-msg-files-using-powershell/
##
## Usage: Expand-MsgAttachment *
##
##
function Expand-MsgAttachment
{
[CmdletBinding()]
Param
(
[Parameter(ParameterSetName="Path", Position=0, Mandatory=$True)]
[String]$Path,
[Parameter(ParameterSetName="LiteralPath", Mandatory=$True)]
[String]$LiteralPath,
[Parameter(ParameterSetName="FileInfo", Mandatory=$True, ValueFromPipeline=$True)]
[System.IO.FileInfo]$Item
)
Begin
{
# Load application
Write-Verbose "Loading Microsoft Outlook..."
$outlook = New-Object -ComObject Outlook.Application
}
Process
{
switch ($PSCmdlet.ParameterSetName)
{
"Path" { $files = Get-ChildItem -Path $Path }
"LiteralPath" { $files = Get-ChildItem -LiteralPath $LiteralPath }
"FileInfo" { $files = $Item }
}
$files | % {
# Work out file names
$msgFn = $_.FullName
# extract path, e.g. 'c:\path\to\'
$msgPath = Split-Path -Path $msgFn
# Skip non-.msg files
if ($msgFn -notlike "*.msg") {
Write-Verbose "Skipping $_ (not an .msg file)..."
return
}
# Extract message body
Write-Verbose "Extracting attachments from $_..."
$msg = $outlook.CreateItemFromTemplate($msgFn)
$msg.Attachments | % {
# Work out attachment file name
#$attFn = $msgFn -replace '\.msg$', " - Attachment - $($_.FileName)"
$attFn = Join-Path -Path $msgPath -ChildPath ($_.FileName)
# Do not try to overwrite existing files
if (Test-Path -literalPath $attFn) {
Write-Verbose "Skipping $($_.FileName) (file already exists)..."
return
}
# Save attachment
Write-Verbose "Saving $($_.FileName)..."
$_.SaveAsFile($attFn)
# Output to pipeline
Get-ChildItem -LiteralPath $attFn
}
}
}
# This function to rename expanded attachment file to study renaming standards
Function RenameExpandedAttachments {
}
End
{
Write-Verbose "Done."
}
}
The currently running script is :
$script:MyInvocation.MyCommand.Path
Use Split-Path to get only the Path,
Split-Path $script:MyInvocation.MyCommand.Path
to get only the last element use again Split-Path with the -Leaf parameter
Split-Path -Leaf (Split-Path $script:MyInvocation.MyCommand.Path)
To extract leading numbers use a Regular Expression with a (capture group).
'^(\d+) (.*)$'
And wrap all this in an if:
If ((Split-Path -Leaf (Split-Path $script:MyInvocation.MyCommand.Path)) -match '^(\d+) (.*)$'){
$NewName = "{0:00000} - {1}" -f $Matches[1],$ExtractedFileName
} else {
"No numbers found in this path"
}

Comparing filehash and outputting files

I am new to PowerShell and am writing a script to get the hash of a directory and store it in a .txt file.
I then want to compare it to an earlier version and check for changes. If there are changes, I want a new .txt or .html file containing which line items have changed, with last modified dates.
So far, I've gotten the comparison to work, and the resulting steps based upon the pass/fail work fine.
What I need help with is outputting the results into a .txt file that lists only the files that have changed, with fields of Algorithm, Hash, Filename, Last edit time. I know I can use
(Get-Item $source).LastWriteTime
To fetch the write time, but I need to do it for every file in the directory, not just the .txt file that contains the hash.
# Variables
$Hashstore = "d:\baseline.txt"
$HashCompare = "d:\hashcompare.txt"
$HashTemp = "d:\hashtemp.txt"
$FileDir = "d:\New2"
$DateTime = Get-Date -format M.d.yyyy.hh.mm.ss
# Email Variables
$smtp_server = '<yourSMTPServer>'
$to_email = '<email>'
$from_email = '<email>'
$dns_server = "<yourExternalDNSServer>"
$domain = "<yourDomain>"
# Check if Baseline.txt Exists
If (Test-Path $Hashstore)
# // File exists
{}
Else {
# // File does not exist - Should never happen!
$RefreshHash = dir $FileDir | Get-FileHash -Algorithm MD5
$RefreshHash | Out-File $Hashstore
}
# Generate new Compare Hash.txt
$HashNew = dir $FileDir -Recurse | Get-FileHash -Algorithm MD5
$HashNew | Out-File $HashCompare
# Get Hash of baseline.txt
$HashBaseline = Get-FileHash -Path d:\baseline.txt -Algorithm MD5
#Get Hash of hashcompare.txt
$HashDiff = Get-FileHash -Path d:\hashcompare.txt -Algorithm MD5
#If changed, output hash to storage, and flag changes
If ($HashBaseline.hash -eq $HashDiff.hash)
{
Add-Content -Path d:\success.$DateTime.txt -Value " Source Files ARE EQUAL </p>"
}
else
{
Add-Content -Path d:\failure.$DateTime.html -Value "Source Files NOT EQUAL </p>"
$HashNew | Out-File $HashTemp
}
# Compare two logs, send email if there is a change
If ($diff_results)
{
#$evt_message = Get-Content .\domain.new.txt | Out-String
#Write-EventLog -LogName Application -EventId 9000 -EntryType Error -Source "Maximo Validation Script" -Message $evt_message
#Send-MailMessage -To $to_email -From $from_email -SmtpServer $smtp_server -Attachments .\domain.new.txt -Subject "ALERT! Change in Records" -Body "A change has been detected in the Maximo system files.`n`n`tACTION REQUIRED!`n`nVerify that this change was authorized."
}
If ($HashNew.HashString -eq $Hashstore.HashString)
{
}
else
{
$HashTemp | Out-File $HashStore
}
I know the add-item may not be the best way to write to this log I'm creating. What would be the best way to add the last write time to every file that is read?
Here is a clean way to ouput the information you need (Algorithm, Hash, Filename, Last edit time) for each file that has changed :
$Hashstore = "d:\baseline.txt"
$HashCompare = "d:\hashcompare.txt"
$HashTemp = "d:\hashtemp.txt"
$FileDir = "d:\New2"
$DateTime = Get-Date -format M.d.yyyy.hh.mm.ss
# Check if Baseline.txt Exists
If (Test-Path $Hashstore)
# // File exists
{
}
Else {
# // File does not exist - Should never happen!
$RefreshHash = dir $FileDir -Recurse | Get-FileHash -Algorithm MD5
$RefreshHash | Export-Csv -Path $Hashstore -NoTypeInformation -Force
}
# Generate new Compare Hash.txt
$HashNew = dir $FileDir -Recurse | Get-FileHash -Algorithm MD5
$HashNew | Export-Csv -Path $HashCompare -NoTypeInformation -Force
# Get Hash of baseline.txt
$HashBaseline = Get-FileHash -Path $Hashstore -Algorithm MD5
#Get Hash of hashcompare.txt
$HashDiff = Get-FileHash -Path $HashCompare -Algorithm MD5
#If changed, output hash to storage, and flag changes
If ($HashBaseline.hash -eq $HashDiff.hash) {
Add-Content -Path D:\success.$DateTime.txt -Value " Source Files ARE EQUAL </p>"
}
Else {
Add-Content -Path D:\failure.$DateTime.txt -Value "Source Files NOT EQUAL </p>"
$HashNew | Export-Csv -Path $HashTemp -NoTypeInformation -Force
# Storing a collection of differences in $Diffs
$Diffs = Compare-Object -ReferenceObject (Import-Csv $Hashstore) -DifferenceObject (Import-Csv $HashCompare)
Foreach ($Diff in $Diffs) {
$DiffHashInfo = $Diff | Select-Object -ExpandProperty InputObject
$DiffFileInfo = Get-ChildItem -Path $DiffHashInfo.Path
# Creating a list of properties for the information you need
$DiffObjProperties = [ordered]#{'Algorithm'=$DiffHashInfo.Algorithm
'Hash'=$DiffHashInfo.Hash
'Filename'=$DiffFileInfo.Name
'Last edit time'=$DiffFileInfo.LastWriteTime
}
# Building a custom object from the list of properties in $DiffObjProperties
$DiffObj = New-Object -TypeName psobject -Property $DiffObjProperties
$DiffObj
}
}
Before creating the files $Hashstore and $HashCompare, I convert the information they contain to CSV format, rather than plain text.
It makes their content much easier to manipulate later , using Import-CSV.
This makes proper objects with properties I can use.
This also makes them easier to compare, and the result of this comparison ($Diffs) is a collection of these proper objects.
So $Diffs contains all the files that have changed and I loop through each of them in a Foreach statement.
This allows you to create a custom object ($DiffObj) with exactly the information you need ($DiffObjProperties) for each of the file that have changed.
PowerShell v3+ Recursive Directory Diff Using MD5 Hashing
I use this pure PowerShell (no dependencies) recursive file content diff. It calculates in-memory the MD5 hash (the algorithm is configurable) for each directories file contents and gives results in standard PowerShell Compare-Object format.
It can optionally export to CSV files along with a summary text file. It can either drop the rdiff.ps1 file into your path or copy the contents into your script.
USAGE: rdiff path/to/left,path/to/right [-s path/to/summary/dir]
Here is the gist. I copied below for reference but I recommend using the gist version as I will be adding new features to it over time.
#########################################################################
### USAGE: rdiff path/to/left,path/to/right [-s path/to/summary/dir] ###
### ADD LOCATION OF THIS SCRIPT TO PATH ###
#########################################################################
[CmdletBinding()]
param (
[parameter(HelpMessage="Stores the execution working directory.")]
[string]$ExecutionDirectory=$PWD,
[parameter(Position=0,HelpMessage="Compare two directories recursively for differences.")]
[alias("c")]
[string[]]$Compare,
[parameter(HelpMessage="Export a summary to path.")]
[alias("s")]
[string]$ExportSummary
)
### FUNCTION DEFINITIONS ###
# SETS WORKING DIRECTORY FOR .NET #
function SetWorkDir($PathName, $TestPath) {
$AbsPath = NormalizePath $PathName $TestPath
Set-Location $AbsPath
[System.IO.Directory]::SetCurrentDirectory($AbsPath)
}
# RESTORES THE EXECUTION WORKING DIRECTORY AND EXITS #
function SafeExit() {
SetWorkDir /path/to/execution/directory $ExecutionDirectory
Exit
}
function Print {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Message to print.")]
[string]$Message,
[parameter(HelpMessage="Specifies a success.")]
[alias("s")]
[switch]$SuccessFlag,
[parameter(HelpMessage="Specifies a warning.")]
[alias("w")]
[switch]$WarningFlag,
[parameter(HelpMessage="Specifies an error.")]
[alias("e")]
[switch]$ErrorFlag,
[parameter(HelpMessage="Specifies a fatal error.")]
[alias("f")]
[switch]$FatalFlag,
[parameter(HelpMessage="Specifies a info message.")]
[alias("i")]
[switch]$InfoFlag = !$SuccessFlag -and !$WarningFlag -and !$ErrorFlag -and !$FatalFlag,
[parameter(HelpMessage="Specifies blank lines to print before.")]
[alias("b")]
[int]$LinesBefore=0,
[parameter(HelpMessage="Specifies blank lines to print after.")]
[alias("a")]
[int]$LinesAfter=0,
[parameter(HelpMessage="Specifies if program should exit.")]
[alias("x")]
[switch]$ExitAfter
)
PROCESS {
if($LinesBefore -ne 0) {
foreach($i in 0..$LinesBefore) { Write-Host "" }
}
if($InfoFlag) { Write-Host "$Message" }
if($SuccessFlag) { Write-Host "$Message" -ForegroundColor "Green" }
if($WarningFlag) { Write-Host "$Message" -ForegroundColor "Orange" }
if($ErrorFlag) { Write-Host "$Message" -ForegroundColor "Red" }
if($FatalFlag) { Write-Host "$Message" -ForegroundColor "Red" -BackgroundColor "Black" }
if($LinesAfter -ne 0) {
foreach($i in 0..$LinesAfter) { Write-Host "" }
}
if($ExitAfter) { SafeExit }
}
}
# VALIDATES STRING MIGHT BE A PATH #
function ValidatePath($PathName, $TestPath) {
If([string]::IsNullOrWhiteSpace($TestPath)) {
Print -x -f "$PathName is not a path"
}
}
# NORMALIZES RELATIVE OR ABSOLUTE PATH TO ABSOLUTE PATH #
function NormalizePath($PathName, $TestPath) {
ValidatePath "$PathName" "$TestPath"
$TestPath = [System.IO.Path]::Combine((pwd).Path, $TestPath)
$NormalizedPath = [System.IO.Path]::GetFullPath($TestPath)
return $NormalizedPath
}
# VALIDATES STRING MIGHT BE A PATH AND RETURNS ABSOLUTE PATH #
function ResolvePath($PathName, $TestPath) {
ValidatePath "$PathName" "$TestPath"
$ResolvedPath = NormalizePath $PathName $TestPath
return $ResolvedPath
}
# VALIDATES STRING RESOLVES TO A PATH AND RETURNS ABSOLUTE PATH #
function RequirePath($PathName, $TestPath, $PathType) {
ValidatePath $PathName $TestPath
If(!(Test-Path $TestPath -PathType $PathType)) {
Print -x -f "$PathName ($TestPath) does not exist as a $PathType"
}
$ResolvedPath = Resolve-Path $TestPath
return $ResolvedPath
}
# Like mkdir -p -> creates a directory recursively if it doesn't exist #
function MakeDirP {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path create.")]
[string]$Path
)
PROCESS {
New-Item -path $Path -itemtype Directory -force | Out-Null
}
}
# GETS ALL FILES IN A PATH RECURSIVELY #
function GetFiles {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path to get files for.")]
[string]$Path
)
PROCESS {
ls $Path -r | where { !$_.PSIsContainer }
}
}
# GETS ALL FILES WITH CALCULATED HASH PROPERTY RELATIVE TO A ROOT DIRECTORY RECURSIVELY #
# RETURNS LIST OF #{RelativePath, Hash, FullName}
function GetFilesWithHash {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Path to get directories for.")]
[string]$Path,
[parameter(HelpMessage="The hash algorithm to use.")]
[string]$Algorithm="MD5"
)
PROCESS {
$OriginalPath = $PWD
SetWorkDir path/to/diff $Path
GetFiles $Path | select #{N="RelativePath";E={$_.FullName | Resolve-Path -Relative}},
#{N="Hash";E={(Get-FileHash $_.FullName -Algorithm $Algorithm | select Hash).Hash}},
FullName
SetWorkDir path/to/original $OriginalPath
}
}
# COMPARE TWO DIRECTORIES RECURSIVELY #
# RETURNS LIST OF #{RelativePath, Hash, FullName}
function DiffDirectories {
[CmdletBinding()]
param (
[parameter(Mandatory=$TRUE,Position=0,HelpMessage="Directory to compare left.")]
[alias("l")]
[string]$LeftPath,
[parameter(Mandatory=$TRUE,Position=1,HelpMessage="Directory to compare right.")]
[alias("r")]
[string]$RightPath
)
PROCESS {
$LeftHash = GetFilesWithHash $LeftPath
$RightHash = GetFilesWithHash $RightPath
diff -ReferenceObject $LeftHash -DifferenceObject $RightHash -Property RelativePath,Hash
}
}
### END FUNCTION DEFINITIONS ###
### PROGRAM LOGIC ###
if($Compare.length -ne 2) {
Print -x "Compare requires passing exactly 2 path parameters separated by comma, you passed $($Compare.length)." -f
}
Print "Comparing $($Compare[0]) to $($Compare[1])..." -a 1
$LeftPath = RequirePath path/to/left $Compare[0] container
$RightPath = RequirePath path/to/right $Compare[1] container
$Diff = DiffDirectories $LeftPath $RightPath
$LeftDiff = $Diff | where {$_.SideIndicator -eq "<="} | select RelativePath,Hash
$RightDiff = $Diff | where {$_.SideIndicator -eq "=>"} | select RelativePath,Hash
if($ExportSummary) {
$ExportSummary = ResolvePath path/to/summary/dir $ExportSummary
MakeDirP $ExportSummary
$SummaryPath = Join-Path $ExportSummary summary.txt
$LeftCsvPath = Join-Path $ExportSummary left.csv
$RightCsvPath = Join-Path $ExportSummary right.csv
$LeftMeasure = $LeftDiff | measure
$RightMeasure = $RightDiff | measure
"== DIFF SUMMARY ==" > $SummaryPath
"" >> $SummaryPath
"-- DIRECTORIES --" >> $SummaryPath
"`tLEFT -> $LeftPath" >> $SummaryPath
"`tRIGHT -> $RightPath" >> $SummaryPath
"" >> $SummaryPath
"-- DIFF COUNT --" >> $SummaryPath
"`tLEFT -> $($LeftMeasure.Count)" >> $SummaryPath
"`tRIGHT -> $($RightMeasure.Count)" >> $SummaryPath
"" >> $SummaryPath
$Diff | Format-Table >> $SummaryPath
$LeftDiff | Export-Csv $LeftCsvPath -f
$RightDiff | Export-Csv $RightCsvPath -f
}
$Diff
SafeExit
Another my version. But without date/time.
# Check images. Display if differ
#
$file_path = "C:\Files"
$last_state = "last_state.json"
# Check last_state.json. If false - create new empty file.
If (!(Test-Path $last_state)) {
New-Item $last_state -ItemType file | Out-Null
}
$last_state_obj = Get-Content $last_state | ConvertFrom-Json
# Get files list and hash. Also you can use -Recurse option
Get-ChildItem $file_path -Filter *.* |
Foreach-Object {
if (!$_.PSIsContainer) {
$current_state += #($_ | Get-FileHash -Algorithm MD5)
}
}
# Compare hash
ForEach ($current_file in $current_state) {
if (($last_state_obj | where {$current_file.Path -eq $_.Path}).Hash -ne $current_file.Hash) {
$changed += #($current_file)
}
}
# Display changed files
$changed
# Save new hash to last_state.json
$current_state | ConvertTo-JSON | Out-File $last_state