PowerShell script to run Duplicati GUI jobs

tl;dr
Made a PS 5.1 script that runs Duplicati GUI jobs based on the day of the week. The GUI is always up-to-date, users can change Source selections, advanced options or do restores from the GUI. The script will try to run the backup job for the current day of the week, if today’s drive is not found it uses the connected drive, if more than one drive is connected it uses today’s backup job or the least recently used backup job to help rotate backup sets. Drives can move from one drive letter to another without issue.

Good day, I’ve recently started using Duplicati and I quite enjoy it. Thank you to all those that help make Duplicati what it is.

I’m an IT consultant that supports mostly small businesses and I’m always being asked to implement a “better backup, for as little as possible”. With that, the Duplicati price tag fits the bill but most of these clients seem to only want to backup to local USB disks which Duplicati doesn’t do multiple disks without a bunch of workarounds.
Also many clients aren’t great at swapping the drive on the daily so I needed a way to run a backup regardless of which drive was connected or what day it was. I also wanted the clients to be able to check the status or do a restore on their own, for which the GUI is perfect.
I looked into the using CLI but users can’t easily edit them, still requires a “calling script” and then there’s the whole doesn’t update the GUI and restores are less friendly.
The main goal being daily encrypted backups to whichever backup drive is connected every day of the week. Most of the “servers” that will be backed up with this are Win7 or Win10 workstations that are sharing a few files, acting like a small file server.
I looked at Pectojin’s Duplicati Client but it turned out to be way more than I needed. I did use a PRTG-Duplicati script by TS-Steff as a base for my script. Thanks to both of them.

After a few days, I managed to get together what I think is a pretty handy script. I’ll post the script in a separate post, the heavily commented script with setup instructions is just over 25,000 characters in length which is probably over the character limiter per post here but I’ll try and if not I’ll trim out the comments or find somewhere to host the file and link back to it.

Verbose setup instructions in script. I’ll put a link to the script here once I get it posted. Edit: The next post down contains the whole script. Thanks to drwtsn32 for getting it formatted.

The basic setup is as follows:

Install Duplicati, run the Tray Icon and create a backup job in the GUI for each day of the week. Each job uses the day of the week as it’s name and each backup drive is labeled the same.

The Tray Icon HAS TO BE RUNNING for this to work.

Jobs are NOT scheduled within Duplicati, the script determines which GUI job will run.

Create and save backup jobs once before adding any of the Advanced options, seems to be a weird issue where they vanish if added during first creation. Once saved, edit the job and add the advance options

Use “alternate-target-paths” and “alternate-destination-marker” so each drive had a folder and a unique marker file to prevent the wrong job triggering with the wrong drive connected. Also avoids backing up your data to your kids/friends/co-workers USB stick that was left in your computer by mistake. Advanced option values per job are “*:\BackupFolder” and “DayOfTheWeek_BackupsGoHere.txt”, respectively. DayOfTheWeek being a place holder for the correct day of the week.
e.g. The Monday drive is labeled “Monday” and has a folder called “BackupFolder” and a marker file in that folder called “Monday_BackupsGoHere.txt”. The Tuesday drive is labeled “Tuesday” and has a folder called “BackupFolder” and a marker file in that folder called “Tuesday_BackupsGoHere.txt”

I’ve also added and enabled “use-block-cache” on all jobs. If backing up open files is an issue add “snapshot-policy” and set it to “ON”. Note: the “Duplicati.GUI.TrayIcon.exe” will need to be setup to “Run as an administrator” or you’ll get errors about snapshots when you run the job.

With that in-place the script can now run any of those GUI jobs and uses the following logic to determine which drive to use.
-Searches for connected drives and saves each drive’s label and drive letter to an array.
-Queries the Duplicati Tray Icon for all existing backup jobs and saves each job name and job id to an array.
-If there is one drive connected and it is today’s drive, the script runs the backup job for the current day of the week.
-If there is one drive connected and it is not today’s drive, the connected drive is used instead.
-If there is more than one drive connected and one of them is today’s drive then today’s drive will be used. (As I write this I’m thinking I should re-write the multiple drives determiner to use the LRU regardless of the current day but that’s a minor thing.)
-If there is more than one drive connected but none of them are today’s drive the least recently used backup will be run. LRU determined by checking each jobs LastBackupFinished date then sorting by oldest and using that backup.
-Once the correct job is found the /run command is issued to the REST server to start that backup job.

Features:
-Works with one to seven drives even if connected simultaneously.
-Uses “the right drive for the day” or “the right drive for yesterday” or “the right drive because it’s the only one currently connected” or “the right drive we haven’t backed up to recently”. Either way, it always has a “right drive” unless there isn’t one connected.
-The script can log all it’s output to a .txt file, that currently saves to the current users Desktop folder.
-There is a test mode that does everything but send the /run command for the backup job.

While I’ve done a fair bit of testing and it all seems good I can’t be sure that this will work for you (I hope it does) but it’s on you to test for yourself in your environment. The script may work (or could probably be made to work) with lower versions <5.1 of powershell but as it stands you can get/use PS 5.1 in Win7 and up, which should cover most of the likely usage scenarios. I’m pretty sure the logic/methods could be adapted to run a similar script for Linux or macOS, but that’s a task for another day.

FYI: I don’t write code for a living so expect my formatting and use of syntax to be a bit off along with my use of camelCase or PascalCase in my variables will vary. you’ve been warned.

You made it to the end, thanks for reading, I hope this post along with the script can help someone get the backups they need or maybe just help them understand some of the code that was used. If you have any questions don’t hesitate to ask.

Have a good one.

1 Like
#region wallOfText

### Requirements ###
#
# Windows OS with PowerShell 5.1 or greater.
#
# Duplicati 2, installed and running with the Tray Icon.
#
# One or more external USB drives.
#

### What does the script do? ###
#
# Let's you backup to multiple local external USB drives on a schedule regardless of which
# drives are currently connected. This script runs GUI jobs directly so any changes
# are instantly reflected in the GUI without having to specify databases then update CLI jobs,
# no getting warnings in the GUI about missed jobs, no need to duplicate jobs between the GUI and CLI
# or worry about what drive letter was assigned and if the backup ran to your kids USB drive
# last night that was accidentally left in the machine, don't worry it won't.
#
# The script will always try to use the drive for the current day of the week. If the current
# days drive is not connected but another valid drive is, it will be used in it's place. If 
# two or more valid drives are connected, that are not the current day of the week, the 
# script will find the LRU (Least Recently Used) drive and run a backup to that drive.
#
# A "valid drive" is one with a matching drive label, contains a specific folder and
# contains a specific file in that folder for that drive. Sure it can all be easily replicated 
# but it's very unlikely for any one random USB drive to have that many matching markers.
#
# Logging all the output to a .txt file is supported just set $logToFile to $true and a log file
# will be created. There is also a "test mode" where everything except sending the very last command
# to Duplicati is enabled, should be obvious but backups will not be created when running in
# test mode.
#

### What the script does not do. ###
#
# Manage or configure Duplicati
#
# Probably won't work if you enable password access to the GUI. I didn't write the headers
# section so I'm not certain but I plan on looking into it in the future.
#
# Probably won't work if Duplicati is running as a Windows Service.
#
# Sadly, it can't make a good cup of tea.
#

### Reasons for creating the script ###
#
# I'm accustomed to Windows Server Backup and it's ability to have multiple drives and 
# if the drive doesn't get swapped, the backup will still run to the connected drive,
# keeping multiple versions per drive.
#
# Many of my clients are small businesses (sub 5 employees) that can't justify a full on
# server but still need a decent backup. With this script they can get encrypted local 
# backups to multiple external USB drives along with an encrypted cloud stored backup.
#
# Also key, is the simplicity for users. The Duplicati GUI is clean and easy to use, 
# users can at a glance see when the backup last ran or change what is being backed up.
#
# The only thing that cannot be adjusted in the Duplicati GUI is when this script 
# runs, that is defined in the Windows Task Scheduler job that runs this script.
#

