Folder backups with PowerShell

Why pay for an expensive backup solution in your home-lab when you can install PowerShell for free? The script below will handle simple backups for you. You only have to add it to the Windows Scheduler or create a Cron job.

This Script’s features:
– Replicates a Source directory to a destination to be specified in the script
– Automatically creates the destination folder structure
– Identifies changes or new files in the Source folder and only copies the changed or new files files to the destination
– If a file is deleted from the source, it will move the file from the backup destination to a recycle bin folder to be specified in the script.
– The script will delete files from the recycle bin after 30 days.
– Events are logged to a logfile located in the same directory as the script. The logfile is rotated every day the script is run and are auto-purged every 90 days
– Progress indicator is displayed on the console

Note: The folders specified in the script below should be modified for your own use. Save the script with an editor (I use Notepad++), give it any name you want and save it as a Windows PowerShell (*.ps1) file.

# Set Source, Destination and Recycle Bin paths here
$sourcePath = "C:\LocalFolder"
$backupPath = "\\RemoteServer\Share\Backup"
$oldFilesPath = "\\RemoteServer\Share\OldFilesDirectory"
$scriptPath = Split-Path -Parent $MyInvocation.MyCommand.Definition
$logPath = Join-Path -Path $scriptPath -ChildPath ("Log_" + (Get-Date -Format "yyyyMMdd") + ".txt")

# Create or append to the log file and optionally display output
function LogWrite {
    Param ([string]$logstring, [bool]$toConsole = $false)
    $timestampedLog = "$(Get-Date -Format "yyyy-MM-dd HH:mm:ss"): $logstring"
    Add-Content $logPath -Value $timestampedLog
    if ($toConsole) {
        Write-Output $timestampedLog
    }
}

# Remove log files older than 90 days
Get-ChildItem -Path $scriptPath -Filter Log_*.txt | Where-Object { $_.LastWriteTime -lt (Get-Date).AddDays(-90) } | ForEach-Object {
    LogWrite "Deleting old log file: $($_.Name)" -toConsole $true
    Remove-Item $_.FullName
}

LogWrite "Starting backup operation." -toConsole $true

# Create backup and old files directories if these don't exist
if (-not (Test-Path -Path $backupPath)) {
    New-Item -ItemType Directory -Path $backupPath
    LogWrite "Created Backup directory: $backupPath" -toConsole $true
}
if (-not (Test-Path -Path $oldFilesPath)) {
    New-Item -ItemType Directory -Path $oldFilesPath
    LogWrite "Created Recycle Bin directory: $oldFilesPath" -toConsole $true
}

# Get the current state of source and backup directories
$sourceFiles = Get-ChildItem -Path $sourcePath -Recurse -File
$backupFiles = Get-ChildItem -Path $backupPath -Recurse -File

# Copy new and modified files with progress bar
$totalFiles = $sourceFiles.Count
$i = 0
foreach ($file in $sourceFiles) {
    $i++
    $percentComplete = ($i / $totalFiles) * 100
    Write-Progress -Activity "Copying new and modified files" -Status "Processing $($file.Name) ($i/$totalFiles)" -PercentComplete $percentComplete

    $backupFile = $backupFiles | Where-Object { $_.FullName -eq $file.FullName.Replace($sourcePath, $backupPath) }
    if ($null -eq $backupFile -or $file.LastWriteTime -gt $backupFile.LastWriteTime) {
        $destPath = $file.FullName.Replace($sourcePath, $backupPath)
        $destDir = [System.IO.Path]::GetDirectoryName($destPath)
        if (-not (Test-Path $destDir)) {
            New-Item -ItemType Directory -Path $destDir
        }
        Copy-Item -Path $file.FullName -Destination $destPath -Force
        LogWrite "Copied or updated file: $($file.FullName) to the Backup location."
    }
}

# Refresh list after copy for moving deleted files
$backupFiles = Get-ChildItem -Path $backupPath -Recurse -File 
$totalFiles = $backupFiles.Count
$i = 0
foreach ($file in $backupFiles) {
    $i++
    $percentComplete = ($i / $totalFiles) * 100
    Write-Progress -Activity "Moving deleted files to OldFiles" -Status "Processing $($file.Name) ($i/$totalFiles)" -PercentComplete $percentComplete

    $sourceFile = $sourceFiles | Where-Object { $_.FullName -eq $file.FullName.Replace($backupPath, $sourcePath) }
    if ($null -eq $sourceFile) {
        $oldFilePath = $file.FullName.Replace($backupPath, $oldFilesPath)
        $oldFileDir = [System.IO.Path]::GetDirectoryName($oldFilePath)
        if (-not (Test-Path $oldFileDir)) {
            New-Item -ItemType Directory -Path $oldFileDir
        }
        Move-Item -Path $file.FullName -Destination $oldFilePath
        LogWrite "Moved deleted file: $($file.FullName) to Recycle Bin at $oldFilesPath."
    }
}

# Delete files from Old Files that are older than 30 days
$oldFiles = Get-ChildItem -Path $oldFilesPath -Recurse -File
$totalFiles = $oldFiles.Count
$i = 0
foreach ($file in $oldFiles) {
    $i++
    $percentComplete = ($i / $totalFiles) * 100
    Write-Progress -Activity "Deleting files older than 30 days from OldFiles" -Status "Processing $($file.Name) ($i/$totalFiles)" -PercentComplete $percentComplete

    if ($file.CreationTime -lt (Get-Date).AddDays(-30)) {
        Remove-Item -Path $file.FullName
        LogWrite "Deleted file older than 30 days: $($file.FullName)."
    }
}

LogWrite "Backup operation completed." -toConsole $true

Leave a Reply

Your email address will not be published. Required fields are marked *