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

Delete FileOrganizer.ps1

Browse files
Files changed (1) hide show
  1. FileOrganizer.ps1 +0 -1408
FileOrganizer.ps1 DELETED
@@ -1,1408 +0,0 @@
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