Enumerate file properties in PowerShell - powershell

I have seen bits of this in other questions, but I am looking for a generic way to write a function that will take a file, and list its properties in such a way that they can be used. I am aware of the function called Get-ItemProperty but it does not list the properties that I am looking for (for example, given a .avi file, it will not tell me the length, frame width, etc).
Am I using the function wrong (all I am doing is: Get-ItemProperty file) or do I have to do this a different way?
I want to be able to say something like $a += $file.Length, or something like that for arbitrary properties.

Sounds like you are looking for extended file attributes. These are not stored in System.IO.FileInfo.
One way is to use the Shell.Application COM object. Here is some example code:
http://web.archive.org/web/20160201231836/http://powershell.com/cs/blogs/tobias/archive/2011/01/07/organizing-videos-and-music.aspx
Say you had a video file: C:\video.wmv
$path = 'C:\video.wmv'
$shell = New-Object -COMObject Shell.Application
$folder = Split-Path $path
$file = Split-Path $path -Leaf
$shellfolder = $shell.Namespace($folder)
$shellfile = $shellfolder.ParseName($file)
You'll need to know what the ID of the extended attribute is. This will show you all of the ID's:
0..287 | Foreach-Object { '{0} = {1}' -f $_, $shellfolder.GetDetailsOf($null, $_) }
Once you find the one you want you can access it like this:
$shellfolder.GetDetailsOf($shellfile, 216)

Another possible method which also uses the Shell.Application COM object but does not require you to know what the ID’s of the extended attributes are. This method is preferred over using the ID’s because ID’s are different in different versions of Window (XP, Vista, 10, etc.)
$FilePath = 'C:\Videos\Test.mp4'
$Folder = Split-Path -Parent -Path $FilePath
$File = Split-Path -Leaf -Path $FilePath
$Shell = New-Object -COMObject Shell.Application
$ShellFolder = $Shell.NameSpace($Folder)
$ShellFile = $ShellFolder.ParseName($File)
Write-Host $ShellFile.ExtendedProperty("System.Title")
Write-Host $ShellFile.ExtendedProperty("System.Media.Duration")
Write-Host $ShellFile.ExtendedProperty("System.Video.FrameWidth")
Write-Host $ShellFile.ExtendedProperty("System.Video.FrameHeight")
The code will display the title of the video (if it is set), duration (100ns units, not milliseconds), and the videos frame width and height.
The names of other extended properties can be found in the file propkey.h, which is part of the Windows SDK.
Additional information:
ShellFolderItem.ExtendedProperty method
Predefined Property Set Format Identifiers

Related

Powershell: Converting Headers from .msg file to .txt - Current directory doesn't pull header information, but specific directory does

