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