|
1 | | -<# |
| 1 | +<# |
2 | 2 | .SYNOPSIS |
3 | | - PowerShell Script for Synchronizing Folders to AD Computers via GPO. |
| 3 | + PowerShell script to synchronize folders from a network share to Active Directory computers via GPO. |
4 | 4 |
|
5 | 5 | .DESCRIPTION |
6 | | - This script synchronizes folders from a network location to AD computers, ensuring that only new |
7 | | - or updated files are copied while outdated files are removed. It also includes full logging of the operation. |
| 6 | + This script synchronizes a folder from a network location (e.g. NETLOGON share) |
| 7 | + to the local Administrator's desktop on AD workstations. It copies only new or updated files, |
| 8 | + and removes obsolete files and folders that are no longer in the source. |
| 9 | + Intended for use as a machine-level GPO startup script. |
8 | 10 |
|
9 | 11 | .AUTHOR |
10 | 12 | Luiz Hamilton Silva - @brazilianscriptguy |
11 | 13 |
|
12 | 14 | .VERSION |
13 | | - Last Updated: October 22, 2024 |
| 15 | + Updated: August 5, 2025 - Refactored for GPO startup context and system execution. |
14 | 16 | #> |
15 | 17 |
|
16 | | -# Determine the script name and set up logging path |
| 18 | +param ( |
| 19 | + [string]$LogDirectory = "C:\Logs-TEMP" |
| 20 | +) |
| 21 | + |
| 22 | +# Get the script name and define the full log path |
17 | 23 | $scriptName = [System.IO.Path]::GetFileNameWithoutExtension($MyInvocation.MyCommand.Name) |
18 | | -$logDir = 'C:\Logs-TEMP' |
19 | 24 | $logFileName = "${scriptName}.log" |
20 | | -$logPath = Join-Path $logDir $logFileName |
| 25 | +$logPath = Join-Path $LogDirectory $logFileName |
21 | 26 |
|
22 | 27 | # Ensure the log directory exists |
23 | | -if (-not (Test-Path $logDir)) { |
| 28 | +if (-not (Test-Path $LogDirectory)) { |
24 | 29 | try { |
25 | | - New-Item -Path $logDir -ItemType Directory -ErrorAction Stop | Out-Null |
26 | | - } catch { |
27 | | - Write-Error "Failed to create log directory at $logDir. Logging will not be possible." |
28 | | - return |
| 30 | + New-Item -Path $LogDirectory -ItemType Directory -ErrorAction Stop | Out-Null |
| 31 | + } |
| 32 | + catch { |
| 33 | + Write-Error "Failed to create log directory at $LogDirectory. Logging will be disabled." |
| 34 | + exit 1 |
29 | 35 | } |
30 | 36 | } |
31 | 37 |
|
32 | | -# Enhanced logging function with error handling |
33 | | -function Log-Message { |
| 38 | +# Logging function with timestamp and severity |
| 39 | +function Write-Log { |
34 | 40 | param ( |
35 | | - [Parameter(Mandatory = $true)] |
36 | | - [string]$Message |
| 41 | + [Parameter(Mandatory = $true)][string]$Message, |
| 42 | + [string]$Severity = "INFO" |
37 | 43 | ) |
38 | 44 | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss" |
39 | | - $logEntry = "[$timestamp] $Message" |
| 45 | + $logEntry = "[$timestamp] [$Severity] $Message" |
40 | 46 | try { |
41 | | - Add-Content -Path $logPath -Value $logEntry -ErrorAction Stop |
42 | | - } catch { |
| 47 | + Add-Content -Path $logPath -Value $logEntry -Encoding UTF8 -ErrorAction Stop |
| 48 | + } |
| 49 | + catch { |
43 | 50 | Write-Error "Failed to write to log: $_" |
44 | 51 | } |
45 | 52 | } |
46 | 53 |
|
47 | | -# Define the source folder path |
| 54 | +# === CONFIGURATION === |
| 55 | + |
| 56 | +# Source network folder (change as needed) |
48 | 57 | $sourceFolderPath = "\\forest-logonserver-name\NETLOGON\Source-Folder-Name" |
49 | 58 |
|
50 | | -# Get the specific desktop path |
51 | | -$userDesktopPath = 'C:\Users\Administrator\Desktop' # Destination path |
| 59 | +# Determine Administrator profile path |
| 60 | +$adminProfilePath = "$env:SystemDrive\Users\Administrator" |
| 61 | +$adminDesktopPath = Join-Path $adminProfilePath "Desktop" |
52 | 62 |
|
53 | | -# Define the destination folder path on the desktop |
54 | | -$destinationFolderPath = Join-Path -Path $userDesktopPath -ChildPath "Destination-Folder-Name" |
| 63 | +# Check if desktop path exists |
| 64 | +if (-not (Test-Path -Path $adminDesktopPath)) { |
| 65 | + Write-Log "Administrator desktop not found at: $adminDesktopPath" -Severity "ERROR" |
| 66 | + exit 1 |
| 67 | +} |
| 68 | + |
| 69 | +# Define destination folder under Administrator desktop |
| 70 | +$destinationFolderPath = Join-Path -Path $adminDesktopPath -ChildPath "Destination-Folder-Name" |
| 71 | + |
| 72 | +# === FOLDER SYNC FUNCTION === |
55 | 73 |
|
56 | | -# Function to copy files if they are newer or missing and remove older files |
57 | 74 | function Sync-Folders { |
58 | 75 | param ( |
59 | 76 | [string]$sourceFolder, |
60 | 77 | [string]$destinationFolder |
61 | 78 | ) |
62 | 79 |
|
63 | | - # Ensure the destination directory exists |
| 80 | + # Create destination folder if it doesn't exist |
64 | 81 | if (-not (Test-Path -Path $destinationFolder)) { |
65 | 82 | try { |
66 | | - New-Item -ItemType Directory -Path $destinationFolder | Out-Null |
67 | | - Log-Message "Created directory: $destinationFolder" |
68 | | - } catch { |
69 | | - Log-Message "Failed to create directory: $destinationFolder. Error: $_" |
| 83 | + New-Item -ItemType Directory -Path $destinationFolder -ErrorAction Stop | Out-Null |
| 84 | + Write-Log "Created destination folder: $destinationFolder" |
| 85 | + } |
| 86 | + catch { |
| 87 | + Write-Log "Failed to create destination folder: $destinationFolder. Error: $_" -Severity "ERROR" |
70 | 88 | return |
71 | 89 | } |
72 | 90 | } |
73 | 91 |
|
74 | | - # Copy new/updated files from source to destination |
75 | | - Get-ChildItem -Path $sourceFolder -Recurse | ForEach-Object { |
76 | | - $destinationPath = $_.FullName.Replace($sourceFolder, $destinationFolder) |
| 92 | + # Copy new or updated files from source to destination |
| 93 | + $sourceItems = Get-ChildItem -Path $sourceFolder -Recurse -Force |
| 94 | + foreach ($item in $sourceItems) { |
| 95 | + $relativePath = $item.FullName.Substring($sourceFolder.Length).TrimStart('\') |
| 96 | + $destinationPath = Join-Path $destinationFolder $relativePath |
77 | 97 |
|
78 | | - if ($_.PSIsContainer) { |
| 98 | + if ($item.PSIsContainer) { |
79 | 99 | if (-not (Test-Path -Path $destinationPath)) { |
80 | 100 | try { |
81 | | - New-Item -ItemType Directory -Path $destinationPath | Out-Null |
82 | | - Log-Message "Created directory: $destinationPath" |
83 | | - } catch { |
84 | | - Log-Message "Failed to create directory: $destinationPath. Error: $_" |
| 101 | + New-Item -ItemType Directory -Path $destinationPath -ErrorAction Stop | Out-Null |
| 102 | + Write-Log "Created directory: $destinationPath" |
| 103 | + } |
| 104 | + catch { |
| 105 | + Write-Log "Failed to create directory: $destinationPath. Error: $_" -Severity "ERROR" |
85 | 106 | } |
86 | 107 | } |
87 | | - } else { |
| 108 | + } |
| 109 | + else { |
88 | 110 | try { |
89 | | - if ((-not (Test-Path -Path $destinationPath)) -or ($_.LastWriteTime -gt (Get-Item -Path $destinationPath).LastWriteTime)) { |
90 | | - Copy-Item -Path $_.FullName -Destination $destinationPath -Force |
91 | | - Log-Message "Updated: $destinationPath" |
92 | | - } else { |
93 | | - Log-Message "Skipped: $destinationPath" |
| 111 | + $destItem = Get-Item -Path $destinationPath -ErrorAction SilentlyContinue |
| 112 | + if ((-not $destItem) -or ($item.LastWriteTime -gt $destItem.LastWriteTime)) { |
| 113 | + Copy-Item -Path $item.FullName -Destination $destinationPath -Force -ErrorAction Stop |
| 114 | + Write-Log "Copied/Updated file: $destinationPath" |
94 | 115 | } |
95 | | - } catch { |
96 | | - Log-Message "Failed to copy: $destinationPath. Error: $_" |
| 116 | + else { |
| 117 | + Write-Log "Skipped (already up-to-date): $destinationPath" |
| 118 | + } |
| 119 | + } |
| 120 | + catch { |
| 121 | + Write-Log "Failed to copy file: $destinationPath. Error: $_" -Severity "ERROR" |
97 | 122 | } |
98 | 123 | } |
99 | 124 | } |
100 | 125 |
|
101 | | - # Remove files/folders from destination if they don't exist in the source |
102 | | - Get-ChildItem -Path $destinationFolder -Recurse | ForEach-Object { |
103 | | - $sourcePath = $_.FullName.Replace($destinationFolder, $sourceFolder) |
| 126 | + # Remove obsolete files/folders in destination that don't exist in source |
| 127 | + $destItems = Get-ChildItem -Path $destinationFolder -Recurse -Force |
| 128 | + foreach ($item in $destItems) { |
| 129 | + $relativePath = $item.FullName.Substring($destinationFolder.Length).TrimStart('\') |
| 130 | + $sourcePath = Join-Path $sourceFolder $relativePath |
104 | 131 |
|
105 | 132 | if (-not (Test-Path -Path $sourcePath)) { |
106 | 133 | try { |
107 | | - Remove-Item -Path $_.FullName -Recurse -Force |
108 | | - Log-Message "Removed: $($_.FullName)" |
109 | | - } catch { |
110 | | - Log-Message "Failed to remove: $($_.FullName). Error: $_" |
| 134 | + Remove-Item -Path $item.FullName -Recurse -Force -ErrorAction Stop |
| 135 | + Write-Log "Removed obsolete item: $($item.FullName)" |
| 136 | + } |
| 137 | + catch { |
| 138 | + Write-Log "Failed to remove obsolete item: $($item.FullName). Error: $_" -Severity "ERROR" |
111 | 139 | } |
112 | 140 | } |
113 | 141 | } |
114 | 142 | } |
115 | 143 |
|
116 | | -# Copy the folder to the desktop, only updating files that are newer and removing older files |
| 144 | +# === EXECUTION === |
| 145 | + |
117 | 146 | if (Test-Path -Path $sourceFolderPath) { |
118 | 147 | Sync-Folders -sourceFolder $sourceFolderPath -destinationFolder $destinationFolderPath |
119 | | - Log-Message "Folder successfully synced to $destinationFolderPath with only new/updated files and older files removed." |
120 | | -} else { |
121 | | - Log-Message "Source folder does not exist: $sourceFolderPath." |
| 148 | + Write-Log "Synchronization completed successfully to $destinationFolderPath." |
| 149 | +} |
| 150 | +else { |
| 151 | + Write-Log "Source folder not found: $sourceFolderPath" -Severity "ERROR" |
122 | 152 | } |
123 | 153 |
|
124 | 154 | # End of script |
0 commit comments