Search This Blog

Tuesday, 4 October 2011

Powershell file & directory purge script

I've been wanting to learn something new for a while and since @darrenparkinson comments on my DNS update batch file I thought I'd have a look at Powershell, aka POSH (I like that).

Started by diligently reading each chapter of 'Mastering Powershell', Dr. Tobias Weltner. I say each chapter, my attention levels waned after about chapter, erm 1.


Plan B.

I already have a few scripts (batch) running in the office so picked on one of those and started trying to write a replacement. In this case a purge files after # days which we run on a fileshare.

Dive in, used the book for reference and of course the worlds saviour our good friend google.


Lots of reading, lots of searching, lots of head scratching, lots of trying others examples and lots of grumpiness resulted in below.


# // ----------------------------------------------------------------------------------//

# // This script will delete files and directories based on path and days set below //
# // Errors are written to the Application event log and an email summary sent. //
# // //
#
# // Revision history :
# // Created : 03/10/2011 - Paul Regan //
# // ----------------------------------------------------------------------------------//

// READ THESE TWO SECTIONS !!!
//
// 1. Before you can start writting to system logs you have to register the source - only once. Not particulary well documented so left here
// for reference :
// New-EventLog -Logname "<name>" -Source "<a name you can reference>"
// eg
// new-EventLog -Logname "Application" -Source "POSH:Script"

// 2. Depending on your local security you may need to set script execution policy.
// http://technet.microsoft.com/en-us/library/ee176961.aspx
//
// set-executionpolicy <level>
// eg
// set-executionpolicy unrestricted

# // *********** Set your variables here !
# //
# // -Days ## is the period you want to retain
$span = new-timespan -Days 30
# // Working directory
$SourcePath = "s:\temporary"
# // Set logging variables
$logname = "s-temp-delete-$todaynumerical.log"
$logpath = "c:\scripts\logs"
# // Set eMail variables
$subject = "POSH Script : Files and directories deleted from $sourcepath on $todaynumerical"
$fromAddress = "[email protected]"
$smtpServerName = "smtpserver.domain.co.uk"
$toAddress = "[email protected]"
# // Send attachment or results in the body of the email [true/false]
$emailattach = "false"
# // System variable - DONT CHANGE unless you know what you are doing (not that I do)
$today = get-date
$todaynumerical = get-date -format "yyyy-d-M"
$CutoffTime = $today-$span

# // Creates a function with all the param needed for the commane Write-EventLog
function WriteEventLog ($message, $entrytype)
{
Write-EventLog -Logname "Application" -Source "FilesScript" -Message $message -EntryType $entrytype -EventID 4001
}

# // Set the two variables
$message = "PS Log file message"
$entrytype = "Information"

# // I think this writes the first message to the Windows event log defined above, gives you a 'start of process' point to find in a busy log.
WriteEventLog $message $entrytype

# // Builds a file list (PS can accept/apply multiple output messages to the same variable.
# // Where-Object {$_.PSIsContainer -eq $False = Only files, directories would return TRUE
$files = get-childitem -recurse $SourcePath | Where-Object {$_.PSIsContainer -eq $False}

# // empty arrays to hold file lists
$removedFiles = @()
$skippedFiles = @()
$failedactionfiles [email protected]()