So I am trying to make a script to take a batch of .msg files, pull their header information and then throw that header information into a .txt file. This is all working totally fine when I use this code:
$directory = "C:\Users\IT\Documents\msg\"
$ol = New-Object -ComObject Outlook.Application
$files = Get-ChildItem $directory -Recurse
foreach ($file in $files)
{
$msg = $ol.CreateItemFromTemplate($directory + $file)
$headers = $msg.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x007D001E")
$headers > ($file.name +".txt")
}
But when I change the directory to use the active directory where the PS script is being run from $directory = ".\msg\", it will make all the files into text documents but they will be completely blank with no header information. I have tried different variations of things like:
$directory = -Path ".\msg\"
$files = Get-ChildItem -Path $directory
$files = Get-ChildItem -Path ".\msg\"
If anyone could share some ideas on how I could run the script from the active directory without needing to edit the code to specify the path each location. I'm trying to set this up so it can be done by simply putting it into a folder and running it.
Thanks! Any help is very appreciated!
Note: I do have outlook installed, so its not an issue of not being able to pull the headers, as it works when specifying a directory in the code
The easiest way might actually be to do it this way
$msg = $ol.CreateItemFromTemplate($file.FullName)
So, the complete script would then look something like this
$directory = ".\msg\"
$ol = New-Object -ComObject Outlook.Application
$files = Get-ChildItem $directory
foreach ($file in $files)
{
$msg = $ol.CreateItemFromTemplate($file.FullName)
$headers = $msg.PropertyAccessor.GetProperty("http://schemas.microsoft.com/mapi/proptag/0x007D001E")
$headers > ($file.name +".txt")
}
All that said, it could be worthwhile reading up on automatic variables (Get-Help about_Automatic_Variables) - for instance the sections about $PWD, $PSScriptRoot and $PSCommandPath might be useful.
Alternative ways - even though they seem unnecessarily complicated.
$msg = $ol.CreateItemFromTemplate((Get-Item $directory).FullName + $file)
Or something like this
$msg = $ol.CreateItemFromTemplate($file.DirectoryName + "\" $file)

Add +1 every time a program has run? [duplicate]

This question already has answers here:
Determine and Grab a Number Within Directory Name and Use That Number In an Expression
(2 answers)
Closed 1 year ago.
I recently wanted to improve my code by cleaning it up a bit. In my simple copy and paste program, I want to create a log file every time after the program ran. The problem is with naming the text file: I don’t want to overwrite any log-files and therefore give the file a unique name. Right now it looks like this:
start-transcript - Path $Sourcefolder\log-$logname_$logH.$logM.$logS
Basically it creates a log file based on the time you ran the program and a unique name will be given (I had to use hours, minutes and seconds in a seperate function otherwise special characters would be used but that’s not the problem)
The result would look like this:
log-08.11.202114.47.56
Not really that great.
My solution to the problem would be adding the variable $i and adding +1 every time the program run.
But if I try to do so, the value will automatically reset to 0 after it’s done.
My question, is there a way to implement this or is there another way?
(Sorry if bad format, currently on mobile)
Why not put an ISO8601-like timestamp in the log filename. Is granularity greater than one (1) second needed?
PS C:\> $LogFilename = 'log-' + (Get-Date -Format 'yyyyMMddTHHmmss') + '.log'
PS C:\> $LogFilename
log-20211108T083105.log
You could also use below reusable helper function to always ensure a unique filename:
function Get-UniqueFileName {
[CmdletBinding()]
Param(
[Parameter(ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true, Mandatory = $true, Position = 0)]
[Alias('FullName')]
[string]$Path # the suggested full path and filename
)
$directory = [System.IO.Path]::GetDirectoryName($Path)
$baseName = [System.IO.Path]::GetFileNameWithoutExtension($Path)
$extension = [System.IO.Path]::GetExtension($Path) # this includes the dot
# get an array of all files with the same extension currently in the directory
$allFiles = #(Get-ChildItem $directory -File -Filter "$baseName*$extension" | Select-Object -ExpandProperty Name)
# construct the possible new file name (just the name, not the full path and name)
$newFile = $baseName + $extension
$seed = 1
while ($allFiles -contains $newFile) {
# add the seed value between brackets. (you can ofcourse leave them out if you like)
$newFile = "{0}({1}){2}" -f $baseName, $seed, $extension
$seed++
}
# return the full path and filename
return Join-Path -Path $directory -ChildPath $newFile
}
In your case you can then do:
$Sourcefolder = 'D:\Test'
$logFile = Join-Path -Path $Sourcefolder -ChildPath ('{0:yyyyMMdd}.log' -f (Get-Date)) | Get-UniqueFileName
Example:
If a file called D:\Test\20211108.log already exists in your source folder, this will return a new filename D:\Test\20211108(1).log. The next one will be D:\Test\20211108(2).log etc.
or, as already commented, you can use the DateTime Ticks property te be pretty sure the filename will be unique:
$logFile = Join-Path -Path $Sourcefolder -ChildPath ('{0}.log' -f (Get-Date).Ticks)
Returns D:\Test\637719832681865978.log

Export or Print Outlook Emails to PDF

I am using PowerShell to loop through designated folders in Outlook and saving the attachments in a tree like structure. This works wonders, but now management has requested the email itself be saved as a PDF as well. I found the PrintOut method in object, but that prompts for a file name. I haven't been able to figure out what to pass to it to have it automatically save to a specific filename. I looked on the MSDN page and it was a bit to high for my current level.
I am using the com object of outlook.application.
Short of saving all of the emails to a temp file and using a third party method is there parameters I can pass to PrintOut? Or another way to accomplish this?
Here is the base of the code to get the emails. I loop through $Emails
$Outlook = New-Object -comobject outlook.application
$Connection = $Outlook.GetNamespace("MAPI")
#Prompt which folder to process
$Folder = $Connection.PickFolder()
$Outlook_Folder_Path = ($Folder.FullFolderPath).Split("\",4)[3]
$BaseFolder += $Outlook_Folder_Path + "\"
$Emails = $Folder.Items
Looks like there are no built-in methods, but if you're willing to use third-party binary, wkhtmltopdf can be used.
Get precompiled binary (use MinGW 32-bit for maximum compatibility).
Install or extract installer with 7Zip and copy wkhtmltopdf.exe to your script directory. It has no external dependencies and can be redistributed with your script, so you don't have to install PDF printer on all PCs.
Use HTMLBody property of MailItem object in your script for PDF conversion.
Here is an example:
# Get path to wkhtmltopdf.exe
$ExePath = Join-Path -Path (
Split-Path -Path $Script:MyInvocation.MyCommand.Path
) -ChildPath 'wkhtmltopdf.exe'
# Set PDF path
$OutFile = Join-Path -Path 'c:\path\to\emails' -ChildPath ($Email.Subject + '.pdf')
# Convert HTML string to PDF file
$ret = $Email.HTMLBody | & $ExePath #('--quiet', '-', $OutFile) 2>&1
# Check for errors
if ($LASTEXITCODE) {
Write-Error $ret
}
Please note, that I've no experience with Outlook and used MSDN to get relevant properties for object, so the code might need some tweaking.
Had this same issue. This is what I did to fix it if anybody else is trying to do something similar.
You could start by taking your msg file and converting it to doc then converting the doc file to pdf.
$outlook = New-Object -ComObject Outlook.Application
$word = New-Object -ComObject Word.Application
Get-ChildItem -Path $folderPath -Filter *.msg? | ForEach-Object {
$msgFullName = $_.FullName
$docFullName = $msgFullName -replace '\.msg$', '.doc'
$pdfFullName = $msgFullName -replace '\.msg$', '.pdf'
$msg = $outlook.CreateItemFromTemplate($msgFullName)
$msg.SaveAs($docFullName, 4)
$doc = $word.Documents.Open($docFullName)
$doc.SaveAs([ref] $pdfFullName, [ref] 17)
$doc.Close()
}
Then, just clean up the unwanted files after

List 'target' of 'links'

I need to get the 'target' inside of a shortcut link to another server.
But… when I use -Recurse, it follows the links, instead of simply just getting the shortcut target.
Here is code that I found online that I edited to serve my purpose. But it goes into the links and tries to find shortcuts within another server:
#Created By Scott
$varLogFile = "E:\Server\GetBriefsTargets.log"
$varCSVFile = "E:\Server\GetBriefsTargets.csv"
function Get-StartMenuShortcuts
{
$Shortcuts = Get-ChildItem -Recurse "F:\Connect" -Include *.lnk
#$Shortcuts = Get-ChildItem -Recurse "D:\Scripts\scott_testing" -Include *.lnk
$Shell = New-Object -ComObject WScript.Shell
foreach ($Shortcut in $Shortcuts)
{
$Properties = #{
ShortcutName = $Shortcut.Name;
ShortcutFull = $Shortcut.FullName;
ShortcutPath = $shortcut.DirectoryName
Target = $Shell.CreateShortcut($Shortcut).targetpath
}
New-Object PSObject -Property $Properties
}
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
$Output = Get-StartMenuShortcuts
$Output
ECHO "$Output" >> $varLogFile
ECHO "$Output" >> $varCSVFile
Could someone please offer advice on what I can change so that it still finds all of the shortcut links in all of the folders? and stops going inside of those links?
ie:
F:\Connect\CLIENTA\shortcut.lnk
F:\Connect\CLIENTB\shortcut.lnk
etc.
There's about 100 clients that I have to get their links and I don't want to do it manually (each month).
Here is the error that I get upon trying to run this script:
Get-ChildItem : The specified path, file name, or both are too long. The fully
qualified file name must be less than 260 characters, and the directory name must
be less than 248 characters.
At D:\Scripts\scott_testing\GetShortcutTarget_edit1.ps1:8 char:18
+ $Shortcuts = Get-ChildItem -Recurse "F:\Connect" -Include *.lnk
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ReadError: (F:\Connect\CLIENTA\briefs\FILENAME:String) [Get-ChildItem], PathTooLongException
+ FullyQualifiedErrorId : DirIOError,Microsoft.PowerShell.Commands.GetChildItemCommand
It is going 'inside' of the link and trying to find shortcuts on another server.
I can't reproduce. All the targets are probably found.
The results shown in the console by the $output-line should show everything as it should be. If not, update the question with the actual output, the desired output.
The only error I see here is that you try to save CSV-objects using output (text) redirection. That would only return the string-representation of the objects, which is not CSV. In this situation it doesn't output anything when I try it because $output is an array of objects that return nothing when the ToString() method is called.
Replace:
ECHO "$Output" >> $varCSVFile
With:
$Output | Select ShortcutName, ShortcutFull, ShortcutPath, Target | Export-CSV -Path $varCSVFile -NoTypeInformation
Sample output:
"ShortcutName","ShortcutFull","ShortcutPath","Target"
"WinSystem.lnk","C:\Users\frode\Desktop\test\WinSystem.lnk","C:\Users\frode\Desktop\test","C:\Windows\System"
"Lol.lnk","C:\Users\frode\Desktop\Lol.lnk","C:\Users\frode\Desktop","C:\Windows\System32\notepad.exe"
"Windows.lnk","C:\Users\frode\Desktop\Windows.lnk","C:\Users\frode\Desktop","\\localhost\c$\Windows"
"WinSystem32.lnk","C:\Users\frode\Desktop\WinSystem32.lnk","C:\Users\frode\Desktop","\\127.0.0.1\c$\Windows\System32"
UPDATE If you want it do a recursive search you could try something like this. Be aware that the target of the "second-level" shortcut (ex \\server\share\shortcutb.nk might have c:\temp as a target which means you'll get a local path and not an UNC for the remote computer (see MyEnd.lnk in sample output below).
Warning: This could easily result in a infinite loop (circular referencing) or long running search (because of recursive search).
function Get-StartMenuShortcuts ($path) {
$Shortcuts = Get-ChildItem -Path $path -Recurse -Include *.lnk
#$Shortcuts = Get-ChildItem -Recurse "D:\Scripts\scott_testing" -Include *.lnk
$Shell = New-Object -ComObject WScript.Shell
foreach ($Shortcut in $Shortcuts)
{
$Properties = #{
ShortcutName = $Shortcut.Name;
ShortcutFull = $Shortcut.FullName;
ShortcutPath = $shortcut.DirectoryName
Target = $Shell.CreateShortcut($Shortcut).targetpath
}
Get-StartMenuShortcuts -path $Properties.Target
New-Object PSObject -Property $Properties
}
[Runtime.InteropServices.Marshal]::ReleaseComObject($Shell) | Out-Null
}
$output = Get-StartMenuShortcuts -path "F:\Connect"
Example output:
"ShortcutName","ShortcutFull","ShortcutPath","Target"
"WinSystem.lnk","C:\Users\frode\desktop\test\WinSystem.lnk","C:\Users\frode\desktop\test","C:\Windows\System"
"MyEnd.lnk","\\127.0.0.1\c$\EFI\MyEnd.lnk","\\127.0.0.1\c$\EFI","C:\Users\frode\Desktop\TheEnd"
"EFI.lnk","C:\Users\frode\desktop\EFI.lnk","C:\Users\frode\desktop","\\127.0.0.1\c$\EFI"
"Lol.lnk","C:\Users\frode\desktop\Lol.lnk","C:\Users\frode\desktop","C:\Windows\System32\notepad.exe"
If you want to only get the deepest level, you should add a "level"-counter to each object that increases for each recursion call and then keep only the highest ones etc. It might get complicated depending on you needs so that would require a separate detailed question.
I found a command that will work in command prompt, but never seemed to work with powershell:
DIR /AL /S F:\Connect
It took a while to run... but it did.
Also, I found a program that did it in like 2 seconds:
http://sumtips.com/2012/10/find-symbolic-links-ntfs-junction-points-windows.html
I was trying to find the 'symbolic links' ... which I was stating targets.

powershell set category for multiple files and subdirectories

I've been putting "tags" into the names of files, but that's a terrible way of organizing a large number of files.
ex: "ABC - file name.docx"
So, I want to set the category attribute to "ABC" instead of having it in the name using PowerShell. The script would have to find all of the files with "ABC" in its name in the subdirectories of a certain folder and set the category attribute to "ABC".
So I have the first part where I am finding the files but I don't know where to go from here.
Get-ChildItem -Filter "ABC*" -Recurse
Any ideas?
Thanks.
So this borrow heavily from the Scripting Guys. What we need to do is for every file we find use the a Word COM object to access those special properties of the file. Using the current file name we extract the "category" by splitting on the first hyphen and saving both parts. First becomes the category and second is the new name we give the file assuming the category update was successful.
There is still margin for error with this but this
$path = "C:\temp"
# Create the Word com object and hide it sight
$wordApplication = New-Object -ComObject word.application
$wordApplication.Visible = $false
# Typing options for located Word Documents. Mainly to prevent changes to timestamps
$binding = "System.Reflection.BindingFlags" -as [type]
# Locate Documents.
$docs = Get-childitem -path $Path -Recurse -Filter "*-*.docx"
$docs | ForEach-Object{
$currentDocumentPath = $_.fullname
$document = $wordApplication.documents.open($currentDocumentPath)
$BuiltinProperties = $document.BuiltInDocumentProperties
$builtinPropertiesType = $builtinProperties.GetType()
$categoryUpdated = $false # Assume false as a reset of the state.
# Get the category from the file name for this particular file.
$filenamesplit = $_.Name.split("-",2)
$category = $filenamesplit[0].Trim()
# Attempt to change the property.
Try{
$BuiltInProperty = $builtinPropertiesType.invokemember("item",$binding::GetProperty,$null,$BuiltinProperties,"Category")
$BuiltInPropertyType = $BuiltInProperty.GetType()
$BuiltInPropertyType.invokemember("value",$binding::SetProperty,$null,$BuiltInProperty,[array]$category)
$categoryUpdated = $true
}Catch [system.exception]{
# Error getting property so most likely is not populated.
Write-Host -ForegroundColor Red "Unable to set the 'Category' for '$currentDocumentPath'"
}
# Close the document. It should save by default.
$document.close()
# Release COM objects to ensure process is terminated and document closed.
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($BuiltinProperties) | Out-Null
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($document) | Out-Null
Remove-Variable -Name document, BuiltinProperties
# Assuming the category was successfully changed lets remove the information from the current filename as it is redundant.
If($categoryUpdated){Rename-Item $currentDocumentPath -NewName $filenamesplit[1].Trim()}
}
$wordApplication.quit()
[System.Runtime.InteropServices.Marshal]::ReleaseComObject($wordApplication) | Out-Null
Remove-Variable -Name wordApplication
[gc]::collect()
[gc]::WaitForPendingFinalizers()
You should see some explanation in comments that I tried to add for clarification. Also read the link above to get more of an explanation as to what is happening with the COM object.