### Setup Overview ###
#
# There are 4 key elements to making this work.
#
# 1.  The backup drive labels. The drive name needs to match the backup jobs name.
#     So the backup drive for Monday will be labeled as "Monday" and in Duplicati you
#     create a job named "Monday".
#
# 1a. You can specify other names in the $days array but you'll have to rework the 
#     day of the week determination functions to make it work properly. 
#     To avoid this, use the days of the week as drive and job names.
#
# 2.  To help ensure that only the proper drive gets used on the proper day we are going to 
#     use two advanced options in Duplicati. These two options are "alternate-target-paths"
#     and "alternate-destination-paths".
#
# 2a. These options let you specify a folder to place backups into along with with a 
#     handy wildcard to find the drive even if it's drive letter has changed and the 
#     other looks for a specific file in the previously specified folder. This means 
#     creating a specific folder on the drive and then placing a specific file in that folder.
#
# 2b. For example in the Monday drive create a folder and name it "YourCompanyBackups",
#     next create a file in that folder and call it "MondayBackupsInThisFolder.txt".
#
# 2c. Repeat the above process for each drive/day of the week. So the Tuesday drive
#     will be labeled "Tuesday", have a folder "YourCompanyBackups" and a file in that
#     folder called "TuesdayBackupsInThisFolder.txt"
#
# 3.  Setup a Scheduled Task in Windows Task Scheduler to run this script when you want backups
#     to occur. This can be once a day or multiple times a day.
#
# 4.  Keep the Duplicati Tray Icon running otherwise the script won't be able to run backups.
#

### Setup in Duplicati to make this work ###
#
# Each backup drive requires it's own backup job in Duplicati. You don't need a drive 
# for every day of the week, but the labels and job names do have to be a day of the week,
# unless you want to re-write the script. If you only have two drives they could be 
# Tuesday and Friday or as Saturday and Sunday, they can be out of sequence it doesn't 
# matter the script will try to use the current days drive but if it's not there the 
# other one will be used.
#
# Creating a backup job in Duplicati has a small issue at the moment where certain options
# don't show up until you've saved the job once. After reopening the job the options will
# be in the correct place. Duplicati has 6 main steps to creating a backup job.
#
# Step 1: From the Home screen (http://localhost:8200/ngax/index.html) click on "Add Backup",
#         in "General backup settings" provide a day of the week (starting with a CAPITAL letter)
#         such as "Thursday" (not "thursday"), give it a description if you feel inclined.
#         Next set a passphrase to encrypt your backups, you can disable encryption but it's
#         a nice piece of mind in-case the drive gets lost or stolen. 
#         ### EXTRA MASSIVE WARNING ### DO NOT LOOSE THE PASSPHRASE!!! Your backups are 
#         useless encrypted garble if you don't know the passphrase. You can also just 
#         disable "Encryption" by setting it to "No Encryption". Click "Next >"
#
# Step 2: Choosing the Backup destination. By default Duplicati sets "Storage Type" to 
#         "Local folder or drive" which is what you want. With the backup drive for 
#         that day connected select it from the "Folder path" list. Click "Next >"
#
# Step 3: Choosing the Source data (what to backup). Pick the folders you want to have
#         backed up, if you choose a whole drive you'll probably want to add "System files"
#         to the Exclude list. Click "Next >"
#
# Step 4: Schedule. We don't want Duplicati to run jobs on it's own so remove the checkmark
#         beside "Automatically run backups." Click "Next>"
#
# Step 5: General options, leave "Remote volume size" at the default of 50MB, weird things
#         can happen when this setting is changed. I change the "Backup retention" to 
#         "Smart backup retention" but that's up to you. We have to add some advanced options
#         but first let's save the job by clicking on "Save".
#
# Step 5a:From the Home screen click the down arrow to the right of the job name to show more
#         details about the job. In the Configuration: section click on "Edit..." and you're
#         back at Step 1, you can click on the dots with numbers 1-5 at the top to jump to 
#         that section or click "Next>" until you get back to Step 5. Next click on 
#         "Advanced options" then in the "Add advanced option" drop-down find "use-block-cache:"
#         located under the "Core options" heading. Place a checkmark to enable it.
#         
#         This option stores the block cache in RAM vs your TEMP folder (likely on your C: drive),
#         if the host system is very low on available RAM then this could cause issues or 
#         slowdowns but chances are most host systems will have plenty enough RAM, a hundred
#         or so free megs (MB) will do. By default during a backup Duplicati writes a bunch 
#         of files to your TEMP folder over and over again, possibly causing more wear/tear,
#         read/write cycles than you want to expose your main drive to. Remember even slow 
#         system RAM is still alot faster than a decent SSD, RAM has nearly no read/write 
#         limit unlike your SSD or HDD.
#
# Step 5b:Again from the "Add advanced option" drop-down find "alternate-target-paths:" under
#         the "Local folder or drive" heading. (the menu scrolls quite quickly, once you click 
#         on the drop-down you can use the up/down arrow keys on your keyboard to scroll) 
#         In the textbox type without quotes "*:\YourCompanyBackups". The folder name can be 
#         different but the "*:\" has to be written as shown, "*" an asterix is a special Duplicati 
#         wildcard that only applies to drive letters in a Windows environment and can only 
#         be used with this one option.
#
# Step 5c:Once more from the "add advanced option" drop-down find "alternate-destination-marker"
#         under the "Local folder or drive" heading. In the textbox type the name of the 
#         marker file, if this was the Monday job name name it "MondayBackupsInThisFolder.txt"
#         or "TuesdayBackupsInThisFolder.txt" for the Tuesday job. The filenames can be 
#         whatever you want but they need to be unique, if two jobs share the same marker file
#         Duplicati can get confused and attempts to compare the wrong file blocks to the wrong 
#         database and a whole pile of errors are generated and most of the time the backup 
#         sets seem to get damaged beyond repair.
#
# Step 6: (Optional) If you need to deal with open files you'll need to add the "snapshot-policy"
#         and set it to "ON" for now. Once you know elevation works switch it to "AUTO". To 
#         access the VSS service the Duplicati Tray Icon will need to be run as an administrator
#         in an elevated state. Locate the exe file for the Tray Icon (Duplicati.GUI.TrayIcon.exe)
#         which should be located in "C:\Program Files\Duplicati 2\", right-click on it as 
#         select "Properties", next click on "Change settings for all users" then place a 
#         checkmark in the box for "Run this program as an administrator". Restart the Tray Icon,
#         clicking Yes when prompted. At this point run the job, if elevation did work it should 
#         run without error.
#

### Schdule this script to run in Windows Task Scheduler###
#
# If you haven't, yet save this script to a safe location on your computer. You will need to know 
# where you saved it to point the scheduler to it. I would suggest somewhere out of the way so it 
# won't get moved or deleted, a folder like "C:\Folder_For_Script" but you can use whatever you fancy.
#
# To open the Task Scheduler, you can search for it by name "Task Scheduler" or you can find it on the 
# Start menu under the "Windows Administrative Tools" folder. The icon is a clock with 12 to 3 filled
# in with orange.
#
# Once Task Scheduler opens click on "Task Scheduler Library", next on the right hand side under the
# "Actions" list click on "Create Basic Task...". You can also right-click on "Task Scheduler Library"
# and click "Create Basic Task..." from there.
#
# The Create Basic Task Wizard will open asking you a few questions. First off give the task a name,
# something that makes sense like "Run Duplicati Backup" or "Daily Backup" and give it a description
# if you want click "Next>". We'll presume you want to run the task daily (default option) so click
# "Next>". Now it wants to know when to run the task, pick a time that's good for you and you probably
# want the task to recur every day so click "Next>". This time it wants to know what to do, which is 
# "Start a program" so click "Next>". In the "Program/script:" box type or copy from here, 
# "%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe" then in the "Add arguments (optional)"
# type "-File" (without quotes), a space then the quoted path to this script. 
# So the line should look like: -File "C:\Folder_For_Script\Script_Name.ps1". The last page will give
# you a summary of the task that's being created, click "Finish".
#
# The wizard will close and the task list will refresh showing your new task at the bottom of the list.
# You can double-click on the task to edit it's options. A few settings worth looking at if you're 
# not familiar with tasks are the Conditions and Settings tabs.
#
# At this point should be able to run the task, with the task highlighted click "Run" in the "Actions"
# list or right-click on the task and choose "Run". Open the Duplicati Home screen and you should see
# the status changes. The first backup will take roughly as long as it would take to make create a
# manual copy of all the selected files, once the initial backup is complete future backups will be 
# much faster.
#
### Credits ###
#
# TS-Steff for their PRTG-Duplicati script which served as a base for this script.
# https://github.com/TS-Steff/PRTG-Duplicati/blob/master/duplicati.ps1
##
# Pectojin for showing that it could be done.
# https://github.com/Pectojin/duplicati-client/blob/master/duplicati_client.py#L881
# This "/api/v1/backup/backupID/run" is the key to running GUI jobs via scripting.
##
# Everyone else that has helped in the past and anyone that is helping to make Duplicati what it is.
##
# Thank you all!
###