# // Start the for loop to read $files and determin the action based on the modified date
foreach ($file in $files)
{
remove-item -ev a -path $file.fullname -recurse -force -whatif | Where-Object {$_.lastwritetime -le $CutoffTime}

<#
#####################################################################################################################
if ($file.lastwritetime -gt $CutoffTime)
{
$skippedFiles += $file.fullname
# $message = "Not moving newer: " -join $file.fullname
# $entrytype = "Information"
# // I don't really want to write files not deleted/moved into the EventLog
# WriteEventLog $message $entrytype
}
else
{
# // Build the newname variable using destination path, old file name and todays date
# $newname = $DestPath + "\" + $file.name + "-" + $file.lastwritetime.tostring("yyyy-MMM-dd")

# // -ev = ErrorVariable which the output from is being set to 'a'
# // $file.fullname pulls the full path from the object (file)
#move-item -ev a -path $file.fullname -destination $newname
remove-item -ev a -path $file.fullname -recurse -force -whatif
# // From the variable $a .count the number of entries, entries against $a would mean an error
#####################################################################################################################
#>
# // Are there any errors ?
if ($a.count -gt 0)
{
# // Creates a $message variable based on $a (output from Remove-Item) and $file (The file we're working on)
# // remote-item generates a nice clean message. move-item does-not, both message options below.
#$message = $file.fullname + " - " + $a[0].tostring()
$message = $a[0].tostring()
# // Writes the error to the event log defined above
$type = "Error"
WriteEventLog $message $type
# // Also add the message to the $failedactionfiles array, which we'll use later ..
$failedactionfiles += $message
}
else
{
$removedFiles += $file.fullname
}
}

# // I have now removed all files older than $span and will start removing all EMPTY directories that match the same cut off time

# // Build a list of home directories from $sourcepath and apply to $rootdirs
$rootdirs = get-childitem $sourcepath | Where-Object {$_.PSIsContainer -eq $True}

<# // This function is not actually being used but I am proud of it, my first, so its staying here for now.
function getemptydirs ($source)
{
# // Command from http://technet.microsoft.com/en-us/library/ff730953.aspx
# // (Get-ChildItem c:\scripts\working\live -recurse | Where-Object {$_.PSIsContainer -eq $True}) | Where-Object {$_.GetFiles().Count -eq 0} | Select-Object FullName,root,parent
(Get-ChildItem $source -recurse | Where-Object {$_.PSIsContainer -eq $True}) | Where-Object {$_.GetFiles().Count -eq 0} | Select-Object FullName
}

# // Get-ChildItem c:\scripts\working\live\paul | get-member << to display available properties
# // (Get-itemproperty -path c:\scripts\working\live\paul).lastwritetime << to display lastwrite time property
# // Get-ChildItem c:\scripts\working\live -recurse | Where {$_.psIsContainer -eq $true} | get-member -membertype property
# // Get-ChildItem c:\scripts\working\live -recurse | Where {$_.psIsContainer -eq $true} | get-itemproperty -name "Parent"

#>

# // Create some empty arrays and default options to be used
$emptydirs = @()
$removeddirs = @()
$message = "PS Log file message"
$entrytype = "Information"

# // Loop through $rootdirs and create an array containing all directories with zero file count and older than # days (set above)
foreach ($directory in $rootdirs)
{
$emptydirs += (Get-ChildItem $directory.fullname -recurse | Where-Object {$_.PSIsContainer -eq $True}) | Where-Object {$_.lastwritetime -le $CutoffTime} | Where-Object {$_.GetFiles().Count -eq 0}
}

# // Now delete the list we just made
# // First compare the count of $rootdirs & $emptydirs. If they are the same it indicates there are no directories to delete.
if ($emptydirs.count -eq $rootdirs.count) {
echo "Zero to delete"}
else {
# // Loop through $emptydirs removing each directory
# // NB// If the root dir is actually empty, ie has no dirs that match the remove filter then it will generate a null error if you watch the script live.
foreach ($emptydir in $emptydirs) {
remove-item -errorvariable err -path $emptydir.fullname -recurse -force -whatif

if ($err.count -gt 0) {
# // Creates a $message variable based on $error (output from remove-item)
$message = $err[0].tostring()
# // Writes the error to the event log defined above
#$type = "Error"
#WriteEventLog $message $type
# // Also add the message to the $failedactiondir array, which we'll use later ..
$failedactiondir += $message }
else {
$removeddirs += $emptydir.fullname}
}
}

# // Create the log files
# // Moosh both the results arrays into a single with output to a txt file
$attachments = $removedfiles+$removeddirs | out-file $logpath\$logname

# // Just to keep the email clean I'll check for empty file actions and set a cute little message
if ($failedactionfiles.count -eq 0) {$failedactionfiles = "No file remove failures"}
if ($removedfiles.count -eq 0) {$removedfiles = "No files removed"}
if ($skippedfiles.count -eq 0) {$skippedfiles = "No files skipped"}
if ($removeddirs.count -eq 0) {$removeddirs = "No directories removed"}

# // Construct the email message
$bodyText = @()
$bodyText += "Failed to remove: "
$bodyText += $failedactionfiles
$bodyText += "----------------------------------------------------"
$bodyText += "Removed files: "
$bodyText += $removedFiles
$bodyText += "----------------------------------------------------"
$bodyText += "Skipped files: "
$bodyText += $skippedFiles
$bodyText += "----------------------------------------------------"
$bodyText += "Removed directories: "
$bodyText += $removeddirs

# // The text from the array $bodyText when sent via email comes out as a single line. | Out-String deals with that.
# // http://social.technet.microsoft.com/Forums/en-US/winserverpowershell/thread/e0213b00-331f-4588-be90-afd0ce5ed50b/
$bodyText = $bodyText | Out-String
$bodyString = @"
$bodyText
"@

if ($emailattach -eq "false") {
# // Send email with results in the body
send-mailmessage -To $toAddress -Subject $subject -From $fromAddress -Body $bodyString -SmtpServer $smtpServerName
}
else {
# // Send email with attachment log file
send-mailmessage -To $toAddress -Subject $subject -From $fromAddress -attachments "$logpath\$logname" -SmtpServer $smtpServerName
}

So my journey is far from over, in fact its just started and I'll re-visit this at some point as its pretty scrappy and no doubt horribly inefficient, but it works and I even managed to improve on the previous script as before empty directories got left behind and thats now addressed with this so I'm happy(ish). I at least now have a much better understanding of the potential. 

I'm also resigned that I'll never be a developer and I can certainly relate to this girl .. 

Replace Pi with many POSH cmdlets and statements I've attempted to understand over the past month or so .....