thenightfury commited on
Commit
cf452a0
·
verified ·
1 Parent(s): b7745ee

Upload 5 files

Browse files
Files changed (5) hide show
  1. .gitignore +2 -0
  2. FileOrganizer.ps1 +1408 -0
  3. LICENSE +26 -0
  4. README.md +31 -0
  5. config.json +99 -0
.gitignore ADDED
@@ -0,0 +1,2 @@
 
 
 
1
+ *.log
2
+ lastused.json
FileOrganizer.ps1 ADDED
@@ -0,0 +1,1408 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FileOrganizer.ps1 - Modern File Organizer with GUI & CLI
2
+ # A professional-grade Windows utility for organizing files by type
3
+ #
4
+ # Default config.json (auto-created if missing):
5
+ # <#
6
+ # {
7
+ # "categories": {
8
+ # "Word Files": ["doc", "docx", "docm", "dotx"],
9
+ # "PowerPoint Files": ["ppt", "pptx", "pptm"],
10
+ # "Excel Files": ["xls", "xlsx", "xlsm", "csv"],
11
+ # "PDF Files": ["pdf"],
12
+ # "Images": ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff"],
13
+ # "Videos": ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
14
+ # "Executables": ["exe", "msi", "bat", "cmd", "ps1"],
15
+ # "Archives": ["zip", "rar", "7z", "tar", "gz"],
16
+ # "Audio": ["mp3", "wav", "flac", "ogg", "aac", "wma"],
17
+ # "Code": ["ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs"],
18
+ # "Text": ["txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg"]
19
+ # },
20
+ # "excludePatterns": ["Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*"],
21
+ # "createTypeSubfolder": true,
22
+ # "defaultAction": "Move",
23
+ # "defaultDepth": 100
24
+ # }
25
+ # #>
26
+ #
27
+ # Usage:
28
+ # .\FileOrganizer.ps1 # Interactive GUI mode
29
+ # .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun
30
+ # .\FileOrganizer.ps1 -SourcePath "C:\Src" -DestPath "D:\Dest" -Action Copy -Depth 2
31
+ # .\FileOrganizer.ps1 -CategoriesFile "custom.json"
32
+ # Get-Help .\FileOrganizer.ps1 -Detailed
33
+
34
+ #Requires -Version 5.1
35
+
36
+ # ============================================================
37
+ # IMPORTANT: param() MUST come first before any executable code
38
+ # ============================================================
39
+
40
+ [CmdletBinding()]
41
+ param (
42
+ [Parameter(HelpMessage="Source folder path to scan")]
43
+ [string]$SourcePath,
44
+
45
+ [Parameter(HelpMessage="Destination folder path")]
46
+ [string]$DestPath,
47
+
48
+ [Parameter(HelpMessage="Scan depth: 0=Main only, 1=Subfolders, 2=SubSubfolders, 100=All")]
49
+ [ValidateRange(0, 100)]
50
+ [int]$Depth = 100,
51
+
52
+ [Parameter(HelpMessage="Action to perform: Move or Copy")]
53
+ [ValidateSet("Move", "Copy")]
54
+ [string]$Action = "Move",
55
+
56
+ [Parameter(HelpMessage="Simulate operation without moving/copying files")]
57
+ [switch]$DryRun,
58
+
59
+ [Parameter(HelpMessage="Path to custom categories JSON file")]
60
+ [string]$CategoriesFile,
61
+
62
+ [Parameter(HelpMessage="Run without looping for additional tasks")]
63
+ [switch]$NoLoop,
64
+
65
+ [Parameter(HelpMessage="Use separate destination folder per file type")]
66
+ [switch]$SeparateDestinations,
67
+
68
+ [Parameter(HelpMessage="Show this help message")]
69
+ [switch]$Help
70
+ )
71
+
72
+ # ============================================================
73
+ # Now we can have executable code after param()
74
+ # ============================================================
75
+
76
+ # Early error handling - don't let script silently fail
77
+ $ErrorActionPreference = "Continue"
78
+ $Script:StartupError = $null
79
+
80
+ # Check execution policy
81
+ try {
82
+ $execPolicy = Get-ExecutionPolicy -ErrorAction SilentlyContinue
83
+ if ($execPolicy -eq "Restricted") {
84
+ Write-Host "WARNING: Execution Policy is Restricted." -ForegroundColor Yellow
85
+ Write-Host "Run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor Yellow
86
+ }
87
+ } catch { }
88
+
89
+ # Fix for $PSScriptRoot being empty when run via right-click "Run with PowerShell"
90
+ $Script:ScriptPath = if ($PSScriptRoot) { $PSScriptRoot } elseif ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
91
+ if (-not $Script:ScriptPath) { $Script:ScriptPath = Get-Location }
92
+
93
+ # Pre-load Windows Forms (needed for GUI dialogs)
94
+ try {
95
+ Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
96
+ } catch {
97
+ $errMsg = $_.Exception.Message
98
+ $Script:StartupError = "Failed to load Windows Forms - $errMsg"
99
+ }
100
+
101
+ #region Global Variables
102
+ $Script:Config = $null
103
+ $Script:LastUsed = $null
104
+ $Script:ScriptName = "FileOrganizer"
105
+ $Script:LogFile = $null
106
+ $Script:IsElevated = $false
107
+ $Script:PSVersion = $PSVersionTable.PSVersion.Major
108
+ #endregion
109
+
110
+ # Check for startup errors
111
+ if ($Script:StartupError) {
112
+ Write-Host "ERROR: $Script:StartupError" -ForegroundColor Red
113
+ Write-Host "Press Enter to exit..." -ForegroundColor Yellow
114
+ Read-Host
115
+ exit 1
116
+ }
117
+
118
+ # Show startup banner
119
+ Write-Host ""
120
+ Write-Host "======================================" -ForegroundColor Cyan
121
+ Write-Host " FileOrganizer v2.0 - Loading..." -ForegroundColor Cyan
122
+ Write-Host "======================================" -ForegroundColor Cyan
123
+ Write-Host "Script Path: $Script:ScriptPath" -ForegroundColor Gray
124
+ Write-Host "PowerShell: $($Script:PSVersion)" -ForegroundColor Gray
125
+ Write-Host ""
126
+
127
+ #region Comment-Based Help
128
+ <#
129
+ .SYNOPSIS
130
+ Organizes files by type into destination folders.
131
+
132
+ .DESCRIPTION
133
+ FileOrganizer is a professional-grade Windows utility that scans a source folder,
134
+ categorizes files by type (Word, PDF, Images, etc.), and moves or copies them to
135
+ destination folders. Supports both CLI and interactive GUI modes.
136
+
137
+ Features:
138
+ - CLI and Interactive GUI hybrid operation
139
+ - Configurable file categories via JSON
140
+ - Move or Copy operations
141
+ - Dry-Run mode for safe testing
142
+ - Modern Out-GridView selection
143
+ - Remembers last used folders
144
+ - Comprehensive logging
145
+
146
+ .PARAMETER SourcePath
147
+ Source folder to scan. If not provided, launches interactive folder browser.
148
+
149
+ .PARAMETER DestPath
150
+ Destination folder for organized files. If not provided, launches interactive browser.
151
+
152
+ .PARAMETER Depth
153
+ Scan depth: 0=Main folder only, 1=One level deep, 2=Two levels, 100=All (default).
154
+
155
+ .PARAMETER Action
156
+ Operation type: "Move" (default) or "Copy". Use Copy for safer testing.
157
+
158
+ .PARAMETER DryRun
159
+ Simulates the operation without actually moving/copying files. Shows what would happen.
160
+
161
+ .PARAMETER CategoriesFile
162
+ Path to custom JSON file with file type categories.
163
+
164
+ .PARAMETER NoLoop
165
+ Runs single operation without prompting for additional tasks.
166
+
167
+ .PARAMETER SeparateDestinations
168
+ Use separate destination folder per file type (prompts for each).
169
+
170
+ .EXAMPLE
171
+ .\FileOrganizer.ps1
172
+
173
+ Launches interactive GUI mode with folder browsers and selections.
174
+
175
+ .EXAMPLE
176
+ .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun
177
+
178
+ Scans Downloads, shows what would be organized to D:\Organized without making changes.
179
+
180
+ .EXAMPLE
181
+ .\FileOrganizer.ps1 -SourcePath "C:\Files" -Action Copy -Depth 2
182
+
183
+ Copies files from C:\Files (including subfolders 2 levels deep) with interactive destination.
184
+
185
+ .NOTES
186
+ Author: FileOrganizer Team
187
+ Version: 2.0.0
188
+ Requires: PowerShell 5.1+
189
+ #>
190
+ #endregion
191
+
192
+ #region Initialization Functions
193
+
194
+ function Initialize-Environment {
195
+ <#
196
+ .SYNOPSIS
197
+ Initializes the script environment - logging, config, paths
198
+ #>
199
+ [CmdletBinding()]
200
+ param()
201
+
202
+ # Set up log file path
203
+ $logDate = Get-Date -Format "yyyy-MM-dd"
204
+ $Script:LogFile = Join-Path $Script:ScriptPath "$($Script:ScriptName)_$logDate.log"
205
+
206
+ # Create script path if it doesn't exist (for when running from other locations)
207
+ if (-not $Script:ScriptPath) {
208
+ $Script:ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
209
+ }
210
+
211
+ # Load configuration
212
+ Load-Config -CategoriesFile $CategoriesFile
213
+
214
+ # Load last used paths
215
+ Load-LastUsed
216
+
217
+ # Check PowerShell version for feature compatibility
218
+ if ($Script:PSVersion -lt 5) {
219
+ Write-Warning "PowerShell version 5.1 or higher recommended. Current: $($Script:PSVersion)"
220
+ }
221
+
222
+ Write-Log "INFO" "FileOrganizer started - PS Version: $($Script:PSVersion)"
223
+ }
224
+
225
+ function Load-Config {
226
+ <#
227
+ .SYNOPSIS
228
+ Loads configuration from JSON file or creates default
229
+ #>
230
+ [CmdletBinding()]
231
+ param(
232
+ [string]$CategoriesFile
233
+ )
234
+
235
+ # Determine config file path
236
+ if ($CategoriesFile) {
237
+ $configPath = $CategoriesFile
238
+ } else {
239
+ $configPath = Join-Path $Script:ScriptPath "config.json"
240
+ }
241
+
242
+ # Default configuration
243
+ $defaultConfig = @{
244
+ categories = @{
245
+ "Word Files" = @("doc", "docx", "docm", "dotx")
246
+ "PowerPoint Files" = @("ppt", "pptx", "pptm")
247
+ "Excel Files" = @("xls", "xlsx", "xlsm", "csv")
248
+ "PDF Files" = @("pdf")
249
+ "Images" = @("jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff")
250
+ "Videos" = @("mp4", "avi", "mkv", "mov", "wmv", "flv")
251
+ "Executables" = @("exe", "msi", "bat", "cmd")
252
+ "Archives" = @("zip", "rar", "7z", "tar", "gz")
253
+ "Audio" = @("mp3", "wav", "flac", "ogg", "aac", "wma")
254
+ "Code" = @("ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs")
255
+ "Text" = @("txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg")
256
+ }
257
+ excludePatterns = @("Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*")
258
+ createTypeSubfolder = $true
259
+ defaultAction = "Move"
260
+ defaultDepth = 100
261
+ }
262
+
263
+ # Load existing or create default config
264
+ if (Test-Path $configPath) {
265
+ try {
266
+ $configContent = Get-Content $configPath -Raw -ErrorAction Stop
267
+ # Handle PS 5.1 vs PS 6+ compatibility
268
+ if ($Script:PSVersion -ge 6) {
269
+ $Script:Config = $configContent | ConvertFrom-Json -AsHashtable -ErrorAction Stop
270
+ } else {
271
+ $loadedConfig = $configContent | ConvertFrom-Json -ErrorAction Stop
272
+ # Convert PSObject to Hashtable for PS 5.1 compatibility
273
+ $Script:Config = @{}
274
+ $loadedConfig.PSObject.Properties | ForEach-Object {
275
+ $Script:Config[$_.Name] = $_.Value
276
+ }
277
+ }
278
+ Write-Log "INFO" "Configuration loaded from: $configPath"
279
+ } catch {
280
+ $errMsg = $_.Exception.Message
281
+ Write-Warning "Failed to load config, using defaults - $errMsg"
282
+ $Script:Config = $defaultConfig
283
+ Save-Config -Config $Script:Config -Path $configPath
284
+ }
285
+ } else {
286
+ # Create default config file
287
+ $Script:Config = $defaultConfig
288
+ try {
289
+ Save-Config -Config $Script:Config -Path $configPath
290
+ Write-Host "Created default config: $configPath" -ForegroundColor Cyan
291
+ } catch {
292
+ $errMsg = $_.Exception.Message
293
+ Write-Warning "Could not create config file - $errMsg"
294
+ }
295
+ }
296
+
297
+ # Ensure required keys exist
298
+ if (-not $Script:Config.categories) { $Script:Config.categories = $defaultConfig.categories }
299
+ if (-not $Script:Config.excludePatterns) { $Script:Config.excludePatterns = $defaultConfig.excludePatterns }
300
+ if (-not $Script:Config.createTypeSubfolder) { $Script:Config.createTypeSubfolder = $true }
301
+ }
302
+
303
+ function Save-Config {
304
+ <#
305
+ .SYNOPSIS
306
+ Saves configuration to JSON file
307
+ #>
308
+ [CmdletBinding()]
309
+ param(
310
+ [hashtable]$Config,
311
+ [string]$Path
312
+ )
313
+
314
+ try {
315
+ $Config | ConvertTo-Json -Depth 10 | Set-Content $Path -Force -ErrorAction Stop
316
+ } catch {
317
+ $errMsg = $_.Exception.Message
318
+ Write-Warning "Failed to save config - $errMsg"
319
+ }
320
+ }
321
+
322
+ function Load-LastUsed {
323
+ <#
324
+ .SYNOPSIS
325
+ Loads last used source/destination paths
326
+ #>
327
+ [CmdletBinding()]
328
+ param()
329
+
330
+ $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
331
+
332
+ if (Test-Path $lastUsedPath) {
333
+ try {
334
+ $content = Get-Content $lastUsedPath -Raw
335
+ # Handle PS 5.1 vs PS 6+ compatibility
336
+ if ($Script:PSVersion -ge 6) {
337
+ $Script:LastUsed = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop
338
+ } else {
339
+ $loaded = $content | ConvertFrom-Json -ErrorAction Stop
340
+ # Convert PSObject to Hashtable for PS 5.1 compatibility
341
+ $Script:LastUsed = @{}
342
+ $loaded.PSObject.Properties | ForEach-Object {
343
+ $Script:LastUsed[$_.Name] = $_.Value
344
+ }
345
+ }
346
+ } catch {
347
+ $Script:LastUsed = @{ sourcePath = ""; destPath = "" }
348
+ }
349
+ } else {
350
+ $Script:LastUsed = @{ sourcePath = ""; destPath = "" }
351
+ }
352
+ }
353
+
354
+ function Save-LastUsed {
355
+ <#
356
+ .SYNOPSIS
357
+ Saves last used paths for future sessions
358
+ #>
359
+ [CmdletBinding()]
360
+ param(
361
+ [string]$SourcePath,
362
+ [string]$DestPath
363
+ )
364
+
365
+ $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
366
+
367
+ $Script:LastUsed.sourcePath = $SourcePath
368
+ $Script:LastUsed.destPath = $DestPath
369
+
370
+ try {
371
+ $Script:LastUsed | ConvertTo-Json | Set-Content $lastUsedPath -Force
372
+ } catch {
373
+ $errMsg = $_.Exception.Message
374
+ Write-Warning "Could not save last used paths - $errMsg"
375
+ }
376
+ }
377
+
378
+ function Write-Log {
379
+ <#
380
+ .SYNOPSIS
381
+ Writes timestamped log entry to file
382
+ #>
383
+ [CmdletBinding()]
384
+ param(
385
+ [Parameter(Mandatory)]
386
+ [string]$Level,
387
+
388
+ [Parameter(Mandatory)]
389
+ [string]$Message
390
+ )
391
+
392
+ $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
393
+ $logEntry = "[$timestamp] [$Level] $Message"
394
+
395
+ # Write to log file
396
+ try {
397
+ Add-Content -Path $Script:LogFile -Value $logEntry -ErrorAction SilentlyContinue
398
+ } catch {
399
+ # Silently continue if logging fails
400
+ }
401
+
402
+ # Also output to console based on level
403
+ switch ($Level) {
404
+ "ERROR" { Write-Host $logEntry -ForegroundColor Red }
405
+ "WARN" { Write-Host $logEntry -ForegroundColor Yellow }
406
+ "INFO" { Write-Verbose $logEntry }
407
+ "DRYRUN" { Write-Host $logEntry -ForegroundColor Cyan }
408
+ }
409
+ }
410
+
411
+ #endregion
412
+
413
+ #region GUI Helper Functions
414
+
415
+ function Test-OutGridViewAvailable {
416
+ <#
417
+ .SYNOPSIS
418
+ Checks if Out-GridView is available (Windows only)
419
+ #>
420
+ if ($IsWindows -or (-not $IsLinux -and -not $IsMacOS)) {
421
+ return $true
422
+ }
423
+ return $false
424
+ }
425
+
426
+ function Select-SourceFolder {
427
+ <#
428
+ .SYNOPSIS
429
+ Shows folder browser dialog for source selection
430
+ #>
431
+ [CmdletBinding()]
432
+ param(
433
+ [string]$InitialPath
434
+ )
435
+
436
+ # Use last used path if available and no initial path provided
437
+ if (-not $InitialPath -and $Script:LastUsed.sourcePath) {
438
+ if (Test-Path $Script:LastUsed.sourcePath) {
439
+ $InitialPath = $Script:LastUsed.sourcePath
440
+ }
441
+ }
442
+
443
+ # Use Windows Forms folder browser
444
+ Add-Type -AssemblyName System.Windows.Forms
445
+ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
446
+ $dialog.Description = "Select Source Folder to Scan"
447
+ $dialog.ShowNewFolderButton = $false
448
+
449
+ if ($InitialPath -and (Test-Path $InitialPath)) {
450
+ $dialog.SelectedPath = $InitialPath
451
+ }
452
+
453
+ $result = $dialog.ShowDialog()
454
+
455
+ if ($result -eq "OK") {
456
+ Write-Log "INFO" "Source folder selected: $($dialog.SelectedPath)"
457
+ return $dialog.SelectedPath
458
+ }
459
+
460
+ return $null
461
+ }
462
+
463
+ function Select-DestinationFolder {
464
+ <#
465
+ .SYNOPSIS
466
+ Shows folder browser dialog for destination selection
467
+ #>
468
+ [CmdletBinding()]
469
+ param(
470
+ [string]$Description = "Select Destination Folder",
471
+ [string]$InitialPath
472
+ )
473
+
474
+ # Use last used path if available
475
+ if (-not $InitialPath -and $Script:LastUsed.destPath) {
476
+ if (Test-Path $Script:LastUsed.destPath) {
477
+ $InitialPath = $Script:LastUsed.destPath
478
+ }
479
+ }
480
+
481
+ Add-Type -AssemblyName System.Windows.Forms
482
+ $dialog = New-Object System.Windows.Forms.FolderBrowserDialog
483
+ $dialog.Description = $Description
484
+ $dialog.ShowNewFolderButton = $true
485
+
486
+ if ($InitialPath -and (Test-Path $InitialPath)) {
487
+ $dialog.SelectedPath = $InitialPath
488
+ }
489
+
490
+ $result = $dialog.ShowDialog()
491
+
492
+ if ($result -eq "OK") {
493
+ Write-Log "INFO" "Destination folder selected: $($dialog.SelectedPath)"
494
+ return $dialog.SelectedPath
495
+ }
496
+
497
+ return $null
498
+ }
499
+
500
+ function Select-DepthGUI {
501
+ <#
502
+ .SYNOPSIS
503
+ Shows depth selection via Out-GridView or console
504
+ #>
505
+ [CmdletBinding()]
506
+ param()
507
+
508
+ $depthOptions = @(
509
+ @{ Name = "Main Folder Only"; Value = 0; Description = "Scan only the main folder (depth=0)" }
510
+ @{ Name = "Subfolders"; Value = 1; Description = "Scan main folder + one level subfolders" }
511
+ @{ Name = "SubSubfolders"; Value = 2; Description = "Scan main + 2 levels of subfolders" }
512
+ @{ Name = "All Subfolders"; Value = 100; Description = "Scan all folders (recursive)" }
513
+ )
514
+
515
+ # Try Out-GridView first (Windows only)
516
+ if (Test-OutGridViewAvailable) {
517
+ try {
518
+ $selected = $depthOptions | Out-GridView -Title "Select Scan Depth" -OutputMode Single
519
+ if ($selected) {
520
+ return $selected.Value
521
+ }
522
+ } catch {
523
+ # Fall back to console
524
+ }
525
+ }
526
+
527
+ # Console fallback
528
+ Write-Host "`nScan Depth:" -ForegroundColor Cyan
529
+ Write-Host " 1 - Main Folder Only (0)"
530
+ Write-Host " 2 - Subfolders (1)"
531
+ Write-Host " 3 - SubSubfolders (2)"
532
+ Write-Host " 4 - All Subfolders (100 - default)" -ForegroundColor Yellow
533
+
534
+ $choice = Read-Host "Enter choice (1-4, default 4)"
535
+
536
+ switch ($choice) {
537
+ "1" { return 0 }
538
+ "2" { return 1 }
539
+ "3" { return 2 }
540
+ default { return 100 }
541
+ }
542
+ }
543
+
544
+ function Show-TypeSelector {
545
+ <#
546
+ .SYNOPSIS
547
+ Shows file type selection with Out-GridView checkboxes
548
+ #>
549
+ [CmdletBinding()]
550
+ param(
551
+ [hashtable]$TypeCounts
552
+ )
553
+
554
+ # Build selection objects for Out-GridView
555
+ $selectionList = @()
556
+ foreach ($type in $TypeCounts.Keys | Sort-Object) {
557
+ $selectionList += [PSCustomObject]@{
558
+ Type = $type
559
+ Count = $TypeCounts[$type]
560
+ Selected = $true # Default all selected
561
+ }
562
+ }
563
+
564
+ # Try Out-GridView with checkboxes
565
+ if (Test-OutGridViewAvailable) {
566
+ try {
567
+ $selected = $selectionList | Out-GridView -Title "Select File Types to Organize (All Selected by Default)" -OutputMode Multiple
568
+
569
+ if ($selected) {
570
+ return $selected.Type
571
+ }
572
+ } catch {
573
+ # Fall back to console
574
+ }
575
+ }
576
+
577
+ # Console fallback with numbered selection
578
+ Write-Host "`nDetected File Types:" -ForegroundColor Green
579
+ $indexMap = @{}
580
+ $i = 1
581
+
582
+ foreach ($type in $TypeCounts.Keys | Sort-Object) {
583
+ Write-Host " $i - $type ($($TypeCounts[$type]) files)" -ForegroundColor White
584
+ $indexMap[$i] = $type
585
+ $i++
586
+ }
587
+
588
+ Write-Host "`nEnter numbers separated by commas (e.g., 1,3,5) or press Enter for ALL" -ForegroundColor Cyan
589
+ $choice = Read-Host "Selection"
590
+
591
+ $selectedTypes = @()
592
+
593
+ if ([string]::IsNullOrWhiteSpace($choice)) {
594
+ # All selected
595
+ return $TypeCounts.Keys
596
+ } else {
597
+ $numbers = $choice.Split(",") | ForEach-Object { $_.Trim() }
598
+ foreach ($n in $numbers) {
599
+ if ($indexMap.ContainsKey([int]$n)) {
600
+ $selectedTypes += $indexMap[[int]$n]
601
+ }
602
+ }
603
+
604
+ if ($selectedTypes.Count -eq 0) {
605
+ throw "No valid selections made"
606
+ }
607
+
608
+ return $selectedTypes
609
+ }
610
+ }
611
+
612
+ function Select-ActionMode {
613
+ <#
614
+ .SYNOPSIS
615
+ Shows Move/Copy selection dialog
616
+ #>
617
+ [CmdletBinding()]
618
+ param()
619
+
620
+ Add-Type -AssemblyName System.Windows.Forms
621
+
622
+ $form = New-Object System.Windows.Forms.Form
623
+ $form.Text = "Select Operation"
624
+ $form.Size = New-Object System.Drawing.Size(400, 180)
625
+ $form.StartPosition = "CenterParent"
626
+ $form.FormBorderStyle = "FixedDialog"
627
+ $form.MaximizeBox = $false
628
+
629
+ $label = New-Object System.Windows.Forms.Label
630
+ $label.Location = New-Object System.Drawing.Point(20, 20)
631
+ $label.Size = New-Object System.Drawing.Size(350, 30)
632
+ $label.Text = "Select Operation Mode:"
633
+ $label.Font = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold)
634
+
635
+ $moveRadio = New-Object System.Windows.Forms.RadioButton
636
+ $moveRadio.Location = New-Object System.Drawing.Point(30, 60)
637
+ $moveRadio.Size = New-Object System.Drawing.Size(150, 25)
638
+ $moveRadio.Text = "Move (default)"
639
+ $moveRadio.Checked = $true
640
+
641
+ $copyRadio = New-Object System.Windows.Forms.RadioButton
642
+ $copyRadio.Location = New-Object System.Drawing.Point(200, 60)
643
+ $copyRadio.Size = New-Object System.Drawing.Size(150, 25)
644
+ $copyRadio.Text = "Copy (safer)"
645
+
646
+ $okButton = New-Object System.Windows.Forms.Button
647
+ $okButton.Location = New-Object System.Drawing.Point(150, 110)
648
+ $okButton.Size = New-Object System.Drawing.Size(100, 30)
649
+ $okButton.Text = "OK"
650
+ $okButton.DialogResult = "OK"
651
+
652
+ $form.Controls.AddRange(@($label, $moveRadio, $copyRadio, $okButton))
653
+ $form.AcceptButton = $okButton
654
+
655
+ $result = $form.ShowDialog()
656
+
657
+ if ($result -eq "OK") {
658
+ if ($copyRadio.Checked) {
659
+ return "Copy"
660
+ }
661
+ }
662
+
663
+ return "Move"
664
+ }
665
+
666
+ function Confirm-Summary {
667
+ <#
668
+ .SYNOPSIS
669
+ Shows rich confirmation MessageBox with operation summary
670
+ #>
671
+ [CmdletBinding()]
672
+ param(
673
+ [string]$SourcePath,
674
+ [string[]]$SelectedTypes,
675
+ [hashtable]$TypeCounts,
676
+ [string]$Action,
677
+ [hashtable]$Destinations,
678
+ [bool]$DryRun
679
+ )
680
+
681
+ # Build summary message
682
+ $totalFiles = 0
683
+ foreach ($type in $SelectedTypes) {
684
+ $totalFiles += $TypeCounts[$type]
685
+ }
686
+
687
+ $summary = @"
688
+ Source Folder: $SourcePath
689
+
690
+ Selected Types ($($SelectedTypes.Count) categories):
691
+ "@
692
+
693
+ foreach ($type in $SelectedTypes | Sort-Object) {
694
+ $count = $TypeCounts[$type]
695
+ $dest = $Destinations[$type]
696
+ $summary += "`n • $type : $count files`n → $dest"
697
+ }
698
+
699
+ $summary += @"
700
+
701
+ Operation: $Action $(if($DryRun){"(DRY RUN - No files will be modified)"})
702
+ Total Files: $totalFiles
703
+
704
+ Proceed with this operation?
705
+ "@
706
+
707
+ Add-Type -AssemblyName System.Windows.Forms
708
+
709
+ $buttons = if ($DryRun) { "YesNo" } else { "YesNo" }
710
+ $icon = if ($DryRun) { "Question" } else { "Question" }
711
+
712
+ $result = [System.Windows.Forms.MessageBox]::Show(
713
+ $summary,
714
+ "Confirm File Organization",
715
+ $buttons,
716
+ $icon
717
+ )
718
+
719
+ return ($result -eq "Yes")
720
+ }
721
+
722
+ function Select-DestinationMode {
723
+ <#
724
+ .SYNOPSIS
725
+ Shows destination mode selection (single vs separate)
726
+ #>
727
+ [CmdletBinding()]
728
+ param()
729
+
730
+ Add-Type -AssemblyName System.Windows.Forms
731
+
732
+ $form = New-Object System.Windows.Forms.Form
733
+ $form.Text = "Destination Mode"
734
+ $form.Size = New-Object System.Drawing.Size(450, 200)
735
+ $form.StartPosition = "CenterParent"
736
+ $form.FormBorderStyle = "FixedDialog"
737
+ $form.MaximizeBox = $false
738
+
739
+ $label = New-Object System.Windows.Forms.Label
740
+ $label.Location = New-Object System.Drawing.Point(20, 20)
741
+ $label.Size = New-Object System.Drawing.Size(400, 30)
742
+ $label.Text = "Select Destination Mode:"
743
+ $label.Font = New-Object System.Drawing.Font("Segoe UI", 11, [System.Drawing.FontStyle]::Bold)
744
+
745
+ $singleRadio = New-Object System.Windows.Forms.RadioButton
746
+ $singleRadio.Location = New-Object System.Drawing.Point(30, 60)
747
+ $singleRadio.Size = New-Object System.Drawing.Size(380, 25)
748
+ $singleRadio.Text = "Single Destination (all types to one folder)"
749
+ $singleRadio.Checked = $true
750
+
751
+ $separateRadio = New-Object System.Windows.Forms.RadioButton
752
+ $separateRadio.Location = New-Object System.Drawing.Point(30, 90)
753
+ $separateRadio.Size = New-Object System.Drawing.Size(380, 25)
754
+ $separateRadio.Text = "Separate Destination (pick folder per file type)"
755
+
756
+ $okButton = New-Object System.Windows.Forms.Button
757
+ $okButton.Location = New-Object System.Drawing.Point(175, 130)
758
+ $okButton.Size = New-Object System.Drawing.Size(100, 30)
759
+ $okButton.Text = "OK"
760
+ $okButton.DialogResult = "OK"
761
+
762
+ $form.Controls.AddRange(@($label, $singleRadio, $separateRadio, $okButton))
763
+ $form.AcceptButton = $okButton
764
+
765
+ $result = $form.ShowDialog()
766
+
767
+ if ($result -eq "OK") {
768
+ if ($separateRadio.Checked) {
769
+ return "Separate"
770
+ }
771
+ }
772
+
773
+ return "Single"
774
+ }
775
+
776
+ #endregion
777
+
778
+ #region Core Processing Functions
779
+
780
+ function Get-FileType {
781
+ <#
782
+ .SYNOPSIS
783
+ Determines file category based on extension
784
+ #>
785
+ [CmdletBinding()]
786
+ param(
787
+ [Parameter(Mandatory)]
788
+ [string]$Extension
789
+ )
790
+
791
+ $ext = $Extension.TrimStart('.').ToLower()
792
+
793
+ # Handle PS 5.1 (PSCustomObject) vs PS 6+ (Hashtable)
794
+ $categories = $Script:Config.categories
795
+ if ($categories -is [System.Collections.Hashtable]) {
796
+ $categoryList = $categories.Keys
797
+ } else {
798
+ # PS 5.1 - convert PSObject to collection
799
+ $categoryList = $categories.PSObject.Properties.Name
800
+ }
801
+
802
+ foreach ($category in $categoryList) {
803
+ if ($categories -is [System.Collections.Hashtable]) {
804
+ $extensions = $categories[$category]
805
+ } else {
806
+ $extensions = $categories.$category
807
+ }
808
+ if ($extensions -contains $ext) {
809
+ return $category
810
+ }
811
+ }
812
+
813
+ return "Others"
814
+ }
815
+
816
+ function Test-ShouldExclude {
817
+ <#
818
+ .SYNOPSIS
819
+ Checks if file matches any exclude patterns
820
+ #>
821
+ [CmdletBinding()]
822
+ param(
823
+ [Parameter(Mandatory)]
824
+ [string]$FileName
825
+ )
826
+
827
+ foreach ($pattern in $Script:Config.excludePatterns) {
828
+ # Handle wildcard patterns
829
+ if ($pattern.Contains("*")) {
830
+ if ($FileName -like $pattern) {
831
+ return $true
832
+ }
833
+ }
834
+ # Handle prefix patterns like ~$ (temporary Office files)
835
+ elseif ($pattern.StartsWith("~")) {
836
+ if ($FileName.StartsWith($pattern)) {
837
+ return $true
838
+ }
839
+ }
840
+ # Exact match
841
+ else {
842
+ if ($FileName -eq $pattern) {
843
+ return $true
844
+ }
845
+ }
846
+ }
847
+
848
+ return $false
849
+ }
850
+
851
+ function Scan-SourceFiles {
852
+ <#
853
+ .SYNOPSIS
854
+ Scans source folder and returns file list with metadata
855
+ #>
856
+ [CmdletBinding()]
857
+ param(
858
+ [Parameter(Mandatory)]
859
+ [string]$SourcePath,
860
+
861
+ [Parameter(Mandatory)]
862
+ [int]$Depth
863
+ )
864
+
865
+ Write-Host "`nScanning files..." -ForegroundColor Cyan
866
+ Write-Log "INFO" "Scanning source: $SourcePath with depth: $Depth"
867
+
868
+ $files = @()
869
+
870
+ try {
871
+ # PowerShell 7+ has -Depth parameter
872
+ if ($Script:PSVersion -ge 7) {
873
+ $files = Get-ChildItem -Path $SourcePath -File -Recurse -Depth $Depth -ErrorAction SilentlyContinue
874
+ } else {
875
+ # PowerShell 5.1 - simulate depth limit
876
+ if ($Depth -ge 100) {
877
+ $files = Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue
878
+ } else {
879
+ # Manual depth filtering for PS 5.1
880
+ $files = Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue |
881
+ Where-Object { $_.FullName.Split([IO.Path]::DirectorySeparatorChar).Count -le
882
+ ($SourcePath.Split([IO.Path]::DirectorySeparatorChar).Count + $Depth) }
883
+ }
884
+ }
885
+ } catch {
886
+ $errMsg = $_.Exception.Message
887
+ Write-Log "ERROR" "Error scanning source - $errMsg"
888
+ throw "Failed to scan source folder - $errMsg"
889
+ }
890
+
891
+ # Filter out excluded files
892
+ $filteredFiles = @()
893
+ foreach ($file in $files) {
894
+ if (-not (Test-ShouldExclude -FileName $file.Name)) {
895
+ $filteredFiles += $file
896
+ }
897
+ }
898
+
899
+ Write-Host "Found $($filteredFiles.Count) files (excluding $($files.Count - $filteredFiles.Count) matches)" -ForegroundColor Green
900
+
901
+ return $filteredFiles
902
+ }
903
+
904
+ function Get-TypeSummary {
905
+ <#
906
+ .SYNOPSIS
907
+ Counts files by category
908
+ #>
909
+ [CmdletBinding()]
910
+ param(
911
+ [Parameter(Mandatory)]
912
+ [array]$Files
913
+ )
914
+
915
+ $typeCount = @{}
916
+
917
+ foreach ($file in $Files) {
918
+ $typeName = Get-FileType -Extension $file.Extension
919
+
920
+ if (-not $typeCount.ContainsKey($typeName)) {
921
+ $typeCount[$typeName] = 0
922
+ }
923
+ $typeCount[$typeName]++
924
+ }
925
+
926
+ return $typeCount
927
+ }
928
+
929
+ function Get-UniqueFileName {
930
+ <#
931
+ .SYNOPSIS
932
+ Generates unique filename to avoid conflicts
933
+ #>
934
+ [CmdletBinding()]
935
+ param(
936
+ [Parameter(Mandatory)]
937
+ [string]$DestinationPath,
938
+
939
+ [Parameter(Mandatory)]
940
+ [string]$FileName
941
+ )
942
+
943
+ $fullPath = Join-Path $DestinationPath $FileName
944
+
945
+ if (-not (Test-Path $fullPath)) {
946
+ return $FileName
947
+ }
948
+
949
+ # File exists, generate unique name
950
+ $baseName = [IO.Path]::GetFileNameWithoutExtension($FileName)
951
+ $extension = [IO.Path]::GetExtension($FileName)
952
+
953
+ $counter = 1
954
+ do {
955
+ $newName = "${baseName}_${counter}${extension}"
956
+ $fullPath = Join-Path $DestinationPath $newName
957
+ $counter++
958
+ } while (Test-Path $fullPath)
959
+
960
+ return $newName
961
+ }
962
+
963
+ function Check-Cancel {
964
+ <#
965
+ .SYNOPSIS
966
+ Checks if user pressed Escape to cancel operation
967
+ #>
968
+ [CmdletBinding()]
969
+ param()
970
+
971
+ if ($Script:PSVersion -ge 7 -and [Console]::IsInputRedirected) {
972
+ return $false
973
+ }
974
+
975
+ if ([Console]::KeyAvailable) {
976
+ $key = [Console]::ReadKey($true)
977
+ if ($key.Key -eq "Escape") {
978
+ Write-Host "`nOperation Cancelled by user." -ForegroundColor Yellow
979
+ Write-Log "WARN" "Operation cancelled by user"
980
+ return $true
981
+ }
982
+ }
983
+ return $false
984
+ }
985
+
986
+ #endregion
987
+
988
+ #region File Operation Functions
989
+
990
+ function Process-Files {
991
+ <#
992
+ .SYNOPSIS
993
+ Main file processing loop with progress and cancellation
994
+ #>
995
+ [CmdletBinding()]
996
+ param(
997
+ [Parameter(Mandatory)]
998
+ [array]$Files,
999
+
1000
+ [Parameter(Mandatory)]
1001
+ [string[]]$SelectedTypes,
1002
+
1003
+ [Parameter(Mandatory)]
1004
+ [hashtable]$TypeDestinationMap,
1005
+
1006
+ [Parameter(Mandatory)]
1007
+ [string]$Action,
1008
+
1009
+ [bool]$DryRun,
1010
+
1011
+ [bool]$CreateTypeSubfolder
1012
+ )
1013
+
1014
+ $processedCount = 0
1015
+ $errorCount = 0
1016
+ $skippedCount = 0
1017
+ $total = $Files.Count
1018
+ $index = 0
1019
+
1020
+ $operationVerb = if ($Action -eq "Move") { "Moving" } else { "Copying" }
1021
+ $logAction = if ($DryRun) { "DRYRUN" } else { "INFO" }
1022
+
1023
+ Write-Host "`n$operationVerb files..." -ForegroundColor $(if($DryRun){"Cyan"}else{"Yellow"})
1024
+
1025
+ foreach ($file in $Files) {
1026
+ # Check for cancellation
1027
+ if (Check-Cancel) {
1028
+ Write-Host "`nOperation cancelled. Stopping..." -ForegroundColor Yellow
1029
+ break
1030
+ }
1031
+
1032
+ $index++
1033
+ $percent = [math]::Round(($index / $total) * 100)
1034
+ Write-Progress -Activity "$operationVerb Files ($Action)" -Status "$index of $total files" -PercentComplete $percent
1035
+
1036
+ # Determine file type
1037
+ $typeName = Get-FileType -Extension $file.Extension
1038
+
1039
+ # Skip if not in selected types
1040
+ if ($SelectedTypes -notcontains $typeName) {
1041
+ $skippedCount++
1042
+ continue
1043
+ }
1044
+
1045
+ # Get destination path
1046
+ $baseDest = $TypeDestinationMap[$typeName]
1047
+
1048
+ # Create type subfolder if enabled
1049
+ if ($CreateTypeSubfolder) {
1050
+ $destPath = Join-Path $baseDest $typeName
1051
+ } else {
1052
+ $destPath = $baseDest
1053
+ }
1054
+
1055
+ # Create destination if it doesn't exist
1056
+ if (-not (Test-Path $destPath)) {
1057
+ try {
1058
+ New-Item -ItemType Directory -Path $destPath -Force | Out-Null
1059
+ Write-Log "INFO" "Created directory: $destPath"
1060
+ } catch {
1061
+ $errMsg = $_.Exception.Message
1062
+ Write-Log "ERROR" "Failed to create directory $destPath - $errMsg"
1063
+ $errorCount++
1064
+ continue
1065
+ }
1066
+ }
1067
+
1068
+ # Get unique filename
1069
+ $newFileName = Get-UniqueFileName -DestinationPath $destPath -FileName $file.Name
1070
+ $newFullPath = Join-Path $destPath $newFileName
1071
+
1072
+ # Perform the operation
1073
+ if ($DryRun) {
1074
+ # Dry run - just log what would happen
1075
+ Write-Log "DRYRUN" "Would $Action '$($file.FullName)' -> '$newFullPath'"
1076
+ $processedCount++
1077
+ } else {
1078
+ # Actual move or copy
1079
+ try {
1080
+ if ($Action -eq "Move") {
1081
+ Move-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop
1082
+ } else {
1083
+ Copy-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop
1084
+ }
1085
+
1086
+ Write-Log "INFO" "$Action completed: $($file.Name) -> $newFullPath"
1087
+ $processedCount++
1088
+ } catch {
1089
+ $errMsg = $_.Exception.Message
1090
+ $fileName = $file.Name
1091
+ Write-Log "ERROR" "Failed to $Action '$fileName' - $errMsg"
1092
+ $errorCount++
1093
+ }
1094
+ }
1095
+ }
1096
+
1097
+ Write-Progress -Activity "$operationVerb Files" -Completed
1098
+
1099
+ # Return summary
1100
+ return @{
1101
+ Processed = $processedCount
1102
+ Errors = $errorCount
1103
+ Skipped = $skippedCount
1104
+ }
1105
+ }
1106
+
1107
+ #endregion
1108
+
1109
+ #region Interactive Mode
1110
+
1111
+ function Start-InteractiveMode {
1112
+ <#
1113
+ .SYNOPSIS
1114
+ Runs the interactive GUI-based workflow
1115
+ #>
1116
+ [CmdletBinding()]
1117
+ param()
1118
+
1119
+ Write-Host "`n=== FileOrganizer - Interactive Mode ===" -ForegroundColor Cyan
1120
+ Write-Host "Press ESC anytime to cancel operation`n" -ForegroundColor Gray
1121
+
1122
+ # Give user a moment to see the startup
1123
+ Start-Sleep -Milliseconds 500
1124
+
1125
+ # Step 1: Select source
1126
+ Write-Host "Step 1: Select Source Folder (a dialog will open)" -ForegroundColor Yellow
1127
+ Write-Host ">>> Click Browse and select a folder <<<" -ForegroundColor Magenta
1128
+ $sourceFolder = Select-SourceFolder
1129
+
1130
+ if (-not $sourceFolder) {
1131
+ Write-Host "No source folder selected. Exiting." -ForegroundColor Yellow
1132
+ return
1133
+ }
1134
+
1135
+ # Step 2: Select depth
1136
+ Write-Host "`nStep 2: Select Scan Depth" -ForegroundColor Yellow
1137
+ $depth = Select-DepthGUI
1138
+ Write-Host "Using depth: $depth" -ForegroundColor Gray
1139
+
1140
+ # Step 3: Scan files
1141
+ $files = Scan-SourcePath -SourcePath $sourceFolder -Depth $depth
1142
+
1143
+ if ($files.Count -eq 0) {
1144
+ Write-Host "No files found in source folder." -ForegroundColor Yellow
1145
+ return
1146
+ }
1147
+
1148
+ # Step 4: Get type summary
1149
+ $typeCounts = Get-TypeSummary -Files $files
1150
+
1151
+ # Step 5: Select types
1152
+ Write-Host "`nStep 3: Select File Types" -ForegroundColor Yellow
1153
+ $selectedTypes = Show-TypeSelector -TypeCounts $typeCounts
1154
+
1155
+ if ($selectedTypes.Count -eq 0) {
1156
+ Write-Host "No file types selected. Exiting." -ForegroundColor Yellow
1157
+ return
1158
+ }
1159
+
1160
+ # Step 6: Select action (Move/Copy)
1161
+ Write-Host "`nStep 4: Select Operation" -ForegroundColor Yellow
1162
+ $action = Select-ActionMode
1163
+ Write-Host "Operation: $action" -ForegroundColor Gray
1164
+
1165
+ # Step 7: Destination mode
1166
+ Write-Host "`nStep 5: Select Destination Mode" -ForegroundColor Yellow
1167
+ $destMode = Select-DestinationMode
1168
+
1169
+ $typeDestinationMap = @{}
1170
+
1171
+ if ($destMode -eq "Separate") {
1172
+ # Separate destination per type
1173
+ foreach ($type in $selectedTypes) {
1174
+ Write-Host "`nSelect destination for: $type" -ForegroundColor Cyan
1175
+ $dest = Select-DestinationFolder -Description "Select destination for $type"
1176
+
1177
+ if (-not $dest) {
1178
+ Write-Host "Destination not selected. Using source folder." -ForegroundColor Yellow
1179
+ $dest = $sourceFolder
1180
+ }
1181
+
1182
+ $typeDestinationMap[$type] = $dest
1183
+ }
1184
+ } else {
1185
+ # Single destination
1186
+ Write-Host "`nSelect single destination folder" -ForegroundColor Cyan
1187
+ $singleDest = Select-DestinationFolder -Description "Select destination for all files"
1188
+
1189
+ if (-not $singleDest) {
1190
+ Write-Host "No destination selected. Using source folder." -ForegroundColor Yellow
1191
+ $singleDest = $sourceFolder
1192
+ }
1193
+
1194
+ foreach ($type in $selectedTypes) {
1195
+ $typeDestinationMap[$type] = $singleDest
1196
+ }
1197
+ }
1198
+
1199
+ # Step 8: DryRun option
1200
+ Write-Host "`nStep 6: Dry Run Mode?" -ForegroundColor Yellow
1201
+ $dryRunChoice = Read-Host "Run as Dry Run (simulate only)? y/n (default: n)"
1202
+ $dryRun = ($dryRunChoice -eq "y")
1203
+
1204
+ if ($dryRun) {
1205
+ Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
1206
+ }
1207
+
1208
+ # Step 9: Confirm summary
1209
+ Write-Host "`nStep 7: Confirm Operation" -ForegroundColor Yellow
1210
+ $confirmed = Confirm-Summary -SourcePath $sourceFolder -SelectedTypes $selectedTypes -TypeCounts $typeCounts -Action $action -Destinations $typeDestinationMap -DryRun $dryRun
1211
+
1212
+ if (-not $confirmed) {
1213
+ Write-Host "Operation cancelled by user." -ForegroundColor Yellow
1214
+ return
1215
+ }
1216
+
1217
+ # Step 10: Process files
1218
+ $result = Process-Files -Files $files -SelectedTypes $selectedTypes -TypeDestinationMap $typeDestinationMap -Action $action -DryRun $dryRun -CreateTypeSubfolder $Script:Config.createTypeSubfolder
1219
+
1220
+ # Step 11: Show results
1221
+ Write-Host "`n=== Operation Complete ===" -ForegroundColor Green
1222
+ Write-Host "Files processed: $($result.Processed)" -ForegroundColor White
1223
+
1224
+ if ($result.Errors -gt 0) {
1225
+ Write-Host "Errors: $($result.Errors)" -ForegroundColor Red
1226
+ }
1227
+
1228
+ if ($result.Skipped -gt 0) {
1229
+ Write-Host "Skipped: $($result.Skipped)" -ForegroundColor Yellow
1230
+ }
1231
+
1232
+ Write-Log "INFO" "Operation completed - Processed: $($result.Processed), Errors: $($result.Errors), Skipped: $($result.Skipped)"
1233
+
1234
+ # Save last used paths on success
1235
+ if ($result.Processed -gt 0 -and -not $dryRun) {
1236
+ $firstDest = ($typeDestinationMap.Values | Select-Object -First 1)
1237
+ Save-LastUsed -SourcePath $sourceFolder -DestPath $firstDest
1238
+ }
1239
+ }
1240
+
1241
+ function Scan-SourcePath {
1242
+ <#
1243
+ .SYNOPSIS
1244
+ Wrapper for scanning with user feedback
1245
+ #>
1246
+ [CmdletBinding()]
1247
+ param(
1248
+ [string]$SourcePath,
1249
+ [int]$Depth
1250
+ )
1251
+
1252
+ return Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
1253
+ }
1254
+
1255
+ function Start-CLIMode {
1256
+ <#
1257
+ .SYNOPSIS
1258
+ Runs the command-line interface mode with provided parameters
1259
+ #>
1260
+ [CmdletBinding()]
1261
+ param()
1262
+
1263
+ Write-Host "`n=== FileOrganizer - CLI Mode ===" -ForegroundColor Cyan
1264
+
1265
+ # Validate source path
1266
+ if (-not $SourcePath) {
1267
+ Write-Error "SourcePath is required in CLI mode. Use -SourcePath parameter."
1268
+ return
1269
+ }
1270
+
1271
+ if (-not (Test-Path $SourcePath)) {
1272
+ Write-Error "Source path does not exist: $SourcePath"
1273
+ return
1274
+ }
1275
+
1276
+ # Set default destination if not provided
1277
+ if (-not $DestPath) {
1278
+ $DestPath = $SourcePath
1279
+ Write-Host "No destination specified, using source path: $DestPath" -ForegroundColor Yellow
1280
+ }
1281
+
1282
+ # Scan files
1283
+ $files = Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
1284
+
1285
+ if ($files.Count -eq 0) {
1286
+ Write-Host "No files found." -ForegroundColor Yellow
1287
+ return
1288
+ }
1289
+
1290
+ # Get type summary
1291
+ $typeCounts = Get-TypeSummary -Files $files
1292
+
1293
+ # Show summary
1294
+ Write-Host "`nFile Type Summary:" -ForegroundColor Green
1295
+ foreach ($type in ($typeCounts.Keys | Sort-Object)) {
1296
+ Write-Host " $type : $($typeCounts[$type]) files" -ForegroundColor White
1297
+ }
1298
+
1299
+ # Determine selected types (all by default in CLI)
1300
+ $selectedTypes = $typeCounts.Keys
1301
+
1302
+ # Set up destination map
1303
+ $typeDestinationMap = @{}
1304
+
1305
+ if ($SeparateDestinations) {
1306
+ Write-Host "`nSeparate destinations requested but not implemented in CLI mode." -ForegroundColor Yellow
1307
+ Write-Host "Using single destination: $DestPath" -ForegroundColor Yellow
1308
+ }
1309
+
1310
+ foreach ($type in $selectedTypes) {
1311
+ $typeDestinationMap[$type] = $DestPath
1312
+ }
1313
+
1314
+ # Show dry run status
1315
+ if ($DryRun) {
1316
+ Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
1317
+ }
1318
+
1319
+ # Process files
1320
+ $result = Process-Files -Files $files -SelectedTypes $selectedTypes -TypeDestinationMap $typeDestinationMap -Action $Action -DryRun $DryRun -CreateTypeSubfolder $Script:Config.createTypeSubfolder
1321
+
1322
+ # Show results
1323
+ Write-Host "`n=== Operation Complete ===" -ForegroundColor Green
1324
+ Write-Host "Files processed: $($result.Processed)" -ForegroundColor White
1325
+
1326
+ if ($result.Errors -gt 0) {
1327
+ Write-Host "Errors: $($result.Errors)" -ForegroundColor Red
1328
+ }
1329
+
1330
+ Write-Log "INFO" "CLI operation completed - Processed: $($result.Processed), Errors: $($result.Errors)"
1331
+
1332
+ # Save last used paths
1333
+ if ($result.Processed -gt 0 -and -not $DryRun) {
1334
+ Save-LastUsed -SourcePath $SourcePath -DestPath $DestPath
1335
+ }
1336
+ }
1337
+
1338
+ #endregion
1339
+
1340
+ #region Main Entry Point
1341
+
1342
+ function Main {
1343
+ <#
1344
+ .SYNOPSIS
1345
+ Main entry point for the script
1346
+ #>
1347
+ [CmdletBinding()]
1348
+ param()
1349
+
1350
+ # Initialize environment
1351
+ Initialize-Environment
1352
+
1353
+ # Check if Help requested
1354
+ if ($Help) {
1355
+ Get-Help $MyInvocation.MyCommand.Path -Detailed
1356
+ return
1357
+ }
1358
+
1359
+ # Determine mode: CLI or Interactive
1360
+ $isCLIMode = $SourcePath -and $DestPath
1361
+
1362
+ if ($isCLIMode) {
1363
+ # Run in CLI mode
1364
+ Start-CLIMode
1365
+ } else {
1366
+ # Run in Interactive mode
1367
+ Start-InteractiveMode
1368
+
1369
+ # Loop for additional tasks unless -NoLoop specified
1370
+ if (-not $NoLoop) {
1371
+ while ($true) {
1372
+ Write-Host "`n" -NoNewline
1373
+ $again = Read-Host "Do another task? y/n"
1374
+
1375
+ if ($again -ne "y") {
1376
+ break
1377
+ }
1378
+
1379
+ Start-InteractiveMode
1380
+ }
1381
+ }
1382
+ }
1383
+
1384
+ Write-Host "`nGoodbye!" -ForegroundColor Cyan
1385
+ Write-Log "INFO" "FileOrganizer ended"
1386
+
1387
+ # Pause to keep window open (unless running with -NoExit or in CI)
1388
+ if (-not ([Environment]::GetCommandLineArgs() -contains '-NoExit')) {
1389
+ Write-Host "`nPress Enter to exit..." -ForegroundColor Gray
1390
+ try { Read-Host } catch { Start-Sleep -Seconds 2 }
1391
+ }
1392
+ }
1393
+
1394
+ # Run main function
1395
+ try {
1396
+ Main
1397
+ } catch {
1398
+ $errMsg = $_.Exception.Message
1399
+ $stackTrace = $_.ScriptStackTrace
1400
+ Write-Host "`n" -ForegroundColor Red
1401
+ Write-Host "FATAL ERROR: $errMsg" -ForegroundColor Red
1402
+ Write-Host "Stack Trace: $stackTrace" -ForegroundColor Red
1403
+ Write-Host "`nPress Enter to exit..." -ForegroundColor Yellow
1404
+ try { Read-Host } catch { Start-Sleep -Seconds 5 }
1405
+ exit 1
1406
+ }
1407
+
1408
+ #endregion
LICENSE ADDED
@@ -0,0 +1,26 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ Pull requests welcome!
2
+ Made with ❤️ for the PowerShell community.
3
+ text### 3. LICENSE
4
+
5
+ ```text
6
+ MIT License
7
+
8
+ Copyright (c) 2026 [Your Name / GitHub Username]
9
+
10
+ Permission is hereby granted, free of charge, to any person obtaining a copy
11
+ of this software and associated documentation files (the "Software"), to deal
12
+ in the Software without restriction, including without limitation the rights
13
+ to use, copy, modify, merge, publish, distribute, sublicense, and/or sell
14
+ copies of the Software, and to permit persons to whom the Software is
15
+ furnished to do so, subject to the following conditions:
16
+
17
+ The above copyright notice and this permission notice shall be included in all
18
+ copies or substantial portions of the Software.
19
+
20
+ THE SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
21
+ IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
22
+ FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT. IN NO EVENT SHALL THE
23
+ AUTHORS OR COPYRIGHT HOLDERS BE LIABLE FOR ANY CLAIM, DAMAGES OR OTHER
24
+ LIABILITY, WHETHER IN AN ACTION OF CONTRACT, TORT OR OTHERWISE, ARISING FROM,
25
+ OUT OF OR IN CONNECTION WITH THE SOFTWARE OR THE USE OR OTHER DEALINGS IN THE
26
+ SOFTWARE.
README.md ADDED
@@ -0,0 +1,31 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ # FileOrganizer
2
+
3
+ **Modern PowerShell File Organizer** – GUI + CLI – Move or Copy files by category with dry-run, logging, and full configurability.
4
+
5
+ ## Features
6
+ - Beautiful GUI (Out-GridView multi-select, confirmation dialogs, progress bar)
7
+ - Fully configurable categories via `config.json`
8
+ - Move **or** Copy
9
+ - Safe dry-run preview
10
+ - Automatic duplicate renaming (`file_1.ext`)
11
+ - ESC key to cancel
12
+ - Remembers last used folders
13
+ - Full timestamped logging
14
+ - Works on PowerShell 5.1 and 7+
15
+ - CLI support for scripts and automation
16
+
17
+ ## Quick Start (GUI – recommended)
18
+ 1. Download the repository
19
+ 2. Run `FileOrganizer.ps1` (double-click or right-click → Run with PowerShell)
20
+ 3. Follow the friendly prompts
21
+
22
+ ## CLI Examples
23
+ ```powershell
24
+ # Move to single folder with type subfolders
25
+ .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized"
26
+
27
+ # Copy + dry-run first
28
+ .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -Action Copy -DryRun
29
+
30
+ # Separate folder per type
31
+ .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -SeparateTypes
config.json ADDED
@@ -0,0 +1,99 @@
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
 