#endregion

### On to the script ###

# Enable to prevent running backups, set to false if you want backups to run!
$TestMode = $false
#$TestMode = $true

# Warning that gets added to screen/logs when $TestMode is enabled.
$TestModeWarning = "Test Mode Enabled -- Backups will NOT run"

# Enable log file generation
$LogToFile = $true

# Path/filename to save the log file to. By default it saves to the current users Desktop
# with a filename of "PS_BackupLog.txt".
$LogPath = "C:\Users\$env:USERNAME\Desktop\PS_BackupLog.txt"

# Makes a backup log of everything that happens in the script and saves it to a log file.
# Note: Stop-Transcript, near the end of the file must be in place or the log won't be saved.
if($LogToFile){
    Start-Transcript -Append -Path $LogPath
}

# Clear the screen
cls

# Host and port values, adjust to match your config. $urlHost can be an IP or DNS name.
$urlHost = "localhost"
$urlPort = "8200"

# Base URLS 
$urlHttp = "http://"
$url = "$($urlHttp)$($urlHost):$($urlPort)/index.html"

# Location URLS
$urlApi = "api"
$urlVer = "v1"

# API URLS
$urlApiVer = "$($urlHttp)$($urlHost):$($urlPort)/$($urlApi)/$($urlVer)"
$urlBackups = "$($urlApiVer)/backups/"
$urlBackup = "$($urlApiVer)/backup/"

# Cheap Error Logging for one error...
$errors = New-Object System.Collections.ArrayList
$failed = $false

# Date and Time related
$TodayDate = Get-Date -Format "MMM-dd-yyyy"
$TodayDay = Get-Date -Format "dddd"
$TodayTime = Get-Date -Format "HH:mm"

# Days of the week.
$Days = @("Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday")

# Discovered USB drives
$FoundDrive = "Not Good"
$FoundDrives = New-Object System.Collections.ArrayList
$FoundJobs = New-Object System.Collections.ArrayList
$FoundDriveCount = 0

# Drive letter
$DriveLetter = ""

# Array of recently completed backups
$RecentBackups = New-Object System.Collections.ArrayList

### Some headers 'magic' here thanks to TS-Steff. Not sure exactly how it works, but it does. ###
# I suspect this may have authentication issues if the server is password protected, something for another day.

# start by loading duplicati index.html
$headers = @{
"Accept"='text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8';
"Upgrade-Insecure-Requests"="1";
}

Invoke-WebRequest -Uri $url -Method GET -Headers $headers -SessionVariable SFSession -UseBasicParsing | Out-Null

# Gets required tokens
$headers = @{
"Accept"='application/xml, text/xml, */*; q=0.01';
"Content-Length"="0";
"X-Requested-With"="XMLHttpRequest";
"X-Citrix-IsUsingHTTPS"="Yes";
"Referer"=$url;
}

Invoke-WebRequest -Uri ($url) -Method POST -Headers $headers -WebSession $sfsession -UseBasicParsing | Out-Null

$xsrf = $sfsession.cookies.GetCookies($url)|where{$_.name -like "xsrf-token"}

# Setting the Header
$Headers = @{
    "X-XSRF-Token" = [System.Net.WebUtility]::UrlDecode($xsrf.value)
    "Cookie" = "$xsrf"
}

### End of headers stuff ###

### Functions ###

# Runs the specified GUI backup job
function Backup-Run($backupID){

    $urlToRunBackup = $urlBackup+$backupID+"/run"    
    $RunBackup = Invoke-RestMethod -Uri $urlToRunBackup -Method POST -Headers $Headers
    write-host $(Get-JobName($backupID))"job with ID:"$backupID" submitted to Duplicati." -f Green
}

# Compares drives
function Compare-RecentBackups{
    
    $tempArray = @()
    for($i=1;$i -lt $RecentBackups.Count;$i+=2){
        $tempItem = $RecentBackups[$i]
        $tempArray += $tempItem
    }
    $tempArray = $tempArray | Sort
    $oldestJob = $tempArray[0]
    for($i=1;$i -lt $RecentBackups.Count;$i++){
        if($oldestJob -eq $RecentBackups[$i]){
            $JobID = $RecentBackups[$i -1]
        }
    }
    $JobName = Get-JobName($JobID)
    $oldestJob = $oldestJob -split " "
    $leastRecentJob = "The current LRU drive is the "+$JobName+" drive with JobID "+$JobID+", it last ran "+$oldestJob[0]+" @ "+$oldestJob[1]+"."
    write-host $leastRecentJob -f Yellow
    write-host $JobName" job with ID: "$JobID" is currently the oldest backup."
    
    return $JobID
}

# Runs Get-BackupInfo and Get-Backups
function Find-BackupJobs{

    $backup_info = Get-BackupInfo(Get-Backups)

    #return $backup_info
}

# Gets the list of backup jobs defined in the GUI
function Get-Backups{
    
    $backups = Invoke-RestMethod -Uri $urlBackups -Method GET -Headers $Headers -ContentType application/json
    
    $backups = $backups -replace "`t|`n|`r",""

    $backups =  $backups.Substring(6)
    $backups = $backups.Substring(0,$backups.Length -2  )

    $arrBackups = $backups -split '},  {'

    for ($i=0; $i -lt $arrBackups.Length; $i++){
   
        if($i -eq 0){
            $arrBackups[0] = $arrBackups[0] + "}"
        }else{
            $arrBackups[$i] = "{" + $arrBackups[$i] + "}"
        }  
    }

    foreach ($backup in $arrBackups){

        $json=ConvertFrom-Json $backup
        [void]$FoundJobs.Add($json.Backup.Name)
        [void]$FoundJobs.Add($json.Backup.ID)
        $backup_ids += @($json.Backup.ID) 
    }
    
    Get-Drives
    #return $backup_ids
}

# Gets the backup ID for a named job
function Get-BackupID($JobName){

    for($i=0;$i -lt $FoundJobs.Count;$i++){
        if($FoundJobs[$i] -eq $JobName){
            $JobID = $FoundJobs[$i+1]
        }
    }
    return $JobID
}

# Gets details for a backup job
function Get-BackupInfo($JobID){

    $JobData = Invoke-RestMethod -Uri $urlBackup$JobID -Method GET -Headers $Headers -ContentType application/json
    $JobData = $JobData.Substring(3)
    $JobData = ConvertFrom-Json $JobData

    return $JobData
}

