Powershell Loop to Write Password Protected Files - powershell

I'm trying to read excel files into Powershell, open, password protect them and write them back. I can do it individually but within a loop the script fails:
#working individually
$f = ("C:my\path\Out Files\1234dv.xlsx")
$outfile = $f.FullName + "out"
$xlNormal = -4143
$xl = new-object -comobject excel.application
$xl.Visible = $True
$xl.DisplayAlerts = $False
$wb = $xl.Workbooks.Open($f)
$a = $wb.SaveAs("C:my\path\Out Files\test.xls",$xlNormal,"test")
$a = $xl.Quit()
$a = Release-Ref($ws)
$a = Release-Ref($wb)
$a = Release-Ref($xl)
#not working in loop, error after
function Release-Ref ($ref) {
([System.Runtime.InteropServices.Marshal]::ReleaseComObject(
[System.__ComObject]$ref) -gt 0)
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
foreach ($f in Get-ChildItem "C:\my\path\Out Files"){
$ff = $f
$outfile = $f.FullName + "out"
$xlNormal = -4143
$xl = new-object -comobject excel.application
$xl.Visible = $True
$xl.DisplayAlerts = $False
$wb = $xl.Workbooks.Open($ff)
$a = $wb.SaveAs("C:\my\path\Out Files\test.xls",$xlNormal,"test")
$a = $xl.Quit()
$a = Release-Ref($ws)
$a = Release-Ref($wb)
$a = Release-Ref($xl)
}
Sorry, we couldn't find 1234dv.xlsx. Is it possible it was moved,
renamed or deleted? At line:16 char:5
+ $wb = $xl.Workbooks.Open($ff)
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], COMException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.COMException COM object that has been
separated from its underlying RCW cannot be used. At line:17 char:5
+ $a = $wb.SaveAs("C:\my\path ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : OperationStopped: (:) [], InvalidComObjectException
+ FullyQualifiedErrorId : System.Runtime.InteropServices.InvalidComObjectException
That error repeats for all four test files I'm working with.
I'm not really familiar with Powershell so I relied on MS docs, and I couldn't password protect the files in python so thought this would be easier. I know this doesn't address the password yet either but trying to get the loop to work first. Any help would be greatly appreciated. Thank you.

You should use
$wb = $xl.Workbooks.Open($ff.FullName)
To give Excel the full file path. Otherwise, $ff is a FileInfo object where a string (path) is required

Slightly off topic for your question , but not for your intent :
From a security perspective using .xls passwords is not security, it is merely an annoyance.
If you need security, then i suggest you use something like Azure Information protection that allows you to encrypt , and share the file securely only with those that need access.
You still need to create your xls or .xlsx files (or any other file for that matter) then you can the powershell simply loop over them :
PS C:\>foreach ($file in (Get-ChildItem -Path \\server1\Docs -Recurse -Force |
where {!$_.PSIsContainer} |
Where-Object {$_.Extension -eq ".xls"})) {
Protect-RMSFile -File $file.PSPath -InPlace -DoNotPersistEncryptionKey All -TemplateID "e6ee2481-26b9-45e5-b34a-f744eacd53b0" -OwnerEmail "IT#Contoso.com"
}
https://learn.microsoft.com/en-us/powershell/module/azureinformationprotection/protect-rmsfile?view=azureipps

Related

Opening Large Set of Word Documents With Powershell - Automation

I am in the process of assigning a footer to hundreds of word documents with their current filepath. Here is my code, which does the job:
I plan to have $Word.Visible set to false, but it isn't for now for debugging purposes.
This gets all the word docs in a directory, adds footer with their file path, then saves and closes.
I am trying to handle a case like this:
I just want to skip this, or possibly force open and continue. Not sure the best way to go about this, however, and am seeking some help.
Thanks,
Elijah
Set-ExecutionPolicy bypass;
$path = 'somepath';
$documents = Get-ChildItem -Path $path *.docx -Recurse -Force
$filepaths = foreach ($document in $documents) {$document.fullname}
$Word = New-Object -ComObject Word.application;
$Word.Visible = $true;
foreach ($filepath in $filepaths){
$Doc = $Word.Documents.OpenNoRepairDialog($filepath);
$Doc.Unprotect();
$Selection = $Word.Selection;
$Doc.ActiveWindow.ActivePane.View.SeekView = 4;
$Selection.ParagraphFormat.Alignment = 1;
$Selection.TypeText($filepath);
$Doc.Save();
$Doc.Close();
}
$Word.Quit();
Edit1:
I've made an edit where it adds the dynamic field object for the file path, rather than just typing in the file path, that way if you happen to move the file, the file path can be updated to the new path. You will have to press F9 while selecting the footer in word, but this is the best you can do without making a macro and saving the file as a .docm.
Here is the amended code:
$documents = Get-ChildItem -path *docx -recurse -force
$filepaths = foreach($document in $documents){$document.FullName}
Set-Variable -Name wdFieldFileName -Value 29 -Option constant -Force -ErrorAction SilentlyContinue
$word = New-Object -ComObject Word.Application
#$word.Visible = $true
foreach($filepath in $filepaths){
$doc = $word.Documents.Open($filepath)
$sections = $doc.Sections
$item1 = $sections.Item(1)
$footer = $item1.Footers.Item(1)
$range = $footer.Range
$doc.Fields.Add($range, $wdFieldFileName, '\p')
$doc.Save()
$doc.Close()
}
$word.Quit()
I am still running into the error window when trying to open corrupted or document "in need of repair" as diagnosed by word.
Passing in multiple arguments to the Open() method does not yield results as expected. Here is an example:
Exception calling "Open" with "16" argument(s): "Type mismatch. (Exception from HRESULT: 0x80020005 (DISP_E_TYPEMISMATCH))"
At line:1 char:1
+ $doc = $word.Documents.Open($filepath, $False, $False, $False, $null, ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : ComMethodTargetInvocation

Powershell Unable to find type [Microsoft.Office.Interop.Word.WdSaveFormat]

I'm using this script to convert DOC to HTML
param([string]$docpath,[string]$htmlpath = $docpath)
$srcfiles = Get-ChildItem $docPath -filter "*.doc"
$saveFormat = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFormatFilteredHTML");
$word = new-object -comobject word.application
$word.Visible = $False
function saveas-filteredhtml
{
$opendoc = $word.documents.open($doc.FullName);
$opendoc.saveas([ref]"$htmlpath\$doc.fullname.html", [ref]$saveFormat);
$opendoc.close();
}
ForEach ($doc in $srcfiles)
{
Write-Host "Processing :" $doc.FullName
saveas-filteredhtml
$doc = $null
}
$word.quit();
Unfortunately when I run it for the first time in the ISE console I get this error
Unable to find type [Microsoft.Office.Interop.Word.WdSaveFormat].
In F:\PS\NEW\main.ps1:108 car:29
+ ... = [Enum]::Parse([Microsoft.Office.Interop.Word.WdSaveFormat], "wdFor ...
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (Microsoft.Offic...rd.WdSaveFormat:TypeName) [], RuntimeException
+ FullyQualifiedErrorId : TypeNotFound
While if I run it from the same console again the second time it works fine.
How can I solve the problem? Thanks
In this forum they solve the problem:https://gallery.technet.microsoft.com/office/6f7eee4b-1f42-499e-ae59-1aceb26100de/view/Discussions
you add this lines at the beginning of your code:
$wdTypes = Add-Type -AssemblyName 'Microsoft.Office.Interop.Word' -Passthru
$wdSaveFormat = $wdTypes | Where {$_.Name -eq "wdSaveFormat"}

Convert xlsx to CSV without using Excel

I get following error:
Cannot index into a null array.
At C:\tmp\Folder\excel\output\net45\test.ps1:14 char:1
+ $Data = $Reader.AsDataSet().Tables[0].Rows
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : InvalidOperation: (:) [], RuntimeException
+ FullyQualifiedErrorId : NullArray
# Zero based index. The second row has index 1.
$StartRow = 2
# Input File
$InputFileName = "C:\tmp\Folder\excel\output\net20\test.xlsx"
# Output File
$OutputFileName = "C:\tmp\Folder\excel\output\net20\SomeFile.csv"
# Path to Excel.dll is saved (downloaded from http://exceldatareader.codeplex.com/)
$DllPath = "C:\tmp\Folder\excel\output\net45\Excel.4.5.dll"
[void]([Reflection.Assembly]::LoadFrom($DllPath))
$Stream = New-Object IO.FileStream($InputFileName, "Open", "Read")
$Reader = [Excel.ExcelReaderFactory]::CreateBinaryReader($Stream)
$Data = $Reader.AsDataSet().Tables[0].Rows
# Read the column names. Order should be preserved
$Columns = $Data[$StartRow].ItemArray
# Sort the remaining data into an object using the specified columns
$Data[$($StartRow + 1)..$($Data.Count - 1)] | % {
# Create an object
$Output = New-Object Object
# Read each column
for ($i = 0; $i -lt $Columns.Count; $i++) {
$Output | Add-Member NoteProperty $Columns[$i] $_.ItemArray[$i]
}
# Leave it in the output pipeline
$Output
} | Export-CSV $OutputFileName -NoType
You're calling the binary method (.xls) and using an Open XML format file (.xlsx). Try using [Excel.ExcelReaderFactory]::CreateOpenXmlReader($Stream) instead.
This works for me:
$DllPath = 'C:\Excel.DataReader.45\Excel.4.5.dll';
$FilePath = 'C:\Students.xlsx';
$FileMode = [System.IO.FileMode]::Open;
$FileAccess = [System.IO.FileAccess]::Read;
Add-Type -Path $DllPath;
$FileStream = New-Object -TypeName System.IO.FileStream $FilePath, $FileMode, $FileAccess;
$ExcelDataReader = [Excel.ExcelReaderFactory]::CreateOpenXmlReader($FileStream);
$ExcelDataReader.IsFirstRowAsColumnNames = $true;
$ExcelDataSet = $ExcelDataReader.AsDataSet();
$ExcelDataReader.Dispose();
$FileStream.Close();
$FileStream.Dispose();
$ExcelDataSet.Tables | Format-Table -AutoSize
If you're still having trouble, you might consider using the Microsoft.ACE.OLEDB.12.0 provider, which you install separately from Office. There's some doc here.
I've read this "Convert XLS to CSV on command line" and this "convert-xlsx-file-to-csv-using-batch" before in a similar doubt I have. Try too see if it helps.

Get datetaken attribute on file

I am trying to write a script that will get the DATETAKEN attribute from a photo and create a folder structure based on that and move the file to this new location. I have found scripts on google that I'm trying to use but when I running it, it returns:
PS C:\Temp> C:\Temp\MovePhoto.ps1
GAC Version Location
--- ------- -------- True v4.0.30319
C:\Windows\Microsoft.Net\assembly\GAC_MSIL\System.Drawing\v4.0_4.0.0....
Move-Item : The process cannot access the file because it is being
used by another process. At C:\Temp\MovePhoto.ps1:43 char:5
+ Move-Item $FileFullName "$FileDirFull\$FileBaseNameNU"
+ ~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~
+ CategoryInfo : WriteError: (C:\NoBackup\TES...RA\IMG_1372.JPG:FileInfo) [Move- Item],
IOException
+ FullyQualifiedErrorId : MoveFileInfoItemIOError,Microsoft.PowerShell.Commands.MoveItemCommand
If I do the script without the SystemDrawing line it works. But then I can't get the DATETAKEN attribute. I just can't figure out what I am missing.
Here is the script
[reflection.assembly]::LoadWithPartialName("System.Drawing")
$FileAll = (Get-ChildItem $SourcePath -Recurse | where {!$_.psiscontainer} | Select-Object Name,Fullname,BaseName,Extension,CreationTime,LastWriteTime,Length,#{Name="MD5";Expression={Get-Md5Hash $_.fullname}} | group MD5 | Where {$_.Count -gt 1 } | %{$_.Group} | sort MD5)
foreach ($File in $FileAll) {
$FileBaseName = $File.BaseName
$FileExtension = $File.Extension
$FileFullName = $File.FullName
$FileBaseNameNu = $FileBaseName + $FileExtension
$FileName = $File.Name
}
$foo = New-Object -TypeName System.Drawing.Bitmap -ArgumentList $FileFullName
$date = $foo.GetPropertyItem(36867).Value[0..9]
$arYear = [Char]$date[0],[Char]$date[1],[Char]$date[2],[Char]$date[3]
$arMonth = [Char]$date[5],[Char]$date[6]
$arDay = [Char]$date[8],[Char]$date[9]
$strYear = [String]::Join("",$arYear)
$strMonth = [String]::Join("",$arMonth)
$strDay = [String]::Join("",$arDay)
$DateTaken = $strYear + "-" + $strMonth + "-" + $strDay
$FileLastWriteTime = $File.LastWriteTime
$FileDirYear = $FileLastWriteTime.Year
$FileDirDate = $FileLastWriteTime.ToShortDateString()
$FileDirFull = "$DestinationPath\DUBLETTER\$FileDirYear\$DateTaken"
# Create destination path
if ((Test-Path $FileDirFull) -eq $false) {
New-Item -Path $FileDirFull -ItemType Directory
}
if (Test-Path (Join-Path $FileDirFull $File.Name)) {
$n = 0
while ((Test-Path (Join-Path $FileDirFull $FileBaseNameNU)) -eq $true){
$FileBaseNameNU = $FileBaseName + "-" + ++$n + $FileExtension
}
}
Move-Item $FileFullName "$FileDirFull\$FileBaseNameNU"
}
Can you try to replace
[reflection.assembly]::LoadWithPartialName("System.Drawing")
by
Add-Type -AssemblyName "system.drawing"
Forget it, your trouble is with your file C:\NoBackup\TES...RA\IMG_1372.JPG wich can't be moved because it's open (seems to be open for usage in $foo var). Try first to copy it. You perhaps can use $foo.Dispose() before Move-Item $FileFullName "$FileDirFull\$FileBaseNameNU"
I've done this in VBScript, using GetDetailsOf()... see MSDN
This can be done using PowerShell, see... Scripting Guys
There may be a .NET method way of doing this, i.e.: more "native" to PS.

Basic Powershell - batch convert Word Docx to PDF

I am trying to use PowerShell to do a batch conversion of Word Docx to PDF - using a script found on this site:
http://blogs.technet.com/b/heyscriptingguy/archive/2013/03/24/weekend-scripter-convert-word-documents-to-pdf-files-with-powershell.aspx
# Acquire a list of DOCX files in a folder
$Files=GET-CHILDITEM "C:\docx2pdf\*.DOCX"
$Word=NEW-OBJECT –COMOBJECT WORD.APPLICATION
Foreach ($File in $Files) {
# open a Word document, filename from the directory
$Doc=$Word.Documents.Open($File.fullname)
# Swap out DOCX with PDF in the Filename
$Name=($Doc.Fullname).replace("docx","pdf")
# Save this File as a PDF in Word 2010/2013
$Doc.saveas([ref] $Name, [ref] 17)
$Doc.close()
}
And I keep on getting this error and can't figure out why:
PS C:\docx2pdf> .\docx2pdf.ps1
Exception calling "SaveAs" with "16" argument(s): "Command failed"
At C:\docx2pdf\docx2pdf.ps1:13 char:13
+ $Doc.saveas <<<< ([ref] $Name, [ref] 17)
+ CategoryInfo : NotSpecified: (:) [], MethodInvocationException
+ FullyQualifiedErrorId : DotNetMethodException
Any ideas?
Also - how would I need to change it to also convert doc (not docX) files, as well as use the local files (files in same location as the script location)?
Sorry - never done PowerShell scripting...
This will work for doc as well as docx files.
$documents_path = 'c:\doc2pdf'
$word_app = New-Object -ComObject Word.Application
# This filter will find .doc as well as .docx documents
Get-ChildItem -Path $documents_path -Filter *.doc? | ForEach-Object {
$document = $word_app.Documents.Open($_.FullName)
$pdf_filename = "$($_.DirectoryName)\$($_.BaseName).pdf"
$document.SaveAs([ref] $pdf_filename, [ref] 17)
$document.Close()
}
$word_app.Quit()
The above answers all fell short for me, as I was doing a batch job converting around 70,000 word documents this way. As it turns out, doing this repeatedly eventually leads to Word crashing, presumably due to memory issues (the error was some COMException that I didn't know how to parse). So, my hack to get it to proceed was to kill and restart word every 100 docs (arbitrarily chosen number).
Additionally, when it did crash occasionally, there would be resulting malformed pdfs, each of which were generally 1-2 kb in size. So, when skipping already generated pdfs, I make sure they are at least 3kb in size. If you don't want to skip already generated PDFs, you can delete that if statement.
Excuse me if my code doesn't look good, I don't generally use Windows and this was a one-off hack. So, here's the resulting code:
$Files=Get-ChildItem -path '.\path\to\docs' -recurse -include "*.doc*"
$counter = 0
$filesProcessed = 0
$Word = New-Object -ComObject Word.Application
Foreach ($File in $Files) {
$Name="$(($File.FullName).substring(0, $File.FullName.lastIndexOf("."))).pdf"
if ((Test-Path $Name) -And (Get-Item $Name).length -gt 3kb) {
echo "skipping $($Name), already exists"
continue
}
echo "$($filesProcessed): processing $($File.FullName)"
$Doc = $Word.Documents.Open($File.FullName)
$Doc.SaveAs($Name, 17)
$Doc.Close()
if ($counter -gt 100) {
$counter = 0
$Word.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($Word)
$Word = New-Object -ComObject Word.Application
}
$counter = $counter + 1
$filesProcessed = $filesProcessed + 1
}
This works for me (Word 2007):
$wdFormatPDF = 17
$word = New-Object -ComObject Word.Application
$word.visible = $false
$folderpath = Split-Path -parent $MyInvocation.MyCommand.Path
Get-ChildItem -path $folderpath -recurse -include "*.doc" | % {
$path = ($_.fullname).substring(0,($_.FullName).lastindexOf("."))
$doc = $word.documents.open($_.fullname)
$doc.saveas($path, $wdFormatPDF)
$doc.close()
}
$word.Quit()
Neither of the solutions posted here worked for me on Windows 8.1 (btw. I'm using Office 365). My PowerShell somehow does not like the [ref] arguments (I don't know why, I use PowerShell very rarely).
This is the solution that worked for me:
$Files=Get-ChildItem 'C:\path\to\files\*.docx'
$Word = New-Object -ComObject Word.Application
Foreach ($File in $Files) {
$Doc = $Word.Documents.Open($File.FullName)
$Name=($Doc.FullName).replace('docx', 'pdf')
$Doc.SaveAs($Name, 17)
$Doc.Close()
}
I've updated this one to work on latest office :
# Get invocation path
$curr_path = Split-Path -parent $MyInvocation.MyCommand.Path
# Create a PowerPoint object
$ppt_app = New-Object -ComObject PowerPoint.Application
#$ppt.visible = $false
# Get all objects of type .ppt? in $curr_path and its subfolders
Get-ChildItem -Path $curr_path -Recurse -Filter *.ppt? | ForEach-Object {
Write-Host "Processing" $_.FullName "..."
# Open it in PowerPoint
$document = $ppt_app.Presentations.Open($_.FullName,0,0,0)
# Create a name for the PDF document; they are stored in the invocation folder!
# If you want them to be created locally in the folders containing the source PowerPoint file, replace $curr_path with $_.DirectoryName
$pdf_filename = "$($curr_path)\$($_.BaseName).pdf"
# Save as PDF -- 17 is the literal value of `wdFormatPDF`
#$opt= [Microsoft.Office.Interop.PowerPoint.PpSaveAsFileType]::ppSaveAsPDF
$document.SaveAs($pdf_filename,32)
# Close PowerPoint file
$document.Close()
}
# Exit and release the PowerPoint object
$ppt_app.Quit()
[System.Runtime.Interopservices.Marshal]::ReleaseComObject($ppt_app)