mirror of
https://github.com/MAKS-IT-COM/uscheduler.git
synced 2026-02-14 06:37:18 +01:00
374 lines
13 KiB
PowerShell
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
|
|
}
|