# ============================================================================ # EasyStream - Folder Sync Script (Repos -> Docker-Progs) # ============================================================================ # This script syncs changes from E:\repos\easystream-main to E:\docker-progs\easystream-main # # Usage: # .\sync-to-docker-progs.ps1 # One-time sync # .\sync-to-docker-progs.ps1 -Watch # Continuous monitoring # .\sync-to-docker-progs.ps1 -Verbose # Detailed output # # Requirements: PowerShell 5.0 or higher # ============================================================================ param( [switch]$Watch = $false, [switch]$Verbose = $false, [switch]$DryRun = $false ) # Configuration $SourcePath = "E:\repos\easystream-main" $DestPath = "E:\docker-progs\easystream-main" $LogFile = "E:\repos\easystream-main\sync.log" # Exclusions (paths to ignore) $Exclusions = @( ".git", ".gitignore", "node_modules", "vendor", "f_data\cache", "f_data\tmp", "f_data\logs", "f_data\sessions", "f_data\uploads", "*.log", "sync.log", "sync-to-docker-progs.ps1" ) # ============================================================================ # Functions # ============================================================================ function Write-Log { param([string]$Message, [string]$Level = "INFO") $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" $logMessage = "[$timestamp] [$Level] $Message" # Write to console switch ($Level) { "ERROR" { Write-Host $logMessage -ForegroundColor Red } "WARN" { Write-Host $logMessage -ForegroundColor Yellow } "SUCCESS" { Write-Host $logMessage -ForegroundColor Green } default { Write-Host $logMessage -ForegroundColor White } } # Write to log file Add-Content -Path $LogFile -Value $logMessage } function Test-ShouldExclude { param([string]$Path) foreach ($exclusion in $Exclusions) { if ($Path -like "*$exclusion*") { return $true } } return $false } function Sync-Folder { param([bool]$InitialSync = $false) try { # Check if source exists if (-not (Test-Path $SourcePath)) { Write-Log "Source path does not exist: $SourcePath" "ERROR" return $false } # Create destination if it doesn't exist if (-not (Test-Path $DestPath)) { Write-Log "Creating destination directory: $DestPath" "INFO" if (-not $DryRun) { New-Item -ItemType Directory -Path $DestPath -Force | Out-Null } } # Build robocopy exclusion parameters $excludeDirs = @() $excludeFiles = @() foreach ($exclusion in $Exclusions) { if ($exclusion.Contains("\")) { $excludeDirs += $exclusion } elseif ($exclusion.StartsWith("*")) { $excludeFiles += $exclusion } else { $excludeDirs += $exclusion } } # Build robocopy command $robocopyArgs = @( $SourcePath, $DestPath, "/MIR", # Mirror (delete files in dest that don't exist in source) "/R:3", # Retry 3 times "/W:5", # Wait 5 seconds between retries "/MT:8", # Multi-threaded (8 threads) "/NFL", # No file list "/NDL", # No directory list "/NP", # No progress "/BYTES" # Show sizes in bytes ) # Add exclusions if ($excludeDirs.Count -gt 0) { $robocopyArgs += "/XD" $robocopyArgs += $excludeDirs } if ($excludeFiles.Count -gt 0) { $robocopyArgs += "/XF" $robocopyArgs += $excludeFiles } # Add verbose flag if requested if ($Verbose) { $robocopyArgs = $robocopyArgs | Where-Object { $_ -ne "/NFL" -and $_ -ne "/NDL" } } # Execute sync if ($InitialSync) { Write-Log "Starting initial sync..." "INFO" } else { Write-Log "Syncing changes..." "INFO" } if ($DryRun) { Write-Log "DRY RUN - Would execute: robocopy $($robocopyArgs -join ' ')" "WARN" return $true } $result = & robocopy $robocopyArgs $exitCode = $LASTEXITCODE # Robocopy exit codes: # 0 = No files copied # 1 = Files copied successfully # 2 = Extra files or directories detected # 3 = Files copied + extra files detected # 4+ = Error if ($exitCode -ge 8) { Write-Log "Sync failed with exit code: $exitCode" "ERROR" return $false } elseif ($exitCode -gt 0) { Write-Log "Sync completed successfully (Exit code: $exitCode)" "SUCCESS" return $true } else { if ($Verbose) { Write-Log "No changes detected" "INFO" } return $true } } catch { Write-Log "Sync error: $($_.Exception.Message)" "ERROR" return $false } } function Start-Watcher { Write-Log "Starting file system watcher..." "INFO" Write-Log "Monitoring: $SourcePath" "INFO" Write-Log "Syncing to: $DestPath" "INFO" Write-Log "Press Ctrl+C to stop..." "WARN" # Create file system watcher $watcher = New-Object System.IO.FileSystemWatcher $watcher.Path = $SourcePath $watcher.IncludeSubdirectories = $true $watcher.EnableRaisingEvents = $true # Filters $watcher.NotifyFilter = [System.IO.NotifyFilters]::FileName -bor [System.IO.NotifyFilters]::DirectoryName -bor [System.IO.NotifyFilters]::LastWrite -bor [System.IO.NotifyFilters]::Size # Debounce mechanism (prevent multiple syncs for rapid changes) $script:lastSync = Get-Date $script:syncPending = $false $debounceSeconds = 2 # Event handler $onChange = { param($sender, $e) # Check if file should be excluded if (Test-ShouldExclude -Path $e.FullPath) { return } $now = Get-Date $timeSinceLastSync = ($now - $script:lastSync).TotalSeconds if ($timeSinceLastSync -gt $debounceSeconds) { Write-Log "Change detected: $($e.ChangeType) - $($e.Name)" "INFO" Sync-Folder | Out-Null $script:lastSync = Get-Date } else { if (-not $script:syncPending) { $script:syncPending = $true Write-Log "Changes detected, sync scheduled..." "INFO" } } } # Register events Register-ObjectEvent -InputObject $watcher -EventName Changed -Action $onChange | Out-Null Register-ObjectEvent -InputObject $watcher -EventName Created -Action $onChange | Out-Null Register-ObjectEvent -InputObject $watcher -EventName Deleted -Action $onChange | Out-Null Register-ObjectEvent -InputObject $watcher -EventName Renamed -Action $onChange | Out-Null # Timer for debounced syncs $timer = New-Object System.Timers.Timer $timer.Interval = $debounceSeconds * 1000 $timer.AutoReset = $true $onTimer = { if ($script:syncPending) { Sync-Folder | Out-Null $script:syncPending = $false $script:lastSync = Get-Date } } Register-ObjectEvent -InputObject $timer -EventName Elapsed -Action $onTimer | Out-Null $timer.Start() # Keep script running try { while ($true) { Start-Sleep -Seconds 1 } } finally { # Cleanup $watcher.EnableRaisingEvents = $false $watcher.Dispose() $timer.Stop() $timer.Dispose() Get-EventSubscriber | Unregister-Event Write-Log "Watcher stopped" "INFO" } } # ============================================================================ # Main Execution # ============================================================================ Write-Host "" Write-Host "============================================================================" -ForegroundColor Cyan Write-Host " EasyStream Folder Sync - Repos to Docker-Progs" -ForegroundColor Cyan Write-Host "============================================================================" -ForegroundColor Cyan Write-Host "" # Initial sync $syncResult = Sync-Folder -InitialSync $true if (-not $syncResult) { Write-Log "Initial sync failed. Exiting." "ERROR" exit 1 } # Watch mode if ($Watch) { Write-Host "" Start-Watcher } else { Write-Host "" Write-Log "Single sync completed. Use -Watch for continuous monitoring." "INFO" Write-Host "" }