|
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