2 # Script for rotating Drive Snapshot backups monthly
4 # Author: Patrick Canterino <patrick@patrick-canterino.de>
5 # WWW: https://www.patrick-canterino.de/
6 # https://github.com/pcanterino/dsmonrot
7 # License: 2-Clause BSD License
9 # Drive Snapshot is copyright by Tom Ehlert
10 # http://www.drivesnapshot.de/
14 # Path to backup directory
15 # This directory MUST exist, it is not created automatically!
16 [String]$backupDir = "Z:\"
17 # Disks to backup, see http://www.drivesnapshot.de/en/commandline.htm
18 [String]$disksToBackup = "HD1:1"
19 # Path to Drive Snapshot
20 [String]$dsPath = "C:\Users\Patrick\Desktop\DSMonRot\snapshot.exe"
21 # Keep backups for this amount of months (excluding the current month),
23 [Int32]$keepMonths = 2
24 # Rotate BEFORE the beginning of a full backup (default is after a successful
26 # WARNING: If this option is set to $True and the full backup fails you could
28 [Boolean]$rotateBeforeBackup = $False
29 # Set to $True if you want to allow multiple backups for a day
30 [Boolean]$multipleDailyBackups = $False
31 # Path to Drive Snapshot log file (specify only the file name if you set
32 # $dsLogFileToBackup to $True)
33 #[String]$dsLogFile = "C:\Users\Patrick\Desktop\DSMonRot\snapshot.log"
34 [String]$dsLogFile = "snapshot.log"
35 # Set to $True if you want to put the log file of Drive Snapshot into the same
36 # directory as the backup
37 [Boolean]$dsLogFileToBackup = $True
38 # Path to directory where DSMonRot stores the log files
39 # Every month a new log file is created
40 [String]$logDir = "$PSScriptRoot\log"
41 # Keep log files for this amount of months (excluding the current month),
42 # 0 or less for indefinite
43 # You should set this to at least the same as $keepMonths
45 # Comma separated lists of files and directories to exclude from the backup
46 # See http://www.drivesnapshot.de/en/commandline.htm
47 # Comment out if you don't want to use it
48 #[String]$excludedPaths = "Path1,Path2"
50 # Map network share to this drive letter, comment out if you don't want to use it
51 [String]$smbDrive = "Z"
52 # Path to network share
53 [String]$smbPath = "\\192.168.0.3\ds"
54 # User and password for connecting to network share, comment out if you don't want to use it
55 # (for example if you want to pass current Windows credentials)
56 [String]$smbUser = "patrick"
57 [String]$smbPassword = ""
59 # Send an email if an error occured
60 [Boolean]$emailOnError = $True
61 # From address of email notification
62 [String]$emailFromAddress = "alarm@test.local"
63 # To address of email notification
64 [String]$emailToAddress = "patrick@test.local"
65 # Subject of email notification
66 [String]$emailSubject = "DSMonRot on $env:computername"
68 [String]$emailMailserver = "localhost"
70 [Int32]$emailPort = 25
72 [Boolean]$emailSSL = $False
74 [Boolean]$emailAuth = $False
76 [String]$emailUser = ""
78 [String]$emailPassword = ""
84 Write-Log writes a message to a specified log file with the current time stamp.
86 The Write-Log function is designed to add logging capability to other scripts.
87 In addition to writing output and/or verbose you can write to a log file for
90 Created by: Jason Wasser @wasserja
91 Modified: 11/24/2015 09:30:19 AM
94 * Code simplification and clarification - thanks to @juneb_get_help
95 * Added documentation.
96 * Renamed LogPath parameter to Path to keep it standard - thanks to @JeffHicks
97 * Revised the Force switch to work as it should - thanks to @JeffHicks
100 * Add error handling if trying to create a log file in a inaccessible location.
101 * Add ability to write $Message to $Verbose or $Error pipelines to eliminate
104 Message is the content that you wish to add to the log file.
106 The path to the log file to which you would like to write. By default the function will
107 create the path and file if it does not exist.
109 Specify the criticality of the log information being written to the log (i.e. Error, Warning, Informational)
111 Use NoClobber if you do not wish to overwrite an existing file.
113 Write-Log -Message 'Log message'
114 Writes the message to c:\Logs\PowerShellLog.log.
116 Write-Log -Message 'Restarting Server.' -Path c:\Logs\Scriptoutput.log
117 Writes the content to the specified log file and creates the path and file specified.
119 Write-Log -Message 'Folder does not exist.' -Path c:\Logs\Script.log -Level Error
120 Writes the message to the specified log file as an error message, and writes the message to the error pipeline.
122 https://gallery.technet.microsoft.com/scriptcenter/Write-Log-PowerShell-999c32d0
129 [Parameter(Mandatory=$true,
130 ValueFromPipelineByPropertyName=$true)]
131 [ValidateNotNullOrEmpty()]
132 [Alias("LogContent")]
135 [Parameter(Mandatory=$false)]
137 [string]$Path='C:\Logs\PowerShellLog.log',
139 [Parameter(Mandatory=$false)]
140 [ValidateSet("Error","Warn","Info")]
141 [string]$Level="Info",
143 [Parameter(Mandatory=$false)]
149 # Set VerbosePreference to Continue so that verbose messages are displayed.
150 $VerbosePreference = 'Continue'
155 # If the file already exists and NoClobber was specified, do not write to the log.
156 if ((Test-Path $Path) -AND $NoClobber) {
157 Write-Error "Log file $Path already exists, and you specified NoClobber. Either delete the file or specify a different name."
161 # If attempting to write to a log file in a folder/path that doesn't exist create the file including the path.
162 elseif (!(Test-Path $Path)) {
163 Write-Verbose "Creating $Path."
164 $NewLogFile = New-Item $Path -Force -ItemType File
168 # Nothing to see here yet.
171 # Format Date for our Log File
172 $FormattedDate = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
174 # Write message to error, warning, or verbose pipeline and specify $LevelText
178 $LevelText = 'ERROR:'
181 Write-Warning $Message
182 $LevelText = 'WARNING:'
185 Write-Verbose $Message
190 # Write log entry to $Path
191 "$FormattedDate $LevelText $Message" | Out-File -FilePath $Path -Append
198 function Send-Email([String]$body) {
200 # Allow SMTP with SSL and SMTP Auth
201 # see: http://petermorrissey.blogspot.de/2013/01/sending-smtp-emails-with-powershell.html
203 $smtp = New-Object System.Net.Mail.SmtpClient($emailMailServer, $emailPort)
205 $smtp.EnableSSL = $emailSSL
208 $smtp.Credentials = New-Object System.Net.NetworkCredential($emailUser, $emailPassword)
211 $smtp.Send($emailFromAddress, $emailToAddress, $emailSubject, $body)
214 Write-Log "Could not send email: $_.Exception.Message" -Path $logFile -Level Error
218 function Rotate-Backup {
219 if($keepMonths -lt 0) {
223 $keepMonthsCount = $keepMonths
225 Get-ChildItem $backupDir -Directory | Where-Object {($_.Name -ne $currMonth) -and ($_.Name -match "^\d{4,}-\d{2}$")} | Sort-Object -Descending |
227 if($keepMonthsCount -ge 0) {
231 if($keepMonthsCount -eq -1) {
232 Write-Log "Deleting backup $($_.FullName)" -Path $logFile -Level Info
233 Remove-Item -Recurse -Force $_.FullName
238 function Rotate-Log {
239 if($keepLogs -le 0) {
243 $keepLogsCount = $keepLogs
245 Get-ChildItem $logDir -File | Where-Object {($_.Name -ne "$currMonth.log") -and ($_.Name -match "^\d{4,}-\d{2}\.log$")} | Sort-Object -Descending |
247 if($keepLogsCount -ge 0) {
251 if($keepLogsCount -eq -1) {
252 Write-Log "Deleting log file $($_.FullName)" -Path $logFile -Level Info
253 Remove-Item -Force $_.FullName
258 $dsAdditionalArgs = @("--UseVSS")
262 $smbConnected = $False
267 $currMonth = Get-Date -format "yyyy-MM"
268 $currDay = Get-Date -format "yyyy-MM-dd"
269 $currTime = Get-Date -format "HH-mm-ss" # no colon because we need this for a directory name
271 # Check if the directory for the log files exists and create it if necessary
272 if(!(Test-Path $logDir)) {
274 New-Item -ItemType directory -Path $logDir -ErrorAction Stop | Out-Null
277 Write-Error "Could not create log directory $logDir`: $_.Exception.Message"
278 $errorMessages += "Could not create log directory $logDir`: $_.Exception.Message"
282 $logFile = "$logDir\$currMonth.log"
284 # Continue only if the log directory exists or if it was created successfully (no error message added)
285 if($errorMessages.Count -eq 0) {
286 $startTime = Get-Date -format "yyyy-MM-dd HH:mm:ss"
287 Write-Log "Started at $startTime" -Path $logFile
289 # Connect the network drive if necessary
291 Write-Log "Connecting network drive $smbDrive to $smbPath" -Path $logFile
294 if($smbUser -and $smbPassword) {
295 Write-Log "Connecting to network drive with credentials" -Path $logFile
297 $secSmbPassword = $smbPassword | ConvertTo-SecureString -asPlainText -Force
298 $smbCredential = New-Object System.Management.Automation.PSCredential($smbUser, $secSmbPassword)
300 New-PSDrive -Name $smbDrive -PSProvider "FileSystem" -Root $smbPath -Credential $smbCredential -Persist -ErrorAction Stop | Out-Null
303 Write-Log "Connecting to network drive without credentials" -Path $logFile
305 New-PSDrive -Name $smbDrive -PSProvider "FileSystem" -Root $smbPath -Persist -ErrorAction Stop | Out-Null
308 $smbConnected = $True
311 Write-Log "Could not connect to network drive $smbDrive`: $_.Exception.Message" -Path $logFile -Level Error
312 $errorMessages += "Could not connect to network drive $smbDrive`: $_.Exception.Message"
316 # Check if the backup directory exists
317 if(!(Test-Path $backupDir)) {
318 Write-Log "Directory $backupDir does not exist!" -Path $logFile -Level Error
319 $errorMessages += "Directory $backupDir does not exist!"
322 # Continue only if no error message was recorded (i.e. backup directory does not exist)
323 if($errorMessages.Count -eq 0) {
324 # Compose the backup target directories
325 $backupTarget = $backupDir + "\" + $currMonth
326 $backupTargetFull = $backupTarget + "\Full"
328 $backupTargetDiff = $backupTarget + "\Diff-" + $currDay
330 if($multipleDailyBackups) {
331 $backupTargetDiff = $backupTargetDiff + "-" + $currTime
334 # Compose the "exclude" parameter if necessary
336 $dsAdditionalArgs += "--exclude:" + $excludedPaths
339 # Check if the backup target for this month, the directory for the full backup
340 # and the hash files exists. In this case we do a differential backup.
341 if((Test-Path $backupTarget) -and (Test-Path $backupTargetFull) -and (Test-Path "$backupTargetFull\*.hsh")) {
342 # Do a differential backup
344 Write-Log "Doing a differential backup" -Path $logFile
348 if(!(Test-Path $backupTargetDiff)) {
350 New-Item -ItemType directory -Path $backupTargetDiff -ErrorAction Stop | Out-Null
353 Write-Log "Could not create directory $backupTargetDiff`: $_.Exception.Message" -Path $logFile -Level Error
354 $errorMessages += "Could not create directory $backupTargetDiff`: $_.Exception.Message"
357 if($errorMessages.Count -eq 0) {
358 $dsLogPath = if($dsLogFileToBackup) { "$backupTargetDiff\$dsLogFile" } else { $dsLogFile }
360 $dsArgs = @($disksToBackup, "--logfile:$dsLogPath", "$backupTargetDiff\`$disk.sna", "-h$backupTargetFull\`$disk.hsh") + $dsAdditionalArgs
361 $dsCommand = "$dsPath $dsArgs"
363 Write-Log $dsCommand -Path $logFile
367 if($LastExitCode -ne 0) {
368 Write-Log "Drive Snapshot failed to backup! Exit code: $LastExitCode" -Path $logFile -Level Error
369 $errorMessages += "Drive Snapshot failed to backup! Exit code: $LastExitCode"
372 Write-Log "Drive Snapshot succeeded!" -Path $logFile
378 Write-Log "Directory $backupTargetDiff already exists!" -Path $logFile -Level Error
379 $errorMessages += "Directory $backupTargetDiff already exists!"
385 Write-Log "Doing a full backup" -Path $logFile
387 if(!(Test-Path $backupTarget)) {
389 New-Item -ItemType directory -Path $backupTarget -ErrorAction Stop | Out-Null
392 Write-Log "Could not create directory $backupTarget`: $_.Exception.Message" -Path $logFile -Level Error
393 $errorMessages += "Could not create directory $backupTarget`: $_.Exception.Message"
397 if($errorMessages.Count -eq 0) {
398 if(!(Test-Path $backupTargetFull)) {
400 New-Item -ItemType directory -Path $backupTargetFull -ErrorAction Stop | Out-Null
403 Write-Log "Could not create directory $backupTargetFull`: $_.Exception.Message" -Path $logFile -Level Error
404 $errorMessages += "Could not create directory $backupTargetFull`: $_.Exception.Message"
408 if($errorMessages.Count -eq 0) {
409 if($rotateBeforeBackup) {
413 $dsLogPath = if($dsLogFileToBackup) { "$backupTargetFull\$dsLogFile" } else { $dsLogFile }
415 $dsArgs = @($disksToBackup, "--logfile:$dsLogPath", "$backupTargetFull\`$disk.sna") + $dsAdditionalArgs
416 $dsCommand = "$dsPath $dsArgs"
418 Write-Log $dsCommand -Path $logFile
422 if($LastExitCode -ne 0) {
423 Write-Log "Drive Snapshot failed to backup! Exit code: $LastExitCode" -Path $logFile -Level Error
424 $errorMessages += "Drive Snapshot failed to backup! Exit code: $LastExitCode"
427 Write-Log "Drive Snapshot succeeded!" -Path $logFile
431 if($rotateBeforeBackup -eq $False -and $success -eq $True) {
439 # Disconnect the network drive if necessary
441 Write-Log "Disconnecting network drive" -Path $logFile
444 Remove-PSDrive $smbDrive -ErrorAction Stop
447 Write-Log "Could not disconnect network drive $smbDrive`: $_.Exception.Message" -Path $logFile -Level Error
448 $errorMessages += "Could not disconnect network drive $smbDrive`: $_.Exception.Message"
452 # Rotate the log files
456 # If there was any error message recorded, send a mail if configured
457 if($emailOnError -and $errorMessages.Count -gt 0) {
458 $emailBody = "This is DSMonRot on $env:computername, started at $startTime.`n"
459 $emailBody += "An error occured while performing a backup. Below are the error messages and some status information.`n`n"
460 $emailBody += "Backup directory: $backupDir`n"
461 $emailBody += "Log directory: $logDir`n"
462 $emailBody += "Current log file: $logFile`n"
463 $emailBody += "Differential backup: $isDiff`n"
464 $emailBody += "Backup successful: $success`n"
465 $emailBody += "Drive Snapshot command: $dsCommand`n`n"
466 $emailBody += ($errorMessages -join "`n")
468 Send-Email $emailBody
471 $endTime = Get-Date -format "yyyy-MM-dd HH:mm:ss"
472 Write-Log "Ended at $endTime" -Path $logFile