├── ntfs-hardlink-backup ├── ocs_inventory_plugins │ ├── result.png │ ├── cd_ntfshardlinkbackup.png │ ├── cd_ntfshardlinkbackup_a.png │ ├── cd_ntfshardlinkbackup_d.png │ ├── ntfshardlinkbackup_ocs_plugin.vbs │ └── Howto.md ├── backup-reminder │ ├── Readme.md │ ├── backup-reminder-text.ps1 │ └── backup-reminder.ps1 ├── backup-sample.bat ├── backup-to-nas.bat ├── README.md ├── backup-sample.ini └── ntfs-hardlink-backup.ps1 ├── robocopy-backup ├── robocopy-to-remote-office.bat ├── robocopy-backup-to-disk.ini ├── robocopy-to-remote-office.ini ├── robocopy-backup-to-disk.cmd ├── README.md └── robocopy-backup.ps1 ├── README.md ├── crypt-scripts ├── portable-dismount.vbs ├── backup-to-disk.cmd ├── README.md └── portable-mount.vbs ├── change-passwords-remotely ├── Readme.md └── change-passwords-remotely.ps1 ├── delete-old-files └── delete-old-files.ps1 └── LICENSE /ntfs-hardlink-backup/ocs_inventory_plugins/result.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/International-Nepal-Fellowship/Windows-Tools/HEAD/ntfs-hardlink-backup/ocs_inventory_plugins/result.png -------------------------------------------------------------------------------- /ntfs-hardlink-backup/backup-reminder/Readme.md: -------------------------------------------------------------------------------- 1 | This script works together with ntfs-hardlink-backup and reminds the user if the last backup was faulty, a backup is overdue or no backup was set up at all. 2 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/backup-sample.bat: -------------------------------------------------------------------------------- 1 | %SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command "& C:\Tools\Backup\bat\ntfs-hardlink-backup.ps1 -iniFile C:\Tools\Backup\bat\backup-sample.ini" 2 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/ocs_inventory_plugins/cd_ntfshardlinkbackup.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/International-Nepal-Fellowship/Windows-Tools/HEAD/ntfs-hardlink-backup/ocs_inventory_plugins/cd_ntfshardlinkbackup.png -------------------------------------------------------------------------------- /ntfs-hardlink-backup/ocs_inventory_plugins/cd_ntfshardlinkbackup_a.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/International-Nepal-Fellowship/Windows-Tools/HEAD/ntfs-hardlink-backup/ocs_inventory_plugins/cd_ntfshardlinkbackup_a.png -------------------------------------------------------------------------------- /ntfs-hardlink-backup/ocs_inventory_plugins/cd_ntfshardlinkbackup_d.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/International-Nepal-Fellowship/Windows-Tools/HEAD/ntfs-hardlink-backup/ocs_inventory_plugins/cd_ntfshardlinkbackup_d.png -------------------------------------------------------------------------------- /robocopy-backup/robocopy-to-remote-office.bat: -------------------------------------------------------------------------------- 1 | %SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command "& C:\Tools\Backup\bat\robocopy-backup.ps1 -iniFile C:\Tools\Backup\bat\robocopy-to-remote-office.ini" 2 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/backup-reminder/backup-reminder-text.ps1: -------------------------------------------------------------------------------- 1 | $text=@{ 2 | 'no status file'="No Backup was set up in your computer`nPlease contact ICT by writing an email to 'ict@nepal.inf.org'"; 3 | 'last backup failed'="Your last backup did fail`nPlease contact ICT by 'ict@nepal.inf.org'"; 4 | 'backup to old'="You have not done a backup for {0} days. Please run your backup!" 5 | } -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | Windows-Tools 2 | ============= 3 | 4 | This repo contains various handy script tools like: 5 | * change-passwords-remotely - to change local user passwords on a remote computer 6 | * ntfs-hardlink-backup - to make incremental backups keeping a history of old backups 7 | * robocopy-backup - make Robocopy mirror backups 8 | * Crypt-scripts - for mounting and dismounting TrueCrypt or VeraCrypt containers 9 | * delete-old-files - list or delete old files and sub-folders in some folder 10 | 11 | If you find bugs, or have useful enhancements, then you are encouraged to contribute. 12 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/ocs_inventory_plugins/ntfshardlinkbackup_ocs_plugin.vbs: -------------------------------------------------------------------------------- 1 | Dim xmlstatusfiles(1) 2 | xmlstatusfiles(0) = "C:\Logs\network-backup\status.xml" 3 | xmlstatusfiles(1) = "C:\Logs\Backup-To-External-HDD\status.xml" 4 | 5 | Const ForReading = 1 6 | Set objFSO = CreateObject("Scripting.FileSystemObject") 7 | For Each file In xmlstatusfiles 8 | if objFSO.FileExists(file) Then 9 | Set objTest = objFSO.GetFile(file) 10 | If objTest.Size > 0 Then 11 | Set objFile = objFSO.OpenTextFile(file, ForReading) 12 | strText = objFile.ReadAll 13 | wscript.echo strText 14 | objFile.Close 15 | end if 16 | end if 17 | Next 18 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/backup-to-nas.bat: -------------------------------------------------------------------------------- 1 | REM In this example there is a NAS with a "backup" share and we want to put backups in the "server-01" folder 2 | REM For some models of NAS, backing up directly to the share gives trouble 3 | REM So you can try making the share look like a local drive (B: in this example) 4 | REM Then in the INI file you can just specify backupDestination=B: 5 | REM This method sometimes helps a dumb NAS to accept the backup and hard-links. 6 | %SystemRoot%\System32\subst.exe B: \\nas-01.mycompany.example.org\backup\server-01 7 | %SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe -Command "& C:\Tools\Backup\bat\ntfs-hardlink-backup.ps1 -iniFile C:\Tools\Backup\bat\backup-sample.ini" 8 | %SystemRoot%\System32\subst.exe B: /D 9 | -------------------------------------------------------------------------------- /robocopy-backup/robocopy-backup-to-disk.ini: -------------------------------------------------------------------------------- 1 | [Common] 2 | ; This example INI file could be used to make a single robocopy mirror backup of files from D drive to another disk 3 | ; E drive could be an external USB disk, mapped drive, TrueCrypt mounted drive or whatever 4 | backupSources=D:\WorkFolder,D:\MailFolder 5 | backupDestination=E:\BackupTest 6 | logFilesToKeep=5 7 | noshadowcopy=true 8 | Logfile=C:\Logs\Robocopy-Backup-To-External-HDD 9 | statusFile=robocopy-status.xml 10 | SMTPServer=mail.mycompany.com 11 | SMTPUser=backup@mycompany.com 12 | SMTPPassword=blahblah 13 | emailSendRetries=10 14 | emailTo=ict.admin@mycompany.com 15 | emailFrom=backup@mycompany.com 16 | [sydney-*.mycompany.com] 17 | emailTo=ict.admin@sydney.mycompany.com 18 | [london-*.mycompany.com] 19 | emailTo=ict.admin@london.mycompany.com 20 | -------------------------------------------------------------------------------- /robocopy-backup/robocopy-to-remote-office.ini: -------------------------------------------------------------------------------- 1 | [Common] 2 | ; Common settings that can be overridden by any server-specific settings 3 | noShadowCopy=true 4 | logFilesToKeep=20 5 | Logfile=C:\Logs\robocopy-to-remote-office 6 | statusFile=robocopy-status.xml 7 | emailTo=ict.admin@mycompany.com 8 | emailFrom=backup@mycompany.com 9 | SMTPServer=mail.mycompany.com 10 | SMTPUser=backup@mycompany.com 11 | SMTPPassword=blahblah 12 | SMTPTimeout=120000 13 | emailSendRetries=50 14 | [sydney-dc-01.mycompany.com] 15 | ; Make a robocopy mirror of the Admin, Finance and HR files on the Sydney server to the backup share in London 16 | backupDestination=\\london-dc-01\remoteofficebackup\sydney-dc-01 17 | backupSources=D:\Shares\Admin,D:\Shares\Finance,D:\Shares\HR 18 | jobName=Sydney-London-Mirror 19 | emailTo=ict.admin@au.mycompany.com 20 | [london-dc-01.mycompany.com] 21 | ; Make a robocopy mirror of the EU files on the London server to the backup share in Sydney 22 | backupDestination=\\sydney-dc-01\remoteofficebackup\london-dc-01 23 | backupSources=D:\Shares\EU 24 | jobName=London-Sydney-Mirror 25 | emailTo=ict.admin@uk.mycompany.com 26 | -------------------------------------------------------------------------------- /crypt-scripts/portable-dismount.vbs: -------------------------------------------------------------------------------- 1 | ' Dismount any TrueCrypt containers used for the portable external backup 2 | ' 3 | ' TrueCrypt parameters: 4 | ' "/q" - quiet - the TrueCrypt dialog is not displayed 5 | ' "/d" - dismount 6 | ' 7 | currentDirectory = left(WScript.ScriptFullName,(Len(WScript.ScriptFullName))-(len(WScript.ScriptName))) 8 | truecryptFolder = currentDirectory + "truecrypt\" 9 | Set WshShell = CreateObject("WScript.Shell") 10 | firstDriveLetter = "Z" 11 | secondDriveLetter = "X" 12 | firstDriveSpec = firstDriveLetter + ":\" 13 | secondDriveSpec = secondDriveLetter + ":\" 14 | Set objFSO = CreateObject("Scripting.FileSystemObject") 15 | If (objFSO.DriveExists(firstDriveSpec)) Then 16 | ' Dismount the first container 17 | command = truecryptFolder + "truecrypt.exe /q /d " + firstDriveLetter 18 | ' WScript.Echo command 19 | cmds=WshShell.RUN(command, 0, true) 20 | End If 21 | If (objFSO.DriveExists(secondDriveSpec)) Then 22 | ' Dismount the second container 23 | command = truecryptFolder + "truecrypt.exe /q /d " + secondDriveLetter 24 | ' WScript.Echo command 25 | cmds=WshShell.RUN(command, 0, true) 26 | End If 27 | 28 | ' Cleanup 29 | Set WshShell = Nothing 30 | -------------------------------------------------------------------------------- /change-passwords-remotely/Readme.md: -------------------------------------------------------------------------------- 1 | This script changes local user passwords on remote Windows computers. List of computers and users are given in a CSV file 2 | Tested on Windows 7 3 | 4 | 5 | The script picks up the computer and user names from a CSV file, collects the password from the console input and tries to change the passwords. 6 | To make it work you need to turn on "file and printer sharing" in "Control Panel\Network and Internet\Network Sharing Centre\Advanced sharing settings" 7 | 8 | In a domain enviroment you can allow the remote change of passwords by GPO. 9 | Create a new GPO and: 10 | 1. enable "Allow ICMP exceptions" & "Allow inbound remote administration exception" in 11 | "Computer Configuration/Policies/Administrative Templates Policy definitions/Network/Network Connections/Windows Firewall/Domain Profile" 12 | This will open certain ports in the firewall of the client 13 | 14 | 2. enable "Allow automatic configuration of listeners" & "Allow Basic authentication" in 15 | "Computer Configuration/Policies/Administrative Templates Policy definitions/Windows Components/Windows Remote Management (WinRM)/WinRM Service" 16 | 17 | 3. enable "Allow Remote Shell Access" in 18 | "Computer Configuration/Policies/Administrative Templates Policy definitions/Windows Components/Windows Remote Shell" 19 | 20 | 4. apply the policy to the OU you want to update the passwords 21 | -------------------------------------------------------------------------------- /crypt-scripts/backup-to-disk.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | Setlocal EnableDelayedExpansion 3 | :wait 4 | set found=0 5 | rem drivetype 4 is network shares so we do not want to enumerate those 6 | rem that might take some time if the laptop is offline and the share access has to time out 7 | rem also needed lots of those ^ signs to escape the exclamation mark in the string 8 | set disk_cmd="wmic logicaldisk where drivetype^^^!=4 get caption" 9 | 10 | rem the wmic command outputs a top column heading so ignore that first line "skip=1" 11 | rem there is also a blank line at the end, but that does no harm 12 | for /f "skip=1" %%D in ('%disk_cmd%') do ( 13 | rem The folder and file name check here is actually not case sensitive 14 | if exist %%D\Truecrypt\ExternalBackup.tc ( 15 | rem Success - we found a disk with ExternalBackup.tc in the Truecrypt folder 16 | set found=1 17 | set externaldrive=%%D 18 | ) 19 | ) 20 | 21 | if %found% equ 0 ( 22 | rem No ExternalBackup.tc found on any disk 23 | echo PLEASE CONNECT YOUR BACKUP DISK 24 | rem Use a devious way to wait about 10 seconds 25 | ping -n 10 localhost> nul 26 | goto wait 27 | ) 28 | 29 | echo Opening TrueCrypt on disk: %externaldrive% 30 | 31 | rem Looking good - now check that the mount and dismount scripts are there 32 | if exist %externaldrive%\portable-mount.vbs ( 33 | if exist %externaldrive%\portable-dismount.vbs ( 34 | rem All looks good as far as we can check - now mount, do the backup and dismount 35 | %externaldrive%\portable-mount.vbs 36 | 37 | powershell.exe -Command "& C:\Tools\Backup\bat\ntfs-hardlink-backup.ps1 -iniFile C:\Tools\Backup\bat\Backup-To-Disk.ini" 38 | 39 | %externaldrive%\portable-dismount.vbs 40 | ) else ( 41 | echo Error: portable-dismount.vbs script not found on drive %externaldrive% 42 | echo ######## BACKUP WAS NOT DONE ######## 43 | pause 44 | ) 45 | ) else ( 46 | echo Error: portable-mount.vbs script not found on drive %externaldrive% 47 | echo ######## BACKUP WAS NOT DONE ######## 48 | pause 49 | ) 50 | -------------------------------------------------------------------------------- /robocopy-backup/robocopy-backup-to-disk.cmd: -------------------------------------------------------------------------------- 1 | @ECHO OFF 2 | Setlocal EnableDelayedExpansion 3 | :wait 4 | set found=0 5 | rem drivetype 4 is network shares so we do not want to enumerate those 6 | rem that might take some time if the laptop is offline and the share access has to time out 7 | rem also needed lots of those ^ signs to escape the exclamation mark in the string 8 | set disk_cmd="wmic logicaldisk where drivetype^^^!=4 get caption" 9 | 10 | rem the wmic command outputs a top column heading so ignore that first line "skip=1" 11 | rem there is also a blank line at the end, but that does no harm 12 | for /f "skip=1" %%D in ('%disk_cmd%') do ( 13 | rem The folder and file name check here is actually not case sensitive 14 | if exist %%D\Truecrypt\ExternalBackup.tc ( 15 | rem Success - we found a disk with ExternalBackup.tc in the Truecrypt folder 16 | set found=1 17 | set externaldrive=%%D 18 | ) 19 | ) 20 | 21 | if %found% equ 0 ( 22 | rem No ExternalBackup.tc found on any disk 23 | echo PLEASE CONNECT YOUR BACKUP DISK 24 | rem Use a devious way to wait about 10 seconds 25 | ping -n 10 localhost> nul 26 | goto wait 27 | ) 28 | 29 | echo Opening TrueCrypt on disk: %externaldrive% 30 | 31 | rem Looking good - now check that the mount and dismount scripts are there 32 | if exist %externaldrive%\portable-mount.vbs ( 33 | if exist %externaldrive%\portable-dismount.vbs ( 34 | rem All looks good as far as we can check - now mount, do the backup and dismount 35 | %externaldrive%\portable-mount.vbs 36 | 37 | powershell.exe -Command "& C:\Tools\Backup\bat\robocopy-backup.ps1 -iniFile C:\Tools\Backup\bat\Robocopy-Backup-To-Disk.ini" 38 | 39 | %externaldrive%\portable-dismount.vbs 40 | ) else ( 41 | echo Error: portable-dismount.vbs script not found on drive %externaldrive% 42 | echo ######## BACKUP WAS NOT DONE ######## 43 | pause 44 | ) 45 | ) else ( 46 | echo Error: portable-mount.vbs script not found on drive %externaldrive% 47 | echo ######## BACKUP WAS NOT DONE ######## 48 | pause 49 | ) 50 | -------------------------------------------------------------------------------- /robocopy-backup/README.md: -------------------------------------------------------------------------------- 1 | robocopy-backup 2 | ==================== 3 | 4 | This software is used for creating backup mirrors using Robocopy. 5 | It is based on the ntfs-hardlink-backup Powershell code also in this repo. 6 | All kudos to the contributors to that code - it has been cut down here to use just the parameters needed 7 | for making Robocopy mirror copies of data. 8 | 9 | FEATURES 10 | -------- 11 | * NO GUI 12 | * easy to run a scheduled task 13 | * backup multiple sources to one destination 14 | * create Shadow Volume copy before making backup 15 | * send notification emails 16 | * creates ZIP file of the logfile before sending it by Email 17 | * delete old log files 18 | * optionally read parameters from an INI file 19 | * flexible way of using one INI file for a lot of computers 20 | * keep historical log files 21 | 22 | INSTALLATION 23 | ------------- 24 | 1. Download and place robocopy-backup.ps1 into a folder 25 | 2. Navigate with the Explorer to that folder 26 | 3. Right Click on the robocopy-backup.ps1 file and select "Properties" 27 | 4. If you see in the bottom something like "Security: This file came from an other computer ..." Click on "Unblock" 28 | 5. start powershell from windows start menu (you need Windows 7 or Win Server for that, on XP you would need to install PowerShell 2 first) 29 | 6. allow local non-signed scripts to run by typing “Set-ExecutionPolicy RemoteSigned“ 30 | 7. make a batch file and INI file similar to the examples here, as needed 31 | 9. run the batch file interactively, or add a task to Task Scheduler to run as required 32 | 33 | V1.1 RELEASE NOTES 34 | ------------------ 35 | 1. Error messages are improved when checking possible destinations for the Robocopy. 36 | 2. Only try to send email if the computer has at least a network connection that has a default gateway. This saves big delays repeatedly trying to send email if the computer is off-line. 37 | 3. Report host IP addresses and gateways in the log file. This helps with problem diagnosis "after the event". 38 | 4. Use the Powershell "&" "invoke" command to execute the pre-execution, robocopy and post-execution commands rather than "cmd /c". This is more portable across Windows 7/8/8.1/10 and various Windows Server releases with different Powershell versions. 39 | -------------------------------------------------------------------------------- /crypt-scripts/README.md: -------------------------------------------------------------------------------- 1 | Crypt-scripts 2 | ============= 3 | 4 | These scripts are used to manage the mount and dismount of a secure TrueCrypt (or VeraCrypt) 5 | container on an external USB disk, and call ntfs-hardlink-backup to make a backup in that container. 6 | 7 | INSTALLATION AND USE 8 | -------------------- 9 | * On an external USB disk, make a folder "TrueCrypt" 10 | * Make a small TrueCrypt container inside "TrueCrypt". Call it after the user or user title (e.g. CEO.tc). 11 | * Give that container some authentication (password...) that is known to the user. 12 | * Make a TrueCrypt key file with matching name to the small container (e.g. CEO.tckf) and put it in the small container. 13 | * Keep a copy of the key file in some other place secured by the IT department - it can be used to open the large container if the user has forgotten/changed their password to the small container. 14 | * Make a TrueCrypt main container called externalbackup.tc in the "TrueCrypt" folder. 15 | * Use the previously generated key file as the authentication for the main container. 16 | * Put portable-mount.vbs and portable-dismount.vbs in the "TrueCrypt" folder. 17 | * Use TrueCrypt, Tools, Traveler Disk Setup to put the necessary TrueCrypt binaries into the TrueCrypt folder. 18 | * Dismount the TrueCrypt containers you made, disconnect the USB disk. 19 | * Put backup-to-disk.cmd somewhere the user can run it from 20 | * Put ntfs-hardlink-backup.ps1 and a backup-to-disk.ini somewhere (e.g. the scripts use C:\Tools\Backup\bat folder) 21 | * Run backup-to-disk.cmd 22 | * It will prompt the user to connect the USB disk, and check every 10 seconds for a disk with a TrueCrypt folder... 23 | * portable-mount will be run. That will mount the small container as Z:, prompting the user for credentials. 24 | * The key file from the small container will be used to open the large container as X: 25 | * The backup is run into the large container. 26 | * The container/s are closed. 27 | 28 | Modify whatever of the scripts you need to, to change the default drive letters used for the containers (Z and X), 29 | or the expected folder names (like "TrueCrypt" on the external USB disk). 30 | 31 | Note: There is (obviously) no attempt made here to hide the use of TrueCrypt. 32 | If you have high-security needs for plausible deniability, then do not use this method. 33 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/backup-reminder/backup-reminder.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | BACKUP-REMINDER Version: 1.0-BETA2 4 | 5 | This script works together with ntfs-hardlink-backup and reminds the user if the last backup was faulty, a backup is overdue or no backup was set up at all. 6 | 7 | .SYNOPSIS 8 | c:\full\path\bat\backup-reminder.ps1 9 | .PARAMETER statusFile 10 | Path to XML status file generated by ntfs-hardlink-backup 11 | .PARAMETER overdueTimespan 12 | Number of days after that the backup is considered overdue 13 | .PARAMETER backupSources 14 | List of folders to check for existens before showing the reminder. If None of the folders exists, no reminder is shown 15 | 16 | .NOTES 17 | Author: Artur Neumann *INFN* 18 | #> 19 | 20 | [cmdletbinding()] 21 | param ( 22 | [parameter(mandatory = $true)] 23 | $statusFile, 24 | [Parameter(Mandatory=$False)] 25 | [Int32]$overdueTimespan, 26 | [Parameter(Mandatory=$False)] 27 | [String[]]$backupSources 28 | ) 29 | 30 | if ($overdueTimespan -eq 0) { 31 | $overdueTimespan=4 32 | } 33 | if ([string]::IsNullOrEmpty($backupSources)) { 34 | $backupSourcesExists=$True 35 | } else { 36 | $backupSourcesExists=$False 37 | foreach ($backupSource in $backupSources) { 38 | if (Test-Path $backupSource) { 39 | $backupSourcesExists=$True 40 | break 41 | } 42 | } 43 | } 44 | 45 | if ($backupSourcesExists -eq $False) { 46 | exit 47 | } 48 | $scriptPath = Split-Path -parent $MyInvocation.MyCommand.Definition 49 | 50 | 51 | . $scriptPath/backup-reminder-text.ps1 52 | 53 | $wshell = New-Object -ComObject Wscript.Shell -ErrorAction Stop 54 | 55 | If (!(Test-Path $statusFile)){ 56 | $wshell.Popup($text['no status file'],0,"NO BACKUP!",48+4096) 57 | exit 58 | } 59 | 60 | [xml]$backupStatus = Get-Content $statusFile 61 | 62 | if ($backupStatus.NTFSHARDLINKBACKUP.STATUS -ne "OK") { 63 | $wshell.Popup($text['last backup failed'],0,"BACKUP FAILURE!",48+4096) 64 | exit 65 | } 66 | 67 | $lastRun=[DateTime]::Parse($backupStatus.NTFSHARDLINKBACKUP.LASTRUN) 68 | $now=(Get-Date) 69 | $timespan=NEW-TIMESPAN -Start $lastRun -End $now 70 | $timespan=[math]::floor($timespan.TotalDays) 71 | if ($timespan -gt $overdueTimespan) { 72 | $wshell.Popup([string]::Format( $text['backup to old'], $timespan),0,"BACKUP TO OLD!",48+4096) 73 | } 74 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/README.md: -------------------------------------------------------------------------------- 1 | ntfs-hardlink-backup 2 | ==================== 3 | 4 | This software is used for creating hard-link-backups. 5 | The real magic is done by DeLoreanCopy of ln: http://schinagl.priv.at/nt/ln/ln.html 6 | So all credit goes to [Hermann Schinagl](http://schinagl.priv.at)! 7 | FEATURES 8 | -------- 9 | * NO GUI 10 | * easy to run a scheduled task 11 | * backup multiple sources to one destination 12 | * create Shadow Volume copy before making backup 13 | * send notification emails 14 | * takes extra options for ln (timetolerance, traditional, exclude, noads, ...) 15 | * creates ZIP file of the logfile before sending it by Email 16 | * delete old backups and log files 17 | * optionally read parameters from an INI file 18 | * flexible way of using one INI file for a lot of computers 19 | * keep historical log files 20 | * can keep min. old backups per year 21 | * try to run ln.exe from path 22 | * option to choose where ln.exe lives 23 | 24 | 25 | INSTALLATION 26 | ------------- 27 | 1. Read the documentation of "ln" http://schinagl.priv.at/nt/ln/ln.html 28 | 2. Download "ln" and unpack the file. 29 | 3. Download and place ntfs-hardlink-backup.ps1 into ln\bat directory 30 | 4. Navigate with the Explorer to the ln\bat folder 31 | 5. Right Click on the ntfs-hardlink-backup.ps1 file and select "Properties" 32 | 6. If you see in the bottom something like "Security: This file came from an other computer ..." Click on "Unblock" 33 | 7. start powershell from windows start menu (you need Windows 7 or Win Server for that, on XP you would need to install PowerShell 2 first) 34 | 8. allow local non-signed scripts to run by typing “Set-ExecutionPolicy RemoteSigned“ 35 | 9. run ntfs-hardlink-backup.ps1 with full path 36 | 37 | V2.1 RELEASE NOTES 38 | ------------------ 39 | 1. Error messages are improved when checking possible destinations for the backup. 40 | 2. Only try to send email if the computer has at least a network connection that has a default gateway. This saves big delays repeatedly trying to send email if the computer is off-line. 41 | 3. Report host IP addresses and gateways in the log file. This helps with problem diagnosis "after the event". 42 | 4. Use the Powershell "&" "invoke" command to execute the pre-execution, robocopy and post-execution commands rather than "cmd /c". This is more portable across Windows 7/8/8.1/10 and various Windows Server releases with different Powershell versions. 43 | -------------------------------------------------------------------------------- /crypt-scripts/portable-mount.vbs: -------------------------------------------------------------------------------- 1 | ' Mount a small TrueCrypt container to one drive letter (e.g. Z:) - there should be a keyfile in the container 2 | ' then mount another container using the keyfiles to another drive letter (e.g. X:) 3 | ' then dismount the first drive 4 | ' this leaves the second drive mounted 5 | ' Now start an explorer window displaying the contents of the second drive 6 | ' 7 | ' TrueCrypt parameters: 8 | ' "/q" - quiet - the TrueCrypt dialog is not displayed 9 | ' "/v" - volume to mount 10 | ' "/l" - drive letter to mount to 11 | ' "/k" - keyfile to use 12 | ' "/d" - dismount all drives 13 | ' "/m ts" - mountoptions, modify the timestamp on the container when something changes inside it 14 | ' "/e" - open an Explorer window displaying the container contents 15 | ' 16 | ' The run method creates a shell object and uses it to execute commands with the RUN method 17 | ' Run method parameters are: 18 | ' 1) Command to execute 19 | ' 2) Integer 0 - do not display in a window, 1 - display the window for the command 20 | ' 3) True - wait for command to complete, False - do not wait for command to complete 21 | ' 22 | currentDirectory = left(WScript.ScriptFullName,(Len(WScript.ScriptFullName))-(len(WScript.ScriptName))) 23 | ' WScript.Echo currentDirectory 24 | post = "" 25 | firstDriveLetter = "Z" 26 | secondDriveLetter = "X" 27 | 28 | Set objFSO = CreateObject("Scripting.FileSystemObject") 29 | truecryptFolder = currentDirectory + "truecrypt\" 30 | Set objFolder = objFSO.GetFolder(truecryptFolder) 31 | Set colFiles = objFolder.Files 32 | For Each objFile in colFiles 33 | strFileName = objFile.Name 34 | 35 | If objFSO.GetExtensionName(strFileName) = "tc" Then 36 | If Lcase(objFSO.GetBaseName(strFileName)) <> "externalbackup" Then 37 | ' Wscript.Echo objFile.Name 38 | post = objFSO.GetBaseName(strFileName) 39 | End If 40 | End If 41 | Next 42 | 43 | If post = "" Then 44 | Wscript.Echo "Error: No TrueCrypt keyfile container found in " + truecryptFolder 45 | Else 46 | Set WshShell = CreateObject("WScript.Shell") 47 | ' 48 | ' Mount the users own container that should have the keyfile in it 49 | command = truecryptFolder + "truecrypt.exe /q /v " + truecryptFolder + post + ".tc /l " + firstDriveLetter 50 | ' WScript.Echo command 51 | cmds=WshShell.RUN(command, 0, true) 52 | ' 53 | firstDriveSpec = firstDriveLetter + ":\" 54 | keyfile = firstDriveSpec + post + ".tckf" 55 | If (objFSO.FileExists(keyfile)) Then 56 | ' Mount the main container using the keyfile in the first container 57 | command = truecryptFolder + "truecrypt.exe /q /v " + truecryptFolder + "ExternalBackup.tc /k " + keyfile + " /l " + secondDriveLetter + " /m ts" 58 | ' WScript.Echo command 59 | cmds=WshShell.RUN(command, 0, true) 60 | Else 61 | Wscript.Echo "Error: TrueCrypt keyfile not found (" + keyfile + ")" 62 | End If 63 | ' Only try to dismount the small container if it exists - the user might have cancelled giving the password for that 64 | If (objFSO.DriveExists(firstDriveSpec)) Then 65 | ' 66 | ' Dismount the first container 67 | command = truecryptFolder + "truecrypt.exe /q /d " + firstDriveLetter 68 | ' WScript.Echo command 69 | cmds=WshShell.RUN(command, 0, true) 70 | End If 71 | secondDriveSpec = secondDriveLetter + ":\" 72 | If (objFSO.DriveExists(secondDriveSpec)) Then 73 | ' 74 | ' Open an explorer window displaying the files in the main container 75 | cmds=WshShell.RUN(secondDriveSpec, 1, false) 76 | Else 77 | Wscript.Echo "Error: TrueCrypt backup drive letter not found (" + secondDriveSpec + ")" 78 | End If 79 | ' 80 | ' Cleanup 81 | Set WshShell = Nothing 82 | End If 83 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/backup-sample.ini: -------------------------------------------------------------------------------- 1 | [Common] 2 | ; Comments start with a semi-colon like this line. 3 | ; Names of the sections and parameters are not case sensitive. 4 | ; So use upper and lower case as you like. 5 | ; The Common section contains the parameter values you want for most jobs. 6 | ; The order of priority for setting a parameter is: 7 | ; 1 - value specified on command line 8 | ; 2 - value in host-specific section of INI file 9 | ; 3 - value in Common section of INI file 10 | ; 4 - default value hard-coded in ntfs-hardlink-backup.ps1 script 11 | 12 | ; Blank lines also do no harm 13 | 14 | ; Parameters that are strings are specified parameter=string, like this: 15 | backupDestination=X:\Backup 16 | ; most parameter values can contain enviroment variables in them like this: 17 | backupSources=c:\src\\, 18 | ; a not existing environment variable will not be substituted 19 | ; SMTPPassword, preExecutionCommand and postExecutionCommand cannot contain enviroment variables 20 | 21 | ; Parameters that are numbers are specified parameter=number, like this: 22 | backupsToKeep=20 23 | 24 | ; Parameters that are switches (boolean) are turned on by setting to "1", "t" or "true" 25 | ; They are off by default, but you can explicitly switch them off by setting to "0", "f" or "false" 26 | localSubnetOnly=1 27 | NoShadowCopy=t 28 | 29 | Logfile=C:\Logs\Backup 30 | emailTo=backup.admin@mycompany.example.org 31 | emailFrom=backup@mycompany.example.org 32 | SMTPServer=mail.mycompany.example.org 33 | SMTPUser=backup@mycompany.example.org 34 | SMTPPassword=The@Email@Password 35 | SMTPTimeout=120000 36 | emailSendRetries=50 37 | timeTolerance=30000 38 | [server-01.mycompany.example.org] 39 | ; Parameters that are specific to a particular server/computer go in a section for that computer. 40 | ; The section name is the fully-qualified domain name (FQDN) of the computer. 41 | backupSources=D:\Shares\Admin,E:\Shares\Finance,E:\Shares\ICT,D:\Shares\Users 42 | [server-02.mycompany.example.org] 43 | ; This allows you to make a single INI file and use it the same on all computers. 44 | backupSources=E:\Shares\HR,E:\Shares\Software 45 | [*server*.mycompany.example.org] 46 | ; "*" can be used as placeholder. This section would be applied for server-02.mycompany.example.org, server-01.mycompany.example.org and remote-server.mycompany.example.org 47 | ; but not remote-server.branchdomain.mycompany.example.org 48 | ; This section would override the parameters in the common section 49 | ; if multiple sections matching the fully-qualified domain name (FQDN) of the computer and they contain the same 50 | ; parameters the section that is specified later in the file will override the parameters of the sections that are 51 | ; specified earlier in the file 52 | ; sections that match the FQDN exactly (without using "*") have the highest priority and will override parameters of sections that match the FQDN by using "*" 53 | emailTo=ict.admin@mycompany.example.org 54 | 55 | [*client*.mycompany.example.org] 56 | emailTo=ict.client.backups@mycompany.example.org 57 | ; Send the backup to a file share on backup-server, and put it in a folder named after the current commputer 58 | backupDestination=\\backup-server.mycompany.example.org\clientbackup\ 59 | ; Only do the backup if the client computer is in the same subnet as the server with the file share 60 | ; This prevents the backup from running when the user is at another office on the mycompany intranet 61 | ; Maybe the intranet bandwidth over VPN is not good enough for doing backup. 62 | localsubnetonly=true 63 | ; Maybe the client is on a /24 and server is on a /24 but the intranet address allocation is designed such that 64 | ; when the 2 /24 subnets are actually within the same /20 then that means the 2 subnets are really "local" to each other. 65 | ; e.g. on the same campus 66 | ; and so backup is a viable thing to do. 67 | ; Tell it this by specifying how wide the subnet mask match can be to be considered "same campus". 68 | ; In this example 255.255.240.0 is a /20 (16 /24 subnets together) 69 | localSubnetMask=255.255.240.0 70 | 71 | [remote-server.branchdomain.mycompany.example.org] 72 | backupSources=D:\Shares\Admin,D:\Shares\Finance 73 | ; And so you can override a parameter from the common section if it needs to be different 74 | ; on just 1 or a few computers. 75 | emailTo=backup.remoteoffice@mycompany.example.org 76 | 77 | ; Have fun and sleep easy with your backups safely done 78 | -------------------------------------------------------------------------------- /change-passwords-remotely/change-passwords-remotely.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .Synopsis 3 | This script changes local user passwords on remote Windows computers. List of computers and users are given in a CSV file 4 | Tested on Windows 7 5 | 6 | .Description 7 | CHANGE-PASSWORDS-REMOTELY Version: 1.0-BETA1 8 | 9 | This script picks up the computer and user names from a CSV file, collects the password from the console input and tries to change the passwords. 10 | To make it work you need to turn on "file and printer sharing" in "Control Panel\Network and Internet\Network Sharing Centre\Advanced sharing settings" 11 | In a domain enviroment you can allow the remote change of passwords by GPO. 12 | Create a new GPO and: 13 | 1. enable "Allow ICMP exceptions" & "Allow inbound remote administration exception" in 14 | "Computer Configuration/Policies/Administrative Templates Policy definitions/Network/Network Connections/Windows Firewall/Domain Profile" 15 | This will open certain ports in the firewall of the client 16 | 2. enable "Allow automatic configuration of listeners" & "Allow Basic authentication" in 17 | "Computer Configuration/Policies/Administrative Templates Policy definitions/Windows Components/Windows Remote Management (WinRM)/WinRM Service" 18 | 3. enable "Allow Remote Shell Access" in 19 | "Computer Configuration/Policies/Administrative Templates Policy definitions/Windows Components/Windows Remote Shell" 20 | 4. apply the policy to the OU you want to update the passwords 21 | 22 | 23 | .Parameter InputFile 24 | The full path of the CSV file name where computer names and user names are stored. E.g.: C:\temp\computers.csv 25 | The file has to have two columns called "name" and "users". The columns have to be separated by semicolon. 26 | The users column can contain multiple user names separated by comma. 27 | 28 | Example: 29 | --------- 30 | name;users 31 | COMPUTER-001;Administrator 32 | COMPUTER-002;Administrator,user1 33 | COMPUTER-003;user1,user2 34 | 35 | .Parameter ResultFile 36 | The full path of the CSV Result file namer names and user names are stored. E.g.: C:\temp\result.csv 37 | 38 | .Example 39 | change-passwords-remotely.ps1 -InputFile c:\temp\Computers.csv -ResultFile C:\temp\result.csv 40 | 41 | .Notes 42 | NAME: change-passwords-remotely.ps1 43 | AUTHOR: Artur Neumann *INFN* 44 | WEBSITE: https://github.com/International-Nepal-Fellowship/Windows-Tools 45 | CREDITS: This Script is largly based on the ideas of Sitaram Pamarthi http://techibee.com You can find the original Script here: https://4sysops.com/archives/change-the-local-administrator-password-on-multiple-computers-with-powershell/ 46 | 47 | #> 48 | [cmdletbinding()] 49 | param ( 50 | [parameter(mandatory = $true)] 51 | $InputFile, 52 | [parameter(mandatory = $true)] 53 | $ResultFile 54 | ) 55 | 56 | if(!(Test-Path $InputFile)) { 57 | Write-Error "File ($InputFile) not found. Script is exiting" 58 | exit 59 | } 60 | 61 | #list of all unique users, we will collect a password for every user later 62 | [String[]]$uniqueUsers=@() 63 | 64 | $Computers = Import-Csv -Delimiter ";" $InputFile 65 | 66 | #fill $uniqueUsers & make $Computers[x].users into a hash 67 | $Computers | ForEach-Object { 68 | $users=$_.users.split(",") 69 | $uniqueUsers = $uniqueUsers + $users 70 | $_.users=@() 71 | foreach ($user in $users) { 72 | $_.users += @{'name'=$user;"status"="SUCCESS";"error"=""} 73 | } 74 | } 75 | 76 | $uniqueUsers = $uniqueUsers | select -uniq 77 | 78 | # collect a password per user 79 | $userPasswords=@{} 80 | foreach ($user in $uniqueUsers) { 81 | 82 | do { 83 | $password = Read-Host "enter password for $user" -AsSecureString 84 | $confirmpassword = Read-Host "confirm password for $user" -AsSecureString 85 | 86 | $password = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($password)) 87 | $confirmpassword = [Runtime.InteropServices.Marshal]::PtrToStringAuto([Runtime.InteropServices.Marshal]::SecureStringToBSTR($confirmpassword)) 88 | 89 | if($password -ne $confirmpassword) { 90 | Write-Warning "Password and confirm password does not match. Please retry" 91 | } else { 92 | $userPasswords[$user]=$password 93 | } 94 | 95 | } until($password -eq $confirmpassword) 96 | 97 | } 98 | 99 | $outputObj = @() 100 | 101 | foreach ($Computer in $Computers) { 102 | 103 | $Computer | Add-Member Noteproperty Isonline "OFFLINE" 104 | Write-Verbose "Working on $($Computer.name)" 105 | if((Test-Connection -ComputerName $Computer.name -count 1 -ErrorAction 0)) { 106 | $Computer.Isonline = "ONLINE" 107 | } 108 | Write-Verbose "`t$($Computer.name) is $($Computer.Isonline)" 109 | 110 | if ($Computer.Isonline -eq "ONLINE") { 111 | foreach ($user in $Computer.users) { 112 | 113 | try { 114 | $account = [ADSI]("WinNT://$($Computer.name)/$($user.name),user") 115 | $account.psbase.invoke("setpassword",$userPasswords[$user.name]) 116 | Write-Verbose "`tPassword of user $($user.name) was changed successfully" 117 | } 118 | catch { 119 | $user.status = "FAILED" 120 | $user.errorMessage = $_ -replace "`n|`r","" 121 | 122 | Write-Verbose "`tFailed to Change the password of the user $($user.name). Error: $($user.errorMessage)" 123 | } 124 | 125 | $outputLine = New-Object -TypeName PSObject -Property @{ 126 | ComputerName = $($Computer.name) 127 | IsOnline = $($Computer.Isonline) 128 | User = $($user.name) 129 | PasswordChangeStatus = $($user.status) 130 | Error = $($user.errorMessage) 131 | } 132 | 133 | $outputLine | Select ComputerName, IsOnline, User, PasswordChangeStatus 134 | $outputObj += $outputLine 135 | } 136 | } else { 137 | $outputLine = New-Object -TypeName PSObject -Property @{ 138 | ComputerName = $($Computer.name) 139 | IsOnline = $($Computer.Isonline) 140 | User = "*" 141 | PasswordChangeStatus = "FAILED" 142 | Error = "" 143 | } 144 | 145 | $outputLine | Select ComputerName, IsOnline, User, PasswordChangeStatus 146 | $outputObj += $outputLine 147 | } 148 | 149 | } 150 | $outputObj | Select ComputerName, IsOnline, User, PasswordChangeStatus, Error | export-csv -delimiter ";" -NoTypeInformation -Encoding UTF8 -path $ResultFile 151 | Write-Host "`n`nResult are saved in $ResultFile" -------------------------------------------------------------------------------- /ntfs-hardlink-backup/ocs_inventory_plugins/Howto.md: -------------------------------------------------------------------------------- 1 | # OCS Inventory 2 | 3 | Howto display the backup status in [OCSInventory](http://www.ocsinventory-ng.org/en/) 4 | 5 | This should work with OCS Inventory 2.0 & 2.1 6 | 7 | You need at least ntfs-hardlink-backup 2.2 8 | 9 | ## Client preparation 10 | 11 | Copy the file ntfshardlinkbackup_ocs_plugin.vbs into C:\Program Files\OCS Inventory Agent\Plugins. 12 | 13 | Edit that file and adjust the first 3 lines according to your needs: 14 | 15 | ``` 16 | Dim xmlstatusfiles(1) 17 | xmlstatusfiles(0) = "C:\Logs\network-backup\status.xml" 18 | xmlstatusfiles(1) = "C:\Logs\Backup-To-External-HDD\status.xml" 19 | ``` 20 | 21 | List all your status files here. 22 | 23 | ### Check the result 24 | Just doubleclick on the ntfshardlinkbackup_ocs_plugin.vbs file. The content of the status file(s) should be displayed. 25 | 26 | ## Server installation 27 | 28 | ### Create table in database to store informations 29 | 30 | You have to create a new table which will receive new data 31 | 32 | ```` 33 | CREATE TABLE IF NOT EXISTS `ntfshardlinkbackup` ( 34 | `ID` INT(11) NOT NULL AUTO_INCREMENT, 35 | `HARDWARE_ID` INT(11) NOT NULL, 36 | `VERSION` VARCHAR(64) DEFAULT NULL, 37 | `STATUS` VARCHAR(64) DEFAULT NULL, 38 | `JOBNAME` VARCHAR(255) DEFAULT NULL, 39 | `LASTRUN` DATETIME DEFAULT NULL, 40 | `DESTINATION` VARCHAR(255) DEFAULT NULL, 41 | PRIMARY KEY (`ID`,`HARDWARE_ID`) 42 | ) ENGINE=INNODB ; 43 | ```` 44 | 45 | ### Modifiy the engine 46 | 47 | You have to modify Map.pm file. On linux, to find the correct version of Map.pm use this command => ````updatedb; locate Map.pm ```` 48 | 49 | ``` 50 | ntfshardlinkbackup => { 51 | mask => 0, 52 | multi => 1, 53 | auto => 1, 54 | delOnReplace => 1, 55 | sortBy => 'LASTRUN', 56 | writeDiff => 0, 57 | cache => 0, 58 | fields => { 59 | VERSION => {}, 60 | STATUS => {}, 61 | JOBNAME => {}, 62 | LASTRUN => {}, 63 | DESTINATION => {}, 64 | } 65 | }, 66 | ``` 67 | 68 | **Warning: When you will upgrade your OCS Inventory Server, Map.pm will be overwrite. Don't forget to save this file before upgrading.** 69 | 70 | ### Create the workspace 71 | 72 | * create a new folder in the "plugins/computer_detail" directory and name it "cd_ntfshardlinkbackup" 73 | * copy the file "cd_ntfshardlinkbackup.php" into this new folder 74 | * copy your 3 icons (cd_ntfshardlinkbackup*.png) into "plugins/computer_detail/img" 75 | 76 | ### Activate the plugin 77 | 78 | Edit the file /plugins/computer_detail/cd_config.txt 79 | 80 | ``` 81 | 82 | ....... 83 | ....... 84 | 26:cd_ntfshardlinkbackup 85 | 86 | ``` 87 | 88 | ``` 89 | 90 | ....... 91 | ....... 92 | cd_ntfshardlinkbackup:cd_ntfshardlinkbackup 93 | 94 | ``` 95 | 96 | ``` 97 | 98 | ....... 99 | ....... 100 | cd_ntfshardlinkbackup:ntfshardlinkbackup 101 | 102 | ``` 103 | 104 | ``` 105 | 106 | ....... 107 | cd_ntfshardlinkbackup:26 108 | ....... 109 | 110 | ``` 111 | 112 | ### Modification of multicriteria search page 113 | 114 | The goal is to have possibility to do a multicriteria search on these new data 115 | 116 | Files to modify: 117 | 118 | **plugins/language/english/english.txt** 119 | 120 | add at the end: 121 | 122 | ``` 123 | 6050 NTFS Hardlink Backup 124 | 6051 Jobname 125 | 6052 Last Run 126 | 6053 Destination 127 | 6054 Status 128 | ``` 129 | 130 | **plugins/main_sections/ms_multi_search/ms_multi_search.php** 131 | 132 | Code to add : 133 | 134 | ``` 135 | if ($list_id != "") { 136 | $list_fields= array($l->g(652).': id'=>'h.ID', 137 | $l->g(652).': '.$l->g(46)=>'h.LASTDATE', 138 | $l->g(652).": ".$l->g(820)=>'h.LASTCOME', 139 | 'NAME'=>'h.NAME', 140 | $l->g(652).": ".$l->g(24)=>'h.USERID', 141 | $l->g(652).": ".$l->g(25)=>'h.OSNAME', 142 | .............. 143 | $l->g(652).": ".$l->g(1247)=>'h.ARCH', 144 | 145 | $l->g(6050).": ".$l->g(6051)=>'ntfshardlinkbackup.JOBNAME', 146 | $l->g(6050).": ".$l->g(6052)=>'ntfshardlinkbackup.LASTRUN', 147 | $l->g(6050).": ".$l->g(6053)=>'ntfshardlinkbackup.DESTINATION', 148 | $l->g(6050).": ".$l->g(6054)=>'ntfshardlinkbackup.STATUS', 149 | ); 150 | ``` 151 | 152 | ``` 153 | $tab_options['AS']['h.NAME']="name_of_machine"; 154 | $query_add_table=""; 155 | 156 | $query_add_table.=" left join ntfshardlinkbackup on h.id=ntfshardlinkbackup.hardware_id "; 157 | ``` 158 | 159 | 160 | ``` 161 | $sort_list=array("NETWORKS-IPADDRESS" =>$l->g(82).": ".$l->g(34), 162 | "NETWORKS-MACADDR"=>$l->g(82).": ".$l->g(95), 163 | "SOFTWARES-NAME"=>$l->g(20).": ".$l->g(49), 164 | ............ 165 | "CPUS-SOCKET"=>$l->g(54).": ".$l->g(1316), 166 | 167 | "NTFSHARDLINKBACKUP-JOBNAME"=>$l->g(6050).": ".$l->g(6051), 168 | "NTFSHARDLINKBACKUP-LASTRUN"=>$l->g(6050).": ".$l->g(6052), 169 | "NTFSHARDLINKBACKUP-DESTINATION"=>$l->g(6050).": ".$l->g(6053), 170 | "NTFSHARDLINKBACKUP-STATUS"=>$l->g(6050).": ".$l->g(6054), 171 | ); 172 | ``` 173 | 174 | ``` 175 | $optSelectField=array( "NETWORKS-IPADDRESS"=>$sort_list["NETWORKS-IPADDRESS"], 176 | "NETWORKS-MACADDR"=>$sort_list["NETWORKS-MACADDR"],//$l->g(82).": ".$l->g(95), 177 | "SOFTWARES-NAME"=>$sort_list["SOFTWARES-NAME"],//$l->g(20).": ".$l->g(49), 178 | "SOFTWARES-VERSION"=>$sort_list["SOFTWARES-VERSION"],//$l->g(20).": ".$l->g(277), 179 | "SOFTWARES-BITSWIDTH"=> $sort_list["SOFTWARES-BITSWIDTH"], 180 | "SOFTWARES-PUBLISHER"=> $sort_list["SOFTWARES-PUBLISHER"], 181 | "SOFTWARES-COMMENTS"=>$sort_list["SOFTWARES-COMMENTS"], 182 | "HARDWARE-DESCRIPTION"=>$sort_list["HARDWARE-DESCRIPTION"],//$l->g(25).": ".$l->g(53), 183 | ............................... 184 | "CPUS-VOLTAGE-SELECT"=>array("exact"=>$l->g(410),"small"=>$l->g(201),"tall"=>$l->g(202)), 185 | 186 | "NTFSHARDLINKBACKUP-JOBNAME"=>$sort_list["NTFSHARDLINKBACKUP-JOBNAME"], 187 | "NTFSHARDLINKBACKUP-LASTRUN"=>$sort_list["NTFSHARDLINKBACKUP-LASTRUN"], 188 | "NTFSHARDLINKBACKUP-LASTRUN-LBL"=>"calendar", 189 | "NTFSHARDLINKBACKUP-LASTRUN-SELECT"=>array("small"=>$l->g(346),"tall"=>$l->g(347)), 190 | "NTFSHARDLINKBACKUP-DESTINATION"=>$sort_list["NTFSHARDLINKBACKUP-DESTINATION"], 191 | "NTFSHARDLINKBACKUP-STATUS"=>$sort_list["NTFSHARDLINKBACKUP-STATUS"], 192 | ); 193 | ``` 194 | 195 | ``` 196 | $sort_list_2Select=array("HARDWARE-USERAGENT"=>"OCS: ".$l->g(966), 197 | "DEVICES-IPDISCOVER"=>$l->g(107).": ".$l->g(312), 198 | "DEVICES-FREQUENCY"=>$l->g(107).": ".$l->g(429), 199 | "GROUPS_CACHE-GROUP_ID"=>$l->g(583).": ".$l->g(49), 200 | ................. 201 | "CPUS-CURRENT_ADDRESS_WIDTH"=>$l->g(54).": ".$l->g(1313), 202 | 203 | "NTFSHARDLINKBACKUP-JOBNAME"=>$l->g(6050).": ".$l->g(6051), 204 | "NTFSHARDLINKBACKUP-LASTRUN"=>$l->g(6050).": ".$l->g(6052), 205 | "NTFSHARDLINKBACKUP-DESTINATION"=>$l->g(6050).": ".$l->g(6053), 206 | "NTFSHARDLINKBACKUP-STATUS"=>$l->g(6050).": ".$l->g(6054), 207 | ); 208 | ``` 209 | 210 | 211 | **require/function_search.php** 212 | 213 | change the line 214 | 215 | ``` 216 | if ($field == "LASTDATE" or $field == "LASTCOME" or $field == "REGVALUE"){ 217 | ``` 218 | 219 | to 220 | 221 | ``` 222 | if ($field == "LASTDATE" or $field == "LASTCOME" or $field == "REGVALUE" or $field == "LASTRUN"){ 223 | ``` 224 | -------------------------------------------------------------------------------- /delete-old-files/delete-old-files.ps1: -------------------------------------------------------------------------------- 1 | #List Files that have CreationTime older than x days (parameter) 2 | #extra days to list folders based on LastWriteTime 3 | #switch for deleting files 4 | #exclude folders/files INF-Offices 5 | #write log file (with timestamp) 6 | #parameter logfilelocation 7 | 8 | 9 | <# 10 | .DESCRIPTION 11 | DELETE-OLD-FILES Version: 1.0.ALPHA.2 12 | This software is used to find and list or delete files/folders that are older than a specified age 13 | .SYNOPSIS 14 | c:\full\path\delete-old-files.ps1 15 | .PARAMETER location 16 | Path of the files/folders to be processed. 17 | .PARAMETER iniFile 18 | Path to an optional INI file that contains any of the parameters. 19 | .PARAMETER fileAge 20 | Files with that age (in days) and older will be listed and/or deleted (based on CreationTime) 21 | default = 7 22 | .PARAMETER extraFolderAge 23 | Folder with fileAge + extraFolderAge and older will be listed and/or deleted (based on LastWriteTime) 24 | default = 7 25 | .PARAMETER delete 26 | actually delete the files/folders. 27 | .PARAMETER excludeFiles 28 | Exclude files via wildcards. Can be a list separated by comma. 29 | NOT IMPLEMENTED YET 30 | .PARAMETER excludeDirs 31 | Exclude directories via wildcards. Can be a list separated by comma. 32 | NOT IMPLEMENTED YET 33 | .PARAMETER LogFile 34 | Path and filename for the logfile. If just a path is given, then "yyyy-mm-dd hh-mm-ss.log" is written to that folder. 35 | Default is to write "yyyy-mm-dd hh-mm-ss.log" in the delete-old-files script folder. 36 | .PARAMETER version 37 | print the version information and exit. 38 | .EXAMPLE 39 | PS D:\> #ToDo 40 | .NOTES 41 | Author: Artur Neumann *INFN* 42 | #> 43 | 44 | [CmdletBinding()] 45 | Param( 46 | [Parameter(Mandatory=$False)] 47 | [String]$iniFile, 48 | [Parameter(Mandatory=$False)] 49 | [String]$location, 50 | [Parameter(Mandatory=$False)] 51 | [String[]]$excludeFiles, 52 | [Parameter(Mandatory=$False)] 53 | [String[]]$excludeDirs, 54 | [Parameter(Mandatory=$False)] 55 | [Int32]$fileAge, 56 | [Parameter(Mandatory=$False)] 57 | [Int32]$extraFolderAge, 58 | [Parameter(Mandatory=$False)] 59 | [string]$LogFile="", 60 | [Parameter(Mandatory=$False)] 61 | [switch]$version=$False, 62 | [Parameter(Mandatory=$False)] 63 | [switch]$delete=$False 64 | ) 65 | #The path and filename of the script it self 66 | $script_path = Split-Path -parent $MyInvocation.MyCommand.Definition 67 | 68 | Function Get-IniContent 69 | { 70 | <# 71 | .Synopsis 72 | Gets the content of an INI file 73 | 74 | .Description 75 | Gets the content of an INI file and returns it as a hashtable 76 | 77 | .Notes 78 | Author : Oliver Lipkau 79 | Blog : http://oliver.lipkau.net/blog/ 80 | Date : 2014/06/23 81 | Version : 1.1 82 | 83 | #Requires -Version 2.0 84 | 85 | .Inputs 86 | System.String 87 | 88 | .Outputs 89 | System.Collections.Hashtable 90 | 91 | .Parameter FilePath 92 | Specifies the path to the input file. 93 | 94 | .Example 95 | $FileContent = Get-IniContent "C:\myinifile.ini" 96 | ----------- 97 | Description 98 | Saves the content of the c:\myinifile.ini in a hashtable called $FileContent 99 | 100 | .Example 101 | $inifilepath | $FileContent = Get-IniContent 102 | ----------- 103 | Description 104 | Gets the content of the ini file passed through the pipe into a hashtable called $FileContent 105 | 106 | .Example 107 | C:\PS>$FileContent = Get-IniContent "c:\settings.ini" 108 | C:\PS>$FileContent["Section"]["Key"] 109 | ----------- 110 | Description 111 | Returns the key "Key" of the section "Section" from the C:\settings.ini file 112 | 113 | .Link 114 | Out-IniFile 115 | #> 116 | 117 | [CmdletBinding()] 118 | Param( 119 | [ValidateNotNullOrEmpty()] 120 | [ValidateScript({(Test-Path $_) -and ((Get-Item $_).Extension -eq ".ini")})] 121 | [Parameter(ValueFromPipeline=$True,Mandatory=$True)] 122 | [string]$FilePath 123 | ) 124 | 125 | Begin 126 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 127 | 128 | Process 129 | { 130 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing file: $Filepath" 131 | 132 | #changed from HashTable to OrderedDictionary to keep the sections in the order they were added - Artur Neumann 133 | $ini = New-Object System.Collections.Specialized.OrderedDictionary 134 | switch -regex -file $FilePath 135 | { 136 | "^\[(.+)\]$" # Section 137 | { 138 | $section = $matches[1] 139 | # Added ToLower line to make INI file case-insensitive - Phil Davis 140 | $section = $section.ToLower() 141 | $ini[$section] = @{} 142 | $CommentCount = 0 143 | } 144 | "^(;.*)$" # Comment 145 | { 146 | if (!($section)) 147 | { 148 | $section = "No-Section" 149 | $ini[$section] = @{} 150 | } 151 | $value = $matches[1] 152 | $CommentCount = $CommentCount + 1 153 | $name = "Comment" + $CommentCount 154 | $ini[$section][$name] = $value 155 | } 156 | "(.+?)\s*=\s*(.*)" # Key 157 | { 158 | if (!($section)) 159 | { 160 | $section = "No-Section" 161 | $ini[$section] = @{} 162 | } 163 | $name,$value = $matches[1..2] 164 | # Added ToLower line to make INI file case-insensitive - Phil Davis 165 | $name = $name.ToLower() 166 | $ini[$section][$name] = $value 167 | } 168 | } 169 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath" 170 | Return $ini 171 | } 172 | 173 | End 174 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 175 | } 176 | 177 | Function Get-IniParameter 178 | { 179 | # Note: iniFileContent dictionary is not passed in each time. 180 | # Just use the global value to reference that. 181 | [CmdletBinding()] 182 | Param( 183 | [ValidateNotNullOrEmpty()] 184 | [Parameter(Mandatory=$True)] 185 | [string]$ParameterName, 186 | [ValidateNotNullOrEmpty()] 187 | [Parameter(Mandatory=$True)] 188 | [string]$FQDN, 189 | [ValidateNotNullOrEmpty()] 190 | [Parameter(Mandatory=$False)] 191 | [switch]$doNotSubstitute=$False 192 | ) 193 | 194 | Begin 195 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 196 | 197 | Process 198 | { 199 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing for IniSection: $FQDN and ParameterName: $ParameterName" 200 | 201 | # Use ToLower to make all parameter name comparisons case-insensitive 202 | $ParameterName = $ParameterName.ToLower() 203 | $ParameterValue = $Null 204 | 205 | $FQDN=$FQDN.ToLower() 206 | 207 | #search first the "common" section for the parameter, this will have the lowest priority 208 | #as the parameter can be overwritten by other sections 209 | if ($global:iniFileContent.Contains("common")) { 210 | if (-not [string]::IsNullOrEmpty($global:iniFileContent["common"][$ParameterName])) { 211 | $ParameterValue = $global:iniFileContent["common"][$ParameterName] 212 | } 213 | } 214 | 215 | #search if there is a section that matches the FQDN 216 | #this is the second highest priority, as the parameter can still be overwritten by the 217 | #section that meets exactly the FQDN 218 | #If there is more than one section that matches the FQDN with the same parameter 219 | #the section furthest down in the ini file will be used 220 | foreach ($IniSection in $($global:iniFileContent.keys)){ 221 | $EscapedIniSection=$IniSection -replace "([\-\[\]\{\}\(\)\+\?\.\,\\\^\$\|\#])",'\$1' 222 | $EscapedIniSection=$IniSection -replace "\*",'.*' 223 | if ($FQDN -match "^$EscapedIniSection$") { 224 | if (-not [string]::IsNullOrEmpty($global:iniFileContent[$IniSection][$ParameterName])) { 225 | $ParameterValue = $global:iniFileContent[$IniSection][$ParameterName] 226 | } 227 | } 228 | } 229 | 230 | #see if there is section that is called exactly the same as the computer (FQDN) 231 | #this is the highest priority, so if the same parameters are used in other sections 232 | #this section will overwrite them 233 | if ($global:iniFileContent.Contains($FQDN)) { 234 | if (-not [string]::IsNullOrEmpty($global:iniFileContent[$FQDN][$ParameterName])) { 235 | $ParameterValue = $global:iniFileContent[$FQDN][$ParameterName] 236 | } 237 | } 238 | 239 | #replace all with the parameter values 240 | if ($doNotSubstitute -eq $False) { 241 | $substituteMatches=$ParameterValue | Select-String -AllMatches '<[^<]+?>' | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Value 242 | 243 | foreach ($match in $substituteMatches) { 244 | if (![string]::IsNullOrEmpty($match)) { 245 | $match=$($match.Trim()) 246 | $cleanMatch=$match.Replace("<","").Replace(">","") 247 | if ($(test-path env:$($cleanMatch))) { 248 | $substituteValue=$(get-childitem -path env:$($cleanMatch)).Value 249 | $ParameterValue =$ParameterValue.Replace($match,$substituteValue) 250 | } 251 | } 252 | } 253 | } 254 | 255 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing for IniSection: $FQDN and ParameterName: $ParameterName ParameterValue: $ParameterValue" 256 | Return $ParameterValue 257 | } 258 | 259 | End 260 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 261 | } 262 | 263 | Function Is-TrueString 264 | { 265 | # Pass in a string (or nothing) and return a boolean deciding if the string 266 | # is "1", "true", "t" (True) or otherwise it is (False) 267 | [CmdletBinding()] 268 | Param( 269 | [Parameter(Mandatory=$False)] 270 | [string]$TruthString 271 | ) 272 | 273 | Begin 274 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 275 | 276 | Process 277 | { 278 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing for TruthString: $TruthString" 279 | 280 | # Use ToLower to make comparisons case-insensitive 281 | $TruthString = $TruthString.ToLower() 282 | $ParameterValue = $Null 283 | 284 | if (($TruthString -eq "t") -or ($TruthString -eq "true") -or ($TruthString -eq "1")) { 285 | $TruthValue = $True 286 | } else { 287 | $TruthValue = $False 288 | } 289 | 290 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing for TruthString: $TruthString TruthValue: $TruthValue" 291 | Return $TruthValue 292 | } 293 | 294 | End 295 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 296 | } 297 | 298 | Function Get-Version 299 | { 300 | <# 301 | .Synopsis 302 | Gets the version of this script 303 | 304 | .Description 305 | Parses the description for a line that looks like: 306 | DELETE-OLD-FILES Version: 2.0.ALPHA.8 307 | and gets the version information out of it 308 | The version string must be in the .DESCRIPTION scope and must start with 309 | "DELETE-OLD-FILES Version: " 310 | 311 | .Outputs 312 | System.String 313 | #> 314 | 315 | #Get the help-text of my self 316 | $helpText=Get-Help $script_path\delete-old-files.ps1 317 | 318 | #Get-Help returns a PSObjects with other PSObjects inside 319 | #So we are trying some black magic to get a string out of it and then to parse the version 320 | 321 | Foreach ($object in $helpText.psobject.properties) { 322 | #loop through all properties of the PSObject and find the description 323 | if (($object.Value) -and ($object.name -eq "description")) { 324 | #the description is a object of the class System.Management.Automation.PSNoteProperty 325 | #and inside of the properties of that are System.Management.Automation.PSPropertyInfo objects (in our case only one) 326 | #still we loop though, just in case there are more that one and see if the value (what is finally a string), does match the version string 327 | Foreach ($subObject in $object.Value[0].psobject.properties) { 328 | if ($subObject.Value -match "DELETE-OLD-FILES Version: (.*)") { 329 | return $matches[1] 330 | } 331 | } 332 | } 333 | } 334 | } 335 | 336 | function Recurse($path) { 337 | #stolen from: http://superuser.com/questions/528487/list-all-files-and-dirs-without-recursion-with-junctions/528499#528499 338 | 339 | $fc = new-object -com scripting.filesystemobject 340 | $folder = $fc.getfolder($path) 341 | 342 | foreach ($i in $folder.files) { $i } 343 | 344 | foreach ($i in $folder.subfolders) { 345 | 346 | if ( (get-item -force $i.path).Attributes.ToString().Contains("ReparsePoint") -eq $false) { 347 | $i 348 | Recurse($i.path) 349 | } 350 | } 351 | } 352 | 353 | 354 | $FQDN = [System.Net.DNS]::GetHostByName('').HostName 355 | $tempLogContent = "" 356 | 357 | $versionString=Get-Version 358 | 359 | if ($version) { 360 | echo $versionString 361 | exit 362 | } else { 363 | $output = "DELETE-OLD-FILES $versionString`r`n" 364 | $tempLogContent += $output 365 | echo $output 366 | } 367 | 368 | if ($iniFile) { 369 | if (Test-Path -Path $iniFile -PathType leaf) { 370 | $output = "Using ini file`r`n$iniFile`r`n" 371 | $global:iniFileContent = Get-IniContent "${iniFile}" 372 | } else { 373 | $global:iniFileContent = New-Object System.Collections.Specialized.OrderedDictionary 374 | $output = "ERROR: Could not find ini file`r`n$iniFile`r`n" 375 | } 376 | echo $output 377 | } else { 378 | $global:iniFileContent = New-Object System.Collections.Specialized.OrderedDictionary 379 | } 380 | 381 | $parameters_ok = $True 382 | 383 | if ([string]::IsNullOrEmpty($location)) { 384 | $location = Get-IniParameter "location" "${FQDN}" 385 | 386 | if ([string]::IsNullOrEmpty($location)) { 387 | $output = "ERROR: no location was given cannot progress`r`n" 388 | $parameters_ok = $False 389 | } 390 | } 391 | 392 | if ([string]::IsNullOrEmpty($excludeFiles)) { 393 | $excludeFilesList = Get-IniParameter "excludeFiles" "${FQDN}" 394 | if (-not [string]::IsNullOrEmpty($excludeFilesList)) { 395 | $excludeFiles = $excludeFilesList.split(",") 396 | } 397 | } 398 | 399 | if ([string]::IsNullOrEmpty($excludeDirs)) { 400 | $excludeDirsList = Get-IniParameter "excludeDirs" "${FQDN}" 401 | if (-not [string]::IsNullOrEmpty($excludeDirsList)) { 402 | $excludeDirs = $excludeDirsList.split(",") 403 | } 404 | } 405 | 406 | if ($fileAge -eq 0) { 407 | $fileAge = Get-IniParameter "fileAge" "${FQDN}" 408 | if ($fileAge -eq 0) { 409 | $fileAge = 7; 410 | } 411 | } 412 | 413 | if ($extraFolderAge -eq 0) { 414 | $extraFolderAge = Get-IniParameter "extraFolderAge" "${FQDN}" 415 | if ($extraFolderAge -eq 0) { 416 | $extraFolderAge = 7; 417 | } 418 | } 419 | 420 | if (-not $delete.IsPresent) { 421 | $IniFileString = Get-IniParameter "delete" "${FQDN}" 422 | $delete = Is-TrueString "${IniFileString}" 423 | } 424 | 425 | if ([string]::IsNullOrEmpty($LogFile)) { 426 | $LogFile = Get-IniParameter "LogFile" "${FQDN}" 427 | } 428 | 429 | $dateTime = get-date -f "yyyy-MM-dd HH-mm-ss" 430 | 431 | if ([string]::IsNullOrEmpty($LogFile)) { 432 | # No log file specified from command line - put one in the script path destination with date-time stamp. 433 | $logFileDestination = $script_path 434 | $LogFile = "$logFileDestination\$dateTime.log" 435 | } else { 436 | if (Test-Path -Path $LogFile -pathType container) { 437 | # The log file parameter points to a folder, so generate log file names in that folder. 438 | $logFileDestination = $LogFile 439 | $LogFile = "$logFileDestination\$dateTime.log" 440 | } else { 441 | # The log file name has been fully specified - just calculate the parent folder. 442 | $logFileDestination = Split-Path -parent $LogFile 443 | } 444 | } 445 | 446 | try 447 | { 448 | New-Item "$LogFile" -type file -force -erroraction stop | Out-Null 449 | } 450 | catch 451 | { 452 | $output = "ERROR: Could not create new log file`r`n$_`r`n" 453 | echo $output 454 | $LogFile="" 455 | $error_during_process = $True 456 | } 457 | 458 | #write the logs from the time we hadn't a log file into the file 459 | if ($LogFile) { 460 | $tempLogContent | Out-File "$LogFile" -encoding ASCII -append 461 | } 462 | 463 | if ($parameters_ok -eq $True) { 464 | $olderThanDate = (Get-Date).adddays(-$fileAge) 465 | $allFilesAndFolders=Recurse($location) 466 | 467 | #assuming everything that is not a Directory must be a File 468 | #we need to use get-item again. using $_.Attributes only returns a magic number not a string 469 | #need -force to get also hidden files 470 | $filesToDelete = $allFilesAndFolders | ? {(get-item -force $_.path).Attributes -ne $null} | ? {(get-item -force $_.path).Attributes.ToString().Contains("Directory") -eq $false} | ? {$_.DateCreated -lt $olderThanDate} 471 | 472 | $olderThanDate = (Get-Date).adddays(-($fileAge+$extraFolderAge)) 473 | 474 | #get only the folders (Attribute contains "Directory"") and sort them by length to make sure subfolders are checked and deleted before the main folder 475 | $foldersToDelete = $allFilesAndFolders | ? {(get-item -force $_.path).Attributes -ne $null} | ? {(get-item -force $_.path).Attributes.ToString().Contains("Directory")} | ? {$_.DateLastModified -lt $olderThanDate} | Select-Object -Property Path,DateLastModified, @{Name="PathLength";Expression={($_.Path.Length)}} | Sort-Object -Property PathLength -Descending 476 | 477 | if ($delete) { 478 | $output = "DELETING FILES:`r`n" 479 | } else { 480 | $output = "LIST FILES:`r`n" 481 | } 482 | echo $output 483 | if ($LogFile) { 484 | $output | Out-File "$LogFile" -encoding ASCII -append 485 | } 486 | 487 | if ( $filesToDelete -ne $null ) { #Powershell < 3 does iterate also over $null http://serverfault.com/a/457760/282995 488 | foreach ($file in $filesToDelete) { 489 | 490 | $output = $file.Path + " - " + $file.DateCreated.ToString("yyyy.MM.dd") 491 | Write-Host $output 492 | 493 | if ($delete) { 494 | Remove-Item -Force $file.Path 495 | } 496 | 497 | if ($LogFile) { 498 | $output | Out-File "$LogFile" -encoding ASCII -append 499 | } 500 | 501 | } 502 | } 503 | 504 | if ($delete) { 505 | $output = "`r`nDELETING FOLDERS:`r`n" 506 | } else { 507 | $output = "`r`nLIST FOLDERS:`r`n" 508 | } 509 | Write-Host $output 510 | if ($LogFile) { 511 | $output | Out-File "$LogFile" -encoding ASCII -append 512 | } 513 | 514 | if ( $foldersToDelete -ne $null ) { #Powershell < 3 does iterate also over $null http://serverfault.com/a/457760/282995 515 | foreach ($folder in $foldersToDelete) { 516 | if ($delete) { 517 | $subitems = Get-ChildItem -Recurse -Path $folder.Path 518 | if ($subitems -eq $null) { 519 | Remove-Item $folder.Path 520 | $output = $folder.Path + " - " + $folder.DateLastModified.ToString("yyyy.MM.dd") 521 | } else { 522 | $output="" 523 | } 524 | $subitems = $null 525 | } else { 526 | $output = $folder.Path + " - " + $folder.DateLastModified.ToString("yyyy.MM.dd") 527 | } 528 | 529 | if ($output) { 530 | Write-Host $output 531 | if ($LogFile) { 532 | $output | Out-File "$LogFile" -encoding ASCII -append 533 | } 534 | } 535 | 536 | } 537 | } 538 | } else { 539 | echo "ERROR: nothing was done due to problems in the parameters`r`n" 540 | } 541 | -------------------------------------------------------------------------------- /LICENSE: -------------------------------------------------------------------------------- 1 | GNU GENERAL PUBLIC LICENSE 2 | Version 2, June 1991 3 | 4 | Copyright (C) 1989, 1991 Free Software Foundation, Inc., 5 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA 6 | Everyone is permitted to copy and distribute verbatim copies 7 | of this license document, but changing it is not allowed. 8 | 9 | Preamble 10 | 11 | The licenses for most software are designed to take away your 12 | freedom to share and change it. By contrast, the GNU General Public 13 | License is intended to guarantee your freedom to share and change free 14 | software--to make sure the software is free for all its users. This 15 | General Public License applies to most of the Free Software 16 | Foundation's software and to any other program whose authors commit to 17 | using it. (Some other Free Software Foundation software is covered by 18 | the GNU Lesser General Public License instead.) You can apply it to 19 | your programs, too. 20 | 21 | When we speak of free software, we are referring to freedom, not 22 | price. Our General Public Licenses are designed to make sure that you 23 | have the freedom to distribute copies of free software (and charge for 24 | this service if you wish), that you receive source code or can get it 25 | if you want it, that you can change the software or use pieces of it 26 | in new free programs; and that you know you can do these things. 27 | 28 | To protect your rights, we need to make restrictions that forbid 29 | anyone to deny you these rights or to ask you to surrender the rights. 30 | These restrictions translate to certain responsibilities for you if you 31 | distribute copies of the software, or if you modify it. 32 | 33 | For example, if you distribute copies of such a program, whether 34 | gratis or for a fee, you must give the recipients all the rights that 35 | you have. You must make sure that they, too, receive or can get the 36 | source code. And you must show them these terms so they know their 37 | rights. 38 | 39 | We protect your rights with two steps: (1) copyright the software, and 40 | (2) offer you this license which gives you legal permission to copy, 41 | distribute and/or modify the software. 42 | 43 | Also, for each author's protection and ours, we want to make certain 44 | that everyone understands that there is no warranty for this free 45 | software. If the software is modified by someone else and passed on, we 46 | want its recipients to know that what they have is not the original, so 47 | that any problems introduced by others will not reflect on the original 48 | authors' reputations. 49 | 50 | Finally, any free program is threatened constantly by software 51 | patents. We wish to avoid the danger that redistributors of a free 52 | program will individually obtain patent licenses, in effect making the 53 | program proprietary. To prevent this, we have made it clear that any 54 | patent must be licensed for everyone's free use or not licensed at all. 55 | 56 | The precise terms and conditions for copying, distribution and 57 | modification follow. 58 | 59 | GNU GENERAL PUBLIC LICENSE 60 | TERMS AND CONDITIONS FOR COPYING, DISTRIBUTION AND MODIFICATION 61 | 62 | 0. This License applies to any program or other work which contains 63 | a notice placed by the copyright holder saying it may be distributed 64 | under the terms of this General Public License. The "Program", below, 65 | refers to any such program or work, and a "work based on the Program" 66 | means either the Program or any derivative work under copyright law: 67 | that is to say, a work containing the Program or a portion of it, 68 | either verbatim or with modifications and/or translated into another 69 | language. (Hereinafter, translation is included without limitation in 70 | the term "modification".) Each licensee is addressed as "you". 71 | 72 | Activities other than copying, distribution and modification are not 73 | covered by this License; they are outside its scope. The act of 74 | running the Program is not restricted, and the output from the Program 75 | is covered only if its contents constitute a work based on the 76 | Program (independent of having been made by running the Program). 77 | Whether that is true depends on what the Program does. 78 | 79 | 1. You may copy and distribute verbatim copies of the Program's 80 | source code as you receive it, in any medium, provided that you 81 | conspicuously and appropriately publish on each copy an appropriate 82 | copyright notice and disclaimer of warranty; keep intact all the 83 | notices that refer to this License and to the absence of any warranty; 84 | and give any other recipients of the Program a copy of this License 85 | along with the Program. 86 | 87 | You may charge a fee for the physical act of transferring a copy, and 88 | you may at your option offer warranty protection in exchange for a fee. 89 | 90 | 2. You may modify your copy or copies of the Program or any portion 91 | of it, thus forming a work based on the Program, and copy and 92 | distribute such modifications or work under the terms of Section 1 93 | above, provided that you also meet all of these conditions: 94 | 95 | a) You must cause the modified files to carry prominent notices 96 | stating that you changed the files and the date of any change. 97 | 98 | b) You must cause any work that you distribute or publish, that in 99 | whole or in part contains or is derived from the Program or any 100 | part thereof, to be licensed as a whole at no charge to all third 101 | parties under the terms of this License. 102 | 103 | c) If the modified program normally reads commands interactively 104 | when run, you must cause it, when started running for such 105 | interactive use in the most ordinary way, to print or display an 106 | announcement including an appropriate copyright notice and a 107 | notice that there is no warranty (or else, saying that you provide 108 | a warranty) and that users may redistribute the program under 109 | these conditions, and telling the user how to view a copy of this 110 | License. (Exception: if the Program itself is interactive but 111 | does not normally print such an announcement, your work based on 112 | the Program is not required to print an announcement.) 113 | 114 | These requirements apply to the modified work as a whole. If 115 | identifiable sections of that work are not derived from the Program, 116 | and can be reasonably considered independent and separate works in 117 | themselves, then this License, and its terms, do not apply to those 118 | sections when you distribute them as separate works. But when you 119 | distribute the same sections as part of a whole which is a work based 120 | on the Program, the distribution of the whole must be on the terms of 121 | this License, whose permissions for other licensees extend to the 122 | entire whole, and thus to each and every part regardless of who wrote it. 123 | 124 | Thus, it is not the intent of this section to claim rights or contest 125 | your rights to work written entirely by you; rather, the intent is to 126 | exercise the right to control the distribution of derivative or 127 | collective works based on the Program. 128 | 129 | In addition, mere aggregation of another work not based on the Program 130 | with the Program (or with a work based on the Program) on a volume of 131 | a storage or distribution medium does not bring the other work under 132 | the scope of this License. 133 | 134 | 3. You may copy and distribute the Program (or a work based on it, 135 | under Section 2) in object code or executable form under the terms of 136 | Sections 1 and 2 above provided that you also do one of the following: 137 | 138 | a) Accompany it with the complete corresponding machine-readable 139 | source code, which must be distributed under the terms of Sections 140 | 1 and 2 above on a medium customarily used for software interchange; or, 141 | 142 | b) Accompany it with a written offer, valid for at least three 143 | years, to give any third party, for a charge no more than your 144 | cost of physically performing source distribution, a complete 145 | machine-readable copy of the corresponding source code, to be 146 | distributed under the terms of Sections 1 and 2 above on a medium 147 | customarily used for software interchange; or, 148 | 149 | c) Accompany it with the information you received as to the offer 150 | to distribute corresponding source code. (This alternative is 151 | allowed only for noncommercial distribution and only if you 152 | received the program in object code or executable form with such 153 | an offer, in accord with Subsection b above.) 154 | 155 | The source code for a work means the preferred form of the work for 156 | making modifications to it. For an executable work, complete source 157 | code means all the source code for all modules it contains, plus any 158 | associated interface definition files, plus the scripts used to 159 | control compilation and installation of the executable. However, as a 160 | special exception, the source code distributed need not include 161 | anything that is normally distributed (in either source or binary 162 | form) with the major components (compiler, kernel, and so on) of the 163 | operating system on which the executable runs, unless that component 164 | itself accompanies the executable. 165 | 166 | If distribution of executable or object code is made by offering 167 | access to copy from a designated place, then offering equivalent 168 | access to copy the source code from the same place counts as 169 | distribution of the source code, even though third parties are not 170 | compelled to copy the source along with the object code. 171 | 172 | 4. You may not copy, modify, sublicense, or distribute the Program 173 | except as expressly provided under this License. Any attempt 174 | otherwise to copy, modify, sublicense or distribute the Program is 175 | void, and will automatically terminate your rights under this License. 176 | However, parties who have received copies, or rights, from you under 177 | this License will not have their licenses terminated so long as such 178 | parties remain in full compliance. 179 | 180 | 5. You are not required to accept this License, since you have not 181 | signed it. However, nothing else grants you permission to modify or 182 | distribute the Program or its derivative works. These actions are 183 | prohibited by law if you do not accept this License. Therefore, by 184 | modifying or distributing the Program (or any work based on the 185 | Program), you indicate your acceptance of this License to do so, and 186 | all its terms and conditions for copying, distributing or modifying 187 | the Program or works based on it. 188 | 189 | 6. Each time you redistribute the Program (or any work based on the 190 | Program), the recipient automatically receives a license from the 191 | original licensor to copy, distribute or modify the Program subject to 192 | these terms and conditions. You may not impose any further 193 | restrictions on the recipients' exercise of the rights granted herein. 194 | You are not responsible for enforcing compliance by third parties to 195 | this License. 196 | 197 | 7. If, as a consequence of a court judgment or allegation of patent 198 | infringement or for any other reason (not limited to patent issues), 199 | conditions are imposed on you (whether by court order, agreement or 200 | otherwise) that contradict the conditions of this License, they do not 201 | excuse you from the conditions of this License. If you cannot 202 | distribute so as to satisfy simultaneously your obligations under this 203 | License and any other pertinent obligations, then as a consequence you 204 | may not distribute the Program at all. For example, if a patent 205 | license would not permit royalty-free redistribution of the Program by 206 | all those who receive copies directly or indirectly through you, then 207 | the only way you could satisfy both it and this License would be to 208 | refrain entirely from distribution of the Program. 209 | 210 | If any portion of this section is held invalid or unenforceable under 211 | any particular circumstance, the balance of the section is intended to 212 | apply and the section as a whole is intended to apply in other 213 | circumstances. 214 | 215 | It is not the purpose of this section to induce you to infringe any 216 | patents or other property right claims or to contest validity of any 217 | such claims; this section has the sole purpose of protecting the 218 | integrity of the free software distribution system, which is 219 | implemented by public license practices. Many people have made 220 | generous contributions to the wide range of software distributed 221 | through that system in reliance on consistent application of that 222 | system; it is up to the author/donor to decide if he or she is willing 223 | to distribute software through any other system and a licensee cannot 224 | impose that choice. 225 | 226 | This section is intended to make thoroughly clear what is believed to 227 | be a consequence of the rest of this License. 228 | 229 | 8. If the distribution and/or use of the Program is restricted in 230 | certain countries either by patents or by copyrighted interfaces, the 231 | original copyright holder who places the Program under this License 232 | may add an explicit geographical distribution limitation excluding 233 | those countries, so that distribution is permitted only in or among 234 | countries not thus excluded. In such case, this License incorporates 235 | the limitation as if written in the body of this License. 236 | 237 | 9. The Free Software Foundation may publish revised and/or new versions 238 | of the General Public License from time to time. Such new versions will 239 | be similar in spirit to the present version, but may differ in detail to 240 | address new problems or concerns. 241 | 242 | Each version is given a distinguishing version number. If the Program 243 | specifies a version number of this License which applies to it and "any 244 | later version", you have the option of following the terms and conditions 245 | either of that version or of any later version published by the Free 246 | Software Foundation. If the Program does not specify a version number of 247 | this License, you may choose any version ever published by the Free Software 248 | Foundation. 249 | 250 | 10. If you wish to incorporate parts of the Program into other free 251 | programs whose distribution conditions are different, write to the author 252 | to ask for permission. For software which is copyrighted by the Free 253 | Software Foundation, write to the Free Software Foundation; we sometimes 254 | make exceptions for this. Our decision will be guided by the two goals 255 | of preserving the free status of all derivatives of our free software and 256 | of promoting the sharing and reuse of software generally. 257 | 258 | NO WARRANTY 259 | 260 | 11. BECAUSE THE PROGRAM IS LICENSED FREE OF CHARGE, THERE IS NO WARRANTY 261 | FOR THE PROGRAM, TO THE EXTENT PERMITTED BY APPLICABLE LAW. EXCEPT WHEN 262 | OTHERWISE STATED IN WRITING THE COPYRIGHT HOLDERS AND/OR OTHER PARTIES 263 | PROVIDE THE PROGRAM "AS IS" WITHOUT WARRANTY OF ANY KIND, EITHER EXPRESSED 264 | OR IMPLIED, INCLUDING, BUT NOT LIMITED TO, THE IMPLIED WARRANTIES OF 265 | MERCHANTABILITY AND FITNESS FOR A PARTICULAR PURPOSE. THE ENTIRE RISK AS 266 | TO THE QUALITY AND PERFORMANCE OF THE PROGRAM IS WITH YOU. SHOULD THE 267 | PROGRAM PROVE DEFECTIVE, YOU ASSUME THE COST OF ALL NECESSARY SERVICING, 268 | REPAIR OR CORRECTION. 269 | 270 | 12. IN NO EVENT UNLESS REQUIRED BY APPLICABLE LAW OR AGREED TO IN WRITING 271 | WILL ANY COPYRIGHT HOLDER, OR ANY OTHER PARTY WHO MAY MODIFY AND/OR 272 | REDISTRIBUTE THE PROGRAM AS PERMITTED ABOVE, BE LIABLE TO YOU FOR DAMAGES, 273 | INCLUDING ANY GENERAL, SPECIAL, INCIDENTAL OR CONSEQUENTIAL DAMAGES ARISING 274 | OUT OF THE USE OR INABILITY TO USE THE PROGRAM (INCLUDING BUT NOT LIMITED 275 | TO LOSS OF DATA OR DATA BEING RENDERED INACCURATE OR LOSSES SUSTAINED BY 276 | YOU OR THIRD PARTIES OR A FAILURE OF THE PROGRAM TO OPERATE WITH ANY OTHER 277 | PROGRAMS), EVEN IF SUCH HOLDER OR OTHER PARTY HAS BEEN ADVISED OF THE 278 | POSSIBILITY OF SUCH DAMAGES. 279 | 280 | END OF TERMS AND CONDITIONS 281 | 282 | How to Apply These Terms to Your New Programs 283 | 284 | If you develop a new program, and you want it to be of the greatest 285 | possible use to the public, the best way to achieve this is to make it 286 | free software which everyone can redistribute and change under these terms. 287 | 288 | To do so, attach the following notices to the program. It is safest 289 | to attach them to the start of each source file to most effectively 290 | convey the exclusion of warranty; and each file should have at least 291 | the "copyright" line and a pointer to where the full notice is found. 292 | 293 | {description} 294 | Copyright (C) {year} {fullname} 295 | 296 | This program is free software; you can redistribute it and/or modify 297 | it under the terms of the GNU General Public License as published by 298 | the Free Software Foundation; either version 2 of the License, or 299 | (at your option) any later version. 300 | 301 | This program is distributed in the hope that it will be useful, 302 | but WITHOUT ANY WARRANTY; without even the implied warranty of 303 | MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE. See the 304 | GNU General Public License for more details. 305 | 306 | You should have received a copy of the GNU General Public License along 307 | with this program; if not, write to the Free Software Foundation, Inc., 308 | 51 Franklin Street, Fifth Floor, Boston, MA 02110-1301 USA. 309 | 310 | Also add information on how to contact you by electronic and paper mail. 311 | 312 | If the program is interactive, make it output a short notice like this 313 | when it starts in an interactive mode: 314 | 315 | Gnomovision version 69, Copyright (C) year name of author 316 | Gnomovision comes with ABSOLUTELY NO WARRANTY; for details type `show w'. 317 | This is free software, and you are welcome to redistribute it 318 | under certain conditions; type `show c' for details. 319 | 320 | The hypothetical commands `show w' and `show c' should show the appropriate 321 | parts of the General Public License. Of course, the commands you use may 322 | be called something other than `show w' and `show c'; they could even be 323 | mouse-clicks or menu items--whatever suits your program. 324 | 325 | You should also get your employer (if you work as a programmer) or your 326 | school, if any, to sign a "copyright disclaimer" for the program, if 327 | necessary. Here is a sample; alter the names: 328 | 329 | Yoyodyne, Inc., hereby disclaims all copyright interest in the program 330 | `Gnomovision' (which makes passes at compilers) written by James Hacker. 331 | 332 | {signature of Ty Coon}, 1 April 1989 333 | Ty Coon, President of Vice 334 | 335 | This General Public License does not permit incorporating your program into 336 | proprietary programs. If your program is a subroutine library, you may 337 | consider it more useful to permit linking proprietary applications with the 338 | library. If this is what you want to do, use the GNU Lesser General 339 | Public License instead of this License. 340 | -------------------------------------------------------------------------------- /robocopy-backup/robocopy-backup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | ROBOCOPY-BACKUP Version: 1.2-BETA1 4 | 5 | This software is used for creating mirror copies/backups using Robocopy 6 | This code is based on ntfs-hardlink-backup.ps1 from https://github.com/individual-it/ntfs-hardlink-backup 7 | INSTALLATION: 8 | 1. Download and place robocopy-backup.ps1 into a folder of your choice 9 | 2. Navigate with Explorer to the .\bat folder 10 | 3. Right Click on the robocopy-backup.ps1 file and select "Properties" 11 | 6. If you see in the bottom something like "Security: This file came from an other computer ..." Click on "Unblock" 12 | 7. Start powershell from windows start menu (you need Windows 7 or Win Server for that, on XP you would need to install PowerShell 2 first) 13 | 8. Allow local non-signed scripts to run by typing "Set-ExecutionPolicy RemoteSigned" 14 | 9. Run robocopy-backup.ps1 with full path 15 | .SYNOPSIS 16 | c:\full\path\robocopy-backup.ps1 17 | .PARAMETER iniFile 18 | Path to an optional INI file that contains any of the parameters. 19 | .PARAMETER backupSources 20 | Source path of the backup. Can be a list separated by comma. 21 | .PARAMETER backupDestination 22 | Path where the data should go to. Can be a list separated by comma. 23 | The first destination that exists and, if localSubnetOnly is on, is in the local subnet, will be used. 24 | The backup is only ever really done to 1 destination 25 | .PARAMETER subst 26 | Drive letter to substitute (subst) for the path specified in backupDestination. 27 | Often useful if a NAS or other device is a problem when accessed directly by UNC path. 28 | Sometimes if a drive letter is substituted for the UNC path then things work. 29 | .PARAMETER logFilesToKeep 30 | How many log files should be kept. All older log files will be deleted. Default=50 31 | .PARAMETER localSubnetOnly 32 | Switch on to only run the backup when the destination is a local disk or a server in the same subnet. 33 | This is useful for scheduled network backups that should only run when the laptop is on the home office network. 34 | .PARAMETER localSubnetMask 35 | The IPv4 netmask that covers all the networks that should be considered local to the backup destination IPv4 address. 36 | Format like 255.255.255.0 (24 bits set) 255.255.240.0 (20 bits set) 255.255.0.0 (16 bits set) 37 | Or specify a CIDR prefix size (0 to 32) 38 | Use this in an office with multiple subnets that can all be covered (summarised) by a single netmask. 39 | Without this parameter the default is to use the subnet mask of the local machine interface(s), if localSubnetOnly is on. 40 | .PARAMETER emailTo 41 | Address to be notified about success and problems. If not given no Emails will be sent. 42 | .PARAMETER emailFrom 43 | Address the notification email is sent from. If not given no Emails will be sent. 44 | .PARAMETER SMTPServer 45 | Domainname of the SMTP Server. If not given no Emails will be sent. 46 | .PARAMETER SMTPUser 47 | Username if the SMTP Server needs authentication. 48 | .PARAMETER SMTPPassword 49 | Password if the SMTP Server needs authentication. 50 | .PARAMETER SMTPTimeout 51 | Timeout in ms for the Email to be sent. Default 60000. 52 | .PARAMETER NoSMTPOverSSL 53 | Switch off the use of SSL to send Emails. 54 | .PARAMETER NoShadowCopy 55 | Switch off the use of Shadow Copies. Can be useful if you have no permissions to create Shadow Copies. 56 | .PARAMETER ShadowCopySymLinkDir 57 | When using a shadow copy, this script needs to make a symlink to the internal shadow copy location. 58 | Robocopy cannot directly use the internal shadow copy location. It will use this symlink to read the 59 | files in the shadow copy. Specify some temporary place to put this link. Default is C:\ShadowCopySymLink 60 | If you are running multiple different Robocopy Backup jobs using Shadow Copy on one machine, 61 | then be sure to specify a different ShadowCopySymLinkDir for each job. 62 | .PARAMETER SMTPPort 63 | Port of the SMTP Server. Default=587 64 | .PARAMETER jobName 65 | This is added in to the auto-generated email subject "Robocopy mirror of: hostname jobName by: username" and used in the status file. 66 | Using the 'emailJobName' parameter is deprecated 67 | .PARAMETER emailSubject 68 | Subject for the notification Email. This overrides the auto-generated email subject and jobName. 69 | .PARAMETER emailSendRetries 70 | How many times should we try to resend the Email. Default = 100 71 | .PARAMETER msToPauseBetweenEmailSendRetries 72 | Time in ms to wait between the resending of the Email. Default = 60000 73 | .PARAMETER LogFile 74 | Path and filename for the logfile. If just a path is given, then "yyyy-mm-dd hh-mm-ss.log" is written to that folder. 75 | Default is to write "yyyy-mm-dd hh-mm-ss.log" in the backup destination folder. 76 | .PARAMETER statusFile 77 | Path and filename for the status File. If just a path is given, then "status.xml" is written to that folder. 78 | Default is to write "status.xml" in the logfile destination folder. 79 | .PARAMETER StepTiming 80 | Switch on display of the time at each step of the job. 81 | .PARAMETER preExecutionCommand 82 | Command to run before the start of the backup. 83 | Note: Only a single command with parameters is supported. If you need to do multiple commands (separated by "&", "&&", "||"...) 84 | then put them in a batch file and call the batch file from here. 85 | .PARAMETER preExecutionDelay 86 | Time in milliseconds to pause between running the preExecutionCommand and the start of the backup. Default = 0 87 | .PARAMETER postExecutionCommand 88 | Command to run after the backup is done. 89 | Note: Only a single command with parameters is supported. If you need to do multiple commands (separated by "&", "&&", "||"...) 90 | then put them in a batch file and call the batch file from here. 91 | .PARAMETER version 92 | print the version information and exit. 93 | .EXAMPLE 94 | PS D:\> d:\scripts\robocopy-backup.ps1 -backupSources D:\backup_source1 -backupDestination E:\backup_dest -emailTo "me@example.org" -emailFrom "backup@example.org" -SMTPServer example.org -SMTPUser "backup@example.org" -SMTPPassword "secr4et" 95 | Simple backup that will create a mirror of the D:\backup_source1 folder tree to a matching tree E:\backup_dest\backup_source1 96 | .EXAMPLE 97 | PS D:\> d:\scripts\robocopy-backup.ps1 -backupSources "D:\backup_source1","C:\backup_source2" -backupDestination E:\backup_dest -emailTo "me@example.org" -emailFrom "backup@example.org" -SMTPServer example.org -SMTPUser "backup@example.org" -SMTPPassword "secr4et" 98 | Backup with more than one source that will create a mirror of the D:\backup_source1 folder tree to a matching tree E:\backup_dest\backup_source1 and the C:\backup_source2 folder tree to a matching tree E:\backup_dest\backup_source2 99 | .NOTES 100 | Author: Phil Davis *INFN* 101 | #> 102 | 103 | [CmdletBinding()] 104 | Param( 105 | [Parameter(Mandatory=$False)] 106 | [String]$iniFile, 107 | [Parameter(Mandatory=$False)] 108 | [String[]]$backupSources, 109 | [Parameter(Mandatory=$False)] 110 | [String[]]$backupDestination, 111 | [Parameter(Mandatory=$False)] 112 | [String]$subst, 113 | [Parameter(Mandatory=$False)] 114 | [Int32]$logFilesToKeep, 115 | [Parameter(Mandatory=$False)] 116 | [string]$emailTo="", 117 | [Parameter(Mandatory=$False)] 118 | [string]$emailFrom="", 119 | [Parameter(Mandatory=$False)] 120 | [string]$SMTPServer="", 121 | [Parameter(Mandatory=$False)] 122 | [string]$SMTPUser="", 123 | [Parameter(Mandatory=$False)] 124 | [string]$SMTPPassword="", 125 | [Parameter(Mandatory=$False)] 126 | [switch]$NoSMTPOverSSL=$False, 127 | [Parameter(Mandatory=$False)] 128 | [switch]$NoShadowCopy=$False, 129 | [Parameter(Mandatory=$False)] 130 | [string]$ShadowCopySymLinkDir="", 131 | [Parameter(Mandatory=$False)] 132 | [Int32]$SMTPPort, 133 | [Parameter(Mandatory=$False)] 134 | [Int32]$SMTPTimeout, 135 | [Parameter(Mandatory=$False)] 136 | [Int32]$emailSendRetries, 137 | [Parameter(Mandatory=$False)] 138 | [Int32]$msToPauseBetweenEmailSendRetries, 139 | [Parameter(Mandatory=$False)] 140 | [switch]$localSubnetOnly, 141 | [Parameter(Mandatory=$False)] 142 | [string]$localSubnetMask, 143 | [Parameter(Mandatory=$False)] 144 | [string]$emailSubject="", 145 | [Parameter(Mandatory=$False)] 146 | [string]$jobName="", 147 | [Parameter(Mandatory=$False)] 148 | [string]$emailJobName="", 149 | [Parameter(Mandatory=$False)] 150 | [string]$LogFile="", 151 | [Parameter(Mandatory=$False)] 152 | [string]$statusFile="", 153 | [Parameter(Mandatory=$False)] 154 | [switch]$StepTiming=$False, 155 | [Parameter(Mandatory=$False)] 156 | [string]$preExecutionCommand="", 157 | [Parameter(Mandatory=$False)] 158 | [Int32]$preExecutionDelay, 159 | [Parameter(Mandatory=$False)] 160 | [string]$postExecutionCommand="", 161 | [Parameter(Mandatory=$False)] 162 | [switch]$version=$False 163 | ) 164 | 165 | #The path and filename of the script it self 166 | $script_path = Split-Path -parent $MyInvocation.MyCommand.Definition 167 | 168 | Function Get-IniContent 169 | { 170 | <# 171 | .Synopsis 172 | Gets the content of an INI file 173 | 174 | .Description 175 | Gets the content of an INI file and returns it as a hashtable 176 | 177 | .Notes 178 | Author : Oliver Lipkau 179 | Blog : http://oliver.lipkau.net/blog/ 180 | Date : 2014/06/23 181 | Version : 1.1 182 | 183 | #Requires -Version 2.0 184 | 185 | .Inputs 186 | System.String 187 | 188 | .Outputs 189 | System.Collections.Hashtable 190 | 191 | .Parameter FilePath 192 | Specifies the path to the input file. 193 | 194 | .Example 195 | $FileContent = Get-IniContent "C:\myinifile.ini" 196 | ----------- 197 | Description 198 | Saves the content of the c:\myinifile.ini in a hashtable called $FileContent 199 | 200 | .Example 201 | $inifilepath | $FileContent = Get-IniContent 202 | ----------- 203 | Description 204 | Gets the content of the ini file passed through the pipe into a hashtable called $FileContent 205 | 206 | .Example 207 | C:\PS>$FileContent = Get-IniContent "c:\settings.ini" 208 | C:\PS>$FileContent["Section"]["Key"] 209 | ----------- 210 | Description 211 | Returns the key "Key" of the section "Section" from the C:\settings.ini file 212 | 213 | .Link 214 | Out-IniFile 215 | #> 216 | 217 | [CmdletBinding()] 218 | Param( 219 | [ValidateNotNullOrEmpty()] 220 | [ValidateScript({(Test-Path $_) -and ((Get-Item $_).Extension -eq ".ini")})] 221 | [Parameter(ValueFromPipeline=$True,Mandatory=$True)] 222 | [string]$FilePath 223 | ) 224 | 225 | Begin 226 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 227 | 228 | Process 229 | { 230 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing file: $Filepath" 231 | 232 | #changed from HashTable to OrderedDictionary to keep the sections in the order they were added - Artur Neumann 233 | $ini = New-Object System.Collections.Specialized.OrderedDictionary 234 | switch -regex -file $FilePath 235 | { 236 | "^\[(.+)\]$" # Section 237 | { 238 | $section = $matches[1] 239 | # Added ToLower line to make INI file case-insensitive - Phil Davis 240 | $section = $section.ToLower() 241 | $ini[$section] = @{} 242 | $CommentCount = 0 243 | } 244 | "^(;.*)$" # Comment 245 | { 246 | if (!($section)) 247 | { 248 | $section = "No-Section" 249 | $ini[$section] = @{} 250 | } 251 | $value = $matches[1] 252 | $CommentCount = $CommentCount + 1 253 | $name = "Comment" + $CommentCount 254 | $ini[$section][$name] = $value 255 | } 256 | "(.+?)\s*=\s*(.*)" # Key 257 | { 258 | if (!($section)) 259 | { 260 | $section = "No-Section" 261 | $ini[$section] = @{} 262 | } 263 | $name,$value = $matches[1..2] 264 | # Added ToLower line to make INI file case-insensitive - Phil Davis 265 | $name = $name.ToLower() 266 | $ini[$section][$name] = $value 267 | } 268 | } 269 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath" 270 | Return $ini 271 | } 272 | 273 | End 274 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 275 | } 276 | 277 | Function Get-IniParameter 278 | { 279 | # Note: iniFileContent dictionary is not passed in each time. 280 | # Just use the global value to reference that. 281 | [CmdletBinding()] 282 | Param( 283 | [ValidateNotNullOrEmpty()] 284 | [Parameter(Mandatory=$True)] 285 | [string]$ParameterName, 286 | [ValidateNotNullOrEmpty()] 287 | [Parameter(Mandatory=$True)] 288 | [string]$FQDN, 289 | [ValidateNotNullOrEmpty()] 290 | [Parameter(Mandatory=$False)] 291 | [switch]$doNotSubstitute=$False 292 | ) 293 | 294 | Begin 295 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 296 | 297 | Process 298 | { 299 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing for IniSection: $FQDN and ParameterName: $ParameterName" 300 | 301 | # Use ToLower to make all parameter name comparisons case-insensitive 302 | $ParameterName = $ParameterName.ToLower() 303 | $ParameterValue = $Null 304 | 305 | $FQDN=$FQDN.ToLower() 306 | 307 | #search first the "common" section for the parameter, this will have the lowest priority 308 | #as the parameter can be overwritten by other sections 309 | if ($global:iniFileContent.Contains("common")) { 310 | if (-not [string]::IsNullOrEmpty($global:iniFileContent["common"][$ParameterName])) { 311 | $ParameterValue = $global:iniFileContent["common"][$ParameterName] 312 | } 313 | } 314 | 315 | #search if there is a section that matches the FQDN 316 | #this is the second highest priority, as the parameter can still be overwritten by the 317 | #section that meets exactly the FQDN 318 | #If there is more than one section that matches the FQDN with the same parameter 319 | #the section furthest down in the ini file will be used 320 | foreach ($IniSection in $($global:iniFileContent.keys)){ 321 | $EscapedIniSection=$IniSection -replace "([\-\[\]\{\}\(\)\+\?\.\,\\\^\$\|\#])",'\$1' 322 | $EscapedIniSection=$IniSection -replace "\*",'.*' 323 | if ($FQDN -match "^$EscapedIniSection$") { 324 | if (-not [string]::IsNullOrEmpty($global:iniFileContent[$IniSection][$ParameterName])) { 325 | $ParameterValue = $global:iniFileContent[$IniSection][$ParameterName] 326 | } 327 | } 328 | } 329 | 330 | #see if there is section that is called exactly the same as the computer (FQDN) 331 | #this is the highest priority, so if the same parameters are used in other sections 332 | #this section will overwrite them 333 | if ($global:iniFileContent.Contains($FQDN)) { 334 | if (-not [string]::IsNullOrEmpty($global:iniFileContent[$FQDN][$ParameterName])) { 335 | $ParameterValue = $global:iniFileContent[$FQDN][$ParameterName] 336 | } 337 | } 338 | 339 | #replace all with the parameter values 340 | if ($doNotSubstitute -eq $False) { 341 | $substituteMatches=$ParameterValue | Select-String -AllMatches '<[^<]+?>' | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Value 342 | 343 | foreach ($match in $substituteMatches) { 344 | if(![string]::IsNullOrEmpty($match)) { 345 | $match=$($match.Trim()) 346 | $cleanMatch=$match.Replace("<","").Replace(">","") 347 | if ($(test-path env:$($cleanMatch))) { 348 | $substituteValue=$(get-childitem -path env:$($cleanMatch)).Value 349 | $ParameterValue =$ParameterValue.Replace($match,$substituteValue) 350 | } 351 | } 352 | } 353 | } 354 | 355 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing for IniSection: $FQDN and ParameterName: $ParameterName ParameterValue: $ParameterValue" 356 | Return $ParameterValue 357 | } 358 | 359 | End 360 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 361 | } 362 | 363 | Function Is-TrueString 364 | { 365 | # Pass in a string (or nothing) and return a boolean deciding if the string 366 | # is "1", "true", "t" (True) or otherwise it is (False) 367 | [CmdletBinding()] 368 | Param( 369 | [Parameter(Mandatory=$False)] 370 | [string]$TruthString 371 | ) 372 | 373 | Begin 374 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 375 | 376 | Process 377 | { 378 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing for TruthString: $TruthString" 379 | 380 | # Use ToLower to make comparisons case-insensitive 381 | $TruthString = $TruthString.ToLower() 382 | $ParameterValue = $Null 383 | 384 | if (($TruthString -eq "t") -or ($TruthString -eq "true") -or ($TruthString -eq "1")) { 385 | $TruthValue = $True 386 | } else { 387 | $TruthValue = $False 388 | } 389 | 390 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing for TruthString: $TruthString TruthValue: $TruthValue" 391 | Return $TruthValue 392 | } 393 | 394 | End 395 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 396 | } 397 | 398 | Function Get-Version 399 | { 400 | <# 401 | .Synopsis 402 | Gets the version of this script 403 | 404 | .Description 405 | Parses the description for a line that looks like: 406 | ROBOCOPY-BACKUP Version: 1.0.ALPHA.1 407 | and gets the version information out of it 408 | The version string must be in the .DESCRIPTION scope and must start with 409 | "ROBOCOPY-BACKUP Version: " 410 | 411 | .Outputs 412 | System.String 413 | #> 414 | 415 | #Get the help-text of my self 416 | $helpText=Get-Help $script_path/robocopy-backup.ps1 417 | 418 | #Get-Help returns a PSObjects with other PSObjects inside 419 | #So we are trying some black magic to get a string out of it and then to parse the version 420 | 421 | Foreach ($object in $helpText.psobject.properties) { 422 | #loop through all properties of the PSObject and find the description 423 | if (($object.Value) -and ($object.name -eq "description")) { 424 | #the description is a object of the class System.Management.Automation.PSNoteProperty 425 | #and inside of the properties of that are System.Management.Automation.PSPropertyInfo objects (in our case only one) 426 | #still we loop though, just in case there are more that one and see if the value (what is finally a string), does match the version string 427 | Foreach ($subObject in $object.Value[0].psobject.properties) { 428 | if ($subObject.Value -match "ROBOCOPY-BACKUP Version: (.*)") { 429 | return $matches[1] 430 | } 431 | } 432 | } 433 | } 434 | } 435 | 436 | Function Split-CommandLine 437 | { 438 | <# 439 | .Synopsis 440 | Parse command-line arguments using Win32 API CommandLineToArgvW function. 441 | 442 | .Link 443 | https://github.com/beatcracker/Powershell-Misc/blob/master/Split-CommandLine.ps1 444 | http://edgylogic.com/blog/powershell-and-external-commands-done-right/ 445 | 446 | .Description 447 | This is the Cmdlet version of the code from the article http://edgylogic.com/blog/powershell-and-external-commands-done-right. 448 | It can parse command-line arguments using Win32 API function CommandLineToArgvW . 449 | 450 | .Parameter CommandLine 451 | A string representing the command-line to parse. If not specified, the command-line of the current PowerShell host is used. 452 | #> 453 | [CmdletBinding()] 454 | Param 455 | ( 456 | [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)] 457 | [ValidateNotNullOrEmpty()] 458 | [string]$CommandLine 459 | ) 460 | 461 | Begin 462 | { 463 | $Kernel32Definition = @' 464 | [DllImport("kernel32")] 465 | public static extern IntPtr LocalFree(IntPtr hMem); 466 | '@ 467 | $Kernel32 = Add-Type -MemberDefinition $Kernel32Definition -Name 'Kernel32' -Namespace 'Win32' -PassThru 468 | 469 | $Shell32Definition = @' 470 | [DllImport("shell32.dll", SetLastError = true)] 471 | public static extern IntPtr CommandLineToArgvW( 472 | [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, 473 | out int pNumArgs); 474 | '@ 475 | $Shell32 = Add-Type -MemberDefinition $Shell32Definition -Name 'Shell32' -Namespace 'Win32' -PassThru 476 | } 477 | 478 | Process 479 | { 480 | $ParsedArgCount = 0 481 | $ParsedArgsPtr = $Shell32::CommandLineToArgvW($CommandLine, [ref]$ParsedArgCount) 482 | 483 | Try 484 | { 485 | $ParsedArgs = @(); 486 | 487 | 0..$ParsedArgCount | ForEach-Object { 488 | $ParsedArgs += [System.Runtime.InteropServices.Marshal]::PtrToStringUni( 489 | [System.Runtime.InteropServices.Marshal]::ReadIntPtr($ParsedArgsPtr, $_ * [IntPtr]::Size) 490 | ) 491 | } 492 | } 493 | Finally 494 | { 495 | $Kernel32::LocalFree($ParsedArgsPtr) | Out-Null 496 | } 497 | 498 | $ret = @() 499 | 500 | # -lt to skip the last item, which is a NULL ptr 501 | for ($i = 0; $i -lt $ParsedArgCount; $i += 1) { 502 | $ret += $ParsedArgs[$i] 503 | } 504 | 505 | return $ret 506 | } 507 | } 508 | 509 | $emailBody = "" 510 | $error_during_backup = $false 511 | $doBackup = $true 512 | $maxMsToSleepForZipCreation = 1000*60*30 513 | $msToWaitDuringZipCreation = 500 514 | $shadow_drive_letter = "" 515 | $num_shadow_copies = 0 516 | $stepTime = "" 517 | $backupMappedPath = "" 518 | $backupHostName = "" 519 | $deleteOldLogFiles = $False 520 | $FQDN = [System.Net.DNS]::GetHostByName('').HostName 521 | $userName = [Environment]::UserName 522 | $tempLogContent = "" 523 | $substDone = $False 524 | 525 | $versionString=Get-Version 526 | 527 | if ($version) { 528 | echo $versionString 529 | exit 530 | } else { 531 | $output = "ROBOCOPY-BACKUP $versionString`r`n" 532 | $emailBody = "$emailBody`r`n$output`r`n" 533 | $tempLogContent += $output 534 | echo $output 535 | } 536 | 537 | if ($iniFile) { 538 | if (Test-Path -Path $iniFile -PathType leaf) { 539 | $output = "Using ini file`r`n$iniFile`r`n" 540 | $emailBody = "$emailBody`r`n$output`r`n" 541 | $tempLogContent += $output 542 | echo $output 543 | $global:iniFileContent = Get-IniContent "${iniFile}" 544 | } else { 545 | $global:iniFileContent = New-Object System.Collections.Specialized.OrderedDictionary 546 | $output = "ERROR: Could not find ini file`r`n$iniFile`r`n" 547 | $emailBody = "$emailBody`r`n$output`r`n" 548 | $tempLogContent += $output 549 | echo $output 550 | } 551 | } else { 552 | $global:iniFileContent = New-Object System.Collections.Specialized.OrderedDictionary 553 | } 554 | 555 | $parameters_ok = $True 556 | 557 | if ([string]::IsNullOrEmpty($backupSources)) { 558 | $backupsourcelist = Get-IniParameter "backupsources" "${FQDN}" 559 | if (-not [string]::IsNullOrEmpty($backupsourcelist)) { 560 | $backupSources = $backupsourcelist.split(",") 561 | } 562 | } 563 | 564 | if ([string]::IsNullOrEmpty($backupDestination)) { 565 | $backupDestinationList = Get-IniParameter "backupdestination" "${FQDN}" 566 | 567 | if (-not [string]::IsNullOrEmpty($backupDestinationList)) { 568 | $backupDestination = $backupDestinationList.split(",") 569 | } 570 | } 571 | 572 | if ([string]::IsNullOrEmpty($subst)) { 573 | $subst = Get-IniParameter "subst" "${FQDN}" 574 | } 575 | 576 | # This is always a drive-like letter, so it looks usual in Windows to be upper-case 577 | $subst = $subst.toupper() 578 | 579 | if ($logFilesToKeep -eq 0) { 580 | $logFilesToKeep = Get-IniParameter "logfilestokeep" "${FQDN}" 581 | if ($logFilesToKeep -eq 0) { 582 | $logFilesToKeep = 50; 583 | } 584 | } 585 | 586 | if ([string]::IsNullOrEmpty($emailTo)) { 587 | $emailTo = Get-IniParameter "emailTo" "${FQDN}" 588 | } 589 | 590 | if ([string]::IsNullOrEmpty($emailFrom)) { 591 | $emailFrom = Get-IniParameter "emailFrom" "${FQDN}" 592 | } 593 | 594 | if ([string]::IsNullOrEmpty($SMTPServer)) { 595 | $SMTPServer = Get-IniParameter "SMTPServer" "${FQDN}" 596 | } 597 | 598 | if ([string]::IsNullOrEmpty($SMTPUser)) { 599 | $SMTPUser = Get-IniParameter "SMTPUser" "${FQDN}" 600 | } 601 | 602 | if ([string]::IsNullOrEmpty($SMTPPassword)) { 603 | $SMTPPassword = Get-IniParameter "SMTPPassword" "${FQDN}" -doNotSubstitute 604 | } 605 | 606 | if (-not $NoSMTPOverSSL.IsPresent) { 607 | $IniFileString = Get-IniParameter "NoSMTPOverSSL" "${FQDN}" 608 | $NoSMTPOverSSL = Is-TrueString "${IniFileString}" 609 | } 610 | 611 | if (-not $NoShadowCopy.IsPresent) { 612 | $IniFileString = Get-IniParameter "NoShadowCopy" "${FQDN}" 613 | $NoShadowCopy = Is-TrueString "${IniFileString}" 614 | } 615 | 616 | if ([string]::IsNullOrEmpty($ShadowCopySymLinkDir)) { 617 | $ShadowCopySymLinkDir = Get-IniParameter "ShadowCopySymLinkDir" "${FQDN}" 618 | if ([string]::IsNullOrEmpty($ShadowCopySymLinkDir)) { 619 | $ShadowCopySymLinkDir = "C:\ShadowCopySymLink" 620 | } 621 | } 622 | 623 | if ($SMTPPort -eq 0) { 624 | $SMTPPort = Get-IniParameter "SMTPPort" "${FQDN}" 625 | if ($SMTPPort -eq 0) { 626 | $SMTPPort = 587; 627 | } 628 | } 629 | 630 | if ($SMTPTimeout -eq 0) { 631 | $SMTPTimeout = Get-IniParameter "SMTPTimeout" "${FQDN}" 632 | if ($SMTPTimeout -eq 0) { 633 | $SMTPTimeout = 60000; 634 | } 635 | } 636 | 637 | if ($emailSendRetries -eq 0) { 638 | $emailSendRetries = Get-IniParameter "emailSendRetries" "${FQDN}" 639 | if ($emailSendRetries -eq 0) { 640 | $emailSendRetries = 100; 641 | } 642 | } 643 | 644 | if ($msToPauseBetweenEmailSendRetries -eq 0) { 645 | $msToPauseBetweenEmailSendRetries = Get-IniParameter "msToPauseBetweenEmailSendRetries" "${FQDN}" 646 | if ($msToPauseBetweenEmailSendRetries -eq 0) { 647 | $msToPauseBetweenEmailSendRetries = 60000; 648 | } 649 | } 650 | 651 | if (-not $localSubnetOnly.IsPresent) { 652 | $IniFileString = Get-IniParameter "localSubnetOnly" "${FQDN}" 653 | $localSubnetOnly = Is-TrueString "${IniFileString}" 654 | } 655 | 656 | if ([string]::IsNullOrEmpty($localSubnetMask)) { 657 | $localSubnetMask = Get-IniParameter "localSubnetMask" "${FQDN}" 658 | } 659 | 660 | if (![string]::IsNullOrEmpty($localSubnetMask)) { 661 | $CIDRbitCount = 0 662 | # Check if we have an integer 663 | if ([int]::TryParse($localSubnetMask, [ref]$CIDRbitCount)) { 664 | # That is also in the range 0 to 32 665 | if (($CIDRbitCount -ge 0) -and ($CIDRbitCount -le 32)) { 666 | # And turn it into a 255.255.255.0 style string 667 | $CIDRremainder = $CIDRbitCount % 8 668 | $CIDReights = [Math]::Floor($CIDRbitCount / 8) 669 | switch ($CIDRremainder) { 670 | 0 { $CIDRbitText = "0" } 671 | 1 { $CIDRbitText = "128" } 672 | 2 { $CIDRbitText = "192" } 673 | 3 { $CIDRbitText = "224" } 674 | 4 { $CIDRbitText = "240" } 675 | 5 { $CIDRbitText = "248" } 676 | 6 { $CIDRbitText = "252" } 677 | 7 { $CIDRbitText = "254" } 678 | } 679 | switch ($CIDReights) { 680 | 0 { $localSubnetMask = $CIDRbitText + ".0.0.0" } 681 | 1 { $localSubnetMask = "255." + $CIDRbitText + ".0.0" } 682 | 2 { $localSubnetMask = "255.255." + $CIDRbitText + ".0" } 683 | 3 { $localSubnetMask = "255.255.255." + $CIDRbitText } 684 | 4 { $localSubnetMask = "255.255.255.255" } 685 | } 686 | } 687 | } 688 | $validNetMaskNumbers = '0|128|192|224|240|248|252|254|255' 689 | $netMaskRegexArray = @( 690 | "(^($validNetMaskNumbers)\.0\.0\.0$)" 691 | "(^255\.($validNetMaskNumbers)\.0\.0$)" 692 | "(^255\.255\.($validNetMaskNumbers)\.0$)" 693 | "(^255\.255\.255\.($validNetMaskNumbers)$)" 694 | ) 695 | $netMaskRegex = [string]::Join('|', $netMaskRegexArray) 696 | 697 | if (!(($localSubnetMask -Match $netMaskRegex))) { 698 | # The string is not a valid network mask. 699 | # It should be something like 255.255.255.0 700 | $output = "`nERROR: localSubnetMask $localSubnetMask is not valid`n" 701 | echo $output 702 | $emailBody = "$emailBody`r`n$output`r`n" 703 | 704 | $tempLogContent += $output 705 | 706 | $parameters_ok = $False 707 | $localSubnetMask = "" 708 | } 709 | } 710 | 711 | if ([string]::IsNullOrEmpty($emailSubject)) { 712 | $emailSubject = Get-IniParameter "emailSubject" "${FQDN}" 713 | } 714 | 715 | if ([string]::IsNullOrEmpty($emailJobName)) { 716 | $emailJobName = Get-IniParameter "emailJobName" "${FQDN}" 717 | } 718 | 719 | if (-not [string]::IsNullOrEmpty($emailJobName)) { 720 | $output = "`WARNING: using the 'emailJobName' parameter is deprecated! Please use 'jobName'`n" 721 | echo $output 722 | $emailBody = "$emailBody`r`n$output`r`n" 723 | 724 | $tempLogContent += $output 725 | } 726 | 727 | if ([string]::IsNullOrEmpty($jobName)) { 728 | $jobName = Get-IniParameter "jobName" "${FQDN}" 729 | 730 | #if there is still not $jobName set use the deprecated $emailJobName 731 | if ([string]::IsNullOrEmpty($jobName)) { 732 | $jobName=$emailJobName 733 | } 734 | } 735 | 736 | if (-not $StepTiming.IsPresent) { 737 | $IniFileString = Get-IniParameter "StepTiming" "${FQDN}" 738 | $StepTiming = Is-TrueString "${IniFileString}" 739 | } 740 | 741 | if ([string]::IsNullOrEmpty($emailSubject)) { 742 | 743 | $emailJobName = $jobName #to use here the old name makes sense because this is only for Email 744 | 745 | if (-not ([string]::IsNullOrEmpty($emailJobName))) { 746 | $emailJobName += " " 747 | } 748 | $emailSubject = "Robocopy mirror of: ${FQDN} ${emailJobName}by: ${userName}" 749 | } 750 | 751 | if ([string]::IsNullOrEmpty($preExecutionCommand)) { 752 | $preExecutionCommand = Get-IniParameter "preExecutionCommand" "${FQDN}" -doNotSubstitute 753 | } 754 | 755 | if ($preExecutionDelay -eq 0) { 756 | $preExecutionDelay = Get-IniParameter "preExecutionDelay" "${FQDN}" 757 | if ($preExecutionDelay -eq 0) { 758 | # Looks dumb, but left here if you want to change the default from zero. 759 | $preExecutionDelay = 0; 760 | } 761 | } 762 | 763 | if ([string]::IsNullOrEmpty($postExecutionCommand)) { 764 | $postExecutionCommand = Get-IniParameter "postExecutionCommand" "${FQDN}" -doNotSubstitute 765 | } 766 | 767 | $dateTime = get-date -f "yyyy-MM-dd HH-mm-ss" 768 | 769 | if ([string]::IsNullOrEmpty($backupDestination)) { 770 | # No backup destination on command line or in INI file 771 | # backup destination is mandatory, so flag the problem. 772 | $output = "`nERROR: No backup destination specified`n" 773 | echo $output 774 | $emailBody = "$emailBody`r`n$output`r`n" 775 | 776 | $tempLogContent += $output 777 | 778 | $parameters_ok = $False 779 | } else { 780 | foreach ($possibleBackupDestination in $backupDestination) { 781 | # Initialize vars used in this loop to ensure they do not end up with values from previous loop iterations. 782 | $backupDestinationTop = "" 783 | $backupMappedPath = "" 784 | $backupHostName = "" 785 | 786 | # If the user wants to substitute a drive letter for the backup destination, do that now. 787 | # Then following code can process the resulting "subst" in the same way as if the user had done it externally. 788 | if (-not ([string]::IsNullOrEmpty($subst))) { 789 | if ($subst -match "^[A-Z]:?$") { #TODO add check if we try to subst a not UNC path 790 | $substDrive = $subst.Substring(0,1) + ":" 791 | # Delete any previous or externally-defined subst-ed drive on this letter. 792 | # Send the output to null, as usually the first attempted delete will give an error, and we do not care. 793 | $substDone = $False 794 | subst "$substDrive" /d | Out-Null 795 | try { 796 | if (!(Test-Path -Path $possibleBackupDestination)) { 797 | New-Item $possibleBackupDestination -type directory -ea stop | Out-Null 798 | } 799 | subst "$substDrive" $possibleBackupDestination 800 | $possibleBackupDestination = $substDrive 801 | $substDone = $True 802 | } 803 | catch { 804 | $output = "`nWARNING: Destination $possibleBackupDestination was not found and could not be created. $_`n" 805 | echo $output 806 | $destWarningText = "$destWarningText`r`n$output`r`n" 807 | 808 | $tempLogContent += $output 809 | } 810 | 811 | } else { 812 | $output = "`nERROR: subst parameter $subst is invalid`n" 813 | echo $output 814 | $emailBody = "$emailBody`r`n$output`r`n" 815 | 816 | $tempLogContent += $output 817 | 818 | # Flag that there is a problem, but let following code process and report any other problems before bailing out. 819 | $parameters_ok = $False 820 | } 821 | } 822 | 823 | # Process the backup destination to find out where it might be 824 | $backupDestinationArray = $possibleBackupDestination.split("\") 825 | 826 | if (($backupDestinationArray[0] -eq "") -and ($backupDestinationArray[1] -eq "")) { 827 | # The destination is a UNC path (file share) 828 | $backupDestinationTop = "\\" + $backupDestinationArray[2] + "\" + $backupDestinationArray[3] + "\" 829 | $backupMappedPath = $backupDestinationTop 830 | $backupHostName = $backupDestinationArray[2] 831 | } else { 832 | if (-not ($possibleBackupDestination -match ":")) { 833 | # No drive letter specified. This could be an attempt at a relative path, so first resolve it to the full path. 834 | # This allows us to use split-path -Qualifier below to get the actual drive letter 835 | $possibleBackupDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($possibleBackupDestination) 836 | } 837 | $backupDestinationDrive = split-path $possibleBackupDestination -Qualifier 838 | # toupper the backupDestinationDrive string to help findstr below match the upper-case output of subst. 839 | # Also seems a reasonable thing to do in Windows, since drive letters are usually displayed in upper-case. 840 | $backupDestinationDrive = $backupDestinationDrive.toupper() 841 | $backupDestinationTop = $backupDestinationDrive + "\" 842 | # See if the disk letter is mapped to a file share somewhere. 843 | $backupDriveObject = Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceID='$backupDestinationDrive'" 844 | $backupMappedPath = $backupDriveObject.ProviderName 845 | if ($backupMappedPath) { 846 | $backupPathArray = $backupMappedPath.split("\") 847 | if (($backupPathArray[0] -eq "") -and ($backupPathArray[1] -eq "")) { 848 | # The underlying destination is a UNC path (file share) 849 | $backupHostName = $backupPathArray[2] 850 | } 851 | } else { 852 | # Maybe the user did a "subst" command. Check for that. 853 | $substText = (Subst) | findstr "$backupDestinationDrive\\" 854 | # Looks like one of: 855 | # R:\: => UNC\hostname.myoffice.company.org\sharename 856 | # R:\: => C:\some\folder\path 857 | # If a subst exists, it should always split into 3 space-separated parts 858 | $parts = $substText -Split " " 859 | if (($parts[0]) -and ($parts[1]) -and ($parts[2])) { 860 | $backupMappedPath = $parts[2] 861 | if ($backupMappedPath -match "^UNC\\") { 862 | $host_FQDN = $backupMappedPath.split("\")[1] 863 | $backupMappedPath = "\" + $backupMappedPath.Substring(3) 864 | if ($host_FQDN) { 865 | $backupHostName = $host_FQDN 866 | } 867 | } 868 | } 869 | } 870 | } 871 | 872 | if ($backupMappedPath) { 873 | $backupMappedString = " (" + $backupMappedPath + ")" 874 | } else { 875 | $backupMappedString = "" 876 | } 877 | 878 | if (($localSubnetOnly -eq $True) -and ($backupHostName)) { 879 | # Check that the name is in the same subnet as us. 880 | # Note: This also works if the user gives a real IPv4 like "\\10.20.30.40\backupshare" 881 | # $backupHostName would be 10.20.30.40 in that case. 882 | # TODO: Handle IPv6 addresses also some day. 883 | $doBackup = $false 884 | try { 885 | $destinationIpAddresses = [System.Net.Dns]::GetHostAddresses($backupHostName) 886 | [IPAddress]$destinationIp = $destinationIpAddresses[0] 887 | 888 | $localAdapters = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"') 889 | 890 | foreach ($adapter in $localAdapters) { 891 | # Belts and braces here - we have seen some systems that returned unusual adapters that had IPaddress 0.0.0.0 and no IPsubnet 892 | # We want to ignore that sort of rubbish - the mask comparisons do not work. 893 | if ($adapter.IPAddress[0]) { 894 | [IPAddress]$IPv4Address = $adapter.IPAddress[0] 895 | if ($adapter.IPSubnet[0]) { 896 | if ([string]::IsNullOrEmpty($localSubnetMask)) { 897 | [IPAddress]$mask = $adapter.IPSubnet[0] 898 | } else { 899 | [IPAddress]$mask = $localSubnetMask 900 | } 901 | 902 | if (($IPv4address.address -band $mask.address) -eq ($destinationIp.address -band $mask.address)) { 903 | $doBackup = $true 904 | } 905 | } 906 | } 907 | } 908 | } 909 | catch { 910 | $output = "WARNING: Could not get IP address for destination $possibleBackupDestination mapped to $backupMappedPath" 911 | $destWarningText = "$destWarningText`r`n$output`r`n$_" 912 | $error_during_backup = $true 913 | echo $output $_ 914 | } 915 | } 916 | 917 | if (($parameters_ok -eq $True) -and ($doBackup -eq $True) -and (test-path $backupDestinationTop)) { 918 | $selectedBackupDestination = $possibleBackupDestination 919 | break 920 | } 921 | } 922 | } 923 | 924 | if ([string]::IsNullOrEmpty($LogFile)) { 925 | $LogFile = Get-IniParameter "LogFile" "${FQDN}" 926 | } 927 | 928 | if ([string]::IsNullOrEmpty($LogFile)) { 929 | # No log file specified from command line - put one in the backup destination with date-time stamp. 930 | $logFileDestination = $selectedBackupDestination 931 | if ($logFileDestination) { 932 | $LogFile = "$logFileDestination\$dateTime.log" 933 | } else { 934 | # This can happen if both the logfile and backup destination parameters were not in the INI file and not on the command line. 935 | # In this case no log file is made. But we do proceed so there will be an email body and the receiver can find out what is wrong. 936 | $LogFile = "" 937 | } 938 | $deleteOldLogFiles = $True 939 | } else { 940 | if (Test-Path -Path $LogFile -pathType container) { 941 | # The log file parameter points to a folder, so generate log file names in that folder. 942 | $logFileDestination = $LogFile 943 | $LogFile = "$logFileDestination\$dateTime.log" 944 | $deleteOldLogFiles = $True 945 | } else { 946 | # The log file name has been fully specified - just calculate the parent folder. 947 | $logFileDestination = Split-Path -parent $LogFile 948 | } 949 | } 950 | 951 | try 952 | { 953 | New-Item "$LogFile" -type file -force -erroraction stop | Out-Null 954 | } 955 | catch 956 | { 957 | $output = "ERROR: Could not create new log file`r`n$_`r`n" 958 | $emailBody = "$emailBody`r`n$output`r`n" 959 | echo $output 960 | $LogFile="" 961 | $error_during_backup = $True 962 | $deleteOldLogFiles = $False 963 | } 964 | 965 | #write the logs from the time we hadn't a logfile into the file 966 | if ($LogFile) { 967 | $tempLogContent | Out-File "$LogFile" -encoding ASCII -append 968 | } 969 | 970 | if ([string]::IsNullOrEmpty($statusFile)) { 971 | $statusFile = Get-IniParameter "statusFile" "${FQDN}" 972 | } 973 | 974 | if ([string]::IsNullOrEmpty($statusFile)) { 975 | #no status File is specified, use the LogFile Folder as folder and "status.xml" as filename 976 | $statusFile = "$logFileDestination\status.xml" 977 | } else { 978 | if (-not [System.IO.Path]::IsPathRooted($statusFile)) { 979 | #not a full path is given, so use the logfile Folder as basis 980 | $statusFile = "$logFileDestination\$statusFile" 981 | } 982 | if (Test-Path -Path $statusFile -pathType container) { 983 | # The status file parameter points to a folder, so create a "status.xml" in that folder. 984 | $statusFile = "$statusFile\status.xml" 985 | } 986 | } 987 | 988 | if ([string]::IsNullOrEmpty($backupSources)) { 989 | # No backup sources on command line, in host-specific or common section of ini file 990 | # backup sources are mandatory, so flag the problem. 991 | $output = "`nERROR: No backup source(s) specified`n" 992 | echo $output 993 | $emailBody = "$emailBody`r`n$output`r`n" 994 | if ($LogFile) { 995 | $output | Out-File "$LogFile" -encoding ASCII -append 996 | } 997 | $parameters_ok = $False 998 | } 999 | 1000 | # Report to the log file the IP Addresses that this system has. 1001 | # If there is no log file then echo the details. 1002 | # This is useful to be able to work out what might have gone wrong with a backup. 1003 | $localAdapters = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"') 1004 | $output = "Network addresses:`r`n" 1005 | if ($LogFile) { 1006 | $output | Out-File "$LogFile" -encoding ASCII -append 1007 | } else { 1008 | echo $output 1009 | } 1010 | 1011 | foreach ($adapter in $localAdapters) { 1012 | $output = $adapter.IPAddress + " " + $adapter.DefaultIPGateway + " " + $adapter.Description + "`r`n" 1013 | if ($LogFile) { 1014 | $output | Out-File "$LogFile" -encoding ASCII -append 1015 | } else { 1016 | echo $output 1017 | } 1018 | } 1019 | 1020 | if (![string]::IsNullOrEmpty($preExecutionCommand)) { 1021 | # It seems that the first word of the command string (the command itself) has to be sent individually 1022 | # in the "ampersand" invoking. 1023 | # The remaining parameters (if any) need to be passed as an array. 1024 | # So we split the user-provided string into space-separated parts then specifically take out the first part. 1025 | $preExecutionArgs = Split-CommandLine $preExecutionCommand 1026 | $preExecutionCmd = $preExecutionArgs[0] 1027 | $preExecutionNumArgs = $preExecutionArgs.Length - 1 1028 | if ($preExecutionNumArgs -gt 0) { 1029 | $preExecutionArgs = $preExecutionArgs[1..$preExecutionNumArgs] 1030 | } else { 1031 | $preExecutionArgs = "" 1032 | } 1033 | 1034 | $output = "`nRunning preexecution command ($preExecutionCommand)" 1035 | echo $output 1036 | if ($LogFile) { 1037 | $output | Out-File "$LogFile" -encoding ASCII -append 1038 | } 1039 | 1040 | # echo $preExecutionCmd $preExecutionArgs 1041 | if ($preExecutionNumArgs -gt 0) { 1042 | if ($LogFile) { 1043 | & $preExecutionCmd $preExecutionArgs | Out-File "$LogFile" -encoding ASCII -append 1044 | } else { 1045 | & $preExecutionCmd $preExecutionArgs 1046 | } 1047 | } else { 1048 | if ($LogFile) { 1049 | & $preExecutionCmd | Out-File "$LogFile" -encoding ASCII -append 1050 | } else { 1051 | & $preExecutionCmd 1052 | } 1053 | } 1054 | 1055 | # If the command fails we want a message in the Email, otherwise the details will be only shown in the log file 1056 | # Make sure this if statement is directly after the command has been run. 1057 | if (!$?) { 1058 | $output = "`n`nERROR: the pre-execution-command ended with an error" 1059 | $emailBody = "$emailBody`r$output`r`n" 1060 | $error_during_backup = $True 1061 | $output += "`n" 1062 | echo $output 1063 | if ($LogFile) { 1064 | $output | Out-File "$LogFile" -encoding ASCII -append 1065 | } 1066 | } 1067 | } 1068 | 1069 | if ($preExecutionDelay -gt 0) { 1070 | echo "I'm gona be lazy now" 1071 | 1072 | Write-Host -NoNewline " 1073 | 1074 | ___ z 1075 | _/ | z 1076 | |_____|{)_ 1077 | --- ==\/\ | 1078 | [_____] __)| 1079 | | | //| | 1080 | " 1081 | $CursorTop=[Console]::CursorTop 1082 | [Console]::SetCursorPosition(18,$CursorTop-7) 1083 | for ($msSleeped=0;$msSleeped -lt $preExecutionDelay; $msSleeped+=1000){ 1084 | Start-sleep -milliseconds 1000 1085 | Write-Host -NoNewline "z " 1086 | } 1087 | [Console]::SetCursorPosition(0,$CursorTop) 1088 | Write-Host "I guess it's time to wake up.`n" 1089 | } 1090 | 1091 | # Just test for the existence of the top of the backup destination. "ln" will create any folders as needed, as long as the top exists. 1092 | if (($parameters_ok -eq $True) -and ($doBackup -eq $True) -and (test-path $backupDestinationTop)) { 1093 | foreach ($backup_source in $backupSources) 1094 | { 1095 | # Remove any "\" at the end as it is not needed 1096 | if ($backup_source.substring($backup_source.length-1,1) -eq "\") { 1097 | $backup_source=$backup_source.Substring(0,$backup_source.Length-1) 1098 | } 1099 | 1100 | if (test-path -LiteralPath $backup_source) { 1101 | $stepCounter = 1 1102 | $backupSourceArray = $backup_source.split("\") 1103 | if (($backupSourceArray[0] -eq "") -and ($backupSourceArray[1] -eq "")) { 1104 | # The source is a UNC path (file share) which has no drive letter. We cannot do volume shadowing from that. 1105 | $backup_source_drive_letter = "" 1106 | $backup_source_path = "" 1107 | } else { 1108 | if (-not ($backup_source -match ":")) { 1109 | # No drive letter specified. This could be an attempt at a relative path, so first resolve it to the full path. 1110 | # This allows us to use split-path -Qualifier below to get the actual drive letter 1111 | $backup_source = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($backup_source) 1112 | } 1113 | $backup_source_drive_letter = split-path $backup_source -Qualifier 1114 | $backup_source_path = split-path $backup_source -noQualifier 1115 | } 1116 | 1117 | #check if we try to backup a complete drive 1118 | if (($backup_source_drive_letter -ne "") -and ($backup_source_path -eq "")) { 1119 | if ($backup_source_drive_letter -match "([A-Z]):") { 1120 | $backup_source_folder = "["+$matches[1]+"]" 1121 | } 1122 | } else { 1123 | $backup_source_folder = split-path $backup_source -leaf 1124 | } 1125 | 1126 | $actualBackupDestination = "$selectedBackupDestination\$backup_source_folder" 1127 | 1128 | echo "============Creating Robocopy mirror of $backup_source============" 1129 | if ($NoShadowCopy -eq $False) { 1130 | if ($backup_source_drive_letter -ne "") { 1131 | # We can try processing a shadow copy. 1132 | if ($shadow_drive_letter -eq $backup_source_drive_letter) { 1133 | # The previous shadow copy must have succeeded because $NoShadowCopy is still false, and we are looping around with a matching shadow drive letter. 1134 | if ($StepTiming -eq $True) { 1135 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1136 | } 1137 | echo "$stepCounter. $stepTime Re-using previous Shadow Volume Copy" 1138 | $stepCounter++ 1139 | $backup_source_path = $ShadowCopySymLinkDir + $backup_source_path 1140 | } else { 1141 | if ($num_shadow_copies -gt 0) { 1142 | # Delete the previous shadow copy that was from some other drive letter 1143 | foreach ($shadowCopy in $shadowCopies) { 1144 | if ($s2.ID -eq $shadowCopy.ID) { 1145 | if ($StepTiming -eq $True) { 1146 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1147 | } 1148 | echo "$stepCounter. $stepTime Deleting previous Shadow Copy" 1149 | $stepCounter++ 1150 | try { 1151 | $shadowCopy.Delete() 1152 | } 1153 | catch { 1154 | $output = "ERROR: Could not delete Shadow Copy" 1155 | $emailBody = "$emailBody`r`n$output`r`n$_" 1156 | $error_during_backup = $true 1157 | echo $output $_ 1158 | } 1159 | $num_shadow_copies-- 1160 | echo "done`n" 1161 | break 1162 | } 1163 | } 1164 | # Delete the shadow copy sym link dir that pointed to the previous shadow copy. 1165 | If (Test-Path "$ShadowCopySymLinkDir") { 1166 | # Note: Remove-Item does not understand sym links 1167 | cmd /c rmdir $ShadowCopySymLinkDir 1168 | } 1169 | } 1170 | if ($StepTiming -eq $True) { 1171 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1172 | } 1173 | echo "$stepCounter. $stepTime Creating Shadow Volume Copy" 1174 | $stepCounter++ 1175 | try { 1176 | $s1 = (gwmi -List Win32_ShadowCopy).Create("$backup_source_drive_letter\", "ClientAccessible") 1177 | $s2 = gwmi Win32_ShadowCopy | ? { $_.ID -eq $s1.ShadowID } 1178 | 1179 | if ($s1.ReturnValue -ne 0 -OR !$s2) { 1180 | #ToDo add explanation of return codes http://msdn.microsoft.com/en-us/library/aa389391%28v=vs.85%29.aspx 1181 | throw "Shadow Copy Creation failed. Return Code: " + $s1.ReturnValue 1182 | } 1183 | 1184 | $output = "Shadow Volume ID: $($s2.ID)" 1185 | echo "$output" 1186 | if ($LogFile) { 1187 | $output | Out-File "$LogFile" -encoding ASCII -append 1188 | } 1189 | 1190 | $output = "Shadow Volume DeviceObject: $($s2.DeviceObject)" 1191 | echo "$output" 1192 | if ($LogFile) { 1193 | $output | Out-File "$LogFile" -encoding ASCII -append 1194 | } 1195 | 1196 | $output = "Shadowed Drive Letter: $backup_source_drive_letter" 1197 | echo "$output" 1198 | if ($LogFile) { 1199 | $output | Out-File "$LogFile" -encoding ASCII -append 1200 | } 1201 | 1202 | $shadowCopies = Get-WMIObject -Class Win32_ShadowCopy 1203 | 1204 | echo "done`n" 1205 | 1206 | # If the shadow copy sym link dir exists then remove it. 1207 | If (Test-Path "$ShadowCopySymLinkDir") { 1208 | # Note: Remove-Item does not understand sym links 1209 | cmd /c rmdir $ShadowCopySymLinkDir 1210 | } 1211 | 1212 | # Make a symbolic link to the shadow copy 1213 | # Note: mklink is a built-in part of cmd 1214 | $DeviceObjectFull = $s2.DeviceObject + "\" 1215 | cmd /c mklink /D $ShadowCopySymLinkDir $DeviceObjectFull 1216 | 1217 | $output = "Shadow Copy Sym Link Dir: $ShadowCopySymLinkDir" 1218 | echo "$output" 1219 | if ($LogFile) { 1220 | $output | Out-File "$LogFile" -encoding ASCII -append 1221 | } 1222 | 1223 | $backup_source_path = $ShadowCopySymLinkDir + $backup_source_path 1224 | $num_shadow_copies++ 1225 | $shadow_drive_letter = $backup_source_drive_letter 1226 | } 1227 | catch { 1228 | $output = "ERROR: Could not create Shadow Copy`r`n$_ `r`nATTENTION: Skipping creation of Shadow Volume Copy. ATTENTION: if files are changed during the backup process, they might end up being corrupted in the backup!`r`n" 1229 | $emailBody = "$emailBody`r`n$output`r`n" 1230 | $error_during_backup = $true 1231 | echo $output 1232 | if ($LogFile) { 1233 | $output | Out-File "$LogFile" -encoding ASCII -append 1234 | } 1235 | $backup_source_path = $backup_source 1236 | $NoShadowCopy = $True 1237 | } 1238 | } 1239 | } else { 1240 | # We were asked to do shadow copy but the source is a UNC path. 1241 | $output = "Skipping creation of Shadow Volume Copy because source is a UNC path `r`nATTENTION: if files are changed during the backup process, they might end up being corrupted in the backup!`n" 1242 | if ($StepTiming -eq $True) { 1243 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1244 | } 1245 | echo "$stepCounter. $stepTime $output" 1246 | if ($LogFile) { 1247 | $output | Out-File "$LogFile" -encoding ASCII -append 1248 | } 1249 | $stepCounter++ 1250 | $backup_source_path = $backup_source 1251 | } 1252 | } 1253 | else { 1254 | $output = "Skipping creation of Shadow Volume Copy `r`nATTENTION: if files are changed during the backup process, they might end up being corrupted in the backup!`n" 1255 | if ($StepTiming -eq $True) { 1256 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1257 | } 1258 | echo "$stepCounter. $stepTime $output" 1259 | if ($LogFile) { 1260 | $output | Out-File "$LogFile" -encoding ASCII -append 1261 | } 1262 | $stepCounter++ 1263 | $backup_source_path = $backup_source 1264 | } 1265 | 1266 | if ($StepTiming -eq $True) { 1267 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1268 | } 1269 | echo "$stepCounter. $stepTime Running backup" 1270 | $stepCounter++ 1271 | echo "Source: $backup_source_path" 1272 | echo "Destination: $actualBackupDestination$backupMappedString" 1273 | 1274 | $start_time = get-date -f "yyyy-MM-dd HH-mm-ss" 1275 | 1276 | echo "Robocopy mirror from $backup_source_path to $actualBackupDestination$backupMappedString" 1277 | if ($LogFile) { 1278 | "`r`nRobocopy mirror from $backup_source_path to $actualBackupDestination$backupMappedString" | Out-File "$LogFile" -encoding ASCII -append 1279 | } 1280 | 1281 | # Start with an empty array of args to pass to robocopy and build it up as we go. 1282 | $rcArgs = @( ) 1283 | # Z = restartable mode 1284 | $rcArgs += "/Z" 1285 | # MIR = mirror (copy all new/changed files from whole source tree, delete files in destination that are not in source) 1286 | $rcArgs += "/MIR" 1287 | # XJ = exclude Junction Points - sometimes there are DFS hidden folders on shares that are junction points managed by DFS, we do not want those 1288 | $rcArgs += "/XJ" 1289 | $rcArgs += $backup_source_path 1290 | $rcArgs += $actualBackupDestination 1291 | 1292 | # echo robocopy $rcArgs 1293 | if ($LogFile) { 1294 | & robocopy $rcArgs | Out-File "$LogFile" -encoding ASCII -append 1295 | } else { 1296 | & robocopy $rcArgs 1297 | } 1298 | 1299 | # Robocopy exit codes are documented at http://support.microsoft.com/kb/954404 1300 | # and here is the table of codes: 1301 | # The following table lists and describes the return codes that are used by the Robocopy utility. 1302 | # Value Description 1303 | # 0 No files were copied. No failure was encountered. No files were mismatched. The files already exist in the destination directory; therefore, the copy operation was skipped. 1304 | # 1 All files were copied successfully. 1305 | # 2 There are some additional files in the destination directory that are not present in the source directory. No files were copied. 1306 | # 3 Some files were copied. Additional files were present. No failure was encountered. 1307 | # 5 Some files were copied. Some files were mismatched. No failure was encountered. 1308 | # 6 Additional files and mismatched files exist. No files were copied and no failures were encountered. This means that the files already exist in the destination directory. 1309 | # 7 Files were copied, a file mismatch was present, and additional files were present. 1310 | # 8 Several files did not copy. 1311 | # Note Any value greater than 8 indicates that there was at least one failure during the copy operation. 1312 | 1313 | # Here are my comments and observations on these codes: 1314 | # 0 All files in the mirror were already up-to-date. Note that if only some new empty folders are created the exit code is still 0. 1315 | # 1 There were only new and/or changed files to be copied from source to destination and that worked. 1316 | # 2 There was nothing new to copy, but there were extra files in the destination that needed to be deleted, and were deleted successfully. 1317 | # 3 This =1+2 - there were new and/or changed files copied to the destination, and files deleted from the destination. It all worked and the destination is a good mirror of the source. 1318 | # 5,6,7,8 refer to mismatched files. Some research says that mismatch means a file in the source and a directory of the same name in the destination. 1319 | # I have tested that, and it works and returns 3 - the deletion of the directory from the destination (2) plus copy of the file from source to destination (1) 1320 | # I have not been able to produce exit codes 5,6,7 or 8. 1321 | # So any exit code greater than 3 will flag up as an error for now. 1322 | 1323 | $saved_lastexitcode = $LASTEXITCODE 1324 | if ($saved_lastexitcode -gt 3) { 1325 | $output = "`n`nERROR: the robocopy command ended with exit code [$saved_lastexitcode]" 1326 | $error_during_backup = $true 1327 | $robocopy_error = $true 1328 | } else { 1329 | $output = "" 1330 | $robocopy_error = $false 1331 | } 1332 | 1333 | $summary = "" 1334 | if ($LogFile) { 1335 | $backup_response = get-content "$LogFile" 1336 | foreach ( $line in $backup_response.length..1 ) { 1337 | $summary = $backup_response[$line] + "`n" + $summary 1338 | 1339 | if ($backup_response[$line] -match '.*Total\s+Copied\s+Skipped\s+Mismatch.*\s+FAILED\s+Extras.*') { 1340 | break 1341 | } 1342 | } 1343 | } 1344 | 1345 | echo "done`n" 1346 | 1347 | $summary = "`n------Summary-----`nBackup AT: $start_time FROM: $backup_source TO: $selectedBackupDestination$backupMappedString`n" + $summary 1348 | echo $summary 1349 | echo "`n" 1350 | $emailBody = $emailBody + $summary 1351 | 1352 | if ($robocopy_error) 1353 | { 1354 | $emailBody = "$emailBody`r$output`r`n" 1355 | echo $output 1356 | if ($LogFile) { 1357 | $output | Out-File "$LogFile" -encoding ASCII -append 1358 | } 1359 | } 1360 | 1361 | } else { 1362 | # The backup source does not exist - there was no point processing this source. 1363 | $output = "ERROR: Backup source does not exist - $backup_source - backup NOT done for this source`r`n" 1364 | $emailBody = "$emailBody`r`n$output`r`n" 1365 | $error_during_backup = $true 1366 | echo $output 1367 | if ($LogFile) { 1368 | $output | Out-File "$LogFile" -encoding ASCII -append 1369 | } 1370 | } 1371 | } 1372 | 1373 | if (($deleteOldLogFiles -eq $True) -and ($logFileDestination)) { 1374 | $lastLogFiles = @() 1375 | If (Test-Path $logFileDestination -pathType container) { 1376 | $oldLogItems = Get-ChildItem -Force -Path $logFileDestination | Where-Object {$_ -is [IO.FileInfo]} | Sort-Object -Property Name 1377 | 1378 | # get me the old logs if any 1379 | foreach ($item in $oldLogItems) { 1380 | if ($item.Name -match '^\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}.log$' ) { 1381 | $lastLogFiles += $item 1382 | } 1383 | } 1384 | } 1385 | 1386 | if ($StepTiming -eq $True) { 1387 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1388 | } 1389 | echo "$stepCounter. $stepTime Deleting old log files" 1390 | $stepCounter++ 1391 | 1392 | #No need to add 1 here because the new log existed already when we checked for old log files 1393 | $logFilesInDestination = $lastLogFiles.length 1394 | $summary = "`nFound $logFilesInDestination log file(s), keeping maximum of $logFilesToKeep log file(s)`n" 1395 | echo $summary 1396 | if ($LogFile) { 1397 | $summary | Out-File "$LogFile" -encoding ASCII -append 1398 | } 1399 | $emailBody = $emailBody + $summary 1400 | 1401 | $logFilesToDelete=$logFilesInDestination - $logFilesToKeep 1402 | if ($logFilesToDelete -gt 0) { 1403 | echo "Deleting $logFilesToDelete old logfile(s)" 1404 | if ($LogFile) { 1405 | "`r`nDeleting $logFilesToDelete old logfile(s)" | Out-File "$LogFile" -encoding ASCII -append 1406 | } 1407 | $logFilesDeleted = 0 1408 | while ($logFilesDeleted -lt $logFilesToDelete) { 1409 | $logFileToDelete = $logFileDestination +"\"+ $lastLogFiles[$logFilesDeleted].Name 1410 | 1411 | echo "Deleting $logFileToDelete(.zip)" 1412 | if ($LogFile) { 1413 | "`r`nDeleting $logFileToDelete(.zip)" | Out-File "$LogFile" -encoding ASCII -append 1414 | } 1415 | 1416 | If (Test-Path "$logFileToDelete") { 1417 | Remove-Item "$logFileToDelete" 1418 | } 1419 | If (Test-Path "$logFileToDelete.zip") { 1420 | Remove-Item "$logFileToDelete.zip" 1421 | } 1422 | 1423 | $logFilesDeleted++ 1424 | } 1425 | 1426 | $summary = "`nDeleted $logFilesDeleted old logfile(s)`n" 1427 | echo $summary 1428 | if ($LogFile) { 1429 | $summary | Out-File "$LogFile" -encoding ASCII -append 1430 | } 1431 | $emailBody = $emailBody + $summary 1432 | } else { 1433 | $summary = "`nNo old logfiles were deleted`n" 1434 | echo $summary 1435 | if ($LogFile) { 1436 | $summary | Out-File "$LogFile" -encoding ASCII -append 1437 | } 1438 | $emailBody = $emailBody + $summary 1439 | } 1440 | } 1441 | 1442 | # We have processed each backup source. Now cleanup any remaining shadow copy. 1443 | if ($num_shadow_copies -gt 0) { 1444 | # Delete the last shadow copy 1445 | foreach ($shadowCopy in $shadowCopies) { 1446 | if ($s2.ID -eq $shadowCopy.ID) { 1447 | if ($StepTiming -eq $True) { 1448 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1449 | } 1450 | echo "$stepCounter. $stepTime Deleting last Shadow Copy" 1451 | $stepCounter++ 1452 | try { 1453 | $shadowCopy.Delete() 1454 | } 1455 | catch { 1456 | $output = "ERROR: Could not delete Shadow Copy. " 1457 | $emailBody = "$emailBody`r`n$output`r`n$_" 1458 | $error_during_backup = $true 1459 | echo $output $_ 1460 | } 1461 | # If the shadow copy sym link dir exists then remove it. 1462 | If (Test-Path "$ShadowCopySymLinkDir") { 1463 | # Note: Remove-Item does not understand sym links 1464 | cmd /c rmdir $ShadowCopySymLinkDir 1465 | } 1466 | $num_shadow_copies-- 1467 | echo "done`n" 1468 | break 1469 | } 1470 | } 1471 | } 1472 | } else { 1473 | if ($destWarningText) { 1474 | # We might have tested multiple backup destinations and in the end not found a good destination 1475 | # Write out the messages about those checks to the email body so the recipient can see easily the process and problems that happened along the way. 1476 | $emailBody = "$emailBody`r`n$destWarningText`r`n" 1477 | } 1478 | 1479 | if ($parameters_ok -eq $True) { 1480 | if ($doBackup -eq $True) { 1481 | # The destination drive or \\server\share does not exist. 1482 | $output = "ERROR: Destination drive or share $backupDestinationTop$backupMappedString does not exist - backup NOT done`r`n" 1483 | } else { 1484 | # The backup was not done because localSubnetOnly was on, and the destination \\server\share is not in the local subnet. 1485 | $output = "ERROR: Destination share $backupDestinationTop$backupMappedString is not in a local subnet - backup NOT done`r`n" 1486 | } 1487 | } else { 1488 | # There was some error in the supplied parameters. 1489 | # The specific problem will have been mentioned in the email body/log file earlier. 1490 | # Put a general message here. 1491 | $output = "ERROR: There was a problem with the input parameters" 1492 | } 1493 | $emailBody = "$emailBody`r`n$output`r`n" 1494 | $error_during_backup = $true 1495 | echo $output 1496 | if ($LogFile) { 1497 | $output | Out-File "$LogFile" -encoding ASCII -append 1498 | } 1499 | } 1500 | 1501 | #XML Status report 1502 | echo "============Generating XML Status report============" 1503 | $xmlWriter = New-Object System.XMl.XmlTextWriter($statusFile,$Null) 1504 | $xmlWriter.Formatting = 'Indented' 1505 | $xmlWriter.Indentation = 1 1506 | $xmlWriter.IndentChar = "`t" 1507 | 1508 | $xmlWriter.WriteStartDocument() 1509 | 1510 | if ($error_during_backup) { 1511 | $backupStatus = "ERROR" 1512 | } else { 1513 | $backupStatus = "OK" 1514 | } 1515 | 1516 | #make the DateTime more general/SQL like. We have used "-" between h,m & m because we cannot have ":" in the folder/filenames 1517 | #but here we really want to have something that is more general and easier to parse 1518 | $dateTimeforXML = [DateTime]::ParseExact($dateTime, "yyyy-MM-dd HH-mm-ss", $null) 1519 | $dateTimeforXML = Get-Date -Date $dateTimeforXML -f "yyyy-MM-dd HH:mm:ss" 1520 | 1521 | $xmlWriter.WriteStartElement('ROBOCOPYBACKUP') 1522 | $xmlWriter.WriteElementString('VERSION', $versionString) 1523 | $xmlWriter.WriteElementString('STATUS', $backupStatus) 1524 | $xmlWriter.WriteElementString('JOBNAME', $jobName) 1525 | $xmlWriter.WriteElementString('LASTRUN', $dateTimeforXML) 1526 | $xmlWriter.WriteElementString('DESTINATION', "$selectedBackupDestination$backupMappedString") 1527 | 1528 | $xmlWriter.WriteEndElement() 1529 | $xmlWriter.WriteEndDocument() 1530 | $xmlWriter.Flush() 1531 | $xmlWriter.Close() 1532 | 1533 | if ($emailTo -AND $emailFrom -AND $SMTPServer) { 1534 | # Check if we can find any network adapter that has a default gateway 1535 | $localAdapters = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"') 1536 | $defaultGatewayExists = $false 1537 | 1538 | foreach ($adapter in $localAdapters) { 1539 | if ($adapter.DefaultIPGateway) { 1540 | $defaultGatewayExists = $true 1541 | } 1542 | } 1543 | 1544 | if ($defaultGatewayExists) { 1545 | echo "============Sending Email============" 1546 | $stepCounter = 1 1547 | 1548 | if ($LogFile) { 1549 | if ($StepTiming -eq $True) { 1550 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1551 | } 1552 | echo "$stepCounter. $stepTime Zipping log file" 1553 | $stepCounter++ 1554 | $zipFilePath = "$LogFile.zip" 1555 | $fileToZip = get-item $LogFile 1556 | 1557 | try 1558 | { 1559 | New-Item $zipFilePath -type file -force -erroraction stop | Out-Null 1560 | if (-not (test-path $zipFilePath)) { 1561 | set-content $zipFilePath ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18)) 1562 | } 1563 | 1564 | $ZipFile = (new-object -com shell.application).NameSpace($zipFilePath) 1565 | $zipfile.CopyHere($fileToZip.fullname) 1566 | 1567 | $timeSlept = 0 1568 | while ($zipfile.Items().Count -le 0 -AND $timeSlept -le $maxMsToSleepForZipCreation ) { 1569 | Start-sleep -milliseconds $msToWaitDuringZipCreation 1570 | $timeSlept = $timeSlept + $msToWaitDuringZipCreation 1571 | } 1572 | $attachment = New-Object System.Net.Mail.Attachment("$zipFilePath" ) 1573 | } 1574 | catch { 1575 | $error_during_backup = $True 1576 | $output = "`r`nERROR: Could not create log ZIP file. Will try to attach the unzipped log file and hope it's not to big.`r`n$_`r`n" 1577 | $emailBody = "$emailBody`r`n$output`r`n" 1578 | echo $output 1579 | $output | Out-File "$LogFile" -encoding ASCII -append 1580 | $attachment = New-Object System.Net.Mail.Attachment("$LogFile" ) 1581 | } 1582 | } 1583 | 1584 | if ($error_during_backup) { 1585 | $EmailSubject = "ERROR - $EmailSubject" 1586 | } 1587 | $SMTPMessage = New-Object System.Net.Mail.MailMessage($emailFrom,$emailTo,$emailSubject,$emailBody) 1588 | 1589 | if ($LogFile) { 1590 | $SMTPMessage.Attachments.Add($attachment) 1591 | } 1592 | $SMTPClient = New-Object Net.Mail.SmtpClient($SMTPServer, $SMTPPort) 1593 | 1594 | $SMTPClient.Timeout = $SMTPTimeout 1595 | if ($NoSMTPOverSSL -eq $False) { 1596 | $SMTPClient.EnableSsl = $True 1597 | } 1598 | 1599 | $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($SMTPUser, $SMTPPassword); 1600 | 1601 | $emailSendSucess = $False 1602 | if ($StepTiming -eq $True) { 1603 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1604 | } 1605 | echo "$stepCounter. $stepTime Sending email" 1606 | $stepCounter++ 1607 | while ($emailSendRetries -gt 0 -AND !$emailSendSucess) { 1608 | try { 1609 | $emailSendRetries-- 1610 | $SMTPClient.Send($SMTPMessage) 1611 | $emailSendSucess = $True 1612 | } catch { 1613 | if ($StepTiming -eq $True) { 1614 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1615 | } 1616 | $output = "ERROR: $stepTime Could not send Email.`r`n$_`r`n" 1617 | echo $output 1618 | if ($LogFile) { 1619 | $output | Out-File "$LogFile" -encoding ASCII -append 1620 | } 1621 | } 1622 | 1623 | if (!$emailSendSucess) { 1624 | Start-sleep -milliseconds $msToPauseBetweenEmailSendRetries 1625 | } 1626 | } 1627 | 1628 | if ($LogFile) { 1629 | $attachment.Dispose() 1630 | } 1631 | 1632 | echo "done" 1633 | } else { 1634 | if ($StepTiming -eq $True) { 1635 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1636 | } 1637 | $output = "ERROR: $stepTime No valid network connection found. Could not send Email.`r`n" 1638 | echo $output 1639 | if ($LogFile) { 1640 | $output | Out-File "$LogFile" -encoding ASCII -append 1641 | } 1642 | } 1643 | } 1644 | 1645 | if ($substDone) { 1646 | # Delete any drive letter substitution done earlier 1647 | # Note: the subst drive might have contained the log file, so we cannot delete earlier since it is needed to zip and email. 1648 | echo "`nRemoving subst of $substDrive`n" 1649 | subst "$substDrive" /D 1650 | } 1651 | 1652 | if (-not ([string]::IsNullOrEmpty($postExecutionCommand))) { 1653 | echo "`nrunning postexecution command ($postExecutionCommand)" 1654 | $postExecutionArgs = Split-CommandLine $postExecutionCommand 1655 | $postExecutionCmd = $postExecutionArgs[0] 1656 | $postExecutionNumArgs = $postExecutionArgs.Length - 1 1657 | if ($postExecutionNumArgs -gt 0) { 1658 | $postExecutionArgs = $postExecutionArgs[1..$postExecutionNumArgs] 1659 | } else { 1660 | $postExecutionArgs = "" 1661 | } 1662 | # echo $postExecutionCmd $postExecutionArgs 1663 | if ($postExecutionNumArgs -gt 0) { 1664 | & $postExecutionCmd $postExecutionArgs 1665 | } else { 1666 | & $postExecutionCmd 1667 | } 1668 | } 1669 | -------------------------------------------------------------------------------- /ntfs-hardlink-backup/ntfs-hardlink-backup.ps1: -------------------------------------------------------------------------------- 1 | <# 2 | .DESCRIPTION 3 | NTFS-HARDLINK-BACKUP Version: 2.2-BETA1 4 | 5 | This software is used for creating hard-link-backups. 6 | The real magic is done by DeLoreanCopy of ln: http://schinagl.priv.at/nt/ln/ln.html So all credit goes to Hermann Schinagl. 7 | INSTALLATION: 8 | 1. Read the documentation of "ln" http://schinagl.priv.at/nt/ln/ln.html 9 | 2. Download "ln" and unpack the file. 10 | 3. Download and place ntfs-hardlink-backup.ps1 into .\bat directory below the ln program 11 | 4. Navigate with Explorer to the .\bat folder 12 | 5. Right Click on the ntfs-hardlink-backup.ps1 file and select "Properties" 13 | 6. If you see in the bottom something like "Security: This file came from an other computer ..." Click on "Unblock" 14 | 7. Start powershell from windows start menu (you need Windows 7 or Win Server for that, on XP you would need to install PowerShell 2 first) 15 | 8. Allow local non-signed scripts to run by typing "Set-ExecutionPolicy RemoteSigned" 16 | 9. Run ntfs-hardlink-backup.ps1 with full path 17 | .SYNOPSIS 18 | c:\full\path\bat\ntfs-hardlink-backup.ps1 19 | .PARAMETER iniFile 20 | Path to an optional INI file that contains any of the parameters. 21 | .PARAMETER backupSources 22 | Source path of the backup. Can be a list separated by comma. 23 | .PARAMETER backupDestination 24 | Path where the data should go to. Can be a list separated by comma. 25 | The first destination that exists and, if localSubnetOnly is on, is in the local subnet, will be used. 26 | The backup is only ever really done to 1 destination. 27 | .PARAMETER subst 28 | Drive letter to substitute (subst) for the path specified in backupDestination. 29 | Often useful if a NAS or other device is a problem when accessed directly by UNC path. 30 | Sometimes if a drive letter is substituted for the UNC path then things work. 31 | .PARAMETER backupsToKeep 32 | How many backup copies should be kept. All older backups and their log files will be deleted. 1 means mirror. Default=50 33 | .PARAMETER backupsToKeepPerYear 34 | How many backup copies of every year should be kept. This will add to the number of backupsToKeep. Default=0 35 | .PARAMETER timeTolerance 36 | Sometimes useful to not have an exact timestamp comparison between source and dest, but kind of a fuzzy comparison, because the system time of NAS drives is not exactly synced with the host. 37 | To overcome this we use the -timeTolerance switch to specify a value in milliseconds. 38 | .PARAMETER excludeFiles 39 | Exclude files via wildcards. Can be a list separated by comma. 40 | .PARAMETER excludeDirs 41 | Exclude directories via wildcards. Can be a list separated by comma. 42 | .PARAMETER traditional 43 | Some NAS boxes only support a very outdated version of the SMB protocol. SMB is used when network drives are connected. This old version of SMB in certain situations does not support the fast enumeration methods of ln.exe, which causes ln.exe to simply do nothing. 44 | To overcome this use the -traditional switch, which forces ln.exe to enumerate files the old, but a little slower way. 45 | .PARAMETER noads 46 | The -noads option tells ln.exe not to copy Alternative Data Streams (ADS) of files and directories. 47 | This option can be useful if the destination supports NTFS, but can not deal with ADS, which happens on certain NAS drives. 48 | .PARAMETER noea 49 | The -noea option tells ln.exe not to copy EA Records of files and directories. 50 | This option can be useful if the destination supports NTFS, but can not deal with EA Records, which happens on certain NAS drives. 51 | .PARAMETER splice 52 | Splice reconnects Outer Junctions/Symlink directories in the destination to their original targets. 53 | see http://schinagl.priv.at/nt/ln/ln.html#splice 54 | .PARAMETER unroll 55 | Unroll follows Outer Junctions/Symlink Directories and rebuilds the content of Outer Junctions/Symlink Directories inside the hierarchy at the destination location. 56 | Unroll also applies to Outer Symlink Files, which means, that unroll causes the target of Outer Symlink Files to be copied to the destination location. 57 | see http://schinagl.priv.at/nt/ln/ln.html#unroll 58 | .PARAMETER backupModeACLs 59 | Using the Backup Mode ACLs aka Access Control Lists, which contain the security for Files, Folders, Junctions or SymbolicLinks, and Encrypted Files are also copied. 60 | see http://schinagl.priv.at/nt/ln/ln.html#backup 61 | .PARAMETER localSubnetOnly 62 | Switch on to only run the backup when the destination is a local disk or a server in the same subnet. 63 | This is useful for scheduled network backups that should only run when the laptop is on the home office network. 64 | .PARAMETER localSubnetMask 65 | The IPv4 netmask that covers all the networks that should be considered local to the backup destination IPv4 address. 66 | Format like 255.255.255.0 (24 bits set) 255.255.240.0 (20 bits set) 255.255.0.0 (16 bits set) 67 | Or specify a CIDR prefix size (0 to 32) 68 | Use this in an office with multiple subnets that can all be covered (summarised) by a single netmask. 69 | Without this parameter the default is to use the subnet mask of the local machine interface(s), if localSubnetOnly is on. 70 | .PARAMETER emailTo 71 | Address to be notified about success and problems. If not given no Emails will be sent. 72 | .PARAMETER emailFrom 73 | Address the notification email is sent from. If not given no Emails will be sent. 74 | .PARAMETER SMTPServer 75 | Domainname of the SMTP Server. If not given no Emails will be sent. 76 | .PARAMETER SMTPUser 77 | Username if the SMTP Server needs authentication. 78 | .PARAMETER SMTPPassword 79 | Password if the SMTP Server needs authentication. 80 | .PARAMETER SMTPTimeout 81 | Timeout in ms for the Email to be sent. Default 60000. 82 | .PARAMETER NoSMTPOverSSL 83 | Switch off the use of SSL to send Emails. 84 | .PARAMETER NoShadowCopy 85 | Switch off the use of Shadow Copies. Can be useful if you have no permissions to create Shadow Copies. 86 | .PARAMETER SMTPPort 87 | Port of the SMTP Server. Default=587 88 | .PARAMETER jobName 89 | This is added in to the auto-generated email subject "Backup of: hostname jobName by: username" and used in the status file. 90 | Using the 'emailJobName' parameter is deprecated 91 | .PARAMETER emailSubject 92 | Subject for the notification Email. This overrides the auto-generated email subject and jobName. 93 | .PARAMETER emailSendRetries 94 | How many times should we try to resend the Email. Default = 100 95 | .PARAMETER msToPauseBetweenEmailSendRetries 96 | Time in ms to wait between the resending of the Email. Default = 60000 97 | .PARAMETER LogFile 98 | Path and filename for the logfile. If just a path is given, then "yyyy-mm-dd hh-mm-ss.log" is written to that folder. 99 | Default is to write "yyyy-mm-dd hh-mm-ss.log" in the backup destination folder. 100 | .PARAMETER statusFile 101 | Path and filename for the status File. If just a path is given, then "status.xml" is written to that folder. 102 | Default is to write "status.xml" in the logfile destination folder. 103 | .PARAMETER StepTiming 104 | Switch on display of the time at each step of the job. 105 | .PARAMETER preExecutionCommand 106 | Command to run before the start of the backup. 107 | Note: Only a single command with parameters is supported. If you need to do multiple commands (separated by "&", "&&", "||"...) 108 | then put them in a batch file and call the batch file from here. 109 | .PARAMETER preExecutionDelay 110 | Time in milliseconds to pause between running the preExecutionCommand and the start of the backup. Default = 0 111 | .PARAMETER postExecutionCommand 112 | Command to run after the backup is done. 113 | Note: Only a single command with parameters is supported. If you need to do multiple commands (separated by "&", "&&", "||"...) 114 | then put them in a batch file and call the batch file from here. 115 | .PARAMETER lnPath 116 | The full path to the ln executable. e.g. c:\Tools\Backup\ln.exe 117 | .PARAMETER version 118 | print the version information and exit. 119 | .EXAMPLE 120 | PS D:\> d:\ln\bat\ntfs-hardlink-backup.ps1 -backupSources D:\backup_source1 -backupDestination E:\backup_dest -emailTo "me@example.org" -emailFrom "backup@example.org" -SMTPServer example.org -SMTPUser "backup@example.org" -SMTPPassword "secr4et" 121 | Simple backup. 122 | .EXAMPLE 123 | PS D:\> d:\ln\bat\ntfs-hardlink-backup.ps1 -backupSources "D:\backup_source1","C:\backup_source2" -backupDestination E:\backup_dest -emailTo "me@example.org" -emailFrom "backup@example.org" -SMTPServer example.org -SMTPUser "backup@example.org" -SMTPPassword "secr4et" 124 | Backup with more than one source. 125 | .NOTES 126 | Author: Artur Neumann *INFN*, Phil Davis *INFN*, Nikita Feodonit 127 | #> 128 | 129 | [CmdletBinding()] 130 | Param( 131 | [Parameter(Mandatory=$False)] 132 | [String]$iniFile, 133 | [Parameter(Mandatory=$False)] 134 | [String[]]$backupSources, 135 | [Parameter(Mandatory=$False)] 136 | [String[]]$backupDestination, 137 | [Parameter(Mandatory=$False)] 138 | [String]$subst, 139 | [Parameter(Mandatory=$False)] 140 | [Int32]$backupsToKeep, 141 | [Parameter(Mandatory=$False)] 142 | [Int32]$backupsToKeepPerYear, 143 | [Parameter(Mandatory=$False)] 144 | [string]$emailTo="", 145 | [Parameter(Mandatory=$False)] 146 | [string]$emailFrom="", 147 | [Parameter(Mandatory=$False)] 148 | [string]$SMTPServer="", 149 | [Parameter(Mandatory=$False)] 150 | [string]$SMTPUser="", 151 | [Parameter(Mandatory=$False)] 152 | [string]$SMTPPassword="", 153 | [Parameter(Mandatory=$False)] 154 | [switch]$NoSMTPOverSSL=$False, 155 | [Parameter(Mandatory=$False)] 156 | [switch]$NoShadowCopy=$False, 157 | [Parameter(Mandatory=$False)] 158 | [Int32]$SMTPPort, 159 | [Parameter(Mandatory=$False)] 160 | [Int32]$SMTPTimeout, 161 | [Parameter(Mandatory=$False)] 162 | [Int32]$emailSendRetries, 163 | [Parameter(Mandatory=$False)] 164 | [Int32]$msToPauseBetweenEmailSendRetries, 165 | [Parameter(Mandatory=$False)] 166 | [Int32]$timeTolerance, 167 | [Parameter(Mandatory=$False)] 168 | [switch]$traditional, 169 | [Parameter(Mandatory=$False)] 170 | [switch]$noads, 171 | [Parameter(Mandatory=$False)] 172 | [switch]$noea, 173 | [Parameter(Mandatory=$False)] 174 | [switch]$splice, 175 | [Parameter(Mandatory=$False)] 176 | [switch]$unroll, 177 | [Parameter(Mandatory=$False)] 178 | [switch]$backupModeACLs, 179 | [Parameter(Mandatory=$False)] 180 | [switch]$localSubnetOnly, 181 | [Parameter(Mandatory=$False)] 182 | [string]$localSubnetMask, 183 | [Parameter(Mandatory=$False)] 184 | [string]$emailSubject="", 185 | [Parameter(Mandatory=$False)] 186 | [string]$jobName="", 187 | [Parameter(Mandatory=$False)] 188 | [string]$emailJobName="", 189 | [Parameter(Mandatory=$False)] 190 | [String[]]$excludeFiles, 191 | [Parameter(Mandatory=$False)] 192 | [String[]]$excludeDirs, 193 | [Parameter(Mandatory=$False)] 194 | [string]$LogFile="", 195 | [Parameter(Mandatory=$False)] 196 | [string]$statusFile="", 197 | [Parameter(Mandatory=$False)] 198 | [switch]$StepTiming=$False, 199 | [Parameter(Mandatory=$False)] 200 | [string]$preExecutionCommand="", 201 | [Parameter(Mandatory=$False)] 202 | [Int32]$preExecutionDelay, 203 | [Parameter(Mandatory=$False)] 204 | [string]$postExecutionCommand="", 205 | [Parameter(Mandatory=$False)] 206 | [string]$lnPath="", 207 | [Parameter(Mandatory=$False)] 208 | [switch]$version=$False 209 | ) 210 | 211 | # The path and filename of the script it self 212 | $script_path = Split-Path -parent $MyInvocation.MyCommand.Definition 213 | 214 | Function Get-IniContent 215 | { 216 | <# 217 | .Synopsis 218 | Gets the content of an INI file 219 | 220 | .Description 221 | Gets the content of an INI file and returns it as a hashtable 222 | 223 | .Notes 224 | Author : Oliver Lipkau 225 | Blog : http://oliver.lipkau.net/blog/ 226 | Date : 2014/06/23 227 | Version : 1.1 228 | 229 | #Requires -Version 2.0 230 | 231 | .Inputs 232 | System.String 233 | 234 | .Outputs 235 | System.Collections.Hashtable 236 | 237 | .Parameter FilePath 238 | Specifies the path to the input file. 239 | 240 | .Example 241 | $FileContent = Get-IniContent "C:\myinifile.ini" 242 | ----------- 243 | Description 244 | Saves the content of the c:\myinifile.ini in a hashtable called $FileContent 245 | 246 | .Example 247 | $inifilepath | $FileContent = Get-IniContent 248 | ----------- 249 | Description 250 | Gets the content of the ini file passed through the pipe into a hashtable called $FileContent 251 | 252 | .Example 253 | C:\PS>$FileContent = Get-IniContent "c:\settings.ini" 254 | C:\PS>$FileContent["Section"]["Key"] 255 | ----------- 256 | Description 257 | Returns the key "Key" of the section "Section" from the C:\settings.ini file 258 | 259 | .Link 260 | Out-IniFile 261 | #> 262 | 263 | [CmdletBinding()] 264 | Param( 265 | [ValidateNotNullOrEmpty()] 266 | [ValidateScript({(Test-Path $_) -and ((Get-Item $_).Extension -eq ".ini")})] 267 | [Parameter(ValueFromPipeline=$True,Mandatory=$True)] 268 | [string]$FilePath 269 | ) 270 | 271 | Begin 272 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 273 | 274 | Process 275 | { 276 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing file: $Filepath" 277 | 278 | # Changed from HashTable to OrderedDictionary to keep the sections in the order they were added - Artur Neumann 279 | $ini = New-Object System.Collections.Specialized.OrderedDictionary 280 | switch -regex -file $FilePath 281 | { 282 | "^\[(.+)\]$" # Section 283 | { 284 | $section = $matches[1] 285 | # Added ToLower line to make INI file case-insensitive - Phil Davis 286 | $section = $section.ToLower() 287 | $ini[$section] = @{} 288 | $CommentCount = 0 289 | } 290 | "^(;.*)$" # Comment 291 | { 292 | if (!($section)) 293 | { 294 | $section = "No-Section" 295 | $ini[$section] = @{} 296 | } 297 | $value = $matches[1] 298 | $CommentCount = $CommentCount + 1 299 | $name = "Comment" + $CommentCount 300 | $ini[$section][$name] = $value 301 | } 302 | "(.+?)\s*=\s*(.*)" # Key 303 | { 304 | if (!($section)) 305 | { 306 | $section = "No-Section" 307 | $ini[$section] = @{} 308 | } 309 | $name,$value = $matches[1..2] 310 | # Added ToLower line to make INI file case-insensitive - Phil Davis 311 | $name = $name.ToLower() 312 | $ini[$section][$name] = $value 313 | } 314 | } 315 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing file: $FilePath" 316 | Return $ini 317 | } 318 | 319 | End 320 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 321 | } 322 | 323 | Function Get-IniParameter 324 | { 325 | # Note: iniFileContent dictionary is not passed in each time. 326 | # Just use the global value to reference that. 327 | [CmdletBinding()] 328 | Param( 329 | [ValidateNotNullOrEmpty()] 330 | [Parameter(Mandatory=$True)] 331 | [string]$ParameterName, 332 | [ValidateNotNullOrEmpty()] 333 | [Parameter(Mandatory=$True)] 334 | [string]$FQDN, 335 | [ValidateNotNullOrEmpty()] 336 | [Parameter(Mandatory=$False)] 337 | [switch]$doNotSubstitute=$False 338 | ) 339 | 340 | Begin 341 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 342 | 343 | Process 344 | { 345 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing for IniSection: $FQDN and ParameterName: $ParameterName" 346 | 347 | # Use ToLower to make all parameter name comparisons case-insensitive 348 | $ParameterName = $ParameterName.ToLower() 349 | $ParameterValue = $Null 350 | 351 | $FQDN=$FQDN.ToLower() 352 | 353 | # Search the "common" section first for the parameter, this will have the lowest priority 354 | # as the parameter can be overwritten by other sections 355 | if ($global:iniFileContent.Contains("common")) { 356 | if (-not [string]::IsNullOrEmpty($global:iniFileContent["common"][$ParameterName])) { 357 | $ParameterValue = $global:iniFileContent["common"][$ParameterName] 358 | } 359 | } 360 | 361 | # Check if there is a section that matches the FQDN 362 | # This is the second highest priority, as the parameter can still be overwritten by the 363 | # section that exactly matches the FQDN 364 | # If there is more than one section that matches the FQDN with the same parameter 365 | # then the section furthest down in the ini file will be used 366 | foreach ($IniSection in $($global:iniFileContent.keys)){ 367 | $EscapedIniSection=$IniSection -replace "([\-\[\]\{\}\(\)\+\?\.\,\\\^\$\|\#])",'\$1' 368 | $EscapedIniSection=$IniSection -replace "\*",'.*' 369 | if ($FQDN -match "^$EscapedIniSection$") { 370 | if (-not [string]::IsNullOrEmpty($global:iniFileContent[$IniSection][$ParameterName])) { 371 | $ParameterValue = $global:iniFileContent[$IniSection][$ParameterName] 372 | } 373 | } 374 | } 375 | 376 | # See if there is section that is named exactly the same as the computer (FQDN) 377 | # This is the highest priority, so if the same parameters are used in other sections 378 | # then this section will overwrite them 379 | if ($global:iniFileContent.Contains($FQDN)) { 380 | if (-not [string]::IsNullOrEmpty($global:iniFileContent[$FQDN][$ParameterName])) { 381 | $ParameterValue = $global:iniFileContent[$FQDN][$ParameterName] 382 | } 383 | } 384 | 385 | # Replace all with the parameter values 386 | if ($doNotSubstitute -eq $False) { 387 | $substituteMatches=$ParameterValue | Select-String -AllMatches '<[^<]+?>' | Select-Object -ExpandProperty Matches | Select-Object -ExpandProperty Value 388 | 389 | foreach ($match in $substituteMatches) { 390 | if (![string]::IsNullOrEmpty($match)) { 391 | $match=$($match.Trim()) 392 | $cleanMatch=$match.Replace("<","").Replace(">","") 393 | if ($(test-path env:$($cleanMatch))) { 394 | $substituteValue=$(get-childitem -path env:$($cleanMatch)).Value 395 | $ParameterValue =$ParameterValue.Replace($match,$substituteValue) 396 | } 397 | } 398 | } 399 | } 400 | 401 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing for IniSection: $FQDN and ParameterName: $ParameterName ParameterValue: $ParameterValue" 402 | Return $ParameterValue 403 | } 404 | 405 | End 406 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 407 | } 408 | 409 | Function Is-TrueString 410 | { 411 | # Pass in a string (or nothing) and return a boolean deciding if the string 412 | # is "1", "true", "t" (True) or otherwise it is (False) 413 | [CmdletBinding()] 414 | Param( 415 | [Parameter(Mandatory=$False)] 416 | [string]$TruthString 417 | ) 418 | 419 | Begin 420 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function started"} 421 | 422 | Process 423 | { 424 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Processing for TruthString: $TruthString" 425 | 426 | # Use ToLower to make comparisons case-insensitive 427 | $TruthString = $TruthString.ToLower() 428 | $ParameterValue = $Null 429 | 430 | if (($TruthString -eq "t") -or ($TruthString -eq "true") -or ($TruthString -eq "1")) { 431 | $TruthValue = $True 432 | } else { 433 | $TruthValue = $False 434 | } 435 | 436 | Write-Verbose "$($MyInvocation.MyCommand.Name):: Finished Processing for TruthString: $TruthString TruthValue: $TruthValue" 437 | Return $TruthValue 438 | } 439 | 440 | End 441 | {Write-Verbose "$($MyInvocation.MyCommand.Name):: Function ended"} 442 | } 443 | 444 | Function Get-Version 445 | { 446 | <# 447 | .Synopsis 448 | Gets the version of this script 449 | 450 | .Description 451 | Parses the description for a line that looks like: 452 | NTFS-HARDLINK-BACKUP Version: 2.0.ALPHA.8 453 | and gets the version information out of it 454 | The version string must be in the .DESCRIPTION scope and must start with 455 | "NTFS-HARDLINK-BACKUP Version: " 456 | 457 | .Outputs 458 | System.String 459 | #> 460 | 461 | # Get the help-text of my self 462 | $helpText=Get-Help $script_path/ntfs-hardlink-backup.ps1 463 | 464 | # Get-Help returns a PSObjects with other PSObjects inside 465 | # So we are trying some black magic to get a string out of it and then to parse the version 466 | 467 | Foreach ($object in $helpText.psobject.properties) { 468 | # Loop through all properties of the PSObject and find the description 469 | if (($object.Value) -and ($object.name -eq "description")) { 470 | # The description is an object of the class System.Management.Automation.PSNoteProperty 471 | # and inside of the properties of that are System.Management.Automation.PSPropertyInfo objects (in our case only one) 472 | # We still loop though, just in case there are more than one and see if the value (that is finally a string), does match the version string 473 | Foreach ($subObject in $object.Value[0].psobject.properties) { 474 | if ($subObject.Value -match "NTFS-HARDLINK-BACKUP Version: (.*)") { 475 | return $matches[1] 476 | } 477 | } 478 | } 479 | } 480 | } 481 | 482 | function Split-CommandLine 483 | { 484 | <# 485 | .Synopsis 486 | Parse command-line arguments using Win32 API CommandLineToArgvW function. 487 | 488 | .Link 489 | https://github.com/beatcracker/Powershell-Misc/blob/master/Split-CommandLine.ps1 490 | http://edgylogic.com/blog/powershell-and-external-commands-done-right/ 491 | 492 | .Description 493 | This is the Cmdlet version of the code from the article http://edgylogic.com/blog/powershell-and-external-commands-done-right. 494 | It can parse command-line arguments using Win32 API function CommandLineToArgvW . 495 | 496 | .Parameter CommandLine 497 | A string representing the command-line to parse. If not specified, the command-line of the current PowerShell host is used. 498 | #> 499 | [CmdletBinding()] 500 | Param 501 | ( 502 | [Parameter(Mandatory=$true, ValueFromPipeline=$true, ValueFromPipelineByPropertyName=$true, Position=0)] 503 | [ValidateNotNullOrEmpty()] 504 | [string]$CommandLine 505 | ) 506 | 507 | Begin 508 | { 509 | $Kernel32Definition = @' 510 | [DllImport("kernel32")] 511 | public static extern IntPtr LocalFree(IntPtr hMem); 512 | '@ 513 | $Kernel32 = Add-Type -MemberDefinition $Kernel32Definition -Name 'Kernel32' -Namespace 'Win32' -PassThru 514 | 515 | $Shell32Definition = @' 516 | [DllImport("shell32.dll", SetLastError = true)] 517 | public static extern IntPtr CommandLineToArgvW( 518 | [MarshalAs(UnmanagedType.LPWStr)] string lpCmdLine, 519 | out int pNumArgs); 520 | '@ 521 | $Shell32 = Add-Type -MemberDefinition $Shell32Definition -Name 'Shell32' -Namespace 'Win32' -PassThru 522 | } 523 | 524 | Process 525 | { 526 | $ParsedArgCount = 0 527 | $ParsedArgsPtr = $Shell32::CommandLineToArgvW($CommandLine, [ref]$ParsedArgCount) 528 | 529 | Try 530 | { 531 | $ParsedArgs = @(); 532 | 533 | 0..$ParsedArgCount | ForEach-Object { 534 | $ParsedArgs += [System.Runtime.InteropServices.Marshal]::PtrToStringUni( 535 | [System.Runtime.InteropServices.Marshal]::ReadIntPtr($ParsedArgsPtr, $_ * [IntPtr]::Size) 536 | ) 537 | } 538 | } 539 | Finally 540 | { 541 | $Kernel32::LocalFree($ParsedArgsPtr) | Out-Null 542 | } 543 | 544 | $ret = @() 545 | 546 | # -lt to skip the last item, which is a NULL ptr 547 | for ($i = 0; $i -lt $ParsedArgCount; $i += 1) { 548 | $ret += $ParsedArgs[$i] 549 | } 550 | 551 | return $ret 552 | } 553 | } 554 | 555 | $emailBody = "" 556 | $error_during_backup = $false 557 | $doBackup = $true 558 | $maxMsToSleepForZipCreation = 1000*60*30 559 | $msToWaitDuringZipCreation = 500 560 | $shadow_drive_letter = "" 561 | $num_shadow_copies = 0 562 | $stepTime = "" 563 | $backupMappedPath = "" 564 | $backupHostName = "" 565 | $deleteOldLogFiles = $False 566 | $FQDN = [System.Net.DNS]::GetHostByName('').HostName 567 | $userName = [Environment]::UserName 568 | $tempLogContent = "" 569 | $substDone = $False 570 | 571 | $versionString=Get-Version 572 | 573 | if ($version) { 574 | echo $versionString 575 | exit 576 | } else { 577 | $output = "NTFS-HARDLINK-BACKUP $versionString`r`n" 578 | $emailBody = "$emailBody`r`n$output`r`n" 579 | $tempLogContent += $output 580 | echo $output 581 | } 582 | 583 | if ($iniFile) { 584 | if (Test-Path -Path $iniFile -PathType leaf) { 585 | $output = "Using ini file`r`n$iniFile`r`n" 586 | $emailBody = "$emailBody`r`n$output`r`n" 587 | $tempLogContent += $output 588 | echo $output 589 | $global:iniFileContent = Get-IniContent "${iniFile}" 590 | } else { 591 | $global:iniFileContent = New-Object System.Collections.Specialized.OrderedDictionary 592 | $output = "ERROR: Could not find ini file`r`n$iniFile`r`n" 593 | $emailBody = "$emailBody`r`n$output`r`n" 594 | $tempLogContent += $output 595 | echo $output 596 | } 597 | } else { 598 | $global:iniFileContent = New-Object System.Collections.Specialized.OrderedDictionary 599 | } 600 | 601 | $parameters_ok = $True 602 | 603 | if ([string]::IsNullOrEmpty($backupSources)) { 604 | $backupsourcelist = Get-IniParameter "backupsources" "${FQDN}" 605 | if (-not [string]::IsNullOrEmpty($backupsourcelist)) { 606 | $backupSources = $backupsourcelist.split(",") 607 | } 608 | } 609 | 610 | if ([string]::IsNullOrEmpty($backupDestination)) { 611 | $backupDestinationList = Get-IniParameter "backupdestination" "${FQDN}" 612 | 613 | if (-not [string]::IsNullOrEmpty($backupDestinationList)) { 614 | $backupDestination = $backupDestinationList.split(",") 615 | } 616 | } 617 | 618 | if ([string]::IsNullOrEmpty($subst)) { 619 | $subst = Get-IniParameter "subst" "${FQDN}" 620 | } 621 | 622 | # This is always a drive-like letter, so it looks usual in Windows to be upper-case 623 | $subst = $subst.toupper() 624 | 625 | if ($backupsToKeep -eq 0) { 626 | $backupsToKeep = Get-IniParameter "backupstokeep" "${FQDN}" 627 | if ($backupsToKeep -eq 0) { 628 | $backupsToKeep = 50; 629 | } 630 | } 631 | 632 | if ($backupsToKeepPerYear -eq 0) { 633 | $backupsToKeepPerYear = Get-IniParameter "backupsToKeepPerYear" "${FQDN}" 634 | if ($backupsToKeepPerYear -eq 0) { 635 | $backupsToKeepPerYear = 0; 636 | } 637 | } 638 | 639 | if ([string]::IsNullOrEmpty($emailTo)) { 640 | $emailTo = Get-IniParameter "emailTo" "${FQDN}" 641 | } 642 | 643 | if ([string]::IsNullOrEmpty($emailFrom)) { 644 | $emailFrom = Get-IniParameter "emailFrom" "${FQDN}" 645 | } 646 | 647 | if ([string]::IsNullOrEmpty($SMTPServer)) { 648 | $SMTPServer = Get-IniParameter "SMTPServer" "${FQDN}" 649 | } 650 | 651 | if ([string]::IsNullOrEmpty($SMTPUser)) { 652 | $SMTPUser = Get-IniParameter "SMTPUser" "${FQDN}" 653 | } 654 | 655 | if ([string]::IsNullOrEmpty($SMTPPassword)) { 656 | $SMTPPassword = Get-IniParameter "SMTPPassword" "${FQDN}" -doNotSubstitute 657 | } 658 | 659 | if (-not $NoSMTPOverSSL.IsPresent) { 660 | $IniFileString = Get-IniParameter "NoSMTPOverSSL" "${FQDN}" 661 | $NoSMTPOverSSL = Is-TrueString "${IniFileString}" 662 | } 663 | 664 | if (-not $NoShadowCopy.IsPresent) { 665 | $IniFileString = Get-IniParameter "NoShadowCopy" "${FQDN}" 666 | $NoShadowCopy = Is-TrueString "${IniFileString}" 667 | } 668 | 669 | if ($SMTPPort -eq 0) { 670 | $SMTPPort = Get-IniParameter "SMTPPort" "${FQDN}" 671 | if ($SMTPPort -eq 0) { 672 | $SMTPPort = 587; 673 | } 674 | } 675 | 676 | if ($SMTPTimeout -eq 0) { 677 | $SMTPTimeout = Get-IniParameter "SMTPTimeout" "${FQDN}" 678 | if ($SMTPTimeout -eq 0) { 679 | $SMTPTimeout = 60000; 680 | } 681 | } 682 | 683 | if ($emailSendRetries -eq 0) { 684 | $emailSendRetries = Get-IniParameter "emailSendRetries" "${FQDN}" 685 | if ($emailSendRetries -eq 0) { 686 | $emailSendRetries = 100; 687 | } 688 | } 689 | 690 | if ($msToPauseBetweenEmailSendRetries -eq 0) { 691 | $msToPauseBetweenEmailSendRetries = Get-IniParameter "msToPauseBetweenEmailSendRetries" "${FQDN}" 692 | if ($msToPauseBetweenEmailSendRetries -eq 0) { 693 | $msToPauseBetweenEmailSendRetries = 60000; 694 | } 695 | } 696 | 697 | if ($timeTolerance -eq 0) { 698 | $timeTolerance = Get-IniParameter "timeTolerance" "${FQDN}" 699 | if ($timeTolerance -eq 0) { 700 | # Looks dumb, but left here if you want to change the default from zero. 701 | $timeTolerance = 0; 702 | } 703 | } 704 | 705 | if (-not $traditional.IsPresent) { 706 | $IniFileString = Get-IniParameter "traditional" "${FQDN}" 707 | $traditional = Is-TrueString "${IniFileString}" 708 | } 709 | 710 | if (-not $noads.IsPresent) { 711 | $IniFileString = Get-IniParameter "noads" "${FQDN}" 712 | $noads = Is-TrueString "${IniFileString}" 713 | } 714 | 715 | if (-not $noea.IsPresent) { 716 | $IniFileString = Get-IniParameter "noea" "${FQDN}" 717 | $noea = Is-TrueString "${IniFileString}" 718 | } 719 | 720 | if (-not $splice.IsPresent) { 721 | $IniFileString = Get-IniParameter "splice" "${FQDN}" 722 | $splice = Is-TrueString "${IniFileString}" 723 | } 724 | 725 | if (-not $unroll.IsPresent) { 726 | $IniFileString = Get-IniParameter "unroll" "${FQDN}" 727 | $unroll = Is-TrueString "${IniFileString}" 728 | } 729 | 730 | if (-not $backupModeACLs.IsPresent) { 731 | $IniFileString = Get-IniParameter "backupModeACLs" "${FQDN}" 732 | $backupModeACLs = Is-TrueString "${IniFileString}" 733 | } 734 | 735 | if (-not $localSubnetOnly.IsPresent) { 736 | $IniFileString = Get-IniParameter "localSubnetOnly" "${FQDN}" 737 | $localSubnetOnly = Is-TrueString "${IniFileString}" 738 | } 739 | 740 | if ([string]::IsNullOrEmpty($localSubnetMask)) { 741 | $localSubnetMask = Get-IniParameter "localSubnetMask" "${FQDN}" 742 | } 743 | 744 | if (![string]::IsNullOrEmpty($localSubnetMask)) { 745 | $CIDRbitCount = 0 746 | # Check if we have an integer 747 | if ([int]::TryParse($localSubnetMask, [ref]$CIDRbitCount)) { 748 | # That is also in the range 0 to 32 749 | if (($CIDRbitCount -ge 0) -and ($CIDRbitCount -le 32)) { 750 | # And turn it into a 255.255.255.0 style string 751 | $CIDRremainder = $CIDRbitCount % 8 752 | $CIDReights = [Math]::Floor($CIDRbitCount / 8) 753 | switch ($CIDRremainder) { 754 | 0 { $CIDRbitText = "0" } 755 | 1 { $CIDRbitText = "128" } 756 | 2 { $CIDRbitText = "192" } 757 | 3 { $CIDRbitText = "224" } 758 | 4 { $CIDRbitText = "240" } 759 | 5 { $CIDRbitText = "248" } 760 | 6 { $CIDRbitText = "252" } 761 | 7 { $CIDRbitText = "254" } 762 | } 763 | switch ($CIDReights) { 764 | 0 { $localSubnetMask = $CIDRbitText + ".0.0.0" } 765 | 1 { $localSubnetMask = "255." + $CIDRbitText + ".0.0" } 766 | 2 { $localSubnetMask = "255.255." + $CIDRbitText + ".0" } 767 | 3 { $localSubnetMask = "255.255.255." + $CIDRbitText } 768 | 4 { $localSubnetMask = "255.255.255.255" } 769 | } 770 | } 771 | } 772 | $validNetMaskNumbers = '0|128|192|224|240|248|252|254|255' 773 | $netMaskRegexArray = @( 774 | "(^($validNetMaskNumbers)\.0\.0\.0$)" 775 | "(^255\.($validNetMaskNumbers)\.0\.0$)" 776 | "(^255\.255\.($validNetMaskNumbers)\.0$)" 777 | "(^255\.255\.255\.($validNetMaskNumbers)$)" 778 | ) 779 | $netMaskRegex = [string]::Join('|', $netMaskRegexArray) 780 | 781 | if (!(($localSubnetMask -Match $netMaskRegex))) { 782 | # The string is not a valid network mask. 783 | # It should be something like 255.255.255.0 784 | $output = "`nERROR: localSubnetMask $localSubnetMask is not valid`n" 785 | echo $output 786 | $emailBody = "$emailBody`r`n$output`r`n" 787 | 788 | $tempLogContent += $output 789 | 790 | $parameters_ok = $False 791 | $localSubnetMask = "" 792 | } 793 | } 794 | 795 | if ([string]::IsNullOrEmpty($emailSubject)) { 796 | $emailSubject = Get-IniParameter "emailSubject" "${FQDN}" 797 | } 798 | 799 | if ([string]::IsNullOrEmpty($emailJobName)) { 800 | $emailJobName = Get-IniParameter "emailJobName" "${FQDN}" 801 | } 802 | 803 | if (-not [string]::IsNullOrEmpty($emailJobName)) { 804 | $output = "`WARNING: using the 'emailJobName' parameter is deprecated! Please use 'jobName'`n" 805 | echo $output 806 | $emailBody = "$emailBody`r`n$output`r`n" 807 | 808 | $tempLogContent += $output 809 | } 810 | 811 | if ([string]::IsNullOrEmpty($jobName)) { 812 | $jobName = Get-IniParameter "jobName" "${FQDN}" 813 | 814 | #if there is still not $jobName set use the deprecated $emailJobName 815 | if ([string]::IsNullOrEmpty($jobName)) { 816 | $jobName=$emailJobName 817 | } 818 | } 819 | 820 | if ([string]::IsNullOrEmpty($excludeFiles)) { 821 | $excludeFilesList = Get-IniParameter "excludeFiles" "${FQDN}" 822 | if (-not [string]::IsNullOrEmpty($excludeFilesList)) { 823 | $excludeFiles = $excludeFilesList.split(",") 824 | } 825 | } 826 | 827 | if ([string]::IsNullOrEmpty($excludeDirs)) { 828 | $excludeDirsList = Get-IniParameter "excludeDirs" "${FQDN}" 829 | if (-not [string]::IsNullOrEmpty($excludeDirsList)) { 830 | $excludeDirs = $excludeDirsList.split(",") 831 | } 832 | } 833 | 834 | if (-not $StepTiming.IsPresent) { 835 | $IniFileString = Get-IniParameter "StepTiming" "${FQDN}" 836 | $StepTiming = Is-TrueString "${IniFileString}" 837 | } 838 | 839 | if ([string]::IsNullOrEmpty($emailSubject)) { 840 | 841 | $emailJobName = $jobName #to use here the old name makes sense because this is only for Email 842 | 843 | if (-not ([string]::IsNullOrEmpty($emailJobName))) { 844 | $emailJobName += " " 845 | } 846 | $emailSubject = "Backup of: ${FQDN} ${emailJobName}by: ${userName}" 847 | } 848 | 849 | if ([string]::IsNullOrEmpty($preExecutionCommand)) { 850 | $preExecutionCommand = Get-IniParameter "preExecutionCommand" "${FQDN}" -doNotSubstitute 851 | } 852 | 853 | if ($preExecutionDelay -eq 0) { 854 | $preExecutionDelay = Get-IniParameter "preExecutionDelay" "${FQDN}" 855 | if ($preExecutionDelay -eq 0) { 856 | # Looks dumb, but left here if you want to change the default from zero. 857 | $preExecutionDelay = 0; 858 | } 859 | } 860 | 861 | if ([string]::IsNullOrEmpty($postExecutionCommand)) { 862 | $postExecutionCommand = Get-IniParameter "postExecutionCommand" "${FQDN}" -doNotSubstitute 863 | } 864 | 865 | if ([string]::IsNullOrEmpty($lnPath)) { 866 | $lnPath = Get-IniParameter "lnPath" "${FQDN}" 867 | } 868 | 869 | # If lnPath is not given in the ini file nor on the command line or its not there try to find it somewhere else 870 | if ([string]::IsNullOrEmpty($lnPath) -or !(Test-Path -Path $lnPath -PathType leaf) ) { 871 | if (Test-Path -Path "$script_path\ln.exe" -PathType leaf) { 872 | $lnPath="$script_path\ln.exe" 873 | } elseif (Test-Path -Path "$script_path\..\ln.exe" -PathType leaf) { 874 | $lnPath="$script_path\..\ln.exe" 875 | } else { 876 | # Last chance, look for it somewhere in the PATH Environment variable 877 | foreach ($ENVpath in $env:path.split(";")) { 878 | if (Test-Path -Path "$ENVpath\ln.exe" -PathType leaf) { 879 | $lnPath="$ENVpath\ln.exe" 880 | break; 881 | } 882 | } 883 | } 884 | } 885 | 886 | $dateTime = get-date -f "yyyy-MM-dd HH-mm-ss" 887 | 888 | if ([string]::IsNullOrEmpty($backupDestination)) { 889 | # No backup destination on command line or in INI file. 890 | # Backup destination is mandatory, so flag the problem. 891 | $output = "`nERROR: No backup destination specified`n" 892 | echo $output 893 | $emailBody = "$emailBody`r`n$output`r`n" 894 | 895 | $tempLogContent += $output 896 | 897 | $parameters_ok = $False 898 | } else { 899 | foreach ($possibleBackupDestination in $backupDestination) { 900 | # Initialize vars used in this loop to ensure they do not end up with values from previous loop iterations. 901 | $backupDestinationTop = "" 902 | $backupMappedPath = "" 903 | $backupHostName = "" 904 | 905 | # If the user wants to substitute a drive letter for the backup destination, do that now. 906 | # Then the following code can process the resulting "subst" in the same way as if the user had done it externally. 907 | if (-not ([string]::IsNullOrEmpty($subst))) { 908 | if ($subst -match "^[A-Z]:?$") { #TODO add check if we try to subst a not UNC path 909 | $substDrive = $subst.Substring(0,1) + ":" 910 | # Delete any previous or externally-defined subst-ed drive on this letter. 911 | # Send the output to null, as usually the first attempted delete will give an error, and we do not care. 912 | $substDone = $False 913 | subst "$substDrive" /d | Out-Null 914 | try { 915 | if (!(Test-Path -Path $possibleBackupDestination)) { 916 | New-Item $possibleBackupDestination -type directory -ea stop | Out-Null 917 | } 918 | subst "$substDrive" $possibleBackupDestination 919 | $possibleBackupDestination = $substDrive 920 | $substDone = $True 921 | } 922 | catch { 923 | $output = "`nWARNING: Destination $possibleBackupDestination was not found and could not be created. $_`n" 924 | echo $output 925 | $destWarningText = "$destWarningText`r`n$output`r`n" 926 | 927 | $tempLogContent += $output 928 | } 929 | 930 | } else { 931 | $output = "`nERROR: subst parameter $subst is invalid`n" 932 | echo $output 933 | $emailBody = "$emailBody`r`n$output`r`n" 934 | 935 | $tempLogContent += $output 936 | 937 | # Flag that there is a problem, but let the following code process and report any other problems before bailing out. 938 | $parameters_ok = $False 939 | } 940 | } 941 | 942 | # Process the backup destination to find out where it might be 943 | $backupDestinationArray = $possibleBackupDestination.split("\") 944 | 945 | if (($backupDestinationArray[0] -eq "") -and ($backupDestinationArray[1] -eq "")) { 946 | # The destination is a UNC path (file share) 947 | $backupDestinationTop = "\\" + $backupDestinationArray[2] + "\" + $backupDestinationArray[3] + "\" 948 | $backupMappedPath = $backupDestinationTop 949 | $backupHostName = $backupDestinationArray[2] 950 | } else { 951 | if (-not ($possibleBackupDestination -match ":")) { 952 | # No drive letter specified. This could be an attempt at a relative path, so first resolve it to the full path. 953 | # This allows us to use split-path -Qualifier below to get the actual drive letter 954 | $possibleBackupDestination = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($possibleBackupDestination) 955 | } 956 | $backupDestinationDrive = split-path $possibleBackupDestination -Qualifier 957 | # toupper the backupDestinationDrive string to help findstr below match the upper-case output of subst. 958 | # Also seems a reasonable thing to do in Windows, since drive letters are usually displayed in upper-case. 959 | $backupDestinationDrive = $backupDestinationDrive.toupper() 960 | $backupDestinationTop = $backupDestinationDrive + "\" 961 | # See if the disk letter is mapped to a file share somewhere. 962 | $backupDriveObject = Get-WmiObject -Class Win32_LogicalDisk -Filter "DeviceID='$backupDestinationDrive'" 963 | $backupMappedPath = $backupDriveObject.ProviderName 964 | if ($backupMappedPath) { 965 | $backupPathArray = $backupMappedPath.split("\") 966 | if (($backupPathArray[0] -eq "") -and ($backupPathArray[1] -eq "")) { 967 | # The underlying destination is a UNC path (file share) 968 | $backupHostName = $backupPathArray[2] 969 | } 970 | } else { 971 | # Maybe the user did a "subst" command. Check for that. 972 | $substText = (Subst) | findstr "$backupDestinationDrive\\" 973 | # Looks like one of: 974 | # R:\: => UNC\hostname.myoffice.company.org\sharename 975 | # R:\: => C:\some\folder\path 976 | # If a subst exists, it should always split into 3 space-separated parts 977 | $parts = $substText -Split " " 978 | if (($parts[0]) -and ($parts[1]) -and ($parts[2])) { 979 | $backupMappedPath = $parts[2] 980 | if ($backupMappedPath -match "^UNC\\") { 981 | $host_FQDN = $backupMappedPath.split("\")[1] 982 | $backupMappedPath = "\" + $backupMappedPath.Substring(3) 983 | if ($host_FQDN) { 984 | $backupHostName = $host_FQDN 985 | } 986 | } 987 | } 988 | } 989 | } 990 | 991 | if ($backupMappedPath) { 992 | $backupMappedString = " (" + $backupMappedPath + ")" 993 | } else { 994 | $backupMappedString = "" 995 | } 996 | 997 | if (($localSubnetOnly -eq $True) -and ($backupHostName)) { 998 | # Check that the name is in the same subnet as us. 999 | # Note: This also works if the user gives a real IPv4 like "\\10.20.30.40\backupshare" 1000 | # $backupHostName would be 10.20.30.40 in that case. 1001 | # TODO: Handle IPv6 addresses also some day. 1002 | $doBackup = $false 1003 | try { 1004 | $destinationIpAddresses = [System.Net.Dns]::GetHostAddresses($backupHostName) 1005 | [IPAddress]$destinationIp = $destinationIpAddresses[0] 1006 | 1007 | $localAdapters = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"') 1008 | 1009 | foreach ($adapter in $localAdapters) { 1010 | # Belts and braces here - we have seen some systems that returned unusual adapters that had IPaddress 0.0.0.0 and no IPsubnet 1011 | # We want to ignore that sort of rubbish - the mask comparisons do not work. 1012 | if ($adapter.IPAddress[0]) { 1013 | [IPAddress]$IPv4Address = $adapter.IPAddress[0] 1014 | if ($adapter.IPSubnet[0]) { 1015 | if ([string]::IsNullOrEmpty($localSubnetMask)) { 1016 | [IPAddress]$mask = $adapter.IPSubnet[0] 1017 | } else { 1018 | [IPAddress]$mask = $localSubnetMask 1019 | } 1020 | 1021 | if (($IPv4address.address -band $mask.address) -eq ($destinationIp.address -band $mask.address)) { 1022 | $doBackup = $true 1023 | } 1024 | } 1025 | } 1026 | } 1027 | } 1028 | catch { 1029 | $output = "WARNING: Could not get IP address for destination $possibleBackupDestination mapped to $backupMappedPath" 1030 | $destWarningText = "$destWarningText`r`n$output`r`n$_" 1031 | $error_during_backup = $true 1032 | echo $output $_ 1033 | } 1034 | } 1035 | 1036 | if (($parameters_ok -eq $True) -and ($doBackup -eq $True) -and (test-path $backupDestinationTop)) { 1037 | $selectedBackupDestination = $possibleBackupDestination 1038 | break 1039 | } 1040 | } 1041 | } 1042 | 1043 | if ([string]::IsNullOrEmpty($LogFile)) { 1044 | $LogFile = Get-IniParameter "LogFile" "${FQDN}" 1045 | } 1046 | 1047 | if ([string]::IsNullOrEmpty($LogFile)) { 1048 | # No log file specified from command line - put one in the backup destination with date-time stamp. 1049 | $logFileDestination = $selectedBackupDestination 1050 | if ($logFileDestination) { 1051 | $LogFile = "$logFileDestination\$dateTime.log" 1052 | } else { 1053 | # This can happen if both the logfile and backup destination parameters were not in the INI file and not on the command line. 1054 | # In this case no log file is made. But we do proceed so there will be an email body and the receiver can find out what is wrong. 1055 | $LogFile = "" 1056 | } 1057 | $deleteOldLogFiles = $True 1058 | } else { 1059 | if (Test-Path -Path $LogFile -pathType container) { 1060 | # The log file parameter points to a folder, so generate log file names in that folder. 1061 | $logFileDestination = $LogFile 1062 | $LogFile = "$logFileDestination\$dateTime.log" 1063 | $deleteOldLogFiles = $True 1064 | } else { 1065 | # The log file name has been fully specified - just calculate the parent folder. 1066 | $logFileDestination = Split-Path -parent $LogFile 1067 | } 1068 | } 1069 | 1070 | try 1071 | { 1072 | New-Item "$LogFile" -type file -force -erroraction stop | Out-Null 1073 | } 1074 | catch 1075 | { 1076 | $output = "ERROR: Could not create new log file`r`n$_`r`n" 1077 | $emailBody = "$emailBody`r`n$output`r`n" 1078 | echo $output 1079 | $LogFile="" 1080 | $error_during_backup = $True 1081 | $deleteOldLogFiles = $False 1082 | } 1083 | 1084 | # Write the logs from the time we hadn't a logfile into the file 1085 | if ($LogFile) { 1086 | $tempLogContent | Out-File "$LogFile" -encoding ASCII -append 1087 | } 1088 | 1089 | if ([string]::IsNullOrEmpty($statusFile)) { 1090 | $statusFile = Get-IniParameter "statusFile" "${FQDN}" 1091 | } 1092 | 1093 | if ([string]::IsNullOrEmpty($statusFile)) { 1094 | #no status File is specified, use the LogFile Folder as folder and "status.xml" as filename 1095 | $statusFile = "$logFileDestination\status.xml" 1096 | } else { 1097 | if (-not [System.IO.Path]::IsPathRooted($statusFile)) { 1098 | #not a full path is given, so use the logfile Folder as basis 1099 | $statusFile = "$logFileDestination\$statusFile" 1100 | } 1101 | if (Test-Path -Path $statusFile -pathType container) { 1102 | # The status file parameter points to a folder, so create a "status.xml" in that folder. 1103 | $statusFile = "$statusFile\status.xml" 1104 | } 1105 | } 1106 | 1107 | if ([string]::IsNullOrEmpty($backupSources)) { 1108 | # No backup sources on command line, in host-specific or common section of ini file 1109 | # Backup sources are mandatory, so flag the problem. 1110 | $output = "`nERROR: No backup source(s) specified`n" 1111 | echo $output 1112 | $emailBody = "$emailBody`r`n$output`r`n" 1113 | if ($LogFile) { 1114 | $output | Out-File "$LogFile" -encoding ASCII -append 1115 | } 1116 | $parameters_ok = $False 1117 | } 1118 | 1119 | # Report to the log file the IP Addresses that this system has. 1120 | # If there is no log file then echo the details. 1121 | # This is useful to be able to work out what might have gone wrong with a backup. 1122 | $localAdapters = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"') 1123 | $output = "Network addresses:`r`n" 1124 | if ($LogFile) { 1125 | $output | Out-File "$LogFile" -encoding ASCII -append 1126 | } else { 1127 | echo $output 1128 | } 1129 | 1130 | foreach ($adapter in $localAdapters) { 1131 | $output = $adapter.IPAddress + " " + $adapter.DefaultIPGateway + " " + $adapter.Description + "`r`n" 1132 | if ($LogFile) { 1133 | $output | Out-File "$LogFile" -encoding ASCII -append 1134 | } else { 1135 | echo $output 1136 | } 1137 | } 1138 | 1139 | # If we cannot find ln.exe, there is no point in trying to make a backup 1140 | if ([string]::IsNullOrEmpty($lnPath) -or !(Test-Path -Path $lnPath -PathType leaf)) { 1141 | if ([string]::IsNullOrEmpty($lnPath)) { 1142 | $output = "`nERROR: ln.exe not found`n" 1143 | } elseif (!(Test-Path -Path $lnPath -PathType leaf)) { 1144 | $output = "`nERROR: ln.exe is not a file`n" 1145 | } 1146 | echo $output 1147 | $emailBody = "$emailBody`r`n$output`r`n" 1148 | 1149 | if ($LogFile) { 1150 | $output | Out-File "$LogFile" -encoding ASCII -append 1151 | } 1152 | 1153 | $parameters_ok = $False 1154 | } else { 1155 | try { 1156 | # Try to run ln.exe just to check if it can start. 1157 | $lncheckArgs = @( ) 1158 | $lncheckArgs += "-h" 1159 | & $lnPath $lncheckArgs | Out-Null 1160 | if ($LASTEXITCODE -ne 0) { 1161 | $output = "`nERROR: Cannot run ln.exe with help`n" 1162 | echo $output 1163 | $emailBody = "$emailBody`r`n$output`r`n" 1164 | 1165 | if ($LogFile) { 1166 | $output | Out-File "$LogFile" -encoding ASCII -append 1167 | } 1168 | 1169 | $parameters_ok = $False 1170 | } 1171 | } 1172 | catch { 1173 | # It is possible that the ln version does not match the Windows version (e.g. 64bit installed on a 32bit system) 1174 | $output = "`nERROR: Cannot run ln.exe - maybe you have the 64-bit version on a 32-bit system`n" 1175 | echo $output $_ 1176 | $emailBody = "$emailBody`r`n$output`r`n$_" 1177 | 1178 | if ($LogFile) { 1179 | $output | Out-File "$LogFile" -encoding ASCII -append 1180 | } 1181 | 1182 | $parameters_ok = $False 1183 | } 1184 | 1185 | } 1186 | 1187 | if (![string]::IsNullOrEmpty($preExecutionCommand)) { 1188 | # It seems that the first word of the command string (the command itself) has to be sent individually 1189 | # in the "ampersand" invoking. 1190 | # The remaining parameters (if any) need to be passed as an array. 1191 | # So we split the user-provided string into space-separated parts then specifically take out the first part. 1192 | $preExecutionArgs = Split-CommandLine $preExecutionCommand 1193 | $preExecutionCmd = $preExecutionArgs[0] 1194 | $preExecutionNumArgs = $preExecutionArgs.Length - 1 1195 | if ($preExecutionNumArgs -gt 0) { 1196 | $preExecutionArgs = $preExecutionArgs[1..$preExecutionNumArgs] 1197 | } else { 1198 | $preExecutionArgs = "" 1199 | } 1200 | 1201 | $output = "`nRunning preexecution command ($preExecutionCommand)" 1202 | echo $output 1203 | if ($LogFile) { 1204 | $output | Out-File "$LogFile" -encoding ASCII -append 1205 | } 1206 | 1207 | # echo $preExecutionCmd $preExecutionArgs 1208 | if ($preExecutionNumArgs -gt 0) { 1209 | if ($LogFile) { 1210 | & $preExecutionCmd $preExecutionArgs | Out-File "$LogFile" -encoding ASCII -append 1211 | } else { 1212 | & $preExecutionCmd $preExecutionArgs 1213 | } 1214 | } else { 1215 | if ($LogFile) { 1216 | & $preExecutionCmd | Out-File "$LogFile" -encoding ASCII -append 1217 | } else { 1218 | & $preExecutionCmd 1219 | } 1220 | } 1221 | 1222 | # If the command fails we want a message in the Email, otherwise the details will be only shown in the log file 1223 | # Make sure this if statement is directly after the command has been run. 1224 | if (!$?) { 1225 | $output = "`n`nERROR: the pre-execution-command ended with an error" 1226 | $emailBody = "$emailBody`r$output`r`n" 1227 | $error_during_backup = $True 1228 | $output += "`n" 1229 | echo $output 1230 | if ($LogFile) { 1231 | $output | Out-File "$LogFile" -encoding ASCII -append 1232 | } 1233 | } 1234 | } 1235 | 1236 | if ($preExecutionDelay -gt 0) { 1237 | echo "I'm gona be lazy now" 1238 | 1239 | Write-Host -NoNewline " 1240 | 1241 | ___ z 1242 | _/ | z 1243 | |_____|{)_ 1244 | --- ==\/\ | 1245 | [_____] __)| 1246 | | | //| | 1247 | " 1248 | $CursorTop=[Console]::CursorTop 1249 | [Console]::SetCursorPosition(18,$CursorTop-7) 1250 | for ($msSleeped=0;$msSleeped -lt $preExecutionDelay; $msSleeped+=1000){ 1251 | Start-sleep -milliseconds 1000 1252 | Write-Host -NoNewline "z " 1253 | } 1254 | [Console]::SetCursorPosition(0,$CursorTop) 1255 | Write-Host "I guess it's time to wake up.`n" 1256 | } 1257 | 1258 | # Just test for the existence of the top of the backup destination. "ln" will create any folders as needed, as long as the top exists. 1259 | if (($parameters_ok -eq $True) -and ($doBackup -eq $True) -and (test-path $backupDestinationTop)) { 1260 | foreach ($backup_source in $backupSources) 1261 | { 1262 | # We don't want to have "\" at the end because we will quote the path later and ln.exe would 1263 | # treat this as escaping of the quote (\") and can not parse the command line. 1264 | # ln --mirror "x:\" y:\dir\newdir 1265 | # see also https://github.com/individual-it/ntfs-hardlink-backup/issues/16 1266 | if ($backup_source.substring($backup_source.length-1,1) -eq "\") { 1267 | $backup_source=$backup_source.Substring(0,$backup_source.Length-1) 1268 | } 1269 | 1270 | if (test-path -LiteralPath $backup_source) { 1271 | $stepCounter = 1 1272 | $backupSourceArray = $backup_source.split("\") 1273 | if (($backupSourceArray[0] -eq "") -and ($backupSourceArray[1] -eq "")) { 1274 | # The source is a UNC path (file share) which has no drive letter. We cannot do volume shadowing from that. 1275 | $backup_source_drive_letter = "" 1276 | $backup_source_path = "" 1277 | } else { 1278 | if (-not ($backup_source -match ":")) { 1279 | # No drive letter specified. This could be an attempt at a relative path, so first resolve it to the full path. 1280 | # This allows us to use split-path -Qualifier below to get the actual drive letter 1281 | $backup_source = $ExecutionContext.SessionState.Path.GetUnresolvedProviderPathFromPSPath($backup_source) 1282 | } 1283 | $backup_source_drive_letter = split-path $backup_source -Qualifier 1284 | $backup_source_path = split-path $backup_source -noQualifier 1285 | } 1286 | 1287 | # Check if we are trying to backup a complete drive 1288 | if (($backup_source_drive_letter -ne "") -and ($backup_source_path -eq "")) { 1289 | if ($backup_source_drive_letter -match "([A-Z]):") { 1290 | $backup_source_folder = "["+$matches[1]+"]" 1291 | } 1292 | } else { 1293 | $backup_source_folder = split-path $backup_source -leaf 1294 | } 1295 | 1296 | $actualBackupDestination = "$selectedBackupDestination\$backup_source_folder" 1297 | 1298 | # If the user wants to keep just one backup we do a mirror without any date, so we don't need 1299 | # to copy files that are already there 1300 | if ($backupsToKeep -gt 1) { 1301 | $actualBackupDestination = "$actualBackupDestination - $dateTime" 1302 | } 1303 | 1304 | echo "============Creating Backup of $backup_source============" 1305 | if ($NoShadowCopy -eq $False) { 1306 | if ($backup_source_drive_letter -ne "") { 1307 | # We can try processing a shadow copy. 1308 | if ($shadow_drive_letter -eq $backup_source_drive_letter) { 1309 | # The previous shadow copy must have succeeded because $NoShadowCopy is still false, and we are looping around with a matching shadow drive letter. 1310 | if ($StepTiming -eq $True) { 1311 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1312 | } 1313 | echo "$stepCounter. $stepTime Re-using previous Shadow Volume Copy" 1314 | $stepCounter++ 1315 | $backup_source_path = $s2.DeviceObject+$backup_source_path 1316 | } else { 1317 | if ($num_shadow_copies -gt 0) { 1318 | # Delete the previous shadow copy that was from some other drive letter 1319 | foreach ($shadowCopy in $shadowCopies) { 1320 | if ($s2.ID -eq $shadowCopy.ID) { 1321 | if ($StepTiming -eq $True) { 1322 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1323 | } 1324 | echo "$stepCounter. $stepTime Deleting previous Shadow Copy" 1325 | $stepCounter++ 1326 | try { 1327 | $shadowCopy.Delete() 1328 | } 1329 | catch { 1330 | $output = "ERROR: Could not delete Shadow Copy" 1331 | $emailBody = "$emailBody`r`n$output`r`n$_" 1332 | $error_during_backup = $true 1333 | echo $output $_ 1334 | } 1335 | $num_shadow_copies-- 1336 | echo "done`n" 1337 | break 1338 | } 1339 | } 1340 | } 1341 | if ($StepTiming -eq $True) { 1342 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1343 | } 1344 | echo "$stepCounter. $stepTime Creating Shadow Volume Copy" 1345 | $stepCounter++ 1346 | try { 1347 | $s1 = (gwmi -List Win32_ShadowCopy).Create("$backup_source_drive_letter\", "ClientAccessible") 1348 | $s2 = gwmi Win32_ShadowCopy | ? { $_.ID -eq $s1.ShadowID } 1349 | 1350 | if ($s1.ReturnValue -ne 0 -OR !$s2) { 1351 | # ToDo add explanation of return codes http://msdn.microsoft.com/en-us/library/aa389391%28v=vs.85%29.aspx 1352 | throw "Shadow Copy Creation failed. Return Code: " + $s1.ReturnValue 1353 | } 1354 | 1355 | echo "Shadow Volume ID: $($s2.ID)" 1356 | echo "Shadow Volume DeviceObject: $($s2.DeviceObject)" 1357 | 1358 | $shadowCopies = Get-WMIObject -Class Win32_ShadowCopy 1359 | 1360 | echo "done`n" 1361 | 1362 | $backup_source_path = $s2.DeviceObject+$backup_source_path 1363 | $num_shadow_copies++ 1364 | $shadow_drive_letter = $backup_source_drive_letter 1365 | } 1366 | catch { 1367 | $output = "ERROR: Could not create Shadow Copy`r`n$_ `r`nATTENTION: Skipping creation of Shadow Volume Copy. ATTENTION: if files are changed during the backup process, they might end up being corrupted in the backup!`r`n" 1368 | $emailBody = "$emailBody`r`n$output`r`n" 1369 | $error_during_backup = $true 1370 | echo $output 1371 | if ($LogFile) { 1372 | $output | Out-File "$LogFile" -encoding ASCII -append 1373 | } 1374 | $backup_source_path = $backup_source 1375 | $NoShadowCopy = $True 1376 | } 1377 | } 1378 | } else { 1379 | # We were asked to do shadow copy but the source is a UNC path. 1380 | $output = "Skipping creation of Shadow Volume Copy because source is a UNC path `r`nATTENTION: if files are changed during the backup process, they might end up being corrupted in the backup!`n" 1381 | if ($StepTiming -eq $True) { 1382 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1383 | } 1384 | echo "$stepCounter. $stepTime $output" 1385 | if ($LogFile) { 1386 | $output | Out-File "$LogFile" -encoding ASCII -append 1387 | } 1388 | $stepCounter++ 1389 | $backup_source_path = $backup_source 1390 | } 1391 | } 1392 | else { 1393 | $output = "Skipping creation of Shadow Volume Copy `r`nATTENTION: if files are changed during the backup process, they might end up being corrupted in the backup!`n" 1394 | if ($StepTiming -eq $True) { 1395 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1396 | } 1397 | echo "$stepCounter. $stepTime $output" 1398 | if ($LogFile) { 1399 | $output | Out-File "$LogFile" -encoding ASCII -append 1400 | } 1401 | $stepCounter++ 1402 | $backup_source_path = $backup_source 1403 | } 1404 | 1405 | if ($StepTiming -eq $True) { 1406 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1407 | } 1408 | echo "$stepCounter. $stepTime Running backup" 1409 | $stepCounter++ 1410 | echo "Source: $backup_source_path" 1411 | echo "Destination: $actualBackupDestination$backupMappedString" 1412 | 1413 | $yearBackupsKeptText = "" 1414 | $lastBackupFolderName = "" 1415 | $lastBackupFolders = @() 1416 | $lastBackupFoldersPerYear = @{} 1417 | $lastBackupFoldersPerYearToKeep = @{} 1418 | If (Test-Path $selectedBackupDestination -pathType container) { 1419 | $oldBackupItems = Get-ChildItem -Force -Path $selectedBackupDestination | Where-Object {$_ -is [IO.DirectoryInfo]} | Sort-Object -Property Name 1420 | 1421 | # Escape $backup_source_folder if we are doing backup of a full disk like D:\ to folder [D] 1422 | if ($backup_source_folder -match "\[[A-Z]\]") { 1423 | $escaped_backup_source_folder = '\' + $backup_source_folder 1424 | } 1425 | else { 1426 | $escaped_backup_source_folder = $backup_source_folder 1427 | } 1428 | 1429 | 1430 | if ($backupsToKeepPerYear -gt 0) { 1431 | 1432 | # Find all backups per year 1433 | foreach ($item in $oldBackupItems) { 1434 | if ($item.Name -match '^'+$escaped_backup_source_folder+' - (\d{4})-\d{2}-\d{2} \d{2}-\d{2}-\d{2}$' ) { 1435 | if (!($lastBackupFoldersPerYear.ContainsKey($matches[1]))) { 1436 | $lastBackupFoldersPerYear[$matches[1]] = @() 1437 | } 1438 | $lastBackupFoldersPerYear[$matches[1]]+= $item 1439 | } 1440 | } 1441 | 1442 | # Decide which backups from the last year to keep 1443 | foreach ($year in $($lastBackupFoldersPerYear.keys | sort)) { 1444 | # echo $year 1445 | if (!($lastBackupFoldersPerYearToKeep.ContainsKey($year))) { 1446 | $lastBackupFoldersPerYearToKeep[$year] = @() 1447 | } 1448 | 1449 | # If we want to keep more backups than are actually there then just keep the whole array 1450 | if ($backupsToKeepPerYear -ge $lastBackupFoldersPerYear[$year].length) { 1451 | $lastBackupFoldersPerYearToKeep[$year] = $lastBackupFoldersPerYear[$year] 1452 | } else { 1453 | # Calculate the day we ideally would like to have a backup of 1454 | # then find the backup we have that is nearest to that date and keep it 1455 | 1456 | $daysBetweenBackupsToKeep = 365/$backupsToKeepPerYear 1457 | $dayOfYearToKeepBackupOf = 0 1458 | while (($lastBackupFoldersPerYearToKeep[$year].length -lt $backupsToKeepPerYear) -and ($lastBackupFoldersPerYear[$year].length -gt 0)) { 1459 | $dayOfYearToKeepBackupOf = $dayOfYearToKeepBackupOf + $daysBetweenBackupsToKeep 1460 | $previousDaysDifference = 366 1461 | foreach ($backupItem in $lastBackupFoldersPerYear[$year]) { 1462 | 1463 | $backupItem.Name -match '^'+$escaped_backup_source_folder+' - (\d{4}-\d{2}-\d{2}) \d{2}-\d{2}-\d{2}$' | Out-Null 1464 | $daysDifference = [math]::abs($dayOfYearToKeepBackupOf-(Get-Date $matches[1]).DayOfYear) 1465 | 1466 | if ($daysDifference -lt $previousDaysDifference) { 1467 | $bestBackupToKeep=$backupItem 1468 | } 1469 | $previousDaysDifference = $daysDifference 1470 | } 1471 | 1472 | $lastBackupFoldersPerYearToKeep[$year] +=$bestBackupToKeep 1473 | $lastBackupFoldersPerYear[$year] = $lastBackupFoldersPerYear[$year] -ne $bestBackupToKeep 1474 | } 1475 | } 1476 | $thisYearBackupsKept = $lastBackupFoldersPerYearToKeep[$year].length 1477 | $yearBackupsKeptText += "Keeping $thisYearBackupsKept backup(s) from $year `r`n" 1478 | } 1479 | 1480 | } 1481 | 1482 | # Get me the last backup if any 1483 | foreach ($item in $oldBackupItems) { 1484 | if ($item.Name -match '^'+$escaped_backup_source_folder+' - (\d{4})-\d{2}-\d{2} \d{2}-\d{2}-\d{2}$' ) { 1485 | $lastBackupFolderName = $item.Name 1486 | 1487 | # If we have that folder in the list of folders to keep do not add it to the list 1488 | # of lastBackupFolders because they will be used for deleting old folders 1489 | if ($lastBackupFoldersPerYearToKeep[$matches[1]] -notcontains $item) { 1490 | $lastBackupFolders += $item 1491 | } 1492 | } 1493 | } 1494 | 1495 | } 1496 | 1497 | # Start with an empty array of args to pass to ln and build it up as we go. 1498 | $lnArgs = @( ) 1499 | 1500 | if ($traditional -eq $True) { 1501 | $lnArgs += "--traditional" 1502 | } 1503 | 1504 | if ($noads -eq $True) { 1505 | $lnArgs += "--noads" 1506 | } 1507 | 1508 | if ($noea -eq $True) { 1509 | $lnArgs += "--noea" 1510 | } 1511 | 1512 | if ($splice -eq $True) { 1513 | $lnArgs += "--splice" 1514 | } 1515 | 1516 | if ($unroll -eq $True) { 1517 | $lnArgs += "--unroll" 1518 | } 1519 | 1520 | if ($backupModeACLs -eq $True) { 1521 | $lnArgs += "--backup" 1522 | } 1523 | 1524 | if ($timeTolerance -ne 0) { 1525 | $lnArgs += "--timetolerance" 1526 | $lnArgs += "$timeTolerance" 1527 | } 1528 | 1529 | foreach ($item in $excludeFiles) { 1530 | if ($item -AND $item.Trim()) { 1531 | $lnArgs += "--exclude" 1532 | $lnArgs += "$item" 1533 | } 1534 | } 1535 | 1536 | foreach ($item in $excludeDirs) { 1537 | if ($item -AND $item.Trim()) { 1538 | $lnArgs += "--excludedir" 1539 | $lnArgs += "$item" 1540 | } 1541 | } 1542 | 1543 | if ($LogFile) { 1544 | $logFileCommandAppend = " >> `"$LogFile`"" 1545 | } 1546 | 1547 | $start_time = get-date -f "yyyy-MM-dd HH-mm-ss" 1548 | 1549 | if ($lastBackupFolderName -eq "" ) { 1550 | echo "Mirror from $backup_source_path to $actualBackupDestination$backupMappedString" 1551 | if ($LogFile) { 1552 | "`r`nMirror from $backup_source_path to $actualBackupDestination$backupMappedString" | Out-File "$LogFile" -encoding ASCII -append 1553 | } 1554 | 1555 | $lnArgs += "--mirror" 1556 | $lnArgs += $backup_source_path 1557 | $lnArgs += $actualBackupDestination 1558 | } else { 1559 | echo "Delorian copy from $backup_source_path to $actualBackupDestination$backupMappedString against $selectedBackupDestination\$lastBackupFolderName" 1560 | if ($LogFile) { 1561 | "`r`nDelorian copy from $backup_source_path to $actualBackupDestination$backupMappedString against $selectedBackupDestination\$lastBackupFolderName" | Out-File "$LogFile" -encoding ASCII -append 1562 | } 1563 | 1564 | $lnArgs += "--delorean" 1565 | $lnArgs += $backup_source_path 1566 | $lnArgs += $selectedBackupDestination + "\" + $lastBackupFolderName 1567 | $lnArgs += $actualBackupDestination 1568 | } 1569 | 1570 | # echo $lnPath $lnArgs 1571 | if ($LogFile) { 1572 | & $lnPath $lnArgs | Out-File "$LogFile" -encoding ASCII -append 1573 | } else { 1574 | & $lnPath $lnArgs 1575 | } 1576 | 1577 | $saved_lastexitcode = $LASTEXITCODE 1578 | if ($saved_lastexitcode -ne 0) { 1579 | $output = "`n`nERROR: the ln command ended with exit code [$saved_lastexitcode]" 1580 | $error_during_backup = $true 1581 | $ln_error = $true 1582 | } else { 1583 | $output = "" 1584 | $ln_error = $false 1585 | } 1586 | 1587 | $summary = "" 1588 | if ($LogFile) { 1589 | $backup_response = get-content "$LogFile" 1590 | foreach ( $line in $backup_response.length..1 ) { 1591 | $summary = $backup_response[$line] + "`n" + $summary 1592 | 1593 | # Do we need this line if we already checked for the exitcode? 1594 | if ($backup_response[$line] -match '(.*):\s+(?:\d+(?:\,\d*)?|-)\s+(?:\d+(?:\,\d*)?|-)\s+(?:\d+(?:\,\d*)?|-)\s+(?:\d+(?:\,\d*)?|-)\s+(?:\d+(?:\,\d*)?|-)\s+(?:\d+(?:\,\d*)?|-)\s+([1-9]+\d*(?:\,\d*)?)') { 1595 | $error_during_backup = $true 1596 | } 1597 | if ($backup_response[$line] -match '.*Total\s+Copied\s+Linked\s+Skipped.*\s+Excluded\s+Failed.*') { 1598 | break 1599 | } 1600 | } 1601 | } 1602 | 1603 | echo "done`n" 1604 | 1605 | $summary = "`n------Summary-----`nBackup AT: $start_time FROM: $backup_source TO: $selectedBackupDestination$backupMappedString`n" + $summary 1606 | echo $summary 1607 | 1608 | $emailBody = $emailBody + $summary 1609 | 1610 | echo "`n" 1611 | 1612 | if ($ln_error) 1613 | { 1614 | $emailBody = "$emailBody`r$output`r`n" 1615 | echo $output 1616 | if ($LogFile) { 1617 | $output | Out-File "$LogFile" -encoding ASCII -append 1618 | } 1619 | } 1620 | 1621 | if ($StepTiming -eq $True) { 1622 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1623 | } 1624 | 1625 | echo "$stepCounter. $stepTime Deleting old backups" 1626 | $stepCounter++ 1627 | 1628 | # Plus 1 because we just created a new backup but we have checked for old backups before we have 1629 | # created the new one 1630 | $backupsInDestination = $lastBackupFolders.length + 1 1631 | $summary = $yearBackupsKeptText + "Found $backupsInDestination regular backup(s), keeping a maximum of $backupsToKeep regular backup(s)`n" 1632 | echo $summary 1633 | 1634 | if ($LogFile) { 1635 | $summary | Out-File "$LogFile" -encoding ASCII -append 1636 | } 1637 | $emailBody = $emailBody + $summary 1638 | 1639 | $backupsToDelete=$backupsInDestination - $backupsToKeep 1640 | if ($backupsToDelete -gt 0) { 1641 | echo "Deleting $backupsToDelete old backup(s)" 1642 | if ($LogFile) { 1643 | "`r`nDeleting $backupsToDelete old backup(s)" | Out-File "$LogFile" -encoding ASCII -append 1644 | } 1645 | $backupsDeleted = 0 1646 | while ($backupsDeleted -lt $backupsToDelete) { 1647 | $folderToDelete = $selectedBackupDestination +"\"+ $lastBackupFolders[$backupsDeleted].Name 1648 | echo "Deleting $folderToDelete" 1649 | if ($LogFile) { 1650 | "`r`nDeleting $folderToDelete" | Out-File "$LogFile" -encoding ASCII -append 1651 | } 1652 | $backupsDeleted++ 1653 | $lndelArgs = @( ) 1654 | $lndelArgs += "--deeppathdelete" 1655 | $lndelArgs += $folderToDelete 1656 | 1657 | # echo $lnPath $lndelArgs 1658 | if ($LogFile) { 1659 | & $lnPath $lndelArgs | Out-File "$LogFile" -encoding ASCII -append 1660 | } else { 1661 | & $lnPath $lndelArgs 1662 | } 1663 | } 1664 | 1665 | $summary = "`nDeleted $backupsDeleted old backup(s)`n" 1666 | echo $summary 1667 | if ($LogFile) { 1668 | $summary | Out-File "$LogFile" -encoding ASCII -append 1669 | } 1670 | 1671 | $emailBody = $emailBody + $summary 1672 | } else { 1673 | $summary = "`nNo old backups were deleted`n" 1674 | echo $summary 1675 | if ($LogFile) { 1676 | $summary | Out-File "$LogFile" -encoding ASCII -append 1677 | } 1678 | 1679 | $emailBody = $emailBody + $summary 1680 | } 1681 | } else { 1682 | # The backup source does not exist - there was no point processing this source. 1683 | $output = "ERROR: Backup source does not exist - $backup_source - backup NOT done for this source`r`n" 1684 | $emailBody = "$emailBody`r`n$output`r`n" 1685 | $error_during_backup = $true 1686 | echo $output 1687 | if ($LogFile) { 1688 | $output | Out-File "$LogFile" -encoding ASCII -append 1689 | } 1690 | } 1691 | } 1692 | 1693 | if (($deleteOldLogFiles -eq $True) -and ($logFileDestination)) { 1694 | $lastLogFiles = @() 1695 | If (Test-Path $logFileDestination -pathType container) { 1696 | $oldLogItems = Get-ChildItem -Force -Path $logFileDestination | Where-Object {$_ -is [IO.FileInfo]} | Sort-Object -Property Name 1697 | 1698 | # Get me the old logs if any 1699 | foreach ($item in $oldLogItems) { 1700 | if ($item.Name -match '^\d{4}-\d{2}-\d{2} \d{2}-\d{2}-\d{2}.log$' ) { 1701 | $lastLogFiles += $item 1702 | } 1703 | } 1704 | } 1705 | 1706 | if ($StepTiming -eq $True) { 1707 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1708 | } 1709 | echo "$stepCounter. $stepTime Deleting old log files" 1710 | $stepCounter++ 1711 | 1712 | # No need to add 1 here because the new log existed already when we checked for old log files 1713 | $logFilesInDestination = $lastLogFiles.length 1714 | $summary = "`nFound $logFilesInDestination log file(s), keeping maximum of $backupsToKeep log file(s)`n" 1715 | echo $summary 1716 | if ($LogFile) { 1717 | $summary | Out-File "$LogFile" -encoding ASCII -append 1718 | } 1719 | $emailBody = $emailBody + $summary 1720 | 1721 | $logFilesToDelete=$logFilesInDestination - $backupsToKeep 1722 | if ($logFilesToDelete -gt 0) { 1723 | echo "Deleting $logFilesToDelete old logfile(s)" 1724 | if ($LogFile) { 1725 | "`r`nDeleting $logFilesToDelete old logfile(s)" | Out-File "$LogFile" -encoding ASCII -append 1726 | } 1727 | $logFilesDeleted = 0 1728 | while ($logFilesDeleted -lt $logFilesToDelete) { 1729 | $logFileToDelete = $logFileDestination +"\"+ $lastLogFiles[$logFilesDeleted].Name 1730 | 1731 | echo "Deleting $logFileToDelete(.zip)" 1732 | if ($LogFile) { 1733 | "`r`nDeleting $logFileToDelete(.zip)" | Out-File "$LogFile" -encoding ASCII -append 1734 | } 1735 | 1736 | If (Test-Path "$logFileToDelete") { 1737 | Remove-Item "$logFileToDelete" 1738 | } 1739 | If (Test-Path "$logFileToDelete.zip") { 1740 | Remove-Item "$logFileToDelete.zip" 1741 | } 1742 | 1743 | $logFilesDeleted++ 1744 | } 1745 | 1746 | $summary = "`nDeleted $logFilesDeleted old logfile(s)`n" 1747 | echo $summary 1748 | if ($LogFile) { 1749 | $summary | Out-File "$LogFile" -encoding ASCII -append 1750 | } 1751 | $emailBody = $emailBody + $summary 1752 | } else { 1753 | $summary = "`nNo old logfiles were deleted`n" 1754 | echo $summary 1755 | if ($LogFile) { 1756 | $summary | Out-File "$LogFile" -encoding ASCII -append 1757 | } 1758 | $emailBody = $emailBody + $summary 1759 | } 1760 | } 1761 | 1762 | # We have processed each backup source. Now cleanup any remaining shadow copy. 1763 | if ($num_shadow_copies -gt 0) { 1764 | # Delete the last shadow copy 1765 | foreach ($shadowCopy in $shadowCopies) { 1766 | if ($s2.ID -eq $shadowCopy.ID) { 1767 | if ($StepTiming -eq $True) { 1768 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1769 | } 1770 | echo "$stepCounter. $stepTime Deleting last Shadow Copy" 1771 | $stepCounter++ 1772 | try { 1773 | $shadowCopy.Delete() 1774 | } 1775 | catch { 1776 | $output = "ERROR: Could not delete Shadow Copy. " 1777 | $emailBody = "$emailBody`r`n$output`r`n$_" 1778 | $error_during_backup = $true 1779 | echo $output $_ 1780 | } 1781 | $num_shadow_copies-- 1782 | echo "done`n" 1783 | break 1784 | } 1785 | } 1786 | } 1787 | 1788 | } else { 1789 | if ($destWarningText) { 1790 | # We might have tested multiple backup destinations and in the end not found a good destination 1791 | # Write out the messages about those checks to the email body so the recipient can see easily the process and problems that happened along the way. 1792 | $emailBody = "$emailBody`r`n$destWarningText`r`n" 1793 | } 1794 | 1795 | if ($parameters_ok -eq $True) { 1796 | if ($doBackup -eq $True) { 1797 | # The destination drive or \\server\share does not exist. 1798 | $output = "ERROR: Destination drive or share $backupDestinationTop$backupMappedString does not exist - backup NOT done`r`n" 1799 | } else { 1800 | # The backup was not done because localSubnetOnly was on, and the destination \\server\share is not in the local subnet. 1801 | $output = "ERROR: Destination share $backupDestinationTop$backupMappedString is not in a local subnet - backup NOT done`r`n" 1802 | } 1803 | } else { 1804 | # There was some error in the supplied parameters. 1805 | # The specific problem will have been mentioned in the email body/log file earlier. 1806 | # Put a general message here. 1807 | $output = "ERROR: There was a problem with the input parameters" 1808 | } 1809 | $emailBody = "$emailBody`r`n$output`r`n" 1810 | $error_during_backup = $true 1811 | echo $output 1812 | if ($LogFile) { 1813 | $output | Out-File "$LogFile" -encoding ASCII -append 1814 | } 1815 | } 1816 | 1817 | #XML Status report 1818 | echo "============Generating XML Status report============" 1819 | $xmlWriter = New-Object System.XMl.XmlTextWriter($statusFile,$Null) 1820 | $xmlWriter.Formatting = 'Indented' 1821 | $xmlWriter.Indentation = 1 1822 | $xmlWriter.IndentChar = "`t" 1823 | 1824 | $xmlWriter.WriteStartDocument() 1825 | 1826 | if ($error_during_backup) { 1827 | $backupStatus = "ERROR" 1828 | } else { 1829 | $backupStatus = "OK" 1830 | } 1831 | 1832 | #make the DateTime more general/SQL like. We have used "-" between h,m & m because we cannot have ":" in the folder/filenames 1833 | #but here we really want to have something that is more general and easier to parse 1834 | $dateTimeforXML = [DateTime]::ParseExact($dateTime, "yyyy-MM-dd HH-mm-ss", $null) 1835 | $dateTimeforXML = Get-Date -Date $dateTimeforXML -f "yyyy-MM-dd HH:mm:ss" 1836 | 1837 | $xmlWriter.WriteStartElement('NTFSHARDLINKBACKUP') 1838 | $xmlWriter.WriteElementString('VERSION', $versionString) 1839 | $xmlWriter.WriteElementString('STATUS', $backupStatus) 1840 | $xmlWriter.WriteElementString('JOBNAME', $jobName) 1841 | $xmlWriter.WriteElementString('LASTRUN', $dateTimeforXML) 1842 | $xmlWriter.WriteElementString('DESTINATION', "$selectedBackupDestination$backupMappedString") 1843 | 1844 | $xmlWriter.WriteEndElement() 1845 | $xmlWriter.WriteEndDocument() 1846 | $xmlWriter.Flush() 1847 | $xmlWriter.Close() 1848 | 1849 | if ($emailTo -AND $emailFrom -AND $SMTPServer) { 1850 | # Check if we can find any network adapter that has a default gateway 1851 | $localAdapters = (Get-WmiObject -Class Win32_NetworkAdapterConfiguration -Filter 'ipenabled = "true"') 1852 | $defaultGatewayExists = $false 1853 | 1854 | foreach ($adapter in $localAdapters) { 1855 | if ($adapter.DefaultIPGateway) { 1856 | $defaultGatewayExists = $true 1857 | } 1858 | } 1859 | 1860 | if ($defaultGatewayExists) { 1861 | echo "============Sending Email============" 1862 | [Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12 1863 | $stepCounter = 1 1864 | 1865 | if ($LogFile) { 1866 | if ($StepTiming -eq $True) { 1867 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1868 | } 1869 | echo "$stepCounter. $stepTime Zipping log file" 1870 | $stepCounter++ 1871 | $zipFilePath = "$LogFile.zip" 1872 | $fileToZip = get-item $LogFile 1873 | 1874 | try 1875 | { 1876 | New-Item $zipFilePath -type file -force -erroraction stop | Out-Null 1877 | if (-not (test-path $zipFilePath)) { 1878 | set-content $zipFilePath ("PK" + [char]5 + [char]6 + ("$([char]0)" * 18)) 1879 | } 1880 | 1881 | $ZipFile = (new-object -com shell.application).NameSpace($zipFilePath) 1882 | $zipfile.CopyHere($fileToZip.fullname) 1883 | 1884 | $timeSlept = 0 1885 | while ($zipfile.Items().Count -le 0 -AND $timeSlept -le $maxMsToSleepForZipCreation ) { 1886 | Start-sleep -milliseconds $msToWaitDuringZipCreation 1887 | $timeSlept = $timeSlept + $msToWaitDuringZipCreation 1888 | } 1889 | $attachment = New-Object System.Net.Mail.Attachment("$zipFilePath" ) 1890 | } 1891 | catch { 1892 | $error_during_backup = $True 1893 | $output = "`r`nERROR: Could not create log ZIP file. Will try to attach the unzipped log file and hope it's not to big.`r`n$_`r`n" 1894 | $emailBody = "$emailBody`r`n$output`r`n" 1895 | echo $output 1896 | $output | Out-File "$LogFile" -encoding ASCII -append 1897 | $attachment = New-Object System.Net.Mail.Attachment("$LogFile" ) 1898 | } 1899 | } 1900 | 1901 | if ($error_during_backup) { 1902 | $EmailSubject = "ERROR - $EmailSubject" 1903 | } 1904 | $SMTPMessage = New-Object System.Net.Mail.MailMessage($emailFrom,$emailTo,$emailSubject,$emailBody) 1905 | 1906 | if ($LogFile) { 1907 | $SMTPMessage.Attachments.Add($attachment) 1908 | } 1909 | $SMTPClient = New-Object Net.Mail.SmtpClient($SMTPServer, $SMTPPort) 1910 | 1911 | $SMTPClient.Timeout = $SMTPTimeout 1912 | if ($NoSMTPOverSSL -eq $False) { 1913 | $SMTPClient.EnableSsl = $True 1914 | } 1915 | 1916 | $SMTPClient.Credentials = New-Object System.Net.NetworkCredential($SMTPUser, $SMTPPassword); 1917 | 1918 | $emailSendSucess = $False 1919 | if ($StepTiming -eq $True) { 1920 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1921 | } 1922 | echo "$stepCounter. $stepTime Sending email" 1923 | $stepCounter++ 1924 | while ($emailSendRetries -gt 0 -AND !$emailSendSucess) { 1925 | try { 1926 | $emailSendRetries-- 1927 | $SMTPClient.Send($SMTPMessage) 1928 | $emailSendSucess = $True 1929 | } catch { 1930 | if ($StepTiming -eq $True) { 1931 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1932 | } 1933 | $output = "ERROR: $stepTime Could not send Email.`r`n$_`r`n" 1934 | echo $output 1935 | if ($LogFile) { 1936 | $output | Out-File "$LogFile" -encoding ASCII -append 1937 | } 1938 | } 1939 | 1940 | if (!$emailSendSucess) { 1941 | Start-sleep -milliseconds $msToPauseBetweenEmailSendRetries 1942 | } 1943 | } 1944 | 1945 | if ($LogFile) { 1946 | $attachment.Dispose() 1947 | } 1948 | 1949 | echo "done" 1950 | } else { 1951 | if ($StepTiming -eq $True) { 1952 | $stepTime = get-date -f "yyyy-MM-dd HH-mm-ss" 1953 | } 1954 | $output = "ERROR: $stepTime No valid network connection found. Could not send Email.`r`n" 1955 | echo $output 1956 | if ($LogFile) { 1957 | $output | Out-File "$LogFile" -encoding ASCII -append 1958 | } 1959 | } 1960 | } 1961 | 1962 | if ($substDone) { 1963 | # Delete any drive letter substitution done earlier 1964 | # Note: the subst drive might have contained the log file, so we cannot delete earlier since it is needed to zip and email. 1965 | echo "`nRemoving subst of $substDrive`n" 1966 | subst "$substDrive" /D 1967 | } 1968 | 1969 | if (-not ([string]::IsNullOrEmpty($postExecutionCommand))) { 1970 | echo "`nrunning postexecution command ($postExecutionCommand)" 1971 | $postExecutionArgs = Split-CommandLine $postExecutionCommand 1972 | $postExecutionCmd = $postExecutionArgs[0] 1973 | $postExecutionNumArgs = $postExecutionArgs.Length - 1 1974 | if ($postExecutionNumArgs -gt 0) { 1975 | $postExecutionArgs = $postExecutionArgs[1..$postExecutionNumArgs] 1976 | } else { 1977 | $postExecutionArgs = "" 1978 | } 1979 | # echo $postExecutionCmd $postExecutionArgs 1980 | if ($postExecutionNumArgs -gt 0) { 1981 | & $postExecutionCmd $postExecutionArgs 1982 | } else { 1983 | & $postExecutionCmd 1984 | } 1985 | } 1986 | --------------------------------------------------------------------------------