When it comes to backing up files, sometimes the simplest option is best.
If you’re looking for a quick, straightforward, yet flexible solution for backing up files and folders, a PowerShell script can be a great choice. By creating your own backup script, you can stop relying on third-party programs to do the job for you and there is complete freedom to customise the steps carried out by the backup procedure to suit your needs.
In this article, I provide the code listing for a template PowerShell script that copies files from a local directory to a network drive or shared folder. I then walk through the important parts of the script to help you understand what it is doing, and so that you can adjust the logic according to your unique requirements.
The Script
Without further ado, I have included the code listing for the backup script below.
If you’re not overly familiar with PowerShell don’t panic, I’m going to explain the key sections of the script in the sub-sections that follow. I encourage you to focus on the ‘Main’ section where the primary backup logic is located.
You can also check out my PowerShell Quickstart blog article if you’re new to PowerShell.
<# .SYNOPSIS Important Files Backup .DESCRIPTION Copies important files to a network drive (BACKUP). This script should be scheduled to run once daily. .NOTES Author: Anonymous .LINK N/A #> #-------------------------------------------------------------------------------------------------# # Functions #-------------------------------------------------------------------------------------------------# function Get-AllExceptionMessages([Exception]$Exception) { <# .DESCRIPTION Gets all available Exception Messages i.e. the base Exception Message plus Inner Exception Messages. .PARAMETER Exception The Exception to retrieve Exception Messages for. .EXAMPLE Get-AllExceptionMessages -Exception $_.Exception .NOTES Returns a string containing all of the Exception Messages found, each message separated by 2 CRLFs. #> $fullExceptionMessage = $exception.Message; $innerException = $exception.InnerException; while ($innerException -ne $null) { $fullExceptionMessage += [string]::Format("{0}{0}{1}", [Environment]::NewLine, $innerException.Message); $innerException = $innerException.InnerException; } return $fullExceptionMessage; } function Send-Email([string]$Subject, [string]$Body) { <# .DESCRIPTION Sends an email with the specified subject and body. .PARAMETER Subject The subject of the email. .PARAMETER Body The body of the email. .EXAMPLE Send-Email -Subject "Test Subject" -Body "Test Body" .NOTES N/A #> $smtpServer = "smtp.mailgun.org" $port = 587 $username = "postmaster@YOUR_DOMAIN.mailgun.org" $password = ConvertTo-SecureString "YOUR_PASSWORD" -AsPlainText -Force $credentials = New-Object System.Management.Automation.PSCredential($username, $password) $from = "Your Name <mailgun@YOUR_DOMAIN.mailgun.org>" $to = "yourname@gmail.com" Send-MailMessage ` -SmtpServer $smtpServer ` -Port $port ` -UseSsl ` -Credential $credentials ` -From $from ` -To $to ` -Subject $Subject ` -Body $Body } #-------------------------------------------------------------------------------------------------# # Initialisation #-------------------------------------------------------------------------------------------------# # The script will stop if any cmdlets throw an error. $ErrorActionPreference = "Stop" # Variables. $exitCode = 0 #-------------------------------------------------------------------------------------------------# # Main #-------------------------------------------------------------------------------------------------# try { Write-Host "Starting backup..." # Configure paths and credentials. $source = "C:\ImportantFiles\*" $destination = "\\BACKUP\Backups" $username = "BACKUP_USERNAME" $password = ConvertTo-SecureString "BACKUP_PASSWORD" -AsPlainText -Force $credentials = New-Object System.Management.Automation.PSCredential($username, $password) # Create a temporary in-memory drive (J:). New-PSDrive -Name J -PSProvider FileSystem -Root $destination -Credential $credentials -Persist # Create a dated folder for today's files e.g. J:\ImportantFiles\2022-07-14 $datedDestinationDirectory = "J:\ImportantFiles\$((Get-Date).ToString('yyyy-MM-dd'))" New-Item -ItemType Directory -Path $datedDestinationDirectory # Copy files from the source folder to the destination drive folder. Write-Host "Copying files..." Copy-Item -Path $source -Destination $datedDestinationDirectory -Recurse -Force # Only retain backups for 30 days. $destinationBaseDirectory = "J:\ImportantFiles" $backupLimit = (Get-Date).AddDays(-30) # Delete backup sub-folders that are older than the backup retention limit. Write-Host "Deleting old files..." Get-ChildItem -Path $destinationBaseDirectory -Force | Where-Object { $_.PSIsContainer -and $_.CreationTime -lt $backupLimit } | Remove-Item -Force -Recurse Write-Host "Backup completed successfully" } catch { # Alert details of the error. $exitCode = 1 $errors = Get-AllExceptionMessages -Exception $_.Exception Write-Host "Important files backup FAILED: $($errors)" Send-Email -Subject "Important Files Backup (ERROR): File transfer failed" -Body $errors } finally { # Clean up the drive allocation. Remove-PSDrive J Write-Host "Script finished" exit $exitCode }
The above script has been divided into several sections.
First of all, there is the header commentary that details the synopsis (brief summary), description, and author of the script. I recommend that you update these details with relevant information that will be helpful for future reference.
The remainder of the script is broken down into the ‘Functions’, ‘Initialisation’ and ‘Main’ sections which I will cover in detail further below.
Functions
I like to define any helper functions that are specific to the current script near the top of the file.
The first function is named Get-AllExceptionMessages
and specifies an Exception
parameter. The function uses a while
loop to iterate through all of the inner exceptions contained within the specified exception and concatenates (joins) all of the exception messages into a single error message.
The second function is named Send-Email
and, as the name suggests, it provides us with a way of sending an email. The cmdlet defines Subject
and Body
parameters and takes care of defining the email server, credentials, and sender/receiver details to help simplify the calling code.
If you want to use the email functionality, you’ll need to update all of the variables within theSend-Email
function to the appropriate details that apply to you.
Note the use of the backtick (`) character to separate the Send-MailMessage
parameters onto separate lines for readability. Be careful not to accidentally remove these as this will stop the script from calling the Send-MailMessage
cmdlet correctly.
Note the usage of the built-in PowerShell Send-MailMessage
cmdlet to send the email. Send-MailMessage
uses the .NET SmtpClient
class internally which is now deprecated. I’ll be covering how to switch this out to use the Mailgun API in a future article.
Initialisation
The ‘Initialisation’ section deals with setting up the environment before the main script logic gets underway.
#-------------------------------------------------------------------------------------------------# # Initialisation #-------------------------------------------------------------------------------------------------# # The script will stop if any cmdlets throw an error. $ErrorActionPreference = "Stop" # Variables. $exitCode = 0
$ErrorActionPreference
is a PowerShell preference variable that allows us to configure how our script will respond when a non-terminating error occurs. As a best practice, the variable is set to a value of ‘Stop’ so that script execution will not continue further if an unhandled error occurs.
The $exitCode
variable is intended to hold the Exit Code for the script, which can be set to a non-zero value in the event of a backup issue. This allows us to indicate that something has gone wrong to the calling process, for example, if we are running the script automatically via the Windows Task Scheduler we’ll see the Exit Code displayed as the ‘Last Run Result’.
Main
The ‘Main’ section of the script is where the primary backup logic is located.
I have provided a top-level view of the structure below, with additional comments for clarification.
#-------------------------------------------------------------------------------------------------# # Main #-------------------------------------------------------------------------------------------------# try { Write-Host "Starting backup..." # Backup logic. # ... } catch { # Error handling. # ... } finally { # Clean-up. # ... }
The main body of the script is wrapped in a try
/catch
/finally
block.
The try
block contains the backup logic.
The catch
block contains error handling/alerting code.
The finally
block contains clean-up code that will always execute after the try
/catch
blocks, regardless of whether any errors occurred during the script execution.
Setting up paths
Within the try
block, a few variables are initialised containing path and credential details.
# Configure paths and credentials. $source = "C:\ImportantFiles\*" $destination = "\\BACKUP\Backups" $username = "BACKUP_USERNAME" $password = ConvertTo-SecureString "BACKUP_USERNAME" -AsPlainText -Force $credentials = New-Object System.Management.Automation.PSCredential($username, $password)
The $source
variable contains the path to the files that need to be backed up. Notice the ‘\*’ characters at the end of the string, indicating that we want to copy all files contained within the ‘ImportantFiles’ directory.
The $destination
variable contains the path to the ‘Backups’ directory on a network drive named ‘BACKUP’.
The next three lines are used to set the credentials that are needed to connect to the network drive. The ConvertTo-SecureString
cmdlet is used to create a SecureString
version of the password which is required by the PSCredential
type.
Note that you’ll need to update the credential values accordingly.
Security Notice
Depending on your environment and security requirements, a more secure approach would be to use the DPAPI to encrypt the password, instead of storing the password in the backup script in plain text (this also applies to the Send-Email
function).
Alternatively, you could navigate to the ‘Backups’ directory via File Explorer, then enter the credentials when prompted and check the ‘Remember my credentials’ checkbox to store the password in the Windows Credential Manager.
Creating a new drive
The next line of code uses the New-PSDrive
cmdlet to create a temporary in-memory drive that we can use to connect to the network drive. We can then reference the drive by our chosen drive letter, in this case, J.
# Create a temporary in-memory drive (J:). New-PSDrive -Name J -PSProvider FileSystem -Root $destination -Credential $credentials -Persist
The Root
parameter is set to the value of the $destination
variable initialised earlier and the $credentials
variable is used to supply the necessary credentials for connecting to the drive.
Note that if you have stored the credentials in the Windows Credentials Manager for the network share then you don’t need to create a drive.
Creating a new folder
After creating a drive, the script proceeds to create a new dated folder for today’s backup.
# Create a dated folder for today's files e.g. J:\ImportantFiles\2022-07-14 $datedDestinationDirectory = "J:\ImportantFiles\$((Get-Date).ToString('yyyy-MM-dd'))" New-Item -ItemType Directory -Path $datedDestinationDirectory
The Get-Date
cmdlet returns a DateTime
object that represents the current date/time and converts this into a suitable format using the ToString
method.
The full path that is being used underneath will be something like the following.
\\BACKUP\Backups\ImportantFiles\2022-07-14
The New-Item
cmdlet is used to create the new directory on the network drive. Notice that the ItemType
parameter has been specified as Directory
, the New-Item
cmdlet can also be used to create files, symbolic links, and several other item types.
Note that the script is assuming that the script will be run once per day. If this is not the case for your situation, you’ll need to adjust the code to also include the time or check if the destination folder already exists and if so append something else to the directory name to ensure it is unique.
Copying files
After creating a new directory to hold today’s backup data, the script then proceeds to copy the files from the source directory to the destination directory.
# Copy files from the source folder to the destination drive folder. Write-Host "Copying files..." Copy-Item -Path $source -Destination $datedDestinationDirectory -Recurse -Force
The Write-Host
cmdlet is used to write a message to the terminal. This is very useful when running the script on demand as it allows you to see what stage your script is currently at in its execution.
The Copy-Item
cmdlet is used to copy the files from the source to the destination recursively by specifying the Recurse
parameter. This ensures that all files, including files contained within sub-directories, will be copied.
Note that alternatively, you can use the Move-Item
cmdlet to move files if you are looking for an archiving solution.
Deleting old files
When it comes to backups, in most cases we don’t want to keep copies of older backup files forever. It usually makes sense to decide upon a retention period and remove backups that are older than a calculated date.
# Only retain backups for 30 days. $destinationBaseDirectory = "J:\ImportantFiles" $backupLimit = (Get-Date).AddDays(-30) # Delete backup sub-folders that are older than the backup retention limit. Write-Host "Deleting old files..." Get-ChildItem -Path $destinationBaseDirectory -Force | Where-Object { $_.PSIsContainer -and $_.CreationTime -lt $backupLimit } | Remove-Item -Force -Recurse Write-Host "Backup completed successfully"
The above code initialises the $destinationBaseDirectory
variable with the path to the ‘ImportantFiles’ directory that we have been backing up files to.
Warning
It is extremely important to make sure you set the directory variables in your backup script to the correct directory, otherwise, you could end up accidentally deleting a bunch of files that you didn’t intend to!
The $backupLimit
variable is initialised to a date that is 30 days in the past by passing a value of -30 into the AddDays
method of the DateTime
object that is returned by the Get-Date
cmdlet.
The next few lines of code are chained together using the pipe (|) character.
First, the Get-Item
cmdlet is used to get all items contained within the specified path.
The results from the Get-Item
cmdlet are passed to the Where-Object
cmdlet which filters the items. Only items that are containers (i.e. directories) are included, and only items where their creation time is less than the $backupLimit
date.
The items that have been filtered are then passed to the Remove-Item
cmdlet which deletes them.
Errors
If an error occurs within the Try
block where the backup logic is located, the code will fall into the catch
block where the error is handled.
catch { # Alert details of the error. $exitCode = 1 $errors = Get-AllExceptionMessages -Exception $_.Exception Write-Host "Important files backup FAILED: $($errors)" Send-Email -Subject "Important Files Backup (ERROR): File transfer failed" -Body $errors }
Normally if a program/script succeeds the Exit Code returned will be 0. Since an error has occurred the $exitCode
variable is set to a value of 1 to indicate that something went wrong. You can choose to return different exit codes from your backup script depending on the type of error.
Within a PowerShell Catch
block, the exception that was thrown can be referenced via $_.Exception
. The exception object is passed to the Get-AllExceptionMessages
function that is defined near the top of the script. The function result will contain the base exception message, as well as the exception messages of any inner exceptions that there may be.
The errors are written to the terminal using the Write-Host
cmdlet. Alternatively, if you want the exception stack trace you may choose to simply call the ToString
method on the Exception
object.
The Send-Email
function that was defined near the top of the script is used to send an email, alerting us of the backup problem with the errors included in the message body for reference.
Cleaning up
The finally
block is where clean-up operations take place.
finally { # Clean up the drive allocation. Remove-PSDrive J Write-Host "Script finished" exit $exitCode }
The Remove-PSDrive
cmdlet is used to remove the temporary in-memory drive that was created earlier. This helps to ensure that the script doesn’t have any problems creating a new drive with the same drive letter the next time the script is executed.
The last line of code uses the PowerShell exit
keyword to exit the script and at the same time sets the Exit Code according to the value of the $exitCode
variable.
Script automation
Now that we have a working backup script, we can test it by running it on demand from our terminal or by debugging it within the PowerShell ISE.
However, to make the script useful, we need to set it up to run automatically. The Windows Task Scheduler is a great solution for running programs and scripts on a schedule.
I’m not going to go into a lot of detail on how to use the Task Scheduler, but I will provide a few tips in the following sub-sections before wrapping up.
Quick launch
You can quickly launch the Task Scheduler from the ‘Run’ dialog (WIN + R) by typing the following command and then pressing the Enter/Return key.
taskschd.msc
Once opened, click on the ‘Task Scheduler Library’ node on the left-hand side of the window to view existing scheduled tasks that have not been categorised into a folder.
Basic task
You can use the ‘Create Basic Task…’ link on the right-hand side of the Task Scheduler window to open a wizard that will guide you step by step through the creation of a basic task.
When you get to the ‘Action’ section, make sure you have selected the ‘Start a program’ option, then fill in the program/script fields as follows.
Program/script
powershell.exe
Add arguments (optional)
-ExecutionPolicy Unrestricted -NonInteractive -NoProfile -File C:\Scripts\ImportantFilesBackup.ps1
Start in (optional)
C:\Scripts
Note that you will need to change ‘C:\Scripts’ and the PowerShell filename accordingly.
The arguments that are specified above help to ensure reliable script execution regardless of the Execution Policy of the current machine and any PowerShell profiles that may exist are ignored.
Additional properties
At the end of the basic wizard, you can check the ‘Open the Properties dialog for this task when I click Finish’ checkbox.
From the properties dialog, you can change additional properties that affect how the task is executed. Depending on your needs you may need to consider updating things like which user account the task should run under and whether or not to run with highest privileges.
Summary
This article has demonstrated how to use PowerShell to back up files to a network drive or shared folder.
I started by providing a full code listing of a template script and then walked through the key parts to help you understand what the script is doing and how you can adjust it to suit your specific needs.
The example script can back up data, remove old data, and send email alerts in the case of an error. Since PowerShell is a .NET scripting language you can leverage the full power of .NET to make the script do practically anything you need it to.
Before wrapping up, I also touched on the Windows Task Scheduler and how you can use it to run your backup script automatically on a schedule.
Comments
Krzysztof
Hello, thx for awsome script for backups but i have one small error.
for security reason i want to use Windows Credential Manager ( i have saved password and form windows explorer i can access to my shared folder without entering password).
in you script i I cleared the fields
$username = “”
$password = ConvertTo-SecureString “” -AsPlainText -Force
but then script dont map J: drive
September 5, 2022when i enter my login and password it`s work great
what i must change in script to use password saved in ?Windows Credential Manager
Jonathan Crozier
Hi there.
To achieve that you should remove the
$username
,$password
, and$credentials
variables.You should also remove the code relating to the PSDrive and update the paths that reference “J:” to match the path to your file share instead.
September 6, 2022Krzysztof
thx now its work 😉
maybe someone also have other problem like me with copying files.
i have too long path in files on my local drive so i simply replace Copy-Item with robocopy and add full logs from robocopy to J: drive
robocopy $source $datedDestinationDirectory /E /NP /R:5 /W:5 /MT:32 /TBD /Z /ZB /log:”$datedDestinationDirectory\backup_log.txt”
I still have to fix the encoding in the robocopy log file backup_log.txt because I don’t have all the characters and adding “chcp 1250” it doesn’t help but it’s not that important for now script working and copying all my files.
September 7, 2022Krzysztof
I also fixed the log encoding, just use /unilog instead /log
September 7, 2022Krzysztof
during longer use other small bugs emerged. when i use robocopy to deal with copy files with long path now script can’t delete older backup because the names are too long on J: drive. i fix this with replacing line:
October 6, 2022Get-ChildItem -Path $destinationBaseDirectory -Force
with
Get-ChildItem -LiteralPath \\?\$destinationBaseDirectory -Force
but there is one thing I can’t fix.
in emails send to me where, for example, there are some bugs i dont have polish characters but question marks like this: Nie mo?na odnale?? cz??ci ?cie?ki ?
i try add to script $PSDefaultParameterValues[‘*:Encoding’] = ‘utf8’
but with no luck :/
Jonathan Crozier
Hi again, try adding
October 6, 2022-Encoding utf8
to theSend-MailMessage
cmdlet.Krzysztof
Thanks for the advice. for body in email it`s work and now i have all characters but in subject i still have question marks. very strange but i for now i must live with that 😉
October 7, 2022Sathik
Hi there,
how to execute the script in task scheduler. I Tried to run, it’s not working.
September 1, 2023Jonathan Crozier
Hi Sathik, I assume you have followed the steps in the ‘Script automation’ section of this post? Are you able to share some information regarding how you have configured your scheduled task and what the exit code is when the task attempts to run?
September 1, 2023