Related
I have over 10 TB of log file data. I want to find an efficient way to delete log files that are older than 3 years old. Originally, I was thinking about scheduling this PowerShell script:
# Delete all Files in PATH older than 3 years (1095 days) old
$Path = "Path to log files"
$Daysback = "-1095"
$CurrentDate = Get-Date
$DatetoDelete = $CurrentDate.AddDays($Daysback)
Get-ChildItem $Path | Where-Object { $_.LastWriteTime -lt $DatetoDelete } | Remove-Item
I am concerned that looping through 10 TB of data will take hours to run even thought this script appears to run on O(n). Does anybody have any better solution?
If performance matters I'd get rid of pipes and wouldn't use gci (use lower level types from System.IO). If there are millions of files I'd also write it in c# and only call methods from ps runtime. Below is example to remove files older than N days:
Add-Type -TypeDefinition #"
public static class LogCleaner {
public static void Clean(string dirName, int age)
{
foreach (string file in System.IO.Directory.GetFiles(dirName))
{
var fi = new System.IO.FileInfo(file);
if (fi.LastWriteTime < System.DateTime.Now.AddDays(-age))
{
fi.Delete();
}
}
}
}
"#
[LogCleaner]::Clean("C:\Temp\demo", 10)
You may also check how to achive this using windows native utilities (like dir), or maybe install git bash and try to do this linux way (e.g. using find command)
Starting premise: very restrictive environment, Windows 7 SP1, Powershell 3.0. Limited or no possibility of using external libraries.
I'm trying to re-write a bash tool I created previously, this time using PowerShell. In bash I implemented autocompletion to make the tool more user friendly and I want to do the same thing for the PowerShell version.
The bash version worked like this:
./launcher <Tab> => ./launcher test (or dev, prod, etc.)
./launcher test <Tab> => ./launcher test app1 (or app2, app3, etc.)
./launcher test app1 <Tab> => ./launcher test app1 command1 (or command2, command3, etc.).
As you can see, everything was dynamic. The list of environments was dynamic, the list of application was dynamic, depending on the environment selected, the list of commands was also dynamic.
The problem is with the test → application connection. I want to show the correct application based on the environment already selected by the user.
Using PowerShell's DynamicParam I can get a dynamic list of environments based on a folder listing. I can't however (or at least I haven't found out how to) do another folder listing but this time using a variable based on the existing user selection.
Current code:
function ParameterCompletion {
$RuntimeParameterDictionary = New-Object Management.Automation.RuntimeDefinedParameterDictionary
# Block 1.
$AttributeCollection = New-Object Collections.ObjectModel.Collection[System.Attribute]
$ParameterName = "Environment1"
$ParameterAttribute = New-Object Management.Automation.ParameterAttribute
$ParameterAttribute.Mandatory = $true
$ParameterAttribute.Position = 1
$AttributeCollection.Add($ParameterAttribute)
# End of block 1.
$parameterValues = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute = New-Object Management.Automation.ValidateSetAttribute($parameterValues)
$AttributeCollection.Add($ValidateSetAttribute)
$RuntimeParameter = New-Object Management.Automation.RuntimeDefinedParameter($ParameterName, [string], $AttributeCollection)
$RuntimeParameterDictionary.Add($ParameterName, $RuntimeParameter)
# Block 2: same thing as in block 1 just with 2 at the end of variables.
# Problem section: how can I change this line to include ".\configurations\${myVar}"?
# And what's the magic incantation to fill $myVar with the info I need?
$parameterValues2 = $(Get-ChildItem -Path ".\configurations" -Directory | Select-Object -ExpandProperty Name)
$ValidateSetAttribute2 = New-Object Management.Automation.ValidateSetAttribute($parameterValues2)
$AttributeCollection2.Add($ValidateSetAttribute2)
$RuntimeParameter2 = New-Object
Management.Automation.RuntimeDefinedParameter($ParameterName2, [string], $AttributeCollection2)
$RuntimeParameterDictionary.Add($ParameterName2, $RuntimeParameter2)
return $RuntimeParameterDictionary
}
function App {
[CmdletBinding()]
Param()
DynamicParam {
return ParameterCompletion "Environment1"
}
Begin {
$Environment = $PsBoundParameters["Environment1"]
}
Process {
}
}
I would recommend using argument completers, which are semi-exposed in PowerShell 3 and 4, and fully exposed in version 5.0 and higher. For v3 and v4, the underlying functionality is there, but you have to override the TabExpansion2 built-in function to use them. That's OK for your own session, but it's generally frowned upon to distribute tools that do that to other people's sessions (imagine if everyone tried to override that function). A PowerShell team member has a module that does this for you called TabExpansionPlusPlus. I know I said overriding TabExpansion2 was bad, but it's OK if this module does it :)
When I needed to support versions 3 and 4, I would distribute my commands in modules, and have the modules check for the existence of the 'Register-ArgumentCompleter' command, which is a cmdlet in v5+ and is a function if you have the TE++ module. If the module found it, it would register any completer(s), and if it didn't, it would notify the user that argument completion wouldn't work unless they got the TabExpansionPlusPlus module.
Assuming you have the TE++ module or PSv5+, I think this should get you on the right track:
function launcher {
[CmdletBinding()]
param(
[string] $Environment1,
[string] $Environment2,
[string] $Environment3
)
$PSBoundParameters
}
1..3 | ForEach-Object {
Register-ArgumentCompleter -CommandName launcher -ParameterName "Environment${_}" -ScriptBlock {
param($commandName, $parameterName, $wordToComplete, $commandAst, $fakeBoundParameter)
$PathParts = $fakeBoundParameter.Keys | where { $_ -like 'Environment*' } | sort | ForEach-Object {
$fakeBoundParameter[$_]
}
Get-ChildItem -Path ".\configurations\$($PathParts -join '\')" -Directory -ErrorAction SilentlyContinue | select -ExpandProperty Name | where { $_ -like "${wordToComplete}*" } | ForEach-Object {
New-Object System.Management.Automation.CompletionResult (
$_,
$_,
'ParameterValue',
$_
)
}
}
}
For this to work, your current working directory will need a 'configurations' directory contained in it, and you'll need at least three levels of subdirectories (reading through your example, it looked like you were going to enumerate a directory, and you would go deeper into that structure as parameters were added). The enumerating of the directory isn't very smart right now, and you can fool it pretty easy if you just skip a parameter, e.g., launcher -Environment3 <TAB> would try to give you completions for the first sub directory.
This works if you will always have three parameters available. If you need a variable # of parameters, you could still use completers, but it might get a little trickier.
The biggest downside would be that you'd still have to validate the users' input since completers are basically just suggestions, and users don't have to use those suggestions.
If you want to use dynamic parameters, it gets pretty crazy. There may be a better way, but I've never been able to see the value of dynamic parameters at the commandline without using reflection, and at that point you're using functionality that could change at the next release (the members usually aren't public for a reason). It's tempting to try to use $MyInvocation inside the DynamicParam {} block, but it's not populated at the time the user is typing the command into the commandline, and it only shows one line of the command anyway without using reflection.
The below was tested on PowerShell 5.1, so I can't guarantee that any other version has these exact same class members (it's based off of something I first saw Garrett Serack do). Like the previous example, it depends on a .\configurations folder in the current working directory (if there isn't one, you won't see any -Environment parameters).
function badlauncher {
[CmdletBinding()]
param()
DynamicParam {
#region Get the arguments
# In it's current form, this will ignore parameter names, e.g., '-ParameterName ParameterValue' would ignore '-ParameterName',
# and only 'ParameterValue' would be in $UnboundArgs
$BindingFlags = [System.Reflection.BindingFlags] 'Instance, NonPublic, Public'
$Context = $PSCmdlet.GetType().GetProperty('Context', $BindingFlags).GetValue($PSCmdlet)
$CurrentCommandProcessor = $Context.GetType().GetProperty('CurrentCommandProcessor', $BindingFlags).GetValue($Context)
$ParameterBinder = $CurrentCommandProcessor.GetType().GetProperty('CmdletParameterBinderController', $BindingFlags).GetValue($CurrentCommandProcessor)
$UnboundArgs = #($ParameterBinder.GetType().GetProperty('UnboundArguments', $BindingFlags).GetValue($ParameterBinder) | where { $_ } | ForEach-Object {
try {
if (-not $_.GetType().GetProperty('ParameterNameSpecified', $BindingFlags).GetValue($_)) {
$_.GetType().GetProperty('ArgumentValue', $BindingFlags).GetValue($_)
}
}
catch {
# Don't do anything??
}
})
#endregion
$ParamDictionary = New-Object System.Management.Automation.RuntimeDefinedParameterDictionary
# Create an Environment parameter for each argument specified, plus one extra as long as there
# are valid subfolders under .\configurations
for ($i = 0; $i -le $UnboundArgs.Count; $i++) {
$ParameterName = "Environment$($i + 1)"
$ParamAttributes = New-Object System.Collections.ObjectModel.Collection[System.Attribute]
$ParamAttributes.Add((New-Object Parameter))
$ParamAttributes[0].Position = $i
# Build the path that will be enumerated based on previous arguments
$PathSb = New-Object System.Text.StringBuilder
$PathSb.Append('.\configurations\') | Out-Null
for ($j = 0; $j -lt $i; $j++) {
$PathSb.AppendFormat('{0}\', $UnboundArgs[$j]) | Out-Null
}
$ValidParameterValues = Get-ChildItem -Path $PathSb.ToString() -Directory -ErrorAction SilentlyContinue | Select-Object -ExpandProperty Name
if ($ValidParameterValues) {
$ParamAttributes.Add((New-Object ValidateSet $ValidParameterValues))
$ParamDictionary[$ParameterName] = New-Object System.Management.Automation.RuntimeDefinedParameter (
$ParameterName,
[string[]],
$ParamAttributes
)
}
}
return $ParamDictionary
}
process {
$PSBoundParameters
}
}
The cool thing about this one is that it can keep going as long as there are folders, and it automatically does parameter validation. Of course, you're breaking the laws of .NET by using reflection to get at all those private members, so I would consider this a terrible and fragile solution, no matter how fun it was to come up with.
Maybe I am trying to use AWS EC2 incorrectly, please help me out. I would like to make a base ami via a user data script, this is no problem, it works. However, the next step is to make an image, however since the object is untagged its kind of a pain to filter for it, I can add criteria for region, vpc, security group and state, this would find the object and I could build the image.
However I do not want to overwrite the existing image, so ideally i need to tag this with a name and version, no problem. But then I need the child images to find that image, and i would like to find via name and version, but dynamically, i.e. latest. In docker it is pretty straight forward as long as the container is tagged, to use latest the version can be omitted and it will auto pull the latest. Is there a similar technique here? What do you guys use? Am I possibly using this wrong?
Here is what I did:
Note: although this is for is tagging an instance, it can easily be modified to work with images.
Note: I am not a powershell power user, so if you see a glaring inefficiency, please let me know.
I use Jenkins to build the machine, as such it has environment variables that I use for the tagging, however it calls a powershell script that has this signature, so you could manually call or call it via another script:
param(
...
[Parameter(Mandatory=$true)][string]$Tag_Name,
[Parameter(Mandatory=$true)][string]$Tag_Version
)
Inside of this script, I set the instance tags like so:
#Get metadata from ec2 service
$identityDocument = (Invoke-WebRequest http://169.254.169.254/latest/dynamic/instance-identity/document/).Content | ConvertFrom-Json
$tags = #(
#{Key = "Name"; Value = $Tag_Name},
#{Key = "Version"; Value = $Tag_Version}
)
New-EC2Tag -Resource $identityDocument.instanceId -Tag $tags
In another script, I can query by name, find all instances, parse the result into a hash table of [InstanceId, Version], sort by Version and get the top one.
$instanceName = "hello-world"
$instances = GetHashTableOfFilteredInstances $instanceName
$instanceId = GetNewestInstance($instances)
Write-Host 'Information for ' $instanceName
Write-Host '================='
Write-Host 'The newest instance is ' $instanceId
Write-Host '================='
function GetHashTableOfFilteredInstances($tagName){
$instances = Get-EC2Instance -Filter #( `
#{name='tag:Name'; values=$tagName};`
) | Select-Object -ExpandProperty instances
$actInstances= #{}
foreach($instance in $instances){
foreach($tag in $instance.Tag){
if ($tag.Key -ne "Version") {
Continue;
}
$actInstances.Add($tag.Value, $instance.InstanceId)
}
}
return $actInstances
}
function GetNewestInstance($instances){
return ($instances.GetEnumerator() | Sort-Object Key -descending)[0].Value
}
First off I apologize for the extremely long, wordy post. It’s an interesting issue and I wanted to be as detailed as possible. I’ve tried looking through any related PowerShell posts on the site but I couldn’t find anything that helped me with troubleshooting this problem.
I've been working on a PowerShell script with a team that can send Wake-On-Lan packets to a group of computers. It works by reading a .csv file that has the hostnames and MAC’s in two columns, then it creates the WOL packets for each computer and broadcasts them out on the network. After the WOL packets are sent, it waits a minute and then pings the computers to verify they are online, and if any don’t respond it will display a window with what machines didn’t respond to a ping. Up until the final If/Else statement works fine, so I won't be going into too much detail on that part of the script (but of course if you want/need further details please feel free to ask).
The problem I’m having is with the final If/Else statement. The way the script is supposed to work is that in the ForEach loop in the middle of the script, the value of variable $PingResult is true or false depending on whether or not the computer responds to a ping. If the ping fails, $PingResult is $false, and then it adds the hostname to the $PingResult2 variable.
In theory if all of the machines respond, the If statement fires and the message box displays that it was a success and then the script stops. If any machines failed to respond, the Else statement runs and it joins all of the items together from the $PingResult2 variable and displays the list in a window.
What actually happens is that even if all of the machines respond to a ping, the If statement is completely skipped and the Else statement runs instead. However, at that point the $PingResult2 variable is blank and hence it doesn’t display any computer names of machines that failed to respond. In my testing I’ve never seen a case where the script fails to wake a computer up (assuming it’s plugged in, etc.), but the Else statement still runs regardless. In situations where the Else statement runs, I’ve checked the value of the $PingResult2 variable and confirmed that it is blank, and typing $PingResult2 –eq “” returns $true.
To add another wrinkle to the problem, I want to return to the $PingResult2 variable. I had to create the variable as a generic list so that it would support the Add method to allow the variable to grow as needed. As a test, we modified the script to concatenate the results together by using the += operator instead of making $PingResult2 a list, and while that didn’t give a very readable visual result in the final display window if machines failed, it did actually work properly occasionally. If all of the computers responded successfully the If statement would run as expected and display the success message. Like I said, it would sometimes work and sometimes not, with no other changes making a difference in the results. One other thing that we tried was taking out all of the references to the Visual Basic assembly and other GUI elements (besides the Out-GridView window) and that didn’t work either.
Any idea of what could be causing this problem? Me and my team are completely tapped out of ideas at this point and we’d love to figure out what’s causing the issue. We’ve tried it on Windows 7, 8.1, and the latest preview release of Windows 10 with no success. Thanks in advance for any assistance.
P.S Extra brownie points if you can explain what the regular expression on line 29 is called and how it exactly works. I found out about it on a web posting that resolved the issue of adding a colon between every two characters, but the posting didn’t explain what it was called. (Original link http://powershell.org/wp/forums/topic/add-colon-between-every-2-characters/)
Original WOL Script we built the rest of the script around was by John Savill (link http://windowsitpro.com/networking/q-how-can-i-easily-send-magic-packet-wake-machine-my-subnet)
Script
Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.ShowDialog() | Out-Null
$FileVerify = Get-Content -Path $OpenFileDialog.FileName -TotalCount 1
$FileVerify = ($FileVerify -split ',')
If($FileVerify[0] -ne "Machine Name" -or $FileVerify[1] -ne "MAC")
{
$MsgBox = [System.Windows.Forms.MessageBox]::Show("The CSV File's headers must be Machine Name and MAC.",'Invalid CSV File headers!',0,48)
Break
}
$ComputerList = Import-Csv -Path $OpenFileDialog.FileName |
Out-GridView -PassThru -Title "Select Computers to Wake up"
ForEach($Computer in $ComputerList)
{
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
$UDPclient = new-Object System.Net.Sockets.UdpClient
$UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
$packet = [byte[]](,0xFF * 6)
$packet += $MACAddr * 16
[void] $UDPclient.Send($packet, $packet.Length)
write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"
}
Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60
$PingResult2 = New-Object System.Collections.Generic.List[System.String]
ForEach($Computer in $ComputerList)
{
Write-Host "Pinging $($Computer.'Machine Name')"
$PingResult = Test-Connection -ComputerName $Computer.'Machine Name' -Quiet
If ($PingResult -eq $false)
{
$PingResult2.Add($Computer.'Machine Name')
}
}
If($PingResult2 -eq "")
{
[System.Windows.Forms.MessageBox]::Show("All machines selected are online.",'Success',0,48)
Break
}
Else
{
$PingResult2 = ($PingResult2 -join ', ')
[System.Windows.Forms.MessageBox]::Show("The following machines did not respond to a ping: $PingResult2",'Unreachable Machines',0,48)
}
The comparison in your If statement is incorrect because you are comparing $PingResult2, a List<string>, to a string. Instead, try
If ($PingResult2.Count -eq 0)
{
# Show the message box
}
Else
{
# Show the other message box
}
or one of countless other variations on this theme.
The regular expression in question uses a backreference to replace exactly two characters with the same two characters plus a colon character. I am unsure what exactly you are attempting to "define," though.
You are checking if a list has a value of a null string, rather than checking the number of items in the list.
If you change the if statement to the following it should work fine:
If($PingResult2.count -eq 0)
I'm guessing the regex is trying to insert a colon between every two characters of a string to represent 0123456789ab as 01:23:45:67:89:ab.
The code means if there is no hyphen or colon in the MAC, put in a colon every the characters, then split the address using colon as delimiter then represent each as a byte:
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
The other answer have explained quite well why your code does not work. I'm not going there. Instead I'll give some suggestions that I think would improve your script, and explain why I think so. Let's start with functions. Some of the things you do are functions I keep on hand because, well, they work well and are used often enough that I like having them handy.
First, your dialog to get the CSV file path. It works, don't get me wrong, but it could probably be better... As it is you pop up an Open File dialog with no parameters. This function allows you to use a few different parameters as wanted, or none for a very generic Open File dialog, but I think it's a slight improvement here:
Function Get-FilePath{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
Then just call it as such:
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"
That opens the dialog filtering for only CSV files, and starts them looking at their desktop (I find that a lot of people save things to their desktop). That only gets the path, so you would run your validation like you were. Actually, not like you were. You really seem to have over complicated that whole bit. Bit I'll get to that in a moment, first, another function! You call message boxes fairly often, and type out a bunch of options, and call the type, and everything every single time. If you're going to do it more than once, make it easy on yourself, make a function. Here, check this out:
Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}
Then you can specify as much or as little as you want for it. Plus it uses Type'd parameters, so tab completion works, or in the ISE (if that's where you're writing your script, like I do) it will pop up valid options and you just pick from a list for the buttons or icon to show. Plus it doesn't return anything if it's a simple 'OK' response, to keep things clean, but will return Yes/No/Cancel or whatever other option you choose for buttons.
Ok, that's the functions, let's get to the meat of the script. Your file validation... Ok, you pull the first line of the file, so that should just be a string, I'm not sure why you're splitting it and verifying each header individually. Just match the string as a whole. I would suggest doing it case insensitive, since we don't really care about case here. Also, depending on how the CSV file was generated, there could be quotes around headers, which you may want to account for. Using -Match will perform a RegEx match that is a bit more forgiving.
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
break
}
So now we have two functions, and 5 lines of code. Yes, the functions take up more space than what you previously had, but they're friendlier to work with, and IMO more functional. Your MAC address correction, and WOL sending part are all aces so far as I'm concerned. There's no reason to change that part. Now, for validating that computers came back up... here we could use some improvement. Instead of making a [List] just add a member to each object, then filter against that below. The script as a whole would be a little longer, but better off for it I think.
Add-Type -AssemblyName Microsoft.VisualBasic,System.Windows.Forms
Function Get-FilePath{
[CmdletBinding()]
Param(
[String]$Filter = "|*.*",
[String]$InitialDirectory = "C:\")
[void][System.Reflection.Assembly]::LoadWithPartialName("System.windows.forms")
$OpenFileDialog = New-Object System.Windows.Forms.OpenFileDialog
$OpenFileDialog.initialDirectory = $InitialDirectory
$OpenFileDialog.filter = $Filter
[void]$OpenFileDialog.ShowDialog()
$OpenFileDialog.filename
}
Function Show-MsgBox ($Text,$Title="",[Windows.Forms.MessageBoxButtons]$Button = "OK",[Windows.Forms.MessageBoxIcon]$Icon="Information"){
[Windows.Forms.MessageBox]::Show("$Text", "$Title", [Windows.Forms.MessageBoxButtons]::$Button, $Icon) | ?{(!($_ -eq "OK"))}
}
#Get File Path
$CSVFile = Get-FilePath -Filter "Comma Separated Value (.CSV)|*.CSV" -InitialDirectory "$env:USERPROFILE\Desktop"
#Validate Header
If((Get-Content $CSVFile -TotalCount 1) -match '^"?machine name"?,"?mac"?$'){
Show-MsgBox "The CSV File's headers must be Machine Name and MAC." 'Invalid CSV File headers!' -Icon Warning
break
}
$ComputerList = Import-Csv -Path $CSVFile |
Out-GridView -PassThru -Title "Select Computers to Wake up"
ForEach($Computer in $ComputerList)
{
If($Computer.'MAC' -notmatch '([:]|[-])')
{
$Computer.'MAC' = $Computer.'MAC' -replace '(..(?!$))','$1:'
}
$MACAddr = $Computer.'MAC'.split('([:]|[-])') | %{ [byte]('0x' + $_) }
$UDPclient = new-Object System.Net.Sockets.UdpClient
$UDPclient.Connect(([System.Net.IPAddress]::Broadcast),4000)
$packet = [byte[]](,0xFF * 6)
$packet += $MACAddr * 16
[void] $UDPclient.Send($packet, $packet.Length)
write "Wake-On-Lan magic packet sent to $($Computer.'Machine Name'.ToUpper())"
}
Write-Host "Pausing for sixty seconds before verifying connectivity."
Start-Sleep -Seconds 60
$ComputerList|ForEach
{
Write-Host "Pinging $($_.'Machine Name')"
Add-Member -InputObject $_ -NotePropertyName "PingResult" -NotePropertyValue (Test-Connection -ComputerName $Computer.'Machine Name' -Quiet)
}
If(($ComputerList|Where{!($_.PingResult)}).Count -gt 0)
{
Show-MsgBox "All machines selected are online." 'Success'
}
Else
{
Show-MsgBox "The following machines did not respond to a ping: $(($ComputerList|?{!($_.PingResult)}) -join ", ")" 'Unreachable Machines' -Icon Asterisk
}
Ok, I'm going to get off my soap box and go home, my shift's over and it's time for a cold one.
I am creating a nightly database schema file and would like to put all the files created each night, one for each database, into a folder and compress that folder.
I have a PowerShell script that creates the schema.Only creation script of the db's and then adds all the files to a new folder. The problem lies within the compression portion of this process.
Does anybody have any idea if this can be accomplished with the pre-installed Windows utility that handles folder compression?
It would be best to use that utility if possible rather than something like 7zip (I don't feel like installing 7zip on every customers' server and it may take IT years to do it if I ask them).
A native way with latest .NET 4.5 framework, but entirely feature-less:
Creation:
Add-Type -Assembly "System.IO.Compression.FileSystem" ;
[System.IO.Compression.ZipFile]::CreateFromDirectory("c:\your\directory\to\compress", "yourfile.zip") ;
Extraction:
Add-Type -Assembly "System.IO.Compression.FileSystem" ;
[System.IO.Compression.ZipFile]::ExtractToDirectory("yourfile.zip", "c:\your\destination") ;
As mentioned, totally feature-less, so don't expect an overwrite flag.
Here's a couple of zip-related functions that don't rely on extensions: Compress Files with Windows PowerShell.
The main function that you'd likely be interested in is:
function Add-Zip
{
param([string]$zipfilename)
if(-not (test-path($zipfilename)))
{
set-content $zipfilename ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
(dir $zipfilename).IsReadOnly = $false
}
$shellApplication = new-object -com shell.application
$zipPackage = $shellApplication.NameSpace($zipfilename)
foreach($file in $input)
{
$zipPackage.CopyHere($file.FullName)
Start-sleep -milliseconds 500
}
}
Usage:
dir c:\demo\files\*.* -Recurse | Add-Zip c:\demo\myzip.zip
There is one caveat: the shell.application object's NameSpace() function fails to open up the zip file for writing if the path isn't absolute. So, if you passed a relative path to Add-Zip, it'll fail with a null error, so the path to the zip file must be absolute.
Or you could just add a $zipfilename = resolve-path $zipfilename at the beginning of the function.
As of PowersShell 5 there is a Compress-Archive cmdlet that does the task out of the box.
This compresses .\in contents to .\out.zip with System.IO.Packaging.ZipPackage following the example here
$zipArchive = $pwd.path + "\out.zip"
[System.Reflection.Assembly]::Load("WindowsBase,Version=3.0.0.0, Culture=neutral, PublicKeyToken=31bf3856ad364e35")
$ZipPackage=[System.IO.Packaging.ZipPackage]::Open($zipArchive, [System.IO.FileMode]"OpenOrCreate", [System.IO.FileAccess]"ReadWrite")
$in = gci .\in | select -expand fullName
[array]$files = $in -replace "C:","" -replace "\\","/"
ForEach ($file In $files) {
$partName=New-Object System.Uri($file, [System.UriKind]"Relative")
$part=$ZipPackage.CreatePart($partName, "application/zip", [System.IO.Packaging.CompressionOption]"Maximum")
$bytes=[System.IO.File]::ReadAllBytes($file)
$stream=$part.GetStream()
$stream.Write($bytes, 0, $bytes.Length)
$stream.Close()
}
$ZipPackage.Close()
Used voithos' answer to zip files up in powershell, just had one problem with the Add-Zip function, the Start-sleep -milliseconds 500 caused problems if the file couldn't be fully zipped up in that time -> the next one starting before it was complete caused errors and some files not to be zipped.
So after playing around for a bit, first trying to get a counter going to check the count of the $zipPackage.Items() and only continuing after the items count increased (which did not work as it would return 0 in some cases when it should not) I found that it will return 0 if the package is still zipping/copying the files up (I think, haha). Added a simple while loop with the start-sleep inside of it, waiting for the zipPackage.Items().count to be a non-zero value before continuing and this seems to solve the problem.
function Add-Zip
{
param([string]$zipfilename)
if(-not (test-path($zipfilename)))
{
set-content $zipfilename ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18))
(dir $zipfilename).IsReadOnly = $false
}
$shellApplication = new-object -com shell.application
$zipPackage = $shellApplication.NameSpace($zipfilename)
foreach($file in $input)
{
$zipPackage.CopyHere($file.FullName)
do
{
Start-sleep -milliseconds 250
}
while ($zipPackage.Items().count -eq 0)
}
}
Using PowerShell Version 3.0:
Copy-ToZip -File ".\blah" -ZipFile ".\blah.zip" -Force
Hope this helps.