1
+ {
2
+ "categories": {
3
+ "PowerPoint Files": [
4
+ "ppt",
5
+ "pptx",
6
+ "pptm"
7
+ ],
8
+ "Archives": [
9
+ "zip",
10
+ "rar",
11
+ "7z",
12
+ "tar",
13
+ "gz"
14
+ ],
15
+ "Images": [
16
+ "jpg",
17
+ "jpeg",
18
+ "png",
19
+ "gif",
20
+ "bmp",
21
+ "svg",
22
+ "webp",
23
+ "tiff"
24
+ ],
25
+ "Videos": [
26
+ "mp4",
27
+ "avi",
28
+ "mkv",
29
+ "mov",
30
+ "wmv",
31
+ "flv"
32
+ ],
33
+ "Text": [
34
+ "txt",
35
+ "md",
36
+ "log",
37
+ "rtf",
38
+ "json",
39
+ "xml",
40
+ "yaml",
41
+ "yml",
42
+ "ini",
43
+ "cfg"
44
+ ],
45
+ "PDF Files": [
46
+ "pdf"
47
+ ],
48
+ "Executables": [
49
+ "exe",
50
+ "msi",
51
+ "bat",
52
+ "cmd"
53
+ ],
54
+ "Code": [
55
+ "ps1",
56
+ "py",
57
+ "js",
58
+ "ts",
59
+ "cpp",
60
+ "c",
61
+ "h",
62
+ "cs",
63
+ "java",
64
+ "rb",
65
+ "go",
66
+ "rs"
67
+ ],
68
+ "Word Files": [
69
+ "doc",
70
+ "docx",
71
+ "docm",
72
+ "dotx"
73
+ ],
74
+ "Excel Files": [
75
+ "xls",
76
+ "xlsx",
77
+ "xlsm",
78
+ "csv"
79
+ ],
80
+ "Audio": [
81
+ "mp3",
82
+ "wav",
83
+ "flac",
84
+ "ogg",
85
+ "aac",
86
+ "wma"
87
+ ]
88
+ },
89
+ "defaultDepth": 100,
90
+ "defaultAction": "Move",
91
+ "excludePatterns": [
92
+ "Thumbs.db",
93
+ ".DS_Store",
94
+ "desktop.ini",
95
+ "*.tmp",
96
+ "~$*"
97
+ ],
98
+ "createTypeSubfolder": true
99
+ }