Share to Mastodon
Many Linux users have long depended on rsync
as a versatile copy and
backup tool. I've used it myself the few times I've had Linux as my daily driver OS. Since switching
back to Windows, I couldn't find a good enough tool that worked the same way as rsync
, so I stopped
observing International Backup Awareness Day for a while
(this day,
not this one).
I eventually got tired of the anxiety of possibly losing all my data one day, and decided to do an honest
search for a Windows alternative to rsync
. It didn't take long for me to find
Robocopy.
After playing around with Robocopy for a bit, I realized it was too complicated on the surface to use
as a standalone tool. So, I created a simple backup script to encapsulate some of that complexity, and
I've been using it until this writing. It looks similar to the following:
(TL;DR: Skip ahead to the improved backup script)
#Requires -RunAsAdministrator
function SyncFiles {
param (
[Parameter(Mandatory)]
[string]$SourcePath,
[Parameter(Mandatory)]
[string]$DestinationPath,
[Parameter(Mandatory)]
[string]$LogPath
)
robocopy "$SourcePath" "$DestinationPath" *.* /mir /fft /zb /w:5 /eta /unilog:"$LogPath" #/l
}
$drivePath = "G:"
$driveExists = Test-Path $drivePath
if ($driveExists) {
try {
Write-Host "Syncing files..."
SyncFiles -SourcePath (Get-Item ~\Documents).FullName -DestinationPath "$drivePath\Documents" -LogPath "$drivePath\Documents.log"
SyncFiles -SourcePath (Get-Item ~\Pictures).FullName -DestinationPath "$drivePath\Pictures" -LogPath "$drivePath\Pictures.log"
Write-Host "Sync completed!"
}
catch {
Write-Host "A problem was encountered while syncing files:"
Write-Host $_
}
}
else {
Write-Host "Backup drive is not present. Please mount the $drivePath drive."
}
It's simple and does the job, but I think I can make some improvements to it. Before that, let's go over
how this script works, starting with the robocopy
command.
According to the
docs,
the robocopy
command has the following syntax:
robocopy <source> <destination> [<file>[ ...]] [<options>]
Robocopy has a ton of options. I'll quickly summarize the few options I'm using in the backup script.
Option | Description |
---|---|
/mir | Mirrors a directory tree (equivalent to /e plus /purge). Using this option with the /e option and a destination directory, overwrites the destination directory security settings. |
/fft | Assumes FAT file times (two-second precision). |
/zb | Copies files in restartable mode. If file access is denied, switches to backup mode. (combines /z and /b options) |
/w:5 | Specifies the wait time between retries, in seconds. The default value of n is 30 (wait time 30 seconds). In this case n is 5. |
/eta | Shows the estimated time of arrival (ETA) of the copied files. |
/unilog:"$LogPath" | Writes the status output to the log file as Unicode text (overwrites the existing log file). $LogPath is a self-explanatory variable. |
/l | Specifies that files are to be listed only (and not copied, deleted, or time stamped). (Serves as a 'dry run' but commented out in my example) |
In the script, I'm syncing the $SourcePath
files to the $DestinationPath
via /mir
, ensuring deleted
items in the former are purged in the latter. /zb
ensures that the copy continues where it left off if
interrupted (/z
), and that file permissions are ignored when copying (/b
). The /b
option is why we
need #Requires -RunAsAdministrator
at the top of the script, which ensures it doesn't run unless in
Administrator mode. If a file fails to copy, /w:5
ensures that we don't wait the default 30 seconds to
retry. /eta
doesn't display in the terminal with my script, likely because I'm logging to a file with
/unilog
. I don't use /l
unless I'm testing the script, so it gets left commented out. Finally, /fft
ensures files can be copied correctly between different file systems
(more on this here).
There are some problems with the way I call robocopy
here. The /eta
options is useless while I'm
logging to a file. It can be safely omitted. w:5
is good for ensuring there isn't a long wait time
between retries of failed copies, but without a retry limit, the copy will be reattempted 1,000,000
times.
At 5 seconds per retry, that's 57 days 20 hours and 53 minutes. The /r:<n>
option can be used to
limit retries to a more reasonable amount, like 5 for instance. The script also follows symbolic links
by default. This may not be desirable behaviour. The /sl
option will ensure that only the link itself
will be copied.
There are some other issues as well. I'm currently copying files over regardless of whether they were
modified or not. I'm also needlessly copying files such as those in node_modules
directories. These
issues will either use up unnecessary space and/or lengthen the time of the backup. The following
options will remedy these issues:
Option | Description |
---|---|
/xd <directory>[ ...] | Excludes directories that match the specified names and paths. |
/xo | Excludes older files. |
With the above improvements, our robocopy
command now looks like the following as encapsulated in a
function:
function SyncFiles {
param (
[Parameter(Mandatory)]
[string]$SourcePath,
[Parameter(Mandatory)]
[string]$DestinationPath,
[Parameter(Mandatory)]
[string]$LogPath
)
robocopy "$SourcePath" "$DestinationPath" *.* /mir /fft /zb /sl /xo /xd "node_modules" /w:5 /r:5 /unilog:"$LogPath"
}
It's a bit silly to hardcode node_modules
in this function, so let's add a parameter to the SyncFiles
function. While we're at it, we can also add a parameter to execute robocopy
in dry run mode.
function SyncFiles {
param (
[Parameter(Mandatory)]
[string]$SourcePath,
[Parameter(Mandatory)]
[string]$DestinationPath,
[Parameter(Mandatory)]
[string]$LogPath,
[Parameter()]
[string[]]$ExcludedDirs,
[Parameter()]
[switch]$DryRun
)
robocopy "$SourcePath" "$DestinationPath" *.* `
/mir /fft /zb /sl /xo /xd $ExcludedDirs /w:5 /r:5 /unilog:"$LogPath" `
$(if ($DryRun) { "/l" } else { "" })
}
Much better, if a bit opinionated. The rest of the script is a lot more opinionated, since I'd prefer to make my backup experience as painless as possible. Otherwise, I might just procrastinate on backing my data up until the worst happens. While I might not change the files I want backed up very frequently, I may want to backup to a different drive or path than usual. All I have to do is make the backup path a parameter that is supplied to the script and we have the finished product as shown below.
#Requires -RunAsAdministrator | |
param( | |
[Parameter(Mandatory)] | |
[string]$BackupPath | |
) | |
# You can use this function on its own instead of using the entirety of this script | |
function SyncFiles { | |
param ( | |
[Parameter(Mandatory)] | |
[string]$SourcePath, | |
[Parameter(Mandatory)] | |
[string]$DestinationPath, | |
[Parameter(Mandatory)] | |
[string]$LogPath, | |
[Parameter()] | |
[string[]]$ExcludedDirs, | |
[Parameter()] | |
[switch]$DryRun | |
) | |
robocopy "$SourcePath" "$DestinationPath" *.* ` | |
/mir /fft /zb /sl /xo /xd $ExcludedDirs /w:5 /r:5 /unilog:"$LogPath" ` | |
$(if ($DryRun) { "/l" } else { "" }) | |
} | |
$backupPathExists = Test-Path $BackupPath | |
if ($backupPathExists) { | |
try { | |
Write-Host "Syncing files..." | |
# Add or remove directories as necessary | |
SyncFiles ` | |
-SourcePath (Get-Item ~\Documents).FullName ` | |
-DestinationPath "$BackupPath\Documents" ` | |
-LogPath "$BackupPath\Documents.log" ` | |
-ExcludedDirs "node_modules" | |
SyncFiles ` | |
-SourcePath (Get-Item ~\Pictures).FullName ` | |
-DestinationPath "$BackupPath\Pictures" ` | |
-LogPath "$BackupPath\Pictures.log" | |
SyncFiles ` | |
-SourcePath (Get-Item ~\Downloads).FullName ` | |
-DestinationPath "$BackupPath\Downloads" ` | |
-LogPath "$BackupPath\Downloads.log" ` | |
-DryRun | |
Write-Host "Sync completed!" | |
} | |
catch { | |
Write-Host "A problem was encountered while syncing files:" | |
Write-Host $_ | |
} | |
} | |
else { | |
Write-Host "Backup path $BackupPath can not be found." | |
} |
You can run this script, after customizing the directories you'd like backed up, with a simple command.
# Backup to a drive...
.\EasyRobocopyBackup.ps1 -BackupPath G:
# ... or a path
.\EasyRobocopyBackup.ps1 -BackupPath .\some_path
# Remember to run the script as an Administrator
If you need to test backing up to a new directory, you can use the -DryRun
switch on the SyncFiles
function. I could have added this as another parameter to the main script, but I figured once I've
established the directories I want to have backed up, I would seldom need it.
And there you have it, an easy template to help make backing data up on Windows a bit easier. Let me know in the Gist if you experience any issues or have any suggestions for improvements. And always remember...
Every day is International Backup Awareness Day -- Jeff Atwood, Coding Horror, 2009#