# Searches for connected USB drives
function Get-Drives{

    #For each day of the week check if there is a drive connected with the same name
    $Days | % { 
	    $Drive = Get-Volume -FileSystemLabel $_ -ErrorAction:SilentlyContinue 
	    if($?)
	    {
		    $FoundDrive = $_
		    $DriveLetter = $Drive.DriveLetter
		    write-host "Found"$_ "drive at letter"$DriveLetter":" -f Green
            [void]$FoundDrives.Add($_)
            [void]$FoundDrives.Add($DriveLetter)
		    $FoundDriveCount++
	    }
	    else
	    {
		    write-Host "The"$_" drive was NOT found." -f Red
	    }
    }
    write-host
    write-host "Finished searching for drives."
    Show-Drives
    Show-Jobs

    if($FoundDriveCount -gt 0){
        if($FoundDriveCount -eq 1){
            write-host "Found 1 drive"
            if($FoundDrives[0] -eq $TodayDay){
                write-host "Today's backup drive found, running today's backup to it."
                if($TestMode){
                    write-host $TestModeWarning -f Red
                }
                else{
                    Backup-Run(Get-BackupID($TodayDay))
                }
            }
            else{
                write-host "Today's backup drive was NOT found," -f Yellow
                write-host "The $FoundDrive drive was the only one found." -f Yellow
                if($TestMode){
                    write-host $TestModeWarning -f Red
                }
                else{
                    Backup-Run(Get-BackupID($FoundDrive))
                }
            }
        }
        else{
            write-Host "Found"$FoundDriveCount" drives."
            write-host "More than one drive detected." -f Yellow
            $FoundDrives | % {
                if($_ -eq $TodayDay){
                    write-host "Today's backup drive found, running today's backup to it."
                    if($TestMode){
                        write-host $TestModeWarning -f Red
                    }
                    else{
                        Backup-Run(Get-BackupID($TodayDay))
                    }
                }
            }
            write-host "Today's drive was not found."
            write-host "Searching for the LRU(Least Recently Used) drive..."

            for($i=0;$i -lt $FoundDrives.Count;$i+=2){

                $driveID = Get-BackupID($FoundDrives[$i])
                $driveInfo = Get-BackupInfo($driveID)
                $lastFinish = $driveInfo.data.Backup.Metadata.LastBackupFinished
                [void]$RecentBackups.Add($driveID)
                $lastDate = [datetime]::ParseExact($lastFinish,'yyyyMMddTHHmmssZ',$null)
                [void]$RecentBackups.Add($lastDate)
            }

            $JobToRun = Compare-RecentBackups

            if($TestMode){
                write-host $TestModeWarning -f Red
            }
            else{
                Backup-Run($JobToRun)
            }
        }
    }
    else{
        write-host "###############################################" -f Yellow
        write-host "No Backup Drives Found!!! Unable to run backup." -f Red
        write-host "###############################################" -f Yellow
        $failed = $true
        $errors = $errors+"Error #1: No Backup Drives Found, Backup CANCELLED!"
    }
    Show-Footer
}

# Gets the name of the job with the specified ID [int]
function Get-JobName($JobID){

    for($i=1;$i -lt $FoundJobs.Count;$i++){
        if($JobID -eq $FoundJobs[$i]){
            $JobName = $FoundJobs[$i -1]
        }
    }

    return $JobName
}

function Get-ServerState{

    $urlServerState = "$($urlApiVer)/serverstate"
    $ServerData = Invoke-RestMethod -Uri $urlServerState -Method GET -Headers $Headers -ContentType application/json
    $ServerData = $ServerData.Substring(3)
    $ServerData = ConvertFrom-Json $ServerData
    #write-host "Dump Backup" $ServerData.data.Backup
    return $ServerData
}

# Changes a boolean into a Yes or No string
function Get-YesNo($bool){

    if($bool){
        $answer = "Yes"
    }
    else{
        $answer = "No"
    }

    return $answer
}

# Lists connected USB drives along with their drive letter
function Show-Drives{

    write-host 
    $half = $FoundDrives.Count /2
    write-host $half" Backup drives found."

    for($i=0;$i -lt $FoundDrives.Count;$i+=2){
        $Label = $FoundDrives[$i]
        $Letter = $FoundDrives[$i+1]
        write-host $Label" drive @ "$Letter":" -f Green
    }
    write-host
}

# List only the drive labels for connected USB drives
function Show-DriveLabels{

    write-host
    write-host "Drive Labels" -f Green
    for($i=0;$i -lt $FoundDrives.Count; $i+=2) {$FoundDrives[$i]}

}

# List the drive letter (no colon ":") for connected USB drives
function Show-DriveLetters{

    write-host
    write-host "Drive Letters" -f Green
    for($i=1;$i -lt $FoundDrives.Count; $i+=2){$FoundDrives[$i]}
    write-host
}

# Show any errors
function Show-Errors{

    $errors | % {
        write-host $_ -f Red
    }
}

# Prints out a text footer to finish things off
function Show-Footer{

    write-host
    if($failed){
        write-host "Script Completed with"$errors.Count"error(s)."
        Show-Errors
        write-host "FIN :("
    }
    else{
        write-host "Script Completed without errors." -f Green
        write-host "FIN :)"
    }
    write-host
}

# Prints out a text header with the date and time
function Show-Header{

    write-host "------------------------------------------------------------------" -f White
    if($TestMode){
        write-host $TestModeWarning -f Red
    }
    write-host "Today is"$TodayDate", it's a "$TodayDay" at $TodayTime."
    write-host
    write-host "Searching for connected backup drives..."
    write-host
    Get-Backups
}

# Lists backup jobs found in the GUI along with their ID
function Show-Jobs{

    $half = $FoundJobs.Count /2
    write-host $half" Backup jobs found in Duplicati server." 
    for($i=0;$i -lt $FoundJobs.Count;$i+=2){
        $Label = $FoundJobs[$i]
        $ID = $FoundJobs[$i+1]
        write-host $Label" Job with ID:"$ID -f Green
    }
    write-host
}

### End of functions ###

### Start things off ###
Show-Header
### Finish things off ###
if($LogToFile){
    Stop-Transcript
}
### THE END ###
2 Likes

Thanks for your contribution and sharing your script!

I edited your script post a bit so it displays better. I’ll take a peek at this when I get a chance.

1 Like

I’ve updated the script to version 1.1 and will edit my previous post to show this new version in just a minute.
Update: Seems I can’t edit my previous post so I was going to post it below then found the character limit and I was only 505 chars over…
For now I’ll post the wallOfText (Setup Instructions and such) as a post here and then post the script without the wallOfText.

Changes:

  1. Now works with Windows 7, for real. After some additional testing I found out that “Get-Volumes” calls on a part of WSM (Windows Storage Management) which was only introduced in Win8 and up. This meant that in spite of being able to install Powershell 5.1 into Windows7 there was no WSM, so it failed. I rewrote the drive detection to use a call to “[System.IO.DriveInfo]::GetDrives()” instead and now it works properly in Win7. See functions Find-Drives vs Find-Volumes for more details.

  2. Added a basic check to verify that Powershell 5 is installed.

  3. Added a few try/catch blocks to make things fail more gracefully.

  4. Added a few more error messages to the above try/catch blocks to better explain the error.

  5. Script now lists the version of Windows and Powershell on start-up. Currently does not validate the OS version, just displays it. OS validation is something for the next revision.

  6. “Test Mode” is now enabled by default. Make sure to change it to $false once ready to backup.
    This means the script will do everything but send the job to Duplicati. i.e. actually make a backup. To disable “Test Mode” find the line “#$Testmode = $false” and remove the “#” symbol at the beginning of the line. Move down one line and add a “#” in front of the line that says “$Testmode = $true”. Save the script and “Test Mode” should be disabled and backups will run.

  7. Added “Installation Notes” section above to provide extra installation details if needed.

  8. Added a note to the footer to check the Duplicati GUI (with address) for more details.

  9. Added an internal version number to help keep track of changes.

  10. Changed the drive usage pattern when multiple drives are connected. Now uses the LRU drive over the DotW drive. LRU = Least Recently Used, DotW = Day of the Week. Old logic is still in the Get-Drives function just commented out <# #>. This way the script will spread backups across multiple drives using the LRU if more than two drives are connected. Previously if the DotW drive was present all backups would run to that drive for that day. Now the script will use the LRU drive in all cases when more than one drive is detected.

1 Like

Top portion of script, Setup Instructions and such.

#region wallOfText

### Version ###
#
# 1.1
# Release date: Dec 1st 2021
#

### Requirements ###
#
# Windows OS (7,8,8.1,10,11) with PowerShell 5.1
#
# Duplicati 2, installed and running with the Tray Icon.
#
# One or more external USB drives.
#

