FileOrganizer / FileOrganizer.ps1
thenightfury's picture
Upload 3 files
493404f verified
# FileOrganizer.ps1 - Modern File Organizer with GUI & CLI
# A professional-grade Windows utility for organizing files by type
#
# Default config.json (auto-created if missing):
# <#
# {
# "categories": {
# "Word Files": ["doc", "docx", "docm", "dotx"],
# "PowerPoint Files": ["ppt", "pptx", "pptm"],
# "Excel Files": ["xls", "xlsx", "xlsm", "csv"],
# "PDF Files": ["pdf"],
# "Images": ["jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff"],
# "Videos": ["mp4", "avi", "mkv", "mov", "wmv", "flv"],
# "Executables": ["exe", "msi", "bat", "cmd", "ps1"],
# "Archives": ["zip", "rar", "7z", "tar", "gz"],
# "Audio": ["mp3", "wav", "flac", "ogg", "aac", "wma"],
# "Code": ["ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs"],
# "Text": ["txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg"]
# },
# "excludePatterns": ["Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*"],
# "createTypeSubfolder": true,
# "defaultAction": "Move",
# "defaultDepth": 100
# }
# #>
#
# Usage:
# .\FileOrganizer.ps1 # Interactive GUI mode
# .\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun
# .\FileOrganizer.ps1 -SourcePath "C:\Src" -DestPath "D:\Dest" -Action Copy -Depth 2
# .\FileOrganizer.ps1 -CategoriesFile "custom.json"
# Get-Help .\FileOrganizer.ps1 -Detailed
#Requires -Version 5.1
# ============================================================
# IMPORTANT: param() MUST come first before any executable code
# ============================================================
[CmdletBinding()]
param (
[Parameter(HelpMessage = "Source folder path to scan")]
[string]$SourcePath,
[Parameter(HelpMessage = "Destination folder path")]
[string]$DestPath,
[Parameter(HelpMessage = "Scan depth: 0=Main only, 1=Subfolders, 2=SubSubfolders, 100=All")]
[ValidateRange(0, 100)]
[int]$Depth = 100,
[Parameter(HelpMessage = "Action to perform: Move or Copy")]
[ValidateSet("Move", "Copy")]
[string]$Action = "Move",
[Parameter(HelpMessage = "Simulate operation without moving/copying files")]
[switch]$DryRun,
[Parameter(HelpMessage = "Path to custom categories JSON file")]
[string]$CategoriesFile,
[Parameter(HelpMessage = "Run without looping for additional tasks")]
[switch]$NoLoop,
[Parameter(HelpMessage = "Use separate destination folder per file type")]
[switch]$SeparateDestinations,
[Parameter(HelpMessage = "Show this help message")]
[switch]$Help
)
# ============================================================
# Now we can have executable code after param()
# ============================================================
# Early error handling - don't let script silently fail
$ErrorActionPreference = "Continue"
$Script:StartupError = $null
# Check execution policy
try {
$execPolicy = Get-ExecutionPolicy -ErrorAction SilentlyContinue
if ($execPolicy -eq "Restricted") {
Write-Host "WARNING: Execution Policy is Restricted." -ForegroundColor Yellow
Write-Host "Run: Set-ExecutionPolicy -ExecutionPolicy RemoteSigned -Scope CurrentUser" -ForegroundColor Yellow
}
}
catch {
Write-Verbose "Could not get ExecutionPolicy"
}
# Fix for $PSScriptRoot being empty when run via right-click "Run with PowerShell"
$Script:ScriptPath = if ($PSScriptRoot) { $PSScriptRoot } elseif ($PSCommandPath) { Split-Path -Parent $PSCommandPath } else { Split-Path -Parent $MyInvocation.MyCommand.Path }
if (-not $Script:ScriptPath) { $Script:ScriptPath = Get-Location }
# Pre-load Windows Forms (needed for GUI dialogs)
try {
Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
}
catch {
$errMsg = $_.Exception.Message
$Script:StartupError = "Failed to load Windows Forms - $errMsg"
}
#region Global Variables
$Script:Config = $null
$Script:LastUsed = $null
$Script:ScriptName = "FileOrganizer"
$Script:LogFile = $null
$Script:IsElevated = $false
$Script:PSVersion = $PSVersionTable.PSVersion.Major
#endregion
# Check for startup errors
if ($Script:StartupError) {
Write-Host "ERROR: $Script:StartupError" -ForegroundColor Red
Write-Host "Press Enter to exit..." -ForegroundColor Yellow
Read-Host
exit 1
}
# Show startup banner
Write-Host ""
Write-Host "======================================" -ForegroundColor Cyan
Write-Host " FileOrganizer v2.0 - Loading..." -ForegroundColor Cyan
Write-Host "======================================" -ForegroundColor Cyan
Write-Host "Script Path: $Script:ScriptPath" -ForegroundColor Gray
Write-Host "PowerShell: $($Script:PSVersion)" -ForegroundColor Gray
Write-Host ""
#region Comment-Based Help
<#
.SYNOPSIS
Organizes files by type into destination folders.
.DESCRIPTION
FileOrganizer is a professional-grade Windows utility that scans a source folder,
categorizes files by type (Word, PDF, Images, etc.), and moves or copies them to
destination folders. Supports both CLI and interactive GUI modes.
Features:
- CLI and Interactive GUI hybrid operation
- Configurable file categories via JSON
- Move or Copy operations
- Dry-Run mode for safe testing
- Modern Out-GridView selection
- Remembers last used folders
- Comprehensive logging
.PARAMETER SourcePath
Source folder to scan. If not provided, launches interactive folder browser.
.PARAMETER DestPath
Destination folder for organized files. If not provided, launches interactive browser.
.PARAMETER Depth
Scan depth: 0=Main folder only, 1=One level deep, 2=Two levels, 100=All (default).
.PARAMETER Action
Operation type: "Move" (default) or "Copy". Use Copy for safer testing.
.PARAMETER DryRun
Simulates the operation without actually moving/copying files. Shows what would happen.
.PARAMETER CategoriesFile
Path to custom JSON file with file type categories.
.PARAMETER NoLoop
Runs single operation without prompting for additional tasks.
.PARAMETER SeparateDestinations
Use separate destination folder per file type (prompts for each).
.EXAMPLE
.\FileOrganizer.ps1
Launches interactive GUI mode with folder browsers and selections.
.EXAMPLE
.\FileOrganizer.ps1 -SourcePath "C:\Downloads" -DestPath "D:\Organized" -DryRun
Scans Downloads, shows what would be organized to D:\Organized without making changes.
.EXAMPLE
.\FileOrganizer.ps1 -SourcePath "C:\Files" -Action Copy -Depth 2
Copies files from C:\Files (including subfolders 2 levels deep) with interactive destination.
.NOTES
Author: FileOrganizer Team
Version: 2.0.0
Requires: PowerShell 5.1+
#>
#endregion
#region Initialization Functions
function Initialize-Environment {
<#
.SYNOPSIS
Initializes the script environment - logging, config, paths
#>
[CmdletBinding()]
param()
# Set up log file path
$logDate = Get-Date -Format "yyyy-MM-dd"
$Script:LogFile = Join-Path $Script:ScriptPath "$($Script:ScriptName)_$logDate.log"
# Create script path if it doesn't exist (for when running from other locations)
if (-not $Script:ScriptPath) {
$Script:ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
}
# Load configuration
Load-Config -CategoriesFile $CategoriesFile
# Load last used paths
Load-LastUsed
# Check PowerShell version for feature compatibility
if ($Script:PSVersion -lt 5) {
Write-Warning "PowerShell version 5.1 or higher recommended. Current: $($Script:PSVersion)"
}
Write-Log "INFO" "FileOrganizer started - PS Version: $($Script:PSVersion)"
}
function Load-Config {
<#
.SYNOPSIS
Loads configuration from JSON file or creates default
#>
[CmdletBinding()]
param(
[string]$CategoriesFile
)
# Determine config file path
if ($CategoriesFile) {
$configPath = $CategoriesFile
}
else {
$configPath = Join-Path $Script:ScriptPath "config.json"
}
# Default configuration
$defaultConfig = @{
categories = @{
"Word Files" = @("doc", "docx", "docm", "dotx")
"PowerPoint Files" = @("ppt", "pptx", "pptm")
"Excel Files" = @("xls", "xlsx", "xlsm", "csv")
"PDF Files" = @("pdf")
"Images" = @("jpg", "jpeg", "png", "gif", "bmp", "svg", "webp", "tiff")
"Videos" = @("mp4", "avi", "mkv", "mov", "wmv", "flv")
"Executables" = @("exe", "msi", "bat", "cmd")
"Archives" = @("zip", "rar", "7z", "tar", "gz")
"Audio" = @("mp3", "wav", "flac", "ogg", "aac", "wma")
"Code" = @("ps1", "py", "js", "ts", "cpp", "c", "h", "cs", "java", "rb", "go", "rs")
"Text" = @("txt", "md", "log", "rtf", "json", "xml", "yaml", "yml", "ini", "cfg")
}
excludePatterns = @("Thumbs.db", ".DS_Store", "desktop.ini", "*.tmp", "~$*")
createTypeSubfolder = $true
defaultAction = "Move"
defaultDepth = 100
}
# Load existing or create default config
if (Test-Path $configPath) {
try {
$configContent = Get-Content $configPath -Raw -ErrorAction Stop
# Handle PS 5.1 vs PS 6+ compatibility
if ($Script:PSVersion -ge 6) {
$Script:Config = $configContent | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
else {
$loadedConfig = $configContent | ConvertFrom-Json -ErrorAction Stop
# Convert PSObject to Hashtable for PS 5.1 compatibility
$Script:Config = @{}
$loadedConfig.PSObject.Properties | ForEach-Object {
$Script:Config[$_.Name] = $_.Value
}
}
Write-Log "INFO" "Configuration loaded from: $configPath"
}
catch {
$errMsg = $_.Exception.Message
Write-Warning "Failed to load config, using defaults - $errMsg"
$Script:Config = $defaultConfig
Save-Config -Config $Script:Config -Path $configPath
}
}
else {
# Create default config file
$Script:Config = $defaultConfig
try {
Save-Config -Config $Script:Config -Path $configPath
Write-Host "Created default config: $configPath" -ForegroundColor Cyan
}
catch {
$errMsg = $_.Exception.Message
Write-Warning "Could not create config file - $errMsg"
}
}
# Ensure required keys exist
if (-not $Script:Config.categories) { $Script:Config.categories = $defaultConfig.categories }
if (-not $Script:Config.excludePatterns) { $Script:Config.excludePatterns = $defaultConfig.excludePatterns }
if ($null -eq $Script:Config.createTypeSubfolder) { $Script:Config.createTypeSubfolder = $true }
}
function Save-Config {
<#
.SYNOPSIS
Saves configuration to JSON file
#>
[CmdletBinding()]
param(
[hashtable]$Config,
[string]$Path
)
try {
$Config | ConvertTo-Json -Depth 10 | Set-Content $Path -Force -ErrorAction Stop
}
catch {
$errMsg = $_.Exception.Message
Write-Warning "Failed to save config - $errMsg"
}
}
function Load-LastUsed {
<#
.SYNOPSIS
Loads last used source/destination paths
#>
[CmdletBinding()]
param()
$lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
if (Test-Path $lastUsedPath) {
try {
$content = Get-Content $lastUsedPath -Raw
# Handle PS 5.1 vs PS 6+ compatibility
if ($Script:PSVersion -ge 6) {
$Script:LastUsed = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop
}
else {
$loaded = $content | ConvertFrom-Json -ErrorAction Stop
# Convert PSObject to Hashtable for PS 5.1 compatibility
$Script:LastUsed = @{}
$loaded.PSObject.Properties | ForEach-Object {
$Script:LastUsed[$_.Name] = $_.Value
}
}
}
catch {
$Script:LastUsed = @{ sourcePath = ""; destPath = "" }
}
}
else {
$Script:LastUsed = @{ sourcePath = ""; destPath = "" }
}
}
function Save-LastUsed {
<#
.SYNOPSIS
Saves last used paths for future sessions
#>
[CmdletBinding()]
param(
[string]$SourcePath,
[string]$DestPath
)
$lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
$Script:LastUsed.sourcePath = $SourcePath
$Script:LastUsed.destPath = $DestPath
try {
$Script:LastUsed | ConvertTo-Json | Set-Content $lastUsedPath -Force
}
catch {
$errMsg = $_.Exception.Message
Write-Warning "Could not save last used paths - $errMsg"
}
}
function Write-Log {
<#
.SYNOPSIS
Writes timestamped log entry to file
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Level,
[Parameter(Mandatory)]
[string]$Message
)
$timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
$logEntry = "[$timestamp] [$Level] $Message"
# Write to log file
try {
Add-Content -Path $Script:LogFile -Value $logEntry -ErrorAction SilentlyContinue
}
catch {
Write-Verbose "Logging silently failed"
}
# Also output to console based on level
switch ($Level) {
"ERROR" { Write-Host $logEntry -ForegroundColor Red }
"WARN" { Write-Host $logEntry -ForegroundColor Yellow }
"INFO" { Write-Verbose $logEntry }
"DRYRUN" { Write-Host $logEntry -ForegroundColor Cyan }
}
}
#endregion
#region GUI Helper Functions
function Test-OutGridViewAvailable {
<#
.SYNOPSIS
Checks if Out-GridView is available (Windows only)
#>
if ($IsWindows -or (-not $IsLinux -and -not $IsMacOS)) {
return $true
}
return $false
}
function Select-SourceFolder {
<#
.SYNOPSIS
Shows folder browser dialog for source selection
#>
[CmdletBinding()]
param(
[string]$InitialPath
)
# Use last used path if available and no initial path provided
if (-not $InitialPath -and $Script:LastUsed.sourcePath) {
if (Test-Path $Script:LastUsed.sourcePath) {
$InitialPath = $Script:LastUsed.sourcePath
}
}
# Use Windows Forms folder browser
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = "Select Source Folder to Scan"
$dialog.ShowNewFolderButton = $false
if ($InitialPath -and (Test-Path $InitialPath)) {
$dialog.SelectedPath = $InitialPath
}
$result = $dialog.ShowDialog()
if ($result -eq "OK") {
Write-Log "INFO" "Source folder selected: $($dialog.SelectedPath)"
return $dialog.SelectedPath
}
return $null
}
function Select-DestinationFolder {
<#
.SYNOPSIS
Shows folder browser dialog for destination selection
#>
[CmdletBinding()]
param(
[string]$Description = "Select Destination Folder",
[string]$InitialPath
)
# Use last used path if available
if (-not $InitialPath -and $Script:LastUsed.destPath) {
if (Test-Path $Script:LastUsed.destPath) {
$InitialPath = $Script:LastUsed.destPath
}
}
Add-Type -AssemblyName System.Windows.Forms
$dialog = New-Object System.Windows.Forms.FolderBrowserDialog
$dialog.Description = $Description
$dialog.ShowNewFolderButton = $true
if ($InitialPath -and (Test-Path $InitialPath)) {
$dialog.SelectedPath = $InitialPath
}
$result = $dialog.ShowDialog()
if ($result -eq "OK") {
Write-Log "INFO" "Destination folder selected: $($dialog.SelectedPath)"
return $dialog.SelectedPath
}
return $null
}
function Select-DepthGUI {
<#
.SYNOPSIS
Shows depth selection via modern Windows Forms
#>
[CmdletBinding()]
param()
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = "FileOrganizer - Scan Depth"
$form.Size = New-Object System.Drawing.Size(460, 320)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
$form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 50
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$headerLabel = New-Object System.Windows.Forms.Label
$headerLabel.Text = " Select Scan Depth"
$headerLabel.ForeColor = [System.Drawing.Color]::White
$headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
$headerLabel.Dock = "Fill"
$headerLabel.TextAlign = "MiddleLeft"
$headerPanel.Controls.Add($headerLabel)
$radio0 = New-Object System.Windows.Forms.RadioButton
$radio0.Location = New-Object System.Drawing.Point(40, 70)
$radio0.Size = New-Object System.Drawing.Size(380, 28)
$radio0.Text = "Main Folder Only (Current folder only)"
$radio0.FlatStyle = "Flat"
$radio1 = New-Object System.Windows.Forms.RadioButton
$radio1.Location = New-Object System.Drawing.Point(40, 105)
$radio1.Size = New-Object System.Drawing.Size(380, 28)
$radio1.Text = "Subfolders (1 level deep)"
$radio1.FlatStyle = "Flat"
$radio2 = New-Object System.Windows.Forms.RadioButton
$radio2.Location = New-Object System.Drawing.Point(40, 140)
$radio2.Size = New-Object System.Drawing.Size(380, 28)
$radio2.Text = "Sub-Subfolders (2 levels deep)"
$radio2.FlatStyle = "Flat"
$radio100 = New-Object System.Windows.Forms.RadioButton
$radio100.Location = New-Object System.Drawing.Point(40, 175)
$radio100.Size = New-Object System.Drawing.Size(380, 28)
$radio100.Text = "All Subfolders (Recursive)"
$radio100.Checked = $true
$radio100.FlatStyle = "Flat"
$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = New-Object System.Drawing.Point(170, 225)
$okButton.Size = New-Object System.Drawing.Size(100, 36)
$okButton.Text = "OK"
$okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
$okButton.FlatStyle = "Flat"
$okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$okButton.ForeColor = [System.Drawing.Color]::White
$okButton.Cursor = "Hand"
$okButton.DialogResult = "OK"
$form.Controls.AddRange(@($headerPanel, $radio0, $radio1, $radio2, $radio100, $okButton))
$form.AcceptButton = $okButton
$result = $form.ShowDialog()
$form.Dispose()
if ($result -eq "OK") {
if ($radio0.Checked) { return 0 }
if ($radio1.Checked) { return 1 }
if ($radio2.Checked) { return 2 }
if ($radio100.Checked) { return 100 }
}
return 100 # default
}
function Select-FileType {
<#
.SYNOPSIS
Shows type selection using custom modern Windows Forms CheckedListBox
#>
[CmdletBinding()]
param(
[hashtable]$TypeCounts
)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = "FileOrganizer - File Types"
$form.Size = New-Object System.Drawing.Size(420, 500)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
$form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 50
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$headerLabel = New-Object System.Windows.Forms.Label
$headerLabel.Text = " Select File Types"
$headerLabel.ForeColor = [System.Drawing.Color]::White
$headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
$headerLabel.Dock = "Fill"
$headerLabel.TextAlign = "MiddleLeft"
$headerPanel.Controls.Add($headerLabel)
$listBox = New-Object System.Windows.Forms.CheckedListBox
$listBox.Location = New-Object System.Drawing.Point(20, 95)
$listBox.Size = New-Object System.Drawing.Size(360, 280)
$listBox.Font = New-Object System.Drawing.Font("Segoe UI", 10.5)
$listBox.CheckOnClick = $true
# Store plain string items so we can easily map them back
$itemList = @()
foreach ($type in ($TypeCounts.Keys | Sort-Object)) {
$count = $TypeCounts[$type]
$display = "$type ($count files)"
$listBox.Items.Add($display, $true) | Out-Null
$itemList += [PSCustomObject]@{ Display = $display; Type = $type }
}
$selectAllBtn = New-Object System.Windows.Forms.Button
$selectAllBtn.Location = New-Object System.Drawing.Point(20, 395)
$selectAllBtn.Size = New-Object System.Drawing.Size(100, 32)
$selectAllBtn.Text = "Select All"
$selectAllBtn.FlatStyle = "Flat"
$selectAllBtn.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
$selectAllBtn.Cursor = "Hand"
$selectAllBtn.Add_Click({
for ($i = 0; $i -lt $listBox.Items.Count; $i++) {
$listBox.SetItemChecked($i, $true)
}
})
$clearAllBtn = New-Object System.Windows.Forms.Button
$clearAllBtn.Location = New-Object System.Drawing.Point(130, 395)
$clearAllBtn.Size = New-Object System.Drawing.Size(100, 32)
$clearAllBtn.Text = "Clear All"
$clearAllBtn.FlatStyle = "Flat"
$clearAllBtn.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
$clearAllBtn.Cursor = "Hand"
$clearAllBtn.Add_Click({
for ($i = 0; $i -lt $listBox.Items.Count; $i++) {
$listBox.SetItemChecked($i, $false)
}
})
$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = New-Object System.Drawing.Point(280, 390)
$okButton.Size = New-Object System.Drawing.Size(100, 40)
$okButton.Text = "OK"
$okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10.5)
$okButton.FlatStyle = "Flat"
$okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$okButton.ForeColor = [System.Drawing.Color]::White
$okButton.Cursor = "Hand"
$okButton.DialogResult = "OK"
$form.Controls.AddRange(@($headerPanel, $listBox, $selectAllBtn, $clearAllBtn, $okButton))
$form.AcceptButton = $okButton
# Loop until valid selection or cancelled
$selectedTypes = @()
while ($true) {
$result = $form.ShowDialog()
if ($result -eq "OK") {
foreach ($item in $listBox.CheckedItems) {
# Map display back to type name
$mappedType = ($itemList | Where-Object { $_.Display -eq $item }).Type
$selectedTypes += $mappedType
}
if ($selectedTypes.Count -gt 0) {
break
}
else {
[System.Windows.Forms.MessageBox]::Show("Please select at least one file type.", "No Selection", "OK", "Warning")
}
}
else {
break # Cancelled/closed
}
}
$form.Dispose()
if ($selectedTypes.Count -eq 0) { return @() }
return $selectedTypes
}
function Select-ActionMode {
<#
.SYNOPSIS
Shows Move/Copy selection dialog (modern styled)
#>
[CmdletBinding()]
param()
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = "FileOrganizer - Select Operation"
$form.Size = New-Object System.Drawing.Size(440, 220)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
$form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 50
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$headerLabel = New-Object System.Windows.Forms.Label
$headerLabel.Text = " Select Operation Mode"
$headerLabel.ForeColor = [System.Drawing.Color]::White
$headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
$headerLabel.Dock = "Fill"
$headerLabel.TextAlign = "MiddleLeft"
$headerPanel.Controls.Add($headerLabel)
$moveRadio = New-Object System.Windows.Forms.RadioButton
$moveRadio.Location = New-Object System.Drawing.Point(40, 70)
$moveRadio.Size = New-Object System.Drawing.Size(160, 30)
$moveRadio.Text = "Move (default)"
$moveRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$moveRadio.Checked = $true
$moveRadio.FlatStyle = "Flat"
$copyRadio = New-Object System.Windows.Forms.RadioButton
$copyRadio.Location = New-Object System.Drawing.Point(230, 70)
$copyRadio.Size = New-Object System.Drawing.Size(160, 30)
$copyRadio.Text = "Copy (safer)"
$copyRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$copyRadio.FlatStyle = "Flat"
$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = New-Object System.Drawing.Point(110, 125)
$okButton.Size = New-Object System.Drawing.Size(100, 36)
$okButton.Text = "OK"
$okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
$okButton.FlatStyle = "Flat"
$okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$okButton.ForeColor = [System.Drawing.Color]::White
$okButton.Cursor = "Hand"
$okButton.DialogResult = "OK"
$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Location = New-Object System.Drawing.Point(225, 125)
$cancelButton.Size = New-Object System.Drawing.Size(100, 36)
$cancelButton.Text = "Cancel"
$cancelButton.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$cancelButton.FlatStyle = "Flat"
$cancelButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
$cancelButton.Cursor = "Hand"
$cancelButton.DialogResult = "Cancel"
$form.Controls.AddRange(@($headerPanel, $moveRadio, $copyRadio, $okButton, $cancelButton))
$form.AcceptButton = $okButton
$form.CancelButton = $cancelButton
$result = $form.ShowDialog()
if ($result -eq "OK") {
if ($copyRadio.Checked) {
return "Copy"
}
}
return "Move"
}
function Confirm-Summary {
<#
.SYNOPSIS
Shows modern confirmation dialog with operation summary
#>
[CmdletBinding()]
param(
[string]$SourcePath,
[string[]]$SelectedTypes,
[hashtable]$TypeCounts,
[string]$Action,
[hashtable]$Destinations,
[bool]$DryRun
)
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
# Calculate total files
$totalFiles = 0
foreach ($type in $SelectedTypes) {
$totalFiles += $TypeCounts[$type]
}
# Build summary lines using ASCII-safe characters
$summaryLines = @()
$summaryLines += "Source Folder: $SourcePath"
$summaryLines += ""
$summaryLines += "Selected Types ($($SelectedTypes.Count) categories):"
foreach ($type in $SelectedTypes | Sort-Object) {
$count = $TypeCounts[$type]
$dest = $Destinations[$type]
$summaryLines += " - $type : $count files"
$summaryLines += " -> $dest"
}
$dryRunText = if ($DryRun) { " (DRY RUN - No changes)" } else { "" }
$summaryLines += ""
$summaryLines += "Operation: $Action$dryRunText"
$summaryLines += "Total Files: $totalFiles"
$summaryText = $summaryLines -join "`r`n"
# Create modern styled confirmation form
$form = New-Object System.Windows.Forms.Form
$form.Text = "FileOrganizer - Confirm Operation"
$form.Size = New-Object System.Drawing.Size(520, 420)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
$form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
# Header panel
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 50
$accentColor = if ($DryRun) { [System.Drawing.Color]::FromArgb(0, 120, 180) } else { [System.Drawing.Color]::FromArgb(55, 71, 133) }
$headerPanel.BackColor = $accentColor
$headerLabel = New-Object System.Windows.Forms.Label
$headerText = if ($DryRun) { " Confirm Operation (Dry Run)" } else { " Confirm Operation" }
$headerLabel.Text = $headerText
$headerLabel.ForeColor = [System.Drawing.Color]::White
$headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
$headerLabel.Dock = "Fill"
$headerLabel.TextAlign = "MiddleLeft"
$headerPanel.Controls.Add($headerLabel)
# Summary text box (read-only)
$textBox = New-Object System.Windows.Forms.TextBox
$textBox.Location = New-Object System.Drawing.Point(20, 65)
$textBox.Size = New-Object System.Drawing.Size(462, 240)
$textBox.Multiline = $true
$textBox.ReadOnly = $true
$textBox.ScrollBars = "Vertical"
$textBox.Font = New-Object System.Drawing.Font("Segoe UI", 9.5)
$textBox.BackColor = [System.Drawing.Color]::White
$textBox.BorderStyle = "FixedSingle"
$textBox.Text = $summaryText
# Question label
$questionLabel = New-Object System.Windows.Forms.Label
$questionLabel.Location = New-Object System.Drawing.Point(20, 315)
$questionLabel.Size = New-Object System.Drawing.Size(460, 25)
$questionLabel.Text = "Proceed with this operation?"
$questionLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
# Yes button
$yesButton = New-Object System.Windows.Forms.Button
$yesButton.Location = New-Object System.Drawing.Point(140, 345)
$yesButton.Size = New-Object System.Drawing.Size(110, 36)
$yesButton.Text = "Yes, Proceed"
$yesButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
$yesButton.FlatStyle = "Flat"
$yesButton.BackColor = $accentColor
$yesButton.ForeColor = [System.Drawing.Color]::White
$yesButton.Cursor = "Hand"
$yesButton.DialogResult = "Yes"
# No button
$noButton = New-Object System.Windows.Forms.Button
$noButton.Location = New-Object System.Drawing.Point(265, 345)
$noButton.Size = New-Object System.Drawing.Size(110, 36)
$noButton.Text = "Cancel"
$noButton.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$noButton.FlatStyle = "Flat"
$noButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
$noButton.Cursor = "Hand"
$noButton.DialogResult = "No"
$form.Controls.AddRange(@($headerPanel, $textBox, $questionLabel, $yesButton, $noButton))
$form.AcceptButton = $yesButton
$form.CancelButton = $noButton
$result = $form.ShowDialog()
$form.Dispose()
return ($result -eq "Yes")
}
function Select-DestinationMode {
<#
.SYNOPSIS
Shows destination mode selection (modern styled)
#>
[CmdletBinding()]
param()
Add-Type -AssemblyName System.Windows.Forms
Add-Type -AssemblyName System.Drawing
$form = New-Object System.Windows.Forms.Form
$form.Text = "FileOrganizer - Destination Mode"
$form.Size = New-Object System.Drawing.Size(480, 240)
$form.StartPosition = "CenterScreen"
$form.FormBorderStyle = "FixedDialog"
$form.MaximizeBox = $false
$form.MinimizeBox = $false
$form.BackColor = [System.Drawing.Color]::FromArgb(245, 245, 250)
$form.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$headerPanel = New-Object System.Windows.Forms.Panel
$headerPanel.Dock = "Top"
$headerPanel.Height = 50
$headerPanel.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$headerLabel = New-Object System.Windows.Forms.Label
$headerLabel.Text = " Select Destination Mode"
$headerLabel.ForeColor = [System.Drawing.Color]::White
$headerLabel.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 14)
$headerLabel.Dock = "Fill"
$headerLabel.TextAlign = "MiddleLeft"
$headerPanel.Controls.Add($headerLabel)
$singleRadio = New-Object System.Windows.Forms.RadioButton
$singleRadio.Location = New-Object System.Drawing.Point(40, 70)
$singleRadio.Size = New-Object System.Drawing.Size(400, 28)
$singleRadio.Text = "Single Destination (all types to one folder)"
$singleRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$singleRadio.Checked = $true
$singleRadio.FlatStyle = "Flat"
$separateRadio = New-Object System.Windows.Forms.RadioButton
$separateRadio.Location = New-Object System.Drawing.Point(40, 105)
$separateRadio.Size = New-Object System.Drawing.Size(400, 28)
$separateRadio.Text = "Separate Destination (pick folder per file type)"
$separateRadio.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$separateRadio.FlatStyle = "Flat"
$okButton = New-Object System.Windows.Forms.Button
$okButton.Location = New-Object System.Drawing.Point(130, 150)
$okButton.Size = New-Object System.Drawing.Size(100, 36)
$okButton.Text = "OK"
$okButton.Font = New-Object System.Drawing.Font("Segoe UI Semibold", 10)
$okButton.FlatStyle = "Flat"
$okButton.BackColor = [System.Drawing.Color]::FromArgb(55, 71, 133)
$okButton.ForeColor = [System.Drawing.Color]::White
$okButton.Cursor = "Hand"
$okButton.DialogResult = "OK"
$cancelButton = New-Object System.Windows.Forms.Button
$cancelButton.Location = New-Object System.Drawing.Point(245, 150)
$cancelButton.Size = New-Object System.Drawing.Size(100, 36)
$cancelButton.Text = "Cancel"
$cancelButton.Font = New-Object System.Drawing.Font("Segoe UI", 10)
$cancelButton.FlatStyle = "Flat"
$cancelButton.BackColor = [System.Drawing.Color]::FromArgb(220, 220, 225)
$cancelButton.Cursor = "Hand"
$cancelButton.DialogResult = "Cancel"
$form.Controls.AddRange(@($headerPanel, $singleRadio, $separateRadio, $okButton, $cancelButton))
$form.AcceptButton = $okButton
$form.CancelButton = $cancelButton
$result = $form.ShowDialog()
$form.Dispose()
if ($result -eq "OK") {
if ($separateRadio.Checked) {
return "Separate"
}
}
return "Single"
}
#endregion
#region Core Processing Functions
function Get-FileType {
<#
.SYNOPSIS
Determines file category based on extension
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$Extension
)
$ext = $Extension.TrimStart('.').ToLower()
# Handle PS 5.1 (PSCustomObject) vs PS 6+ (Hashtable)
$categories = $Script:Config.categories
if ($categories -is [System.Collections.Hashtable]) {
$categoryList = $categories.Keys
}
else {
# PS 5.1 - convert PSObject to collection
$categoryList = $categories.PSObject.Properties.Name
}
foreach ($category in $categoryList) {
if ($categories -is [System.Collections.Hashtable]) {
$extensions = $categories[$category]
}
else {
$extensions = $categories.$category
}
if ($extensions -contains $ext) {
return $category
}
}
return "Others"
}
function Test-ShouldExclude {
<#
.SYNOPSIS
Checks if file matches any exclude patterns
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$FileName
)
# Get exclude patterns - handle PS 5.1 PSObject vs array
$patterns = $Script:Config.excludePatterns
if ($patterns -is [PSObject] -and $patterns -isnot [Array]) {
$patterns = @($patterns)
}
if (-not $patterns) { return $false }
foreach ($pattern in $patterns) {
# Handle wildcard patterns
if ($pattern.Contains("*")) {
if ($FileName -like $pattern) {
return $true
}
}
# Handle prefix patterns like ~$ (temporary Office files)
elseif ($pattern.StartsWith("~")) {
if ($FileName.StartsWith($pattern)) {
return $true
}
}
# Exact match
else {
if ($FileName -eq $pattern) {
return $true
}
}
}
return $false
}
function Scan-SourceFiles {
<#
.SYNOPSIS
Scans source folder and returns file list with metadata
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$SourcePath,
[Parameter(Mandatory)]
[int]$Depth
)
Write-Host "`nScanning files..." -ForegroundColor Cyan
Write-Log "INFO" "Scanning source: $SourcePath with depth: $Depth"
$files = @()
try {
# PowerShell 7+ has -Depth parameter
if ($Script:PSVersion -ge 7) {
$files = @(Get-ChildItem -Path $SourcePath -File -Recurse -Depth $Depth -ErrorAction SilentlyContinue)
}
else {
# PowerShell 5.1 - simulate depth limit
if ($Depth -ge 100) {
$files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue)
}
else {
# Manual depth filtering for PS 5.1
# Normalize source path to remove trailing backslash for consistent counting
$normalizedSource = $SourcePath.TrimEnd([IO.Path]::DirectorySeparatorChar)
$sourceDepth = $normalizedSource.Split([IO.Path]::DirectorySeparatorChar).Count
$files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue |
Where-Object {
$fileDepth = $_.DirectoryName.TrimEnd([IO.Path]::DirectorySeparatorChar).Split([IO.Path]::DirectorySeparatorChar).Count
($fileDepth - $sourceDepth) -le $Depth
})
}
}
}
catch {
$errMsg = $_.Exception.Message
Write-Log "ERROR" "Error scanning source - $errMsg"
throw "Failed to scan source folder - $errMsg"
}
# Filter out excluded files
$filteredFiles = @()
foreach ($file in $files) {
if (-not (Test-ShouldExclude -FileName $file.Name)) {
$filteredFiles += $file
}
}
Write-Host "Found $($filteredFiles.Count) files (excluding $($files.Count - $filteredFiles.Count) matches)" -ForegroundColor Green
return $filteredFiles
}
function Get-TypeSummary {
<#
.SYNOPSIS
Counts files by category
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[array]$Files
)
$typeCount = @{}
foreach ($file in $Files) {
$typeName = Get-FileType -Extension $file.Extension
if (-not $typeCount.ContainsKey($typeName)) {
$typeCount[$typeName] = 0
}
$typeCount[$typeName]++
}
return $typeCount
}
function Get-UniqueFileName {
<#
.SYNOPSIS
Generates unique filename to avoid conflicts
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[string]$DestinationPath,
[Parameter(Mandatory)]
[string]$FileName
)
$fullPath = Join-Path $DestinationPath $FileName
if (-not (Test-Path $fullPath)) {
return $FileName
}
# File exists, generate unique name
$baseName = [IO.Path]::GetFileNameWithoutExtension($FileName)
$extension = [IO.Path]::GetExtension($FileName)
$counter = 1
do {
$newName = "${baseName}_${counter}${extension}"
$fullPath = Join-Path $DestinationPath $newName
$counter++
} while (Test-Path $fullPath)
return $newName
}
function Test-Cancel {
<#
.SYNOPSIS
Checks if user pressed Escape to cancel operation
#>
[CmdletBinding()]
param()
if ($Script:PSVersion -ge 7 -and [Console]::IsInputRedirected) {
return $false
}
if ([Console]::KeyAvailable) {
$key = [Console]::ReadKey($true)
if ($key.Key -eq "Escape") {
Write-Host "`nOperation Cancelled by user." -ForegroundColor Yellow
Write-Log "WARN" "Operation cancelled by user"
return $true
}
}
return $false
}
#endregion
#region File Operation Functions
function Process-Files {
<#
.SYNOPSIS
Main file processing loop with progress and cancellation
#>
[CmdletBinding()]
param(
[Parameter(Mandatory)]
[array]$Files,
[Parameter(Mandatory)]
[string[]]$SelectedTypes,
[Parameter(Mandatory)]
[hashtable]$TypeDestinationMap,
[Parameter(Mandatory)]
[string]$Action,
[bool]$DryRun,
[bool]$CreateTypeSubfolder
)
$processedCount = 0
$errorCount = 0
$total = $Files.Count
$index = 0
$operationVerb = if ($Action -eq "Move") { "Moving" } else { "Copying" }
$logAction = if ($DryRun) { "DRYRUN" } else { "INFO" }
Write-Host "`n$operationVerb files..." -ForegroundColor $(if ($DryRun) { "Cyan" }else { "Yellow" })
foreach ($file in $Files) {
# Check for cancellation
if (Test-Cancel) {
Write-Host "`nOperation cancelled. Stopping..." -ForegroundColor Yellow
break
}
$index++
$percent = [math]::Round(($index / $total) * 100)
Write-Progress -Activity "$operationVerb Files ($Action)" -Status "$index of $total files" -PercentComplete $percent
# Determine file type
$typeName = Get-FileType -Extension $file.Extension
# Get destination path
$baseDest = $TypeDestinationMap[$typeName]
# Create type subfolder if enabled
if ($CreateTypeSubfolder) {
$destPath = Join-Path $baseDest $typeName
}
else {
$destPath = $baseDest
}
# Create destination if it doesn't exist
if (-not (Test-Path $destPath)) {
try {
New-Item -ItemType Directory -Path $destPath -Force | Out-Null
Write-Log "INFO" "Created directory: $destPath"
}
catch {
$errMsg = $_.Exception.Message
Write-Log "ERROR" "Failed to create directory $destPath - $errMsg"
$errorCount++
continue
}
}
# Get unique filename
$newFileName = Get-UniqueFileName -DestinationPath $destPath -FileName $file.Name
$newFullPath = Join-Path $destPath $newFileName
# Perform the operation
if ($DryRun) {
# Dry run - just log what would happen
Write-Log "DRYRUN" "Would $Action '$($file.FullName)' -> '$newFullPath'"
$processedCount++
}
else {
# Actual move or copy
try {
if ($Action -eq "Move") {
Move-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop
}
else {
Copy-Item -Path $file.FullName -Destination $newFullPath -Force -ErrorAction Stop
}
Write-Log "INFO" "$Action completed: $($file.Name) -> $newFullPath"
$processedCount++
}
catch {
$errMsg = $_.Exception.Message
$fileName = $file.Name
Write-Log "ERROR" "Failed to $Action '$fileName' - $errMsg"
$errorCount++
}
}
}
Write-Progress -Activity "$operationVerb Files" -Completed
# Return summary
return @{
Processed = $processedCount
Errors = $errorCount
}
}
#endregion
#region Interactive Mode
function Start-InteractiveMode {
<#
.SYNOPSIS
Runs the interactive GUI-based workflow
#>
[CmdletBinding()]
param()
Write-Host "`n=== FileOrganizer - Interactive Mode ===" -ForegroundColor Cyan
Write-Host "Press ESC anytime to cancel operation`n" -ForegroundColor Gray
# Give user a moment to see the startup
Start-Sleep -Milliseconds 500
# Step 1: Select source
Write-Host "Step 1: Select Source Folder (a dialog will open)" -ForegroundColor Yellow
Write-Host ">>> Click Browse and select a folder <<<" -ForegroundColor Magenta
$sourceFolder = Select-SourceFolder
if (-not $sourceFolder) {
Write-Host "No source folder selected. Exiting." -ForegroundColor Yellow
return
}
# Step 2: Select depth
Write-Host "`nStep 2: Select Scan Depth" -ForegroundColor Yellow
$depth = Select-DepthGUI
Write-Host "Using depth: $depth" -ForegroundColor Gray
# Step 3: Scan files
$files = Scan-SourcePath -SourcePath $sourceFolder -Depth $depth
if ($files.Count -eq 0) {
Write-Host "No files found in source folder." -ForegroundColor Yellow
return
}
# Step 4: Get type summary
$typeCounts = Get-TypeSummary -Files $files
# Step 5: Select types
Write-Host "`nStep 3: Select File Types" -ForegroundColor Yellow
$selectedTypes = Select-FileType -TypeCounts $typeCounts
if ($selectedTypes.Count -eq 0) {
Write-Host "No file types selected. Exiting." -ForegroundColor Yellow
return
}
# Step 6: Select action (Move/Copy)
Write-Host "`nStep 4: Select Operation" -ForegroundColor Yellow
$action = Select-ActionMode
Write-Host "Operation: $action" -ForegroundColor Gray
# Step 7: Destination mode
Write-Host "`nStep 5: Select Destination Mode" -ForegroundColor Yellow
$destMode = Select-DestinationMode
$typeDestinationMap = @{}
if ($destMode -eq "Separate") {
# Separate destination per type
foreach ($type in $selectedTypes) {
Write-Host "`nSelect destination for: $type" -ForegroundColor Cyan
$dest = Select-DestinationFolder -Description "Select destination for $type"
if (-not $dest) {
Write-Host "Destination not selected. Using source folder." -ForegroundColor Yellow
$dest = $sourceFolder
}
$typeDestinationMap[$type] = $dest
}
}
else {
# Single destination
Write-Host "`nSelect single destination folder" -ForegroundColor Cyan
$singleDest = Select-DestinationFolder -Description "Select destination for all files"
if (-not $singleDest) {
Write-Host "No destination selected. Using source folder." -ForegroundColor Yellow
$singleDest = $sourceFolder
}
foreach ($type in $selectedTypes) {
$typeDestinationMap[$type] = $singleDest
}
}
# Step 8: DryRun option
Write-Host "`nStep 6: Dry Run Mode?" -ForegroundColor Yellow
$dryRunChoice = Read-Host "Run as Dry Run (simulate only)? y/n (default: n)"
$dryRun = ($dryRunChoice -eq "y")
if ($dryRun) {
Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
}
# Step 9: Confirm summary
Write-Host "`nStep 7: Confirm Operation" -ForegroundColor Yellow
$confirmed = Confirm-Summary -SourcePath $sourceFolder -SelectedTypes $selectedTypes -TypeCounts $typeCounts -Action $action -Destinations $typeDestinationMap -DryRun $dryRun
if (-not $confirmed) {
Write-Host "Operation cancelled by user." -ForegroundColor Yellow
return
}
# Step 10: Filter files to only selected types, then process
$filteredFiles = @($files | Where-Object {
$typeName = Get-FileType -Extension $_.Extension
@($selectedTypes) -contains $typeName
})
Write-Host "`nFiles to process: $($filteredFiles.Count) (of $($files.Count) total scanned)" -ForegroundColor Gray
$createSubfolder = [bool]$Script:Config.createTypeSubfolder
$result = Process-Files -Files $filteredFiles -SelectedTypes @($selectedTypes) -TypeDestinationMap $typeDestinationMap -Action $action -DryRun $dryRun -CreateTypeSubfolder $createSubfolder
# Step 11: Show results
Write-Host "`n=== Operation Complete ===" -ForegroundColor Green
Write-Host "Files processed: $($result.Processed)" -ForegroundColor White
if ($result.Errors -gt 0) {
Write-Host "Errors: $($result.Errors)" -ForegroundColor Red
}
Write-Log "INFO" "Operation completed - Processed: $($result.Processed), Errors: $($result.Errors)"
# Save last used paths on success
if ($result.Processed -gt 0 -and -not $dryRun) {
$firstDest = ($typeDestinationMap.Values | Select-Object -First 1)
Save-LastUsed -SourcePath $sourceFolder -DestPath $firstDest
}
}
function Scan-SourcePath {
<#
.SYNOPSIS
Wrapper for scanning with user feedback
#>
[CmdletBinding()]
param(
[string]$SourcePath,
[int]$Depth
)
return Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
}
function Start-CLIMode {
<#
.SYNOPSIS
Runs the command-line interface mode with provided parameters
#>
[CmdletBinding()]
param()
Write-Host "`n=== FileOrganizer - CLI Mode ===" -ForegroundColor Cyan
# Validate source path
if (-not $SourcePath) {
Write-Error "SourcePath is required in CLI mode. Use -SourcePath parameter."
return
}
if (-not (Test-Path $SourcePath)) {
Write-Error "Source path does not exist: $SourcePath"
return
}
# Set default destination if not provided
if (-not $DestPath) {
$DestPath = $SourcePath
Write-Host "No destination specified, using source path: $DestPath" -ForegroundColor Yellow
}
# Scan files
$files = Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
if ($files.Count -eq 0) {
Write-Host "No files found." -ForegroundColor Yellow
return
}
# Get type summary
$typeCounts = Get-TypeSummary -Files $files
# Show summary
Write-Host "`nFile Type Summary:" -ForegroundColor Green
foreach ($type in ($typeCounts.Keys | Sort-Object)) {
Write-Host " $type : $($typeCounts[$type]) files" -ForegroundColor White
}
# Determine selected types (all by default in CLI)
$selectedTypes = $typeCounts.Keys
# Set up destination map
$typeDestinationMap = @{}
if ($SeparateDestinations) {
Write-Host "`nSeparate destinations requested but not implemented in CLI mode." -ForegroundColor Yellow
Write-Host "Using single destination: $DestPath" -ForegroundColor Yellow
}
foreach ($type in $selectedTypes) {
$typeDestinationMap[$type] = $DestPath
}
# Show dry run status
if ($DryRun) {
Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
}
# Filter files to selected types, then process
$filteredFiles = @($files | Where-Object {
$typeName = Get-FileType -Extension $_.Extension
@($selectedTypes) -contains $typeName
})
$createSubfolder = [bool]$Script:Config.createTypeSubfolder
$result = Process-Files -Files $filteredFiles -TypeDestinationMap $typeDestinationMap -Action $Action -DryRun $DryRun -CreateTypeSubfolder $createSubfolder
# Show results
Write-Host "`n=== Operation Complete ===" -ForegroundColor Green
Write-Host "Files processed: $($result.Processed)" -ForegroundColor White
if ($result.Errors -gt 0) {
Write-Host "Errors: $($result.Errors)" -ForegroundColor Red
}
Write-Log "INFO" "CLI operation completed - Processed: $($result.Processed), Errors: $($result.Errors)"
# Save last used paths
if ($result.Processed -gt 0 -and -not $DryRun) {
Save-LastUsed -SourcePath $SourcePath -DestPath $DestPath
}
}
#endregion
#region Main Entry Point
function Main {
<#
.SYNOPSIS
Main entry point for the script
#>
[CmdletBinding()]
param()
# Initialize environment
Initialize-Environment
# Check if Help requested
if ($Help) {
Get-Help $MyInvocation.MyCommand.Path -Detailed
return
}
# Determine mode: CLI or Interactive
$isCLIMode = $SourcePath -and $DestPath
if ($isCLIMode) {
# Run in CLI mode
Start-CLIMode
}
else {
# Run in Interactive mode
Start-InteractiveMode
# Loop for additional tasks unless -NoLoop specified
if (-not $NoLoop) {
while ($true) {
Write-Host "`n" -NoNewline
$again = Read-Host "Do another task? y/n"
if ($again -ne "y") {
break
}
Start-InteractiveMode
}
}
}
Write-Host "`nGoodbye!" -ForegroundColor Cyan
Write-Log "INFO" "FileOrganizer ended"
# Pause to keep window open
Write-Host "`nPress Enter to exit..." -ForegroundColor Gray
try { [void](Read-Host) } catch { Start-Sleep -Seconds 5 }
}
# Run main function
try {
Main
}
catch {
$errMsg = $_.Exception.Message
$stackTrace = $_.ScriptStackTrace
Write-Host "`n" -ForegroundColor Red
Write-Host "FATAL ERROR: $errMsg" -ForegroundColor Red
Write-Host "Stack Trace: $stackTrace" -ForegroundColor Red
Write-Host "`nPress Enter to exit..." -ForegroundColor Yellow
try { Read-Host } catch { Start-Sleep -Seconds 5 }
exit 1
}
#endregion