File backup to network drive using PowerShell

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.


I hope you enjoyed this post! Comments are always welcome and I respond to all questions.

If you like my content and it helped you out, please check out the button below 🙂

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
when i enter my login and password it`s work great
what i must change in script to use password saved in ?Windows Credential Manager

September 5, 2022

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, 2022

Krzysztof

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, 2022

Krzysztof

I also fixed the log encoding, just use /unilog instead /log

September 7, 2022

Krzysztof

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:
Get-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 :/

October 6, 2022

Jonathan Crozier

Hi again, try adding -Encoding utf8 to the Send-MailMessage cmdlet.

October 6, 2022

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, 2022

Sathik

Hi there,

how to execute the script in task scheduler. I Tried to run, it’s not working.

September 1, 2023

Jonathan 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