uscheduler/examples/File-Sync/file-sync.ps1
2026-01-25 14:18:40 +01:00

374 lines
13 KiB
PowerShell

[CmdletBinding()]
param (
[switch]$Automated,
[string]$CurrentDateTimeUtc
)
#Requires -RunAsAdministrator
<#
.SYNOPSIS
FreeFileSync Automated Backup Script
.DESCRIPTION
Production-ready file synchronization solution with scheduling and secure credential management.
.VERSION
1.0.0
.DATE
2026-01-24
.NOTES
- Requires FreeFileSync installed
- Requires SchedulerTemplate.psm1 module
- Requires sync.ffs_batch configuration file
#>
# Script Version
$ScriptVersion = "1.0.0"
$ScriptDate = "2026-01-24"
try {
Import-Module "$PSScriptRoot\..\SchedulerTemplate.psm1" -Force -ErrorAction Stop
}
catch {
Write-Error "Failed to load SchedulerTemplate.psm1: $_"
exit 1
}
# Load Settings ============================================================
$settingsFile = Join-Path $PSScriptRoot "scriptsettings.json"
if (-not (Test-Path $settingsFile)) {
Write-Error "Settings file not found: $settingsFile"
exit 1
}
try {
$settings = Get-Content $settingsFile -Raw | ConvertFrom-Json
Write-Verbose "Loaded settings from $settingsFile"
}
catch {
Write-Error "Failed to load settings from $settingsFile : $_"
exit 1
}
# Process Settings =========================================================
# Validate required settings
$requiredSettings = @('freeFileSyncExe', 'batchFile', 'nasRootShare')
foreach ($setting in $requiredSettings) {
if (-not $settings.$setting) {
Write-Error "Required setting '$setting' is missing or empty in $settingsFile"
exit 1
}
}
$FreeFileSyncExe = $settings.freeFileSyncExe
$FfsBatchFile = Join-Path $PSScriptRoot $settings.batchFile
$NasRootShare = $settings.nasRootShare
$CredentialEnvVar = $settings.credentialEnvVar
# Schedule Configuration
$Config = @{
RunMonth = $settings.schedule.runMonth
RunWeekday = $settings.schedule.runWeekday
RunTime = $settings.schedule.runTime
MinIntervalMinutes = $settings.schedule.minIntervalMinutes
}
# End Settings =============================================================
# Global variables
$script:SyncStats = @{
StartTime = Get-Date
EndTime = $null
Success = $false
ExitCode = $null
ErrorMessage = $null
}
# Global reference to the process so that the exit handler can see it
$global:FFS_Process = $null
# Helper Functions =========================================================
function Test-Prerequisites {
param([switch]$Automated)
Write-Log "Checking prerequisites..." -Level Info -Automated:$Automated
# Check if FreeFileSync executable exists
if (-not (Test-Path -LiteralPath $FreeFileSyncExe)) {
Write-Log "FreeFileSync not found: $FreeFileSyncExe" -Level Error -Automated:$Automated
Write-Log "Please install FreeFileSync or update the path in scriptsettings.json" -Level Error -Automated:$Automated
return $false
}
# Verify FreeFileSync version
try {
$ffsVersion = (Get-Item $FreeFileSyncExe).VersionInfo.FileVersion
Write-Log "FreeFileSync version: $ffsVersion" -Level Info -Automated:$Automated
}
catch {
Write-Log "Warning: Could not determine FreeFileSync version" -Level Warning -Automated:$Automated
}
# Check if batch file exists
if (-not (Test-Path -LiteralPath $FfsBatchFile)) {
Write-Log "Batch config not found: $FfsBatchFile" -Level Error -Automated:$Automated
Write-Log "Please create sync.ffs_batch configuration file" -Level Error -Automated:$Automated
return $false
}
Write-Log "All prerequisites passed" -Level Success -Automated:$Automated
return $true
}
function Connect-NasShare {
param(
[string]$SharePath,
[switch]$Automated
)
# Check if path is UNC
if (-not $SharePath.StartsWith("\\")) {
Write-Log "NAS path is local, no authentication needed" -Level Info -Automated:$Automated
return $true
}
Write-Log "Authenticating to NAS share: $SharePath" -Level Info -Automated:$Automated
# Validate credential environment variable name is configured
if (-not $CredentialEnvVar) {
Write-Log "credentialEnvVar is not configured in settings for UNC path authentication" -Level Error -Automated:$Automated
return $false
}
try {
# Retrieve credentials from environment variable
$creds = Get-CredentialFromEnvVar -EnvVarName $CredentialEnvVar -Automated:$Automated
if (-not $creds) {
return $false
}
# Check if already connected
$existingConnection = net use | Select-String $SharePath
if ($existingConnection) {
Write-Log "Already connected to $SharePath" -Level Info -Automated:$Automated
return $true
}
# Connect to share
$netUseResult = cmd /c "net use $SharePath $($creds.Password) /user:$($creds.Username) 2>&1"
if ($LASTEXITCODE -ne 0) {
Write-Log "Failed to connect to $SharePath : $netUseResult" -Level Error -Automated:$Automated
return $false
}
Write-Log "Successfully authenticated to $SharePath" -Level Success -Automated:$Automated
return $true
}
catch {
Write-Log "Error connecting to share: $_" -Level Error -Automated:$Automated
return $false
}
}
function Test-NasReachability {
param(
[string]$SharePath,
[switch]$Automated
)
if (-not (Test-Path $SharePath)) {
Write-Log "NAS share $SharePath is not reachable after authentication" -Level Warning -Automated:$Automated
return $false
}
Write-Log "NAS share is reachable: $SharePath" -Level Success -Automated:$Automated
return $true
}
function Start-FreeFileSyncProcess {
param([switch]$Automated)
Write-Log "Starting FreeFileSync batch synchronization..." -Level Info -Automated:$Automated
Write-Log " EXE: $FreeFileSyncExe" -Level Info -Automated:$Automated
Write-Log " BATCH: $FfsBatchFile" -Level Info -Automated:$Automated
# Ensure FreeFileSync is killed if PowerShell exits gracefully
Register-EngineEvent PowerShell.Exiting -Action {
if ($global:FFS_Process -and -not $global:FFS_Process.HasExited) {
try {
$global:FFS_Process.Kill()
}
catch {
}
}
} | Out-Null
try {
# Use ProcessStartInfo to ensure that no window is shown
$psi = New-Object System.Diagnostics.ProcessStartInfo
$psi.FileName = $FreeFileSyncExe
$psi.Arguments = "`"$FfsBatchFile`""
$psi.WorkingDirectory = [System.IO.Path]::GetDirectoryName($FreeFileSyncExe)
$psi.UseShellExecute = $false
$psi.CreateNoWindow = $true
$psi.WindowStyle = [System.Diagnostics.ProcessWindowStyle]::Hidden
$psi.RedirectStandardOutput = $true
$psi.RedirectStandardError = $true
$proc = [System.Diagnostics.Process]::Start($psi)
$global:FFS_Process = $proc
# Capture FFS console output
$stdOut = $proc.StandardOutput.ReadToEnd()
$stdErr = $proc.StandardError.ReadToEnd()
$proc.WaitForExit()
$exitCode = $proc.ExitCode
if ($stdOut) {
Write-Log "FreeFileSync STDOUT:" -Level Info -Automated:$Automated
Write-Log $stdOut.TrimEnd() -Level Info -Automated:$Automated
}
if ($stdErr) {
Write-Log "FreeFileSync STDERR:" -Level Warning -Automated:$Automated
Write-Log $stdErr.TrimEnd() -Level Warning -Automated:$Automated
}
# Store exit code
$script:SyncStats.ExitCode = $exitCode
# Interpret exit code
switch ($exitCode) {
0 {
Write-Log "FreeFileSync completed successfully (exit code 0)" -Level Success -Automated:$Automated
$script:SyncStats.Success = $true
return $true
}
1 {
Write-Log "FreeFileSync completed with warnings (exit code 1)" -Level Warning -Automated:$Automated
$script:SyncStats.Success = $true
return $true
}
2 {
Write-Log "FreeFileSync completed with errors (exit code 2)" -Level Error -Automated:$Automated
$script:SyncStats.ErrorMessage = "FreeFileSync reported errors"
return $false
}
3 {
Write-Log "FreeFileSync was cancelled (exit code 3)" -Level Warning -Automated:$Automated
$script:SyncStats.ErrorMessage = "FreeFileSync was cancelled"
return $false
}
default {
Write-Log "FreeFileSync exited with unexpected code $exitCode" -Level Error -Automated:$Automated
$script:SyncStats.ErrorMessage = "Unexpected exit code: $exitCode"
return $false
}
}
}
catch {
Write-Log "Failed to execute FreeFileSync: $_" -Level Error -Automated:$Automated
$script:SyncStats.ErrorMessage = "Execution failed: $_"
return $false
}
finally {
if ($global:FFS_Process -and -not $global:FFS_Process.HasExited) {
Write-Log "PowerShell is stopping. Killing FreeFileSync process (PID $($global:FFS_Process.Id))..." -Level Warning -Automated:$Automated
try {
$global:FFS_Process.Kill()
}
catch {
Write-Log "Failed to kill FreeFileSync process: $_" -Level Error -Automated:$Automated
}
}
}
}
function Write-SyncSummary {
param([switch]$Automated)
$script:SyncStats.EndTime = Get-Date
$duration = $script:SyncStats.EndTime - $script:SyncStats.StartTime
Write-Log "" -Level Info -Automated:$Automated
Write-Log "========================================" -Level Info -Automated:$Automated
Write-Log "SYNC SUMMARY" -Level Info -Automated:$Automated
Write-Log "========================================" -Level Info -Automated:$Automated
Write-Log "Start Time : $($script:SyncStats.StartTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Info -Automated:$Automated
Write-Log "End Time : $($script:SyncStats.EndTime.ToString('yyyy-MM-dd HH:mm:ss'))" -Level Info -Automated:$Automated
Write-Log "Duration : $($duration.Hours)h $($duration.Minutes)m $($duration.Seconds)s" -Level Info -Automated:$Automated
Write-Log "Status : $(if ($script:SyncStats.Success) { 'SUCCESS' } else { 'FAILED' })" -Level $(if ($script:SyncStats.Success) { 'Success' } else { 'Error' }) -Automated:$Automated
Write-Log "Exit Code : $($script:SyncStats.ExitCode)" -Level Info -Automated:$Automated
if ($script:SyncStats.ErrorMessage) {
Write-Log "Error : $($script:SyncStats.ErrorMessage)" -Level Error -Automated:$Automated
}
Write-Log "========================================" -Level Info -Automated:$Automated
}
# Main Business Logic ======================================================
function Start-BusinessLogic {
param([switch]$Automated)
Write-Log "========================================" -Level Info -Automated:$Automated
Write-Log "FreeFileSync Process Started" -Level Info -Automated:$Automated
Write-Log "Script Version: $ScriptVersion ($ScriptDate)" -Level Info -Automated:$Automated
Write-Log "========================================" -Level Info -Automated:$Automated
# Check prerequisites
if (-not (Test-Prerequisites -Automated:$Automated)) {
Write-Log "Prerequisites check failed. Aborting sync." -Level Error -Automated:$Automated
exit 1
}
# Connect to NAS share
if (-not (Connect-NasShare -SharePath $NasRootShare -Automated:$Automated)) {
Write-Log "Failed to connect to NAS share. Aborting sync." -Level Error -Automated:$Automated
exit 1
}
# Verify NAS reachability
if (-not (Test-NasReachability -SharePath $NasRootShare -Automated:$Automated)) {
Write-Log "NAS share is not reachable. Continuing anyway..." -Level Warning -Automated:$Automated
}
# Execute FreeFileSync
$syncResult = Start-FreeFileSyncProcess -Automated:$Automated
# Print summary
Write-SyncSummary -Automated:$Automated
# Exit with appropriate code
if (-not $syncResult) {
exit 1
}
}
# Entry Point ==============================================================
if ($Automated) {
if (Get-Command Invoke-ScheduledExecution -ErrorAction SilentlyContinue) {
Invoke-ScheduledExecution `
-Config $Config `
-Automated:$Automated `
-CurrentDateTimeUtc $CurrentDateTimeUtc `
-ScriptBlock { Start-BusinessLogic -Automated:$Automated }
}
else {
Write-Log "Invoke-ScheduledExecution not available. Execution aborted." -Level Error -Automated:$Automated
exit 1
}
}
else {
Write-Log "Manual execution started" -Level Info -Automated:$Automated
Start-BusinessLogic -Automated:$Automated
}