### What does the script do? ###
#
# Let's you backup to multiple local external USB drives on a schedule regardless of which
# drives are currently connected. This script runs GUI jobs directly so any changes
# are instantly reflected in the GUI without having to specify databases then update CLI jobs,
# no getting warnings in the GUI about missed jobs, no need to duplicate jobs between the GUI and CLI
# or worry about what drive letter was assigned and if the backup ran to your kids USB drive
# last night that was accidentally left in the machine, don't worry it won't.
#
# The script will always try to use the drive for the current day of the week. If the current
# days drive is not connected but another valid drive is, it will be used in it's place. If 
# two or more valid drives are connected, the script will find the LRU (Least Recently Used)
# drive and run a backup to that drive.
#
# A "valid drive" is one with a matching drive label, contains a specific folder and
# contains a specific file in that folder for that drive. Sure it can all be easily replicated 
# but it's very unlikely for any one random USB drive to have that many matching markers.
#
# Logging all the output to a .txt file is supported just set $logToFile to $true and a log file
# will be created. There is also a "test mode" (now enabled by default as of 1.1) where everything
# except sending the very last command to Duplicati is enabled, should be obvious but backups will
# not be created when running in test mode.
#

### What the script does not do. ###
#
# Manage or configure Duplicati
#
# Probably won't work if you enable password access to the GUI. I didn't write the headers
# section so I'm not certain but I plan on looking into it in the future.
#
# Probably won't work if Duplicati is running as a Windows Service.
#
# Sadly, it can't make a good cup of tea.
#

### Reasons for creating the script ###
#
# I'm accustomed to Windows Server Backup and it's ability to have multiple drives and 
# if the drive doesn't get swapped, the backup will still run to the connected drive,
# keeping multiple versions per drive.
#
# Many of my clients are small businesses (sub 5 employees) that can't justify a full on
# server but still need a decent backup. With this script they can get encrypted local 
# backups to multiple external USB drives along with an encrypted cloud stored backup.
#
# Also key, is the simplicity for users. The Duplicati GUI is clean and easy to use, 
# users can at a glance see when the backup last ran or change what is being backed up.
#
# The only thing that cannot be adjusted in the Duplicati GUI is when this script 
# runs, that is defined in the Windows Task Scheduler job that runs this script.
#

### Setup Overview ###
#
# There are 4 key elements to making this work.
#
# 1.  The backup drive labels. The drive name needs to match the backup jobs name.
#     So the backup drive for Monday will be labeled as "Monday" and in Duplicati you
#     create a job named "Monday".
#
# 1a. You can specify other names in the $days array but you'll have to rework the 
#     day of the week determination functions to make it work properly. 
#     To avoid this, use the days of the week as drive and job names.
#
# 2.  To help ensure that only the proper drive gets used on the proper day we are going to 
#     use two advanced options in Duplicati. These two options are "alternate-target-paths"
#     and "alternate-destination-paths".
#
# 2a. These options let you specify a folder to place backups into along with with a 
#     handy wildcard to find the drive even if it's drive letter has changed and the 
#     other looks for a specific file in the previously specified folder. This means 
#     creating a specific folder on the drive and then placing a specific file in that folder.
#
# 2b. For example in the Monday drive create a folder and name it "YourCompanyBackups",
#     next create a file in that folder and call it "MondayBackupsInThisFolder.txt".
#
# 2c. Repeat the above process for each drive/day of the week. So the Tuesday drive
#     will be labeled "Tuesday", have a folder "YourCompanyBackups" and a file in that
#     folder called "TuesdayBackupsInThisFolder.txt"
#
# 3.  Setup a Scheduled Task in Windows Task Scheduler to run this script when you want backups
#     to occur. This can be once a day or multiple times a day.
#
# 4.  Keep the Duplicati Tray Icon running otherwise the script won't be able to run backups.
#

### Setup in Duplicati to make this work ###
#
# Each backup drive requires it's own backup job in Duplicati. You don't need a drive 
# for every day of the week, but the labels and job names do have to be a day of the week,
# unless you want to re-write the script. If you only have two drives they could be 
# Tuesday and Friday or as Saturday and Sunday, they can be out of sequence it doesn't 
# matter the script will try to use the current days drive but if it's not there the 
# other one will be used.
#
# Creating a backup job in Duplicati has a small issue at the moment where certain options
# don't show up until you've saved the job once. After reopening the job the options will
# be in the correct place. Duplicati has 6 main steps to creating a backup job.
#
# Step 1: From the Home screen (http://localhost:8200/ngax/index.html) click on "Add Backup",
#         in "General backup settings" provide a day of the week (starting with a CAPITAL letter)
#         such as "Thursday" (not "thursday"), give it a description if you feel inclined.
#         Next set a passphrase to encrypt your backups, you can disable encryption but it's
#         a nice piece of mind in-case the drive gets lost or stolen. 
#         ### EXTRA MASSIVE WARNING ### DO NOT LOOSE THE PASSPHRASE!!! Your backups are 
#         useless encrypted garble if you don't know the passphrase. You can also just 
#         disable "Encryption" by setting it to "No Encryption". Click "Next >"
#
# Step 2: Choosing the Backup destination. By default Duplicati sets "Storage Type" to 
#         "Local folder or drive" which is what you want. With the backup drive for 
#         that day connected select it from the "Folder path" list. Click "Next >"
#
# Step 3: Choosing the Source data (what to backup). Pick the folders you want to have
#         backed up, if you choose a whole drive you'll probably want to add "System files"
#         to the Exclude list. Click "Next >"
#
# Step 4: Schedule. We don't want Duplicati to run jobs on it's own so remove the checkmark
#         beside "Automatically run backups." Click "Next>"
#
# Step 5: General options, leave "Remote volume size" at the default of 50MB, weird things
#         can happen when this setting is changed. I change the "Backup retention" to 
#         "Smart backup retention" but that's up to you. We have to add some advanced options
#         but first let's save the job by clicking on "Save".
#
# Step 5a:From the Home screen click the down arrow to the right of the job name to show more
#         details about the job. In the Configuration: section click on "Edit..." and you're
#         back at Step 1, you can click on the dots with numbers 1-5 at the top to jump to 
#         that section or click "Next>" until you get back to Step 5. Next click on 
#         "Advanced options" then in the "Add advanced option" drop-down find "use-block-cache:"
#         located under the "Core options" heading. Place a checkmark to enable it.
#         
#         This option stores the block cache in RAM vs your TEMP folder (likely on your C: drive),
#         if the host system is very low on available RAM then this could cause issues or 
#         slowdowns but chances are most host systems will have plenty enough RAM, a hundred
#         or so free megs (MB) will do. By default during a backup Duplicati writes a bunch 
#         of files to your TEMP folder over and over again, possibly causing more wear/tear,
#         read/write cycles than you want to expose your main drive to. Remember even slow 
#         system RAM is still alot faster than a decent SSD, RAM has nearly no read/write 
#         limit unlike your SSD or HDD.
#
# Step 5b:Again from the "Add advanced option" drop-down find "alternate-target-paths:" under
#         the "Local folder or drive" heading. (the menu scrolls quite quickly, once you click 
#         on the drop-down you can use the up/down arrow keys on your keyboard to scroll) 
#         In the textbox type without quotes "*:\YourCompanyBackups". The folder name can be 
#         different but the "*:\" has to be written as shown, "*" an asterix is a special Duplicati 
#         wildcard that only applies to drive letters in a Windows environment and can only 
#         be used with this one option.
#
# Step 5c:Once more from the "add advanced option" drop-down find "alternate-destination-marker"
#         under the "Local folder or drive" heading. In the textbox type the name of the 
#         marker file, if this was the Monday job name name it "MondayBackupsInThisFolder.txt"
#         or "TuesdayBackupsInThisFolder.txt" for the Tuesday job. The filenames can be 
#         whatever you want but they need to be unique, if two jobs share the same marker file
#         Duplicati can get confused and attempts to compare the wrong file blocks to the wrong 
#         database and a whole pile of errors are generated and most of the time the backup 
#         sets seem to get damaged beyond repair.
#
# Step 6: (Optional) If you need to deal with open files you'll need to add the "snapshot-policy"
#         and set it to "ON" for now. Once you know elevation works switch it to "AUTO". To 
#         access the VSS service the Duplicati Tray Icon will need to be run as an administrator
#         in an elevated state. Locate the exe file for the Tray Icon (Duplicati.GUI.TrayIcon.exe)
#         which should be located in "C:\Program Files\Duplicati 2\", right-click on it as 
#         select "Properties", next click on "Change settings for all users" then place a 
#         checkmark in the box for "Run this program as an administrator". Restart the Tray Icon,
#         clicking Yes when prompted. At this point run the job, if elevation did work it should 
#         run without error.
#

