I have a power shell script that combines power points. The issue is it only works on power points in the current directory (the directory the script is in) and saves the combined power point to documents. How do I change the script to work from any directory that is given as a parameter. I run the power shell script like this ./Merge-Presentation -Source $Presentations -Destination $save -Verbose -Open;. Where $Presentations is the path of the individual power point and $save is the path where the combined power point is saved. Here is the script.
#region function definitions
#Function for releasing a COM object
Function Remove-Ref
{
param
(
[Object]
$ref
)
$null = Remove-Variable -Name $ref -ErrorAction SilentlyContinue
while ([System.Runtime.InteropServices.Marshal]::ReleaseComObject([System.__ComObject]$ref) -gt 0)
{
}
[System.GC]::Collect()
[System.GC]::WaitForPendingFinalizers()
}
#Main function for merging PowerPoint presentations
Function Merge-PowerPointPresentation
{
<#
.SYNOPSIS
Merge multiple PowerPoint presentation files to one file
.DESCRIPTION
Merge multiple PowerPoint presentation files to one file
.PARAMETER Source
The PowerPoint presentation files to merge specified by its full name
.PARAMETER Destination
The target PowerPoint presentation file specified by its full name
.PARAMETER Open
A switch to specify if we keep the PowerPoint application opened after the processing
.EXAMPLE
$Get-ChildItem -Path $CurrentDir -filter *.pptx | Sort-Object -Property Name | Merge-PowerPointPresentation -Verbose -Open
Will merge all the PowerPoint files into the current directory into one single Powerpoint file by using a timestamped filename (ie. yyyyMMddTHHmmss.pptx like 20170126T091011.pptx)
The output will be verbose
The PowerPoint application won't be left after the processing
.EXAMPLE
$Presentations = "$CurrentDir\0.pptx","$CurrentDir\1.pptx","$CurrentDir\2.pptx","$CurrentDir\3.pptx","$CurrentDir\4.pptx","$CurrentDir\5.pptx","$CurrentDir\6.pptx","$CurrentDir\7.pptx","$CurrentDir\8.pptx","$CurrentDir\9.pptx"
Merge-PowerPointPresentation -Source $Presentations -Destination C:\Temp\MergedPresentation.pptx
Will merge all the specified PowerPoint files into into the C:\Temp\MergedPresentation.pptx Powerpoint file
#>
[CmdletBinding()]
Param(
#The collection of the powerpoint files to merge
[Parameter(Mandatory = $True,ValueFromPipeline = $True,ValueFromPipelineByPropertyName = $True)]
[ValidateScript({
(Test-Path -Path $_ -PathType Leaf) -and ($_ -match "\.ppt(x{0,1})$")
})]
[alias('FilePath', 'Path', 'FullName')]
[string[]]$Source,
#The path of the generated powerpoint file
[Parameter(Mandatory = $False)]
[ValidateNotNullOrEmpty()]
[alias('OutputFile')]
[string]$Destination = $(Join-Path -Path $([Environment]::GetFolderPath('MyDocuments')) -ChildPath $('{0:yyyyMMddTHHmmss}' -f (Get-Date))),
#To keep open the generated Powerpoint presentation
[parameter(Mandatory = $False)]
[switch]$Open
)
begin
{
#Opening the PowerPoint application once
Add-Type -AssemblyName Microsoft.Office.Interop.PowerPoint
$Powerpoint = New-Object -ComObject Powerpoint.Application
#Creating a new PowerPoint presentation
$NewPresentation = $Powerpoint.Presentations.Add($True)
# Adding an empty slide : mandatory
$null = $NewPresentation.Slides.Add(1, [Microsoft.Office.Interop.PowerPoint.PpSlideLayout]::ppLayoutBlank)
$SlidesNb = 0
}
process
{
#For all files passed as argument outside a pipeline context
foreach ($CurrentSource in $Source)
{
#Getting the base name of the processed presentation
$CurrentPresentationName = (Get-Item -Path $CurrentSource).BaseName
#Inserting the slide of the current presentationt o the new one
$InsertedSlidesNb = $NewPresentation.Slides.InsertFromfile($CurrentSource, $SlidesNb)
#Applying the original template
$NewPresentation.Slides.Range(($SlidesNb+1)..($SlidesNb+$InsertedSlidesNb)).ApplyTemplate($CurrentSource)
#Adding a new section for the inserted context with the name of the processed presentation
Write-Verbose -Message "Adding the section $CurrentPresentationName before Slide $($SlidesNb+1)..."
$null = $NewPresentation.SectionProperties.AddBeforeSlide($SlidesNb+1, $CurrentPresentationName)
Write-Verbose -Message "Processed file $CurrentSource by inserting $InsertedSlidesNb slides ($($SlidesNb+1) ==> $($SlidesNb+$InsertedSlidesNb)) ..."
$SlidesNb += $InsertedSlidesNb
}
}
end
{
#Deleting the useless empty slide (added at the beginning)
$NewPresentation.Slides.Range($SlidesNb+1).Delete()
#Saving the final file
$NewPresentation.SaveAs($Destination)
Write-Host -Object "The new presentation was saved in $($NewPresentation.FullName) ($SlidesNb slides)"
#If the -Open switch is specified we keep the PowerPoint application opened
if (!$Open)
{
$NewPresentation.Close()
#$Powerpoint.Quit() | Out-Null
Write-Verbose -Message 'Releasing PowerPoint ...'
Remove-Ref -ref ($NewPresentation)
Remove-Ref -ref ($Powerpoint)
}
}
}
#endregion
Clear-Host
#Getting the current directory (where this script file resides)
$CurrentDir = Split-Path -Path $MyInvocation.MyCommand.Path
#Loading the PowerPoint assembly
#Example 1 : Processing all the PowerPoint presentation in current directory in the alphabetical order
Get-ChildItem -Path $CurrentDir -Filter *.pptx |
Sort-Object -Property Name |
Merge-PowerPointPresentation -Verbose -Open
#Example 2 : Processing a list of some PowerPoint presentations specified by their absolute path
$Presentations = "$CurrentDir\0.pptx", "$CurrentDir\1.pptx", "$CurrentDir\2.pptx", "$CurrentDir\3.pptx", "$CurrentDir\4.pptx", "$CurrentDir\5.pptx", "$CurrentDir\6.pptx", "$CurrentDir\7.pptx", "$CurrentDir\8.pptx", "$CurrentDir\9.pptx"
Merge-PowerPointPresentation -Source $Presentations -Destination $CurrentDir\all.pptx -Verbose
The expected result is to load the power points from the directory specified as parameter and save the combined power point in the directory specified as a parameter.
You can just add a parameter declaration at the top, something like:
Param(
[parameter(Mandatory = $false, Position = 1)]
[ValidateScript( {Test-Path $_} )]
[String]$SourcePath = ( Split-Path $MyInvocation.MyCommand.Path ),
[parameter(Mandatory = $false, Position = 2)]
[ValidateScript( {Test-Path $_} )]
[String]$DestinationPath
) #End Parameter Block...
I used Path as the parameter because it's quite common and $CurrentDirector wouldn't make sense here. I defaulted the parameter to the same as you had it. In the script body you'll want to replace $CurrentPath with $Path.
Get-ChildItem -Path $SourcePath -Filter *.pptx |
Sort-Object -Property Name |
Merge-PowerPointPresentation -Verbose -Open -Destination $DestinationPath
Note: The addition of -DestinationPath in the above call. That should override the default value in the Merge-PowerPointPresentation function.
If you don't use the parameters the script should behave same as before. If you do use it It will operate against whatever paths you gave it.
Obviously not tested, but let me know if that helps. Thanks.
Related
Is it possible to temporarily expand the contents of a .ZIP file (7-Zip) to a variable in memory, manipulate the contents and discard it, using PowerShell?
I'm currently expanding the archive which extracts a "log.dat" file. Then I read the contents of this log file, do the analysis and erase the "log.dat" file. But I have to do it 500,000 times which can be harmful to the drive. So right now my workaround for this was to create an R:\ RamDrive and use it like this
$zipFiles = Get-ChildItem -Filter '*.zip' -r
foreach($zip in $zipFiles) {
Expand-7Zip -ArchiveFileName $zip.FullName -TargetPath 'R:\'
Select-String -Path 'R:\log.dat' -Pattern "dataToSearchFor" | ForEach-Object {
# do analysis
}
Remove-Item 'R:\log.dat'
}
What I need is something like
$zipFiles = Get-ChildItem -Filter '*.zip' -r
foreach($zip in $zipFiles) {
$extractedFiles = Expand-7Zip -ArchiveFileName $zip.FullName
$logFile = $extractedFiles[0] # log.dat file is unique in file
Select-String $logFile -Pattern "dataToSearchFor" | ForEach-Object {
# do analysis
}
}
BTW: I have to use the 7-zip library for PowerShell because of the compression method used for the archives
[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12
Install-PackageProvider -Name NuGet -MinimumVersion 2.8.5.201 -Force
Set-PSRepository -Name 'PSGallery' -SourceLocation "https://www.powershellgallery.com/api/v2" -InstallationPolicy Trusted
Install-Module -Name 7Zip4PowerShell -Force
They say "The third time is a charm."
Well, this my 3rd attempt at solving this. Info for second attempt is still valid, but only for certain zip files, so you can find that info farther down in this answer.
First, install the latest version of 7-zip from https://www.7-zip.org/.
In my case, installed 7z2201-x64.exe.
Second, Download Nuget package for SevenZipSharp, then, using 7-Zip to open the package, navigate to sevenzipsharp.net45.1.0.19.nupkg\lib\net45\ and save SevenZipSharp.dll to same location as your PowerShell script.
Either of the following seems to work for the download:
https://www.nuget.org/api/v2/package/SevenZipSharp.Net45/1.0.19
Or
https://globalcdn.nuget.org/packages/sevenzipsharp.net45.1.0.19.nupkg
Third, take note of where 7-Zip's 7z.dll file is installed. In my case, it was C:\Program Files\7-Zip\7z.dll.
Forth, add the following lines to the top of your PowerShell script, making sure the path given to SetLibraryPath is set to that of 7-Zip's 7z.dll found in the Third step from above.
using namespace System.IO
Add-Type -Path "$PSScriptRoot\SevenZipSharp.dll"
[SevenZip.SevenZipExtractor]::SetLibraryPath('C:\Program Files\7-Zip\7z.dll')
Fifth, add the code you want to run.
This example reads all the file path names found in the archive file SevenZipTest.zip which is found in the same path as the PowerShell script:
function ReadFilenamesIn7Zip {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path
)
[SevenZip.SevenZipExtractor]$ZipArchive = [SevenZip.SevenZipExtractor]::new($Path)
foreach($ArchiveFileInfo in $ZipArchive.ArchiveFileData) {
$ArchiveFileInfo.FileName
}
$ZipArchive.Dispose()
}
ReadFilenamesIn7Zip "$PSScriptRoot\SevenZipTest.zip"
This example reads all the file lines from the first internal file named Test.TXT that is found in the archive file SevenZipTest.zip which is found in the same path as the PowerShell script:
function ReadFileIn7Zip {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path,
[Parameter(Mandatory = $true, Position = 1)]
[string]$FileToUnzip,
[Parameter(Mandatory = $false, Position = 2)]
[int]$FileIndex = -1
)
[SevenZip.SevenZipExtractor]$ZipArchive = [SevenZip.SevenZipExtractor]::new($Path)
$ThisFileIndex = 0
foreach($ArchiveFileInfo in $ZipArchive.ArchiveFileData) {
$FileNameNoPath = Split-Path $ArchiveFileInfo.FileName -leaf
if($FileNameNoPath -eq $FileToUnzip) {
if($FileIndex -lt 0 -or $FileIndex -eq $ThisFileIndex) {
$MemoryStream = [System.IO.MemoryStream]::new()
$ZipArchive.ExtractFile($ArchiveFileInfo.Index, $MemoryStream)
[StreamReader]$ZipReader = [StreamReader]::new($MemoryStream)
$MemoryStream.Position = 0
while ($null -ne ($line = $ZipReader.ReadLine())) {
$line
}
$ZipReader.Dispose()
# $MemoryStream.Dispose() # Not needed: https://learn.microsoft.com/en-us/dotnet/api/system.io.memorystream?view=net-6.0#remarks
}
$ThisFileIndex++
}
}
$ZipArchive.Dispose()
}
ReadFileIn7Zip "$PSScriptRoot\SevenZipTest.zip" "Test.TXT" 0
The functionality of ReadFilenamesIn7Zip and ReadFileIn7Zip is essentially the same as the ReadFilenamesInZip and ReadFileInZip examples below. For example, if you look at the functionality of the ReadFileInZip function below, when calling it without the -FileIndex parameter, it will return all text from all files matching the -FileToUnzip parameter, which is also true for ReadFileIn7Zip.
NOTE: {Info from second attempt is below this point.}
Info below appears to be valid only for zip files compressed with * Deflate, BZip2, and LZMA
This example takes the zip file 01_SQLite.zip and searches for any file by the name App.config. This is strongly similar to a reading version, and PowerShell equivalent, of the link jdweng provided in the comments, but several modifications such as storing the file in a StringBuilder.
UPDATE: The code was working in VSCode, but discovered it wasn't working in PowerShell 5.1 Terminal. Both should be the same, but for some reason they are not - and VSCode is set to reload PowerShell prior to each run of a script, so there shouldn't be any assemblies pre-loaded.
SOLUTION: Thank you Santiago, Added Add-Type -Assembly System.IO.Compression, System.IO.Compression.FileSystem to the code.
Verified this worked by closing PowerShell terminal, re-open it, and running script:
using namespace System.IO
using namespace System.IO.Compression
using namespace System.IO.MemoryStream
using namespace System.Text
Add-Type -Assembly System.IO.Compression, System.IO.Compression.FileSystem
$ZipFilePath = "$PSScriptRoot\01_SQLite.zip"
[ZipArchive]$ZipArchive = [ZipFile]::Open($ZipFilePath, [ZipArchiveMode]::Read)
[StringBuilder]$SB = [StringBuilder]::new()
foreach($ZipEntry in $ZipArchive.Entries) {
if($ZipEntry.Name -eq "App.config") {
[StreamReader]$ZipReader = [StreamReader]::new($ZipEntry.Open())
while ($null -ne ($line = $ZipReader.ReadLine())) {
$null = $SB.AppendLine($line)
}
# Do something with the file stored in StringBuilder $SB
Write-Host "Found file $($ZipEntry.FullName)"
Write-Host $SB.ToString()
Write-Host
$null = $SB.Clear()
$ZipReader.Dispose()
}
}
$ZipArchive.Dispose()
More Versatile and Useful Code:
This function returns the file paths and names found in the Zip file:
using namespace System.IO
using namespace System.IO.Compression
Add-Type -Assembly System.IO.Compression, System.IO.Compression.FileSystem
function ReadFilenamesInZip {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path
)
[ZipArchive]$ZipArchive = [ZipFile]::Open($Path, [ZipArchiveMode]::Read)
foreach($ZipEntry in $ZipArchive.Entries) {
$ZipEntry.FullName
}
$ZipArchive.Dispose()
}
Example use, reading file pathnames from 01_SQLite.zip file:
$ZipFilePath = "$PSScriptRoot\01_SQLite.zip"
$FileNames = ReadFilenamesInZip -Path $ZipFilePath
$FileNames
Resulting in this output:
screenshot.png
sqlite_test.sln
sqlite_test/App.config
sqlite_test/App.xaml
sqlite_test/App.xaml.cs
sqlite_test/MainWindow.xaml
sqlite_test/MainWindow.xaml.cs
sqlite_test/packages.config
sqlite_test/Properties/AssemblyInfo.cs
sqlite_test/Properties/Resources.Designer.cs
sqlite_test/Properties/Resources.resx
sqlite_test/Properties/Settings.Designer.cs
sqlite_test/Properties/Settings.settings
sqlite_test/sqlite_test.csproj
Example use, reading file pathnames from a zip file I created named TestZip.zip:
$ZipFilePath = "$PSScriptRoot\TestZip.zip"
$FileNames = ReadFilenamesInZip -Path $ZipFilePath
$FileNames
Resulting in this output:
Folder1/Test.TXT
Folder2/Test.TXT
Test.TXT
This function returns the content of all files matching a certain file name:
using namespace System.IO
using namespace System.IO.Compression
Add-Type -Assembly System.IO.Compression, System.IO.Compression.FileSystem
function ReadFileInZip {
param (
[Parameter(Mandatory = $true, Position = 0)]
[string]$Path,
[Parameter(Mandatory = $true, Position = 1)]
[string]$FileToUnzip,
[Parameter(Mandatory = $false, Position = 2)]
[int]$FileIndex = -1
)
[ZipArchive]$ZipArchive = [ZipFile]::Open($Path, [ZipArchiveMode]::Read)
$ThisFileIndex = 0
foreach($ZipEntry in $ZipArchive.Entries) {
if($ZipEntry.Name -eq $FileToUnzip) {
if($FileIndex -lt 0 -or $FileIndex -eq $ThisFileIndex) {
[StreamReader]$ZipReader = [StreamReader]::new($ZipEntry.Open())
while ($null -ne ($line = $ZipReader.ReadLine())) {
$line
}
$ZipReader.Dispose()
}
$ThisFileIndex++
}
}
$ZipArchive.Dispose()
}
Example use of extracting from TestZip.zip the content of all internal file matching the file name Test.TXT:
$ZipFilePath = "$PSScriptRoot\TestZip.zip"
$FileLines = ReadFileInZip -Path $ZipFilePath -FileToUnzip 'Test.TXT'
if ($null -ne $FileLines) {
Write-Host 'Found File(s):'
$FileLines
} else {
Write-Host 'File NOT found.'
}
Resulting in this output:
Found File(s):
### Folder 1 Text File ###
Random info in Folder 1 text file
### Folder 2 Text File ###
Random info in Folder 2 text file
### Root Text File ###
Random info in root text file
Example reading the content of only the first file with matching name -
Take note of the added -FileIndex 0:
$ZipFilePath = "$PSScriptRoot\TestZip.zip"
$FileLines = ReadFileInZip -Path $ZipFilePath -FileToUnzip 'Test.TXT' -FileIndex 0
if ($null -ne $FileLines) {
Write-Host 'Found File(s):'
$FileLines
} else {
Write-Host 'File NOT found.'
}
Resulting in this output:
Found File(s):
### Folder 1 Text File ###
Random info in Folder 1 text file
Changing -FileIndex 0 to -FileIndex 2 gives these results:
Found File(s):
### Root Text File ###
Random info in root text file
Changing FileIndex to a value that does not point to a file inside the zip, such as -FileIndex 3, gives these results:
File NOT found.
It is a file organiser script I wrote for myself. For a specific purpose of mine. Whenever I try to run it It runs and closes off. But the move operation is not happening. The below comments may help you understand what the code is doing.Please help me on what am i doing wrong here. I am extremely new to Powershell Scripting.
Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser
# Global variable declarations
$global:pathsFromConfig = Get-Content -Path $PSScriptRoot"\MoverPaths.txt"
$global:categoriesFromConfig = Get-Content -Path $PSScriptRoot"\MoverCategories.txt"
$global:categryHash = #{}
# Method call to read configs, create dirs, & move files
readCreateAndMoveFiles
# Method definition
function readCreateAndMoveFiles{
# Reads categories config.txt and splits them line by line
# Adds each line as a key value pair to a hashtable
foreach($category in $categoriesFromConfig)
{
$temp = $category -split ":"
$categryHash.add($temp[0].trim().toString(),($temp[1]).trim().toString())
}
# For each category in the hash table, calls create directory method, and then moves the files based on current category
foreach($hashItem in $categryHash.GetEnumerator()){
# Creates a directory with the Hash Key
Foreach($pathToMonitor in $pathsFromConfig){
$categoryFullPath = $pathToMonitor+$hashItem.Name
createDirectory($categoryFullPath)
# Moves files into that directory
Set-Location -Path $pathToMonitor
$extentions = $hashItem.Value
Get-Item $extentions | Move-Item -Destination $categoryFullPath
$categoryFullPath = ""
}
}
}
# Method Definition
function createDirectory ($categoryName)
{
if(Test-Path -Path $categoryName)
{
# Directory already Exists!
}
else
{
# Creates Directory
md $categoryName
}
}
The config files are hereby:
MoverCategories.txt
Images:*.jpg,*.jpeg,*.png,*.tiff,*.raw,*.heic,*.gif,*.svg,*.eps,*.ico
Documents:*.txt,*.pdf,*.doc,*.docx,*.xls,*.xlsx,*.ppt,*.pptx,*.html,*.xls,*.csv,*.rtx
MoverPaths.txt
D:\Downloads\
Found a way to do this. Thanks for all of your input. Now the script moves files. Instead of sending all extentions in a single shot, i made it into an array and sent it one by one. Now it works fine. If you guys could help me reduce the time of execution that would be great.But the code works now I am happy.
foreach($hashItem in $categryHash.GetEnumerator()){
# Creates a directory with the Hash Key
Foreach($pathToMonitor in $pathsFromConfig){
$categoryFullPath = $pathToMonitor+$hashItem.Name
createDirectory($categoryFullPath)
# Moves files into that directory
[String[]]$extentions = #()
$extentions = $hashItem.Value -split ','
foreach($string in $extentions)
{
Get-Item $pathToMonitor\* -Include $string | Move-Item -Destination $categoryFullPath
}
}
}
Try this
#specify path(s)
$path = "$env:USERPROFILE\Downloads"
## this is make an array of the extensions in the foloder
$extensions = Get-ChildItem -Path $path | Select-Object -Unique -Property #{label = 'ext'
expression = { $_.Extension.substring(1) }
}
## this function will
function New-FoldersByName {
param
(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = 'Data to process')]
$InputObject
)
process {
Set-Location $path
if (!(Test-Path -PathType Container $InputObject )) {
New-Item -ItemType directory -Name $InputObject.ext -WhatIf
Write-Host -Message "A folder named $($InputObject.ext) does not exist. Creating..."
}
else {
Write-Host -Message "A folder named $($InputObject.ext) already exists. Skipping..."
}
}
}
##this is a reuseable function to moves items in a folder into a subfolder named after the files extension
## if extension is .exe the file with be moved to ./EXE/filename.exe
function Move-ItemsByName {
param
(
[Parameter(Mandatory = $true, ValueFromPipeline = $true, HelpMessage = 'Data to process')]
$InputObject
)
process {
Set-Location -Path $path
Move-Item -Path ('*.{0}' -f $InputObject.ext) -Destination ('{0}' -f $InputObject.ext) -WhatIf
}
}
$extensions | New-FoldersByName
$extensions | Move-ItemsByName
I inherited a script that should simply move files from -source to -target. I am prompted for both, and after supplying the paths, it tells me that it cannot find the path while showing a path that I absolutely did not submit, but can't figure out how it's arriving there.
[Parameter(
Mandatory = $true,
Position = 0,
HelpMessage = "Root of the folders or share to archive"
)]
[String] $source,
[Parameter(
Mandatory = $true,
Position = 1,
HelpMessage = "Path of the folder or share of archive"
)]
[string] $target,
[Parameter(
Mandatory = $false,
Position = 3
)]
[int] $days = 30
)
# Get all the files from the source path, that are not shortcuts (*.lnk) and older than the days set
Get-ChildItem $source -Recurse |
Where-Object {!$_.psiscontainer -and ((get-date) - $_.lastwritetime).totaldays -gt $days -and $_.extension -ne ".lnk"} |
ForEach-Object {
# For each file build the destination path
$dest = $_.fullname -replace ([regex]::escape($source)), $target
# Move the files into the destination
Move-Item -Path $_.fullname -Destination $dest -ErrorAction silentlycontinue
}
The log says "Cannot find path '\\appserver\abc$\Automation\Daily\Archive\appserver\abc$\Storage' because it does not exist" - see how it starts repeating itself? \\appserver\abc$\Automation\Daily\Archive\ is the location of the script, whereas \\appserver\abc$\Storage\ is what I am entering as -source. So I have no idea why it is looking at the path to the script, then appending the source path concurrently.
EDIT: This is how I am calling the script (from a little-known finance application called APX):
SHELL PowerShell \\appserver\abc$\Automation\Daily\Archive\ArchiveFiles.ps1 -source \\appserver\abc$\Dataport\dpdata -target \\appserver\abc$\Dataport\archived -days 30
When your script starts, it is beginning in the directory that you are running it from, so it already in \\appserver\abc$\Automation\Daily\Archive\ and if you do not supply a UNC resource prefix such as \\ or A:\ then it will look for ChildItems from that directory down. So when you're supplying the folder path, it's appending that to its current directory and unable to find the new path.
As this would only happen if you had omitted the \\ at the beginning of your string, I would only expect your output if you had submitted appserver\abc$\Storage\ as your source. If you are sure you did supply the \\, then look more closely at whatever line of script is passing the command to this script, to see if there's a reason it's stripping the \\ off beforehand.
I am trying to join multiple log files into a single archive file, then move that archive file to another location, in an effort to both clean up old log files, and save hard drive space. We have a bunch of tools that all log to the same root, with a per-tool folder for their logs. (E.g.,
C:\ServerLogs
C:\ServerLogs\App1
C:\ServerLogs\2ndApp
each of which will have log files inside, like
C:\ServerLogs\App1\June1.log
C:\ServerLogs\App1\June2.log
C:\ServerLogs\2ndApp\June1.log
C:\ServerLogs\2ndApp\June2.log
I want to go into each of these subfolders, archive up all the files older than 5 days, then move the archive to another (long-term storage) drive and delete the now-zipped files. The tools I'm using are PowerShell and 7zip. The below code is using test locations.
I have cobbled together two scripts from various sources online, over the course of two full shifts, but neither one works right. Here's the first:
# Alias for 7-zip
if (-not (test-path "$env:ProgramFiles\7-Zip\7z.exe")) {throw "$env:ProgramFiles\7-Zip\7z.exe needed"}
set-alias 7zip "$env:ProgramFiles\7-Zip\7z.exe"
$Days = 5 #minimum age of files to archive; in other words, newer than this many days ago are ignored
$SourcePath = C:\WorkingFolder\FolderSource\
$DestinationPath = C:\Temp\
$LogsToArchive = Get-ChildItem -Recurse -Path $SourcePath | Where-Object {$_.lastwritetime -le (get-date).addDays(-$Days)}
$archive = $DestinationPath + $now + ".7z"
#endregion
foreach ($log in $LogsToArchive) {
#define Args
$Args = a -mx9 $archive $log
$Command = 7zip
#write-verbose $command
#invoke the command
invoke-expression -command $Command $Args
The problem with this one is that I get errors trying to invoke the expression. I've tried restructuring it, but then I get errors because my $Args have an "a"
So I abandoned this method (despite it being my preferred), and tried this set.
#region Params
param(
[Parameter(Position=0, Mandatory=$true)]
[ValidateScript({Test-Path -Path $_ -PathType 'container'})]
[System.String]
$SourceDirectory,
[Parameter(Position=1, Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$DestinationDirectory
)
#endregion
function Compress-File{
#region Params
param(
[Parameter(Position=0, Mandatory=$true)]
[ValidateScript({Test-Path -Path $_ -PathType 'leaf'})]
[System.String]
$InputFile,
[Parameter(Position=1, Mandatory=$true)]
[ValidateNotNullOrEmpty()]
[System.String]
$OutputFile
)
#endregion
try{
#Creating buffer with size 50MB
$bytesGZipFileBuffer = New-Object -TypeName byte[](52428800)
$streamGZipFileInput = New-Object -TypeName System.IO.FileStream($InputFile,[System.IO.FileMode]::Open,[System.IO.FileAccess]::Read)
$streamGZipFileOutput = New-Object -TypeName System.IO.FileStream($OutputFile,[System.IO.FileMode]::Create,[System.IO.FileAccess]::Write)
$streamGZipFileArchive = New-Object -TypeName System.IO.Compression.GZipStream($streamGZipFileOutput,[System.IO.Compression.CompressionMode]::Compress)
for($iBytes = $streamGZipFileInput.Read($bytesGZipFileBuffer, 0,$bytesGZipFileBuffer.Count);
$iBytes -gt 0;
$iBytes = $streamGZipFileInput.Read($bytesGZipFileBuffer, 0,$bytesGZipFileBuffer.Count)){
$streamGZipFileArchive.Write($bytesGZipFileBuffer,0,$iBytes)
}
$streamGZipFileArchive.Dispose()
$streamGZipFileInput.Close()
$streamGZipFileOutput.Close()
Get-Item $OutputFile
}
catch { throw $_ }
}
Get-ChildItem -Path $SourceDirectory -Recurse -Exclude "*.7z"|ForEach-Object{
if($($_.Attributes -band [System.IO.FileAttributes]::Directory) -ne [System.IO.FileAttributes]::Directory){
#Current file
$curFile = $_
#Check the file wasn't modified recently
if($curFile.LastWriteTime.Date -le (get-date).adddays(-5)){
$containedDir=$curFile.Directory.FullName.Replace($SourceDirectory,$DestinationDirectory)
#if target directory doesn't exist - create
if($(Test-Path -Path "$containedDir") -eq $false){
New-Item -Path "$containedDir" -ItemType directory
}
Write-Host $("Archiving " + $curFile.FullName)
Compress-File -InputFile $curFile.FullName -OutputFile $("$containedDir\" + $curFile.Name + ".7z")
Remove-Item -Path $curFile.FullName
}
}
}
This actually seems to work, insofar as it creates individual archives for each eligible log, but I need to "bundle" up the logs into one mega-archive, and I can't seem to figure out how to recurse (to get sub-level items) and do a foreach (to confirm age) without having that foreach produce individual archives.
I haven't even gotten into the Move and Delete phase, because I can't seem to get the archiving stage to work properly, but I certainly don't mind grinding away at that once this gets figured out (I've already spent two full days trying to figure this one!).
I greatly appreciate any and all suggestions! If I've not explained something, or been a bit unclear, please let me know!
EDIT1: Part of the requirement, which I completely forgot to mention, is that I need to keep the structure in the new location. So the new location will have
C:\ServerLogs --> C:\Archive\
C:\ServerLogs\App1 --> C:\Archive\App1
C:\ServerLogs\2ndApp --> C:\Archive\2ndApp
C:\Archive
C:\Archive\App1\archivedlogs.zip
C:\Archive\2ndApp\archivedlogs.zip
And I have absolutely no idea how to specify that the logs from App1 need to go to App1.
EDIT2: For this latter part, I used Robocopy - It maintains the folder structure, and if you feed it ".zip" as an argument, it'll only do the .zip files.
this line $Args = a -mx9 $archive $log likely needs to have the right side value wrapped in double quotes OR each non-variable wrapped in quotes with a comma between each so that you get an array of args.
another method would be to declare an array of args explicitly. something like this ...
$ArgList = #(
'a'
'-mx9'
$archive
$log
)
i also recommend you NOT use an automatic $Var name. take a look at Get-Help about_Automatic_Variables and you will see that $Args is one of those. you are strongly recommended NOT to use any of them for anything other than reading. writing to them is iffy. [grin]
I am struggling with my script - for some reason, the PSDrive that my script creates is not accessible for Resolve-Path.
In general, in the script there is "Start-RDP" function which starts RDP with preloaded credentials (autologon), and then checks if the Powershell profile on the target host is up to date (by comparing the filehashes). However, in order for the script to access the remote filesystem I need to mount it as PSDrive.
Here is the script that is offending. All the variables are set properly during that time, above in the script.
New-PSDrive -name "$computername" -Root "\\$computername\c$" -Credential $CurrentCred -PSProvider FileSystem | out-null
Start-Sleep -Seconds 10
while (!(Test-Path -Path ${Computername}:\$Userpath\$Documents\)) { Write-host "UserDir not created yet!" ; start-sleep -Seconds 5 }
if (Test-Path -Path ${Computername}:\$Userpath\$Documents\WindowsPowerShell) {
$ProfileHash = Get-FileHash $Profile.CurrentUserAllHosts
if (!(Test-Path "${computername}:\$Userpath\$Documents\WindowsPowerShell\profile.ps1")) { Copy-Item -Force -Path "$env:userprofile\WindowsPowershell\profile.ps1" -Destination "${computername}:\$Userpath\$Documents\WindowsPowerShell\" }
$RemoteProfileHash = Get-FileHash "${computername}:\$Userpath\$Documents\WindowsPowerShell\profile.ps1"
if ($ProfileHash -ne $RemoteProfileHash) { Copy-Item -Force -Path "$env:userprofile\$Documents\WindowsPowershell\profile.ps1" -Destination "${computername}:\$userpath\$Documents\WindowsPowerShell\" }
}
The error I am getting is at second Test-Path (where I check if WindowsPowerShell directory exists).
Resolve-Path : Cannot find drive. A drive with the name 'server01' does not exist.
At C:\windows\system32\windowspowershell\v1.0\Modules\Microsoft.PowerShell.Utility\Microsoft.PowerShell.Utility.psm1:35 char:32
+ $pathsToProcess += Resolve-Path $Path | Foreach-Object ProviderPath
+ ~~~~~~~~~~~~~~~~~~
+ CategoryInfo : ObjectNotFound: (server01:String) [Resolve-Path], DriveNotFoundException
+ FullyQualifiedErrorId : DriveNotFound,Microsoft.PowerShell.Commands.ResolvePathCommand
I am unable to trace down the specific reason this error occurs. The drive is there (I checked using PSBreakpoint)
I'm kind of stuck at this for some time now, do you have any ideas on that one?
I see what you did there.
The problem is that you are using the variable $Profile.CurrentUserAllHosts which powershell is trying to resolve as a complete variable name. $Profile is a string, which has no property called CurrentUserAllHosts. To fix, use the following:
$ProfileHash = Get-FileHash "${Profile}.CurrentUserAllHosts"
After some more investigation, I found this snippet on a blog
commands like Resolve-Path and $PSCmdlet.GetUnresolvedProviderPathFromPSPath() don’t normalize UNC paths properly, even when the FileSystem provider handles them.
Which then links to the Get-NormalizedFileSystemPath script on technet.
Since Get-FileHash is a system provided method, you'll want to Get-NormalizedFileSystemPath before passing it to Get-FileHash
And for posterity sake, here's the script:
function Get-NormalizedFileSystemPath
{
<#
.Synopsis
Normalizes file system paths.
.DESCRIPTION
Normalizes file system paths. This is similar to what the Resolve-Path cmdlet does, except Get-NormalizedFileSystemPath also properly handles UNC paths and converts 8.3 short names to long paths.
.PARAMETER Path
The path or paths to be normalized.
.PARAMETER IncludeProviderPrefix
If this switch is passed, normalized paths will be prefixed with 'FileSystem::'. This allows them to be reliably passed to cmdlets such as Get-Content, Get-Item, etc, regardless of Powershell's current location.
.EXAMPLE
Get-NormalizedFileSystemPath -Path '\\server\share\.\SomeFolder\..\SomeOtherFolder\File.txt'
Returns '\\server\share\SomeOtherFolder\File.txt'
.EXAMPLE
'\\server\c$\.\SomeFolder\..\PROGRA~1' | Get-NormalizedFileSystemPath -IncludeProviderPrefix
Assuming you can access the c$ share on \\server, and PROGRA~1 is the short name for "Program Files" (which is common), returns:
'FileSystem::\\server\c$\Program Files'
.INPUTS
String
.OUTPUTS
String
.NOTES
Paths passed to this command cannot contain wildcards; these will be treated as invalid characters by the .NET Framework classes which do the work of validating and normalizing the path.
.LINK
Resolve-Path
#>
[CmdletBinding()]
param (
[Parameter(Mandatory = $true, ValueFromPipeline = $true, ValueFromPipelineByPropertyName = $true)]
[Alias('PSPath', 'FullName')]
[string[]]
$Path,
[switch]
$IncludeProviderPrefix
)
process
{
foreach ($_path in $Path)
{
$_resolved = $_path
if ($_resolved -match '^([^:]+)::')
{
$providerName = $matches[1]
if ($providerName -ne 'FileSystem')
{
Write-Error "Only FileSystem paths may be passed to Get-NormalizedFileSystemPath. Value '$_path' is for provider '$providerName'."
continue
}
$_resolved = $_resolved.Substring($matches[0].Length)
}
if (-not [System.IO.Path]::IsPathRooted($_resolved))
{
$_resolved = Join-Path -Path $PSCmdlet.SessionState.Path.CurrentFileSystemLocation -ChildPath $_resolved
}
try
{
$dirInfo = New-Object System.IO.DirectoryInfo($_resolved)
}
catch
{
$exception = $_.Exception
while ($null -ne $exception.InnerException)
{
$exception = $exception.InnerException
}
Write-Error "Value '$_path' could not be parsed as a FileSystem path: $($exception.Message)"
continue
}
$_resolved = $dirInfo.FullName
if ($IncludeProviderPrefix)
{
$_resolved = "FileSystem::$_resolved"
}
Write-Output $_resolved
}
} # process
} # function Get-NormalizedFileSystemPath