### Schdule this script to run in Windows Task Scheduler###
#
# If you haven't, yet save this script to a safe location on your computer. You will need to know 
# where you saved it to point the scheduler to it. I would suggest somewhere out of the way so it 
# won't get moved or deleted, a folder like "C:\Folder_For_Script" but you can use whatever you fancy.
#
# To open the Task Scheduler, you can search for it by name "Task Scheduler" or you can find it on the 
# Start menu under the "Windows Administrative Tools" folder. The icon is a clock with 12 to 3 filled
# in with orange.
#
# Once Task Scheduler opens click on "Task Scheduler Library", next on the right hand side under the
# "Actions" list click on "Create Basic Task...". You can also right-click on "Task Scheduler Library"
# and click "Create Basic Task..." from there.
#
# The Create Basic Task Wizard will open asking you a few questions. First off give the task a name,
# something that makes sense like "Run Duplicati Backup" or "Daily Backup" and give it a description
# if you want click "Next>". We'll presume you want to run the task daily (default option) so click
# "Next>". Now it wants to know when to run the task, pick a time that's good for you and you probably
# want the task to recur every day so click "Next>". This time it wants to know what to do, which is 
# "Start a program" so click "Next>". In the "Program/script:" box type or copy from here, 
# "%SystemRoot%\system32\WindowsPowerShell\v1.0\powershell.exe" then in the "Add arguments (optional)"
# type "-File" (without quotes), a space then the quoted path to this script. 
# So the line should look like: -File "C:\Folder_For_Script\Script_Name.ps1". The last page will give
# you a summary of the task that's being created, click "Finish".
#
# The wizard will close and the task list will refresh showing your new task at the bottom of the list.
# You can double-click on the task to edit it's options. A few settings worth looking at if you're 
# not familiar with tasks are the Conditions and Settings tabs.
#
# At this point should be able to run the task, with the task highlighted click "Run" in the "Actions"
# list or right-click on the task and choose "Run". Open the Duplicati Home screen and you should see
# the status changes. The first backup will take roughly as long as it would take to make create a
# manual copy of all the selected files, once the initial backup is complete future backups will be 
# much faster.
#

### Installation Notes ###
#
# Windows 10 and 11 both have Powershell 5.1 by default but Windows 7, 8 ,8.1 will need to be
# updated. You can get the WMF 5.1 update from the link for KB3191566 below. The script now checks to 
# see if PS5 is installed and quits with an error if version 5 is not detected.
# https://www.microsoft.com/en-us/download/details.aspx?id=54616
# Note, the installation of WMF 5.1 took a very long time to complete and looks stuck at 75%ish 
# percent for a long time. 
#
# The script should now run on Windows 7, 8, 8.1, 10 and 11, this should also work on any equivalent
# server OSes, from Windows 2008 R2 and up but I have not tested it to confirm.
# Will NOT work with: Windows 2000, XP, Vista, Server 2003 or Server 2008(non R2).
#
# When running the script in Powershell ISE the colors from the write-host lines may "bleed" into
# the next line or two. This only happens in the ISE and only on the first few lines. This seems
# be a somewhat known issue in ISE, my suggestion ignore it for now. It will eat away at me and
# I'll find a fix in a new revision.
#

### Version History ###
#
# Version 1.0 Initial release.

# Version 1.1
# Changes:
#
#     1. Now works with Windows 7, for real. After some additional testing I found out that 
#        "Get-Volumes" calls on a part of WSM (Windows Storage Management) which was only introduced
#        in Win8 and up. This meant that in spite of being able to install Powershell 5.1 into 
#        Windows7 there was no WSM, so it failed. I rewrote the drive detection to use a call to 
#        "[System.IO.DriveInfo]::GetDrives()" instead and now it works properly in Win7.
#        See functions Find-Drives vs Find-Volumes for more details.
#
#     2. Added a basic check to verify that Powershell 5 is installed.
#
#     3. Added a few try/catch blocks to make things fail more gracefully.
#
#     4. Added a few more error messages to the above try/catch blocks to better explain the error.
#
#     5. Script now lists the version of Windows and Powershell on start-up. Currently does not
#        validate the OS version, just displays it. OS validation is something for the next revision.
#
#     6. "Test Mode" is now enabled by default. Make sure to change it to $false once ready to backup.
#        This means the script will do everything but send the job to Duplicati. i.e. actually make
#        a backup.
#        To disable "Test Mode" find the line "#$Testmode = $false" and remove the "#" symbol at
#        the beginning of the line. Move down one line and add a "#" in front of the line that says
#        "$Testmode = $true". Save the script and "Test Mode" should be disabled and backups will run.
#
#     7. Added "Installation Notes" section above to provide extra installation details if needed.
#
#     8. Added a note to the footer to check the Duplicati GUI (with address) for more details.
#
#     9. Added an internal version number to help keep track of changes.
#
#    10. Changed the drive usage pattern when multiple drives are connected. Now uses the LRU drive
#        over the DotW drive. LRU = Least Recently Used, DotW = Day of the Week. Old logic is still
#        in the Get-Drives function just commented out <# #>.
#        This way the script will spread backups across multiple drives using the LRU if more than
#        two drives are connected. Previously if the DotW drive was present all backups would run
#        to that drive for that day. Now the script will use the LRU drive in all cases when more
#        than one drive is detected.
#

### Credits ###
#
# TS-Steff for their PRTG-Duplicati script which served as a base for this script.
# https://github.com/TS-Steff/PRTG-Duplicati/blob/master/duplicati.ps1
##
# Pectojin for showing that it could be done.
# https://github.com/Pectojin/duplicati-client/blob/master/duplicati_client.py#L881
# This "/api/v1/backup/backupID/run" is the key to running GUI jobs via scripting.
##
# Everyone else that has helped in the past and anyone that is helping to make Duplicati what it is.
##
# Thank you all!
###

#endregion

The working parts of the script, copy and save as “script_name.ps1”, with “script_name” being a placeholder for whatever you want to name the script file.

### On to the script ###

# Used to keep track of the version.
$internalVersion = "1.1"

# Enable to prevent running backups, set to false if you want backups to run!
#$TestMode = $false
$TestMode = $true

# Warning that gets added to screen/logs when $TestMode is enabled.
$TestModeWarning = "Test Mode Enabled -- Backups will NOT run!"

# Enable log file generation
$LogToFile = $true
#$LogToFile = $false

# Path/filename to save the log file to. By default it saves to the current users Desktop
# with a filename of "PS_BackupLog.txt".
$LogPath = "C:\Users\$env:USERNAME\Desktop\PS_BackupLog.txt"

# Makes a backup log of everything that happens in the script and saves it to a log file.
# Note: Stop-Transcript, near the end of the file must be in place or the log won't be saved.
if($LogToFile){
    Start-Transcript -Append -Path $LogPath
}

# Clear the screen
cls

# Host and port values, adjust to match your config. $urlHost can be an IP or DNS name.
$urlHost = "localhost"
$urlPort = "8200"

# Base URLS 
$urlHttp = "http://"
$url = "$($urlHttp)$($urlHost):$($urlPort)/index.html"

# Location URLS
$urlApi = "api"
$urlVer = "v1"

# API URLS
$urlApiVer = "$($urlHttp)$($urlHost):$($urlPort)/$($urlApi)/$($urlVer)"
$urlBackups = "$($urlApiVer)/backups/"
$urlBackup = "$($urlApiVer)/backup/"

# Cheap Error Logging for one error...
$errors = New-Object System.Collections.ArrayList
$failed = $false

# Date and Time related
$TodayDate = Get-Date -Format "MMM-dd-yyyy"
$TodayDay = Get-Date -Format "dddd"
$TodayTime = Get-Date -Format "HH:mm"

# Days of the week.
$Days = @("Sunday","Monday","Tuesday","Wednesday","Thursday","Friday","Saturday")

# Discovered USB drives
$FoundDrive = "Not Good"
$FoundDrives = New-Object System.Collections.ArrayList
$FoundJobs = New-Object System.Collections.ArrayList
$FoundDriveCount = 0

# Drive letter
$DriveLetter = ""

# Array of recently completed backups
$RecentBackups = New-Object System.Collections.ArrayList

# Check to verify if Powershell verison 5 is installed.
write-host "Checking Powershell Version..." -f Yellow
$PsVerMajor = $PSVersionTable.PSVersion.Major
if($PsVerMajor -ne 5){
    $error = "Version:"+$PsVerMajor+" was found."
    $failed = $true
    $errors += "Error 02: Powershell version 5 was not found. PS version "+$PsVerMajor+" was found. See Microsoft KB3191566.`n"
}
else{
    write-host "Version 5 found." -f Green
}

# If the version check fails, don't send headers.
if($failed){
    write-host "Version Check Fail" -f Red
}
else{
    ### Some headers 'magic' here thanks to TS-Steff. Not sure exactly how it works, but it does. ###
    # I suspect this may have authentication issues if the server is password protected, something for another day.

    # start by loading duplicati index.html
    $headers = @{
    "Accept"='text/html,application/xhtml+xml,application/xml;q=0.9,image/webp,*/*;q=0.8';
    "Upgrade-Insecure-Requests"="1";
    }
    try{
        Invoke-WebRequest -Uri $url -Method GET -Headers $headers -SessionVariable SFSession -UseBasicParsing | Out-Null
    }
    catch{
        "Unable to connect to server."
        $failed = $true
        $errors += "Error 03: Unable to connect. Check tray icon and host/port settings."
    }

    # Gets required tokens
    $headers = @{
    "Accept"='application/xml, text/xml, */*; q=0.01';
    "Content-Length"="0";
    "X-Requested-With"="XMLHttpRequest";
    "X-Citrix-IsUsingHTTPS"="Yes";
    "Referer"=$url;
    }
    
    if($failed){
        #An error occured
    }
    else{
        Invoke-WebRequest -Uri ($url) -Method POST -Headers $headers -WebSession $sfsession -UseBasicParsing | Out-Null
    }

    $xsrf = $sfsession.cookies.GetCookies($url)|where{$_.name -like "xsrf-token"}

    # Setting the Header
    $Headers = @{
        "X-XSRF-Token" = [System.Net.WebUtility]::UrlDecode($xsrf.value)
        "Cookie" = "$xsrf"
    }
}
### End of headers stuff ###

### Functions ###

# Runs the specified GUI backup job
function Backup-Run($backupID){

    $urlToRunBackup = $urlBackup+$backupID+"/run"    
    $RunBackup = Invoke-RestMethod -Uri $urlToRunBackup -Method POST -Headers $Headers
    write-host $(Get-JobName($backupID))"job with ID:"$backupID" submitted to Duplicati." -f Green
}

# Compares drives
function Compare-RecentBackups{
    
    $tempArray = @()
    for($i=1;$i -lt $RecentBackups.Count;$i+=2){
        $tempItem = $RecentBackups[$i]
        $tempArray += $tempItem
    }
    $tempArray = $tempArray | Sort
    $oldestJob = $tempArray[0]
    for($i=1;$i -lt $RecentBackups.Count;$i++){
        if($oldestJob -eq $RecentBackups[$i]){
            $JobID = $RecentBackups[$i -1]
        }
    }
    $JobName = Get-JobName($JobID)
    $oldestJob = $oldestJob -split " "
    $leastRecentJob = "The current LRU drive is the "+$JobName+" drive with JobID "+$JobID+", it last ran "+$oldestJob[0]+" @ "+$oldestJob[1]+"."
    write-host $leastRecentJob -f Yellow
    write-host $JobName" job with ID: "$JobID" is currently the oldest backup."
    
    return $JobID
}

# Runs Get-BackupInfo and Get-Backups
function Find-BackupJobs{

    $backup_info = Get-BackupInfo(Get-Backups)

    #return $backup_info
}

# Finds local drives then adds them to $FoundDrives if the volumelabel matches a day of the week.
function Find-Drives{
    write-host "Searching for backup drives..."
    write-host
    $Drive = [System.IO.DriveInfo]::GetDrives()
    $Days | % {
        write-host "Looking for the"$_" drive..."
        $Day = $_
        $Match = $false
        $Drive | % {
            if($_.VolumeLabel -eq $Day){
                $FoundDrive = $Day
                $DriveLetter = $_.RootDirectory.ToString() #save as a string
                $DriveLetter = $DriveLetter.Substring(0,1) #get the letter only, removes the ":\"
                [void]$FoundDrives.Add($FoundDrive)
                [void]$FoundDrives.Add($DriveLetter)
                $FoundDriveCount++
                $Match = $true
            }
        }
        if($Match){
            write-host "Found the"$Day" drive, @ "$DriveLetter":\" -f Green
        }
        else{
            write-host "The"$Day" drive was NOT found." -f Red
        }
        write-host
    }
    write-host "Finished finding drives,"$FoundDriveCount" drives found."
    return $FoundDriveCount
}

# For each day of the week check if there is a drive connected with the same name
function Find-Volumes{
    $Days | % { 
	    $Drive = Get-Volume -FileSystemLabel $_ -ErrorAction:SilentlyContinue
	    if($?)
	    {
		    $FoundDrive = $_
		    $DriveLetter = $Drive.DriveLetter
		    write-host "Found"$_ "drive at letter"$DriveLetter":" -f Green
            [void]$FoundDrives.Add($_)
            [void]$FoundDrives.Add($DriveLetter)
		    $FoundDriveCount++
	    }
	    else
	    {
		    write-Host "The"$_" drive was NOT found." -f Red
	    }
    }
    write-host "Finished finding drives."$FoundDriveCount" drives found."
    return $FoundDriveCount
}

# Gets the list of backup jobs defined in the GUI
function Get-Backups{
    
    #error catch? no jobs in Duplicati
    $backups = Invoke-RestMethod -Uri $urlBackups -Method GET -Headers $Headers -ContentType application/json
    
    $backups = $backups -replace "`t|`n|`r",""

    try{
        $backups =  $backups.Substring(6)
    }
    catch{
        "No Data Received From Host"
        $failed = $true
        $errors += "Error 04: No jobs returned. You need to setup at least one job in the GUI."
    }
    if($failed){
        #An error occured
    }
    else{
        $backups = $backups.Substring(0,$backups.Length -2  )

        $arrBackups = $backups -split '},  {'

        for ($i=0; $i -lt $arrBackups.Length; $i++){
   
            if($i -eq 0){
                $arrBackups[0] = $arrBackups[0] + "}"
            }else{
                $arrBackups[$i] = "{" + $arrBackups[$i] + "}"
            }  
        }

        foreach ($backup in $arrBackups){

            $json=ConvertFrom-Json $backup
            [void]$FoundJobs.Add($json.Backup.Name)
            [void]$FoundJobs.Add($json.Backup.ID)
            $backup_ids += @($json.Backup.ID) 
        }
    
        Get-Drives
        }
    #if errors have occured, report the errors, stop logging if enabled and quit the script.
    if($errors){
        Show-Errors
        if($LogToFile){
            Stop-Transcript
        }
        break
    }
}

# Gets the backup ID for a named job
function Get-BackupID($JobName){

    for($i=0;$i -lt $FoundJobs.Count;$i++){
        if($FoundJobs[$i] -eq $JobName){
            $JobID = $FoundJobs[$i+1]
        }
    }
    return $JobID
}

# Gets details for a backup job
function Get-BackupInfo($JobID){

    $JobData = Invoke-RestMethod -Uri $urlBackup$JobID -Method GET -Headers $Headers -ContentType application/json
    $JobData = $JobData.Substring(3)
    $JobData = ConvertFrom-Json $JobData

    return $JobData
}

# Searches for connected USB drives
function Get-Drives{

    $FoundDriveCount = Find-Drives
    Show-Drives
    Show-Jobs

    #write-host "FDC"$FoundDriveCount

    if($FoundDriveCount -gt 0){
        if($FoundDriveCount -eq 1){
            write-host "Found 1 drive"
            if($FoundDrives[0] -eq $TodayDay){
                write-host "Today's backup drive found, running today's backup to it."
                if($TestMode){
                    write-host $TestModeWarning -f Red
                }
                else{
                    Backup-Run(Get-BackupID($TodayDay))
                }
            }
            else{
                write-host "Today's backup drive was NOT found," -f Yellow
                write-host "The $FoundDrive drive was the only one found." -f Yellow
                if($TestMode){
                    write-host $TestModeWarning -f Red
                }
                else{
                    Backup-Run(Get-BackupID($FoundDrive))
                }
            }
        }
        else{
            write-Host "Found"$FoundDriveCount" drives."
            write-host "More than one drive detected." -f Yellow
            <#
            $FoundDrives | % {
                if($_ -eq $TodayDay){
                    write-host "Today's backup drive found, running today's backup to it."
                    if($TestMode){
                        write-host $TestModeWarning -f Red
                    }
                    else{
                        Backup-Run(Get-BackupID($TodayDay))
                    }
                }
            }
            write-host "Today's drive was not found."
            #>
            write-host "Searching for the LRU(Least Recently Used) drive..."

            for($i=0;$i -lt $FoundDrives.Count;$i+=2){

                $driveID = Get-BackupID($FoundDrives[$i])
                $driveInfo = Get-BackupInfo($driveID)
                $lastFinish = $driveInfo.data.Backup.Metadata.LastBackupFinished
                [void]$RecentBackups.Add($driveID)
                $lastDate = [datetime]::ParseExact($lastFinish,'yyyyMMddTHHmmssZ',$null)
                [void]$RecentBackups.Add($lastDate)
            }

            $JobToRun = Compare-RecentBackups

            if($TestMode){
                write-host $TestModeWarning -f Red
            }
            else{
                Backup-Run($JobToRun)
            }
        }
    }
    else{
        write-host "###############################################" -f Yellow
        write-host "No Backup Drives Found!!! Unable to run backup." -f Red
        write-host "###############################################" -f Yellow
        $failed = $true
        $errors = $errors+"Error #1: No Backup Drives Found, Backup CANCELLED!"
    }
    Show-Footer
}

# Gets the name of the job with the specified ID [int]
function Get-JobName($JobID){

    for($i=1;$i -lt $FoundJobs.Count;$i++){
        if($JobID -eq $FoundJobs[$i]){
            $JobName = $FoundJobs[$i -1]
        }
    }

    return $JobName
}

# Gets the name of the Windows operating system
function Get-OsName{
    $OsName = $(((Get-WMIObject win32_operatingsystem).name).split('|')[0])
    return $OsName
}

# Gets the major and minor version numbers for Powershell.
function Get-PsVersion{

    $PsVer = @(0,0)
    $PsVer[0] = $PSVersionTable.PSVersion.Major
    $PsVer[1] = $PSVersionTable.PSVersion.Minor
    $MaxMinPsVer = ""+$PsVer[0]+"."+$PsVer[1]

    return $MaxMinPsVer
}

function Get-ServerState{

    $urlServerState = "$($urlApiVer)/serverstate"
    $ServerData = Invoke-RestMethod -Uri $urlServerState -Method GET -Headers $Headers -ContentType application/json
    $ServerData = $ServerData.Substring(3)
    $ServerData = ConvertFrom-Json $ServerData
    #write-host "Dump Backup" $ServerData.data.Backup
    return $ServerData
}

# Changes a boolean into a Yes or No string
function Get-YesNo($bool){

    if($bool){
        $answer = "Yes"
    }
    else{
        $answer = "No"
    }

    return $answer
}

# Lists connected USB drives along with their drive letter
function Show-Drives{

    write-host 
    $half = $FoundDrives.Count /2
    write-host $half" Backup drives found."

    for($i=0;$i -lt $FoundDrives.Count;$i+=2){
        $Label = $FoundDrives[$i]
        $Letter = $FoundDrives[$i+1]
        write-host $Label" drive @ "$Letter":" -f Green
    }
    write-host
}

# List only the drive labels for connected USB drives
function Show-DriveLabels{

    write-host
    write-host "Drive Labels" -f Green
    for($i=0;$i -lt $FoundDrives.Count; $i+=2) {$FoundDrives[$i]}

}

# List the drive letter (no colon ":") for connected USB drives
function Show-DriveLetters{

    write-host
    write-host "Drive Letters" -f Green
    for($i=1;$i -lt $FoundDrives.Count; $i+=2){$FoundDrives[$i]}
    write-host
}

# Show any errors
function Show-Errors{

    $errors | % {
        write-host $_ -f Red
    }
}

# Prints out a text footer to finish things off
function Show-Footer{

    write-host
    if($failed){
        write-host "Script Completed with"$errors.Count"error(s)."
        Show-Errors
        write-host "FIN :("
    }
    else{
        write-host "Script Completed without errors." -f Green
        write-host "FIN :)"
    }
    write-host
    write-host "Check the Dupicati GUI at"$url" for more details."
}

# Prints out a text header with the date and time
function Show-Header{

    if($errors){
        Show-Errors
        if($LogToFile){
            Stop-Transcript
        }
        break
    }
    write-host "------------------------------------------------------------------" -f White
    write-host "Windows Version:"$(Get-OsName)
    write-host "Powershell Version:"$(Get-PsVersion) 
    write-host "------------------------------------------------------------------" -f White
    if($TestMode){
        write-host $TestModeWarning -f Red
    }
    write-host "Today is"$TodayDate", it's a "$TodayDay" at $TodayTime."
    write-host
    Get-Backups
}

# Lists backup jobs found in the GUI along with their ID
function Show-Jobs{

    $half = $FoundJobs.Count /2
    write-host $half" Backup jobs found in Duplicati server." 
    for($i=0;$i -lt $FoundJobs.Count;$i+=2){
        $Label = $FoundJobs[$i]
        $ID = $FoundJobs[$i+1]
        write-host $Label" Job with ID:"$ID -f Green
    }
    write-host
}

### End of functions ###

### Start things off ###
Show-Header
### Finish things off ###
if($LogToFile){
    Stop-Transcript
}
### THE END ###

Known Issues:

  1. Backup jobs need to have been run once via “Run Now” in the GUI before the script can gain access to that job.

Discovered an issue, if only one drive is connected the backup will only run if it matches the day of the week, this was not the intended behavior.

Fix, open the script in PSH ISE or in a text editor (with line numbers enabled) and add a line, see below for details.

In the function Get-Drives (line 302, if you copied from above).
Find the line that says
write-host "Today's backup drive was NOT found," -f Yellow (line 323)
at the end of that line hit return and add this to the new line (line 324)
$FoundDrive = $FoundDrives[0]

After the edit it should look like the following,

line 323: write-host "Today's backup drive was NOT found," -f Yellow
line 324: $FoundDrive = $FoundDrives[0]
line 325: write-host "The $FoundDrive drive was the only one found." -f Yellow

Once saved, backups will run even if the day/driveNames do not match, as originally intended.

Side note, lines 337 - 350 (after the addition of the above line, otherwise they would be lines 336 - 349) can also be deleted as they are already commented <# #> out and provide no purpose as is.

Extra side note, I’ve begun working on version 1.3 that will add some new few features and hopefully clean up the code a bit. Planned features include delayed/deferred job scheduling and saving a copy of the local DB post run (for what it’s worth) to the destination, tossing a few other ideas around but we’ll see what makes it in.

1 Like