| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | [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
|
| | )
|
| |
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | $ErrorActionPreference = "Continue"
|
| | $Script:StartupError = $null
|
| |
|
| |
|
| | 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"
|
| | }
|
| |
|
| |
|
| | $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 }
|
| |
|
| |
|
| | try {
|
| | Add-Type -AssemblyName System.Windows.Forms -ErrorAction Stop
|
| | }
|
| | catch {
|
| | $errMsg = $_.Exception.Message
|
| | $Script:StartupError = "Failed to load Windows Forms - $errMsg"
|
| | }
|
| |
|
| |
|
| | $Script:Config = $null
|
| | $Script:LastUsed = $null
|
| | $Script:ScriptName = "FileOrganizer"
|
| | $Script:LogFile = $null
|
| | $Script:IsElevated = $false
|
| | $Script:PSVersion = $PSVersionTable.PSVersion.Major
|
| |
|
| |
|
| |
|
| | if ($Script:StartupError) {
|
| | Write-Host "ERROR: $Script:StartupError" -ForegroundColor Red
|
| | Write-Host "Press Enter to exit..." -ForegroundColor Yellow
|
| | Read-Host
|
| | exit 1
|
| | }
|
| |
|
| |
|
| | 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 ""
|
| |
|
| |
|
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| | |
| |
|
| |
|
| |
|
| |
|
| |
|
| | function Initialize-Environment {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param()
|
| |
|
| |
|
| | $logDate = Get-Date -Format "yyyy-MM-dd"
|
| | $Script:LogFile = Join-Path $Script:ScriptPath "$($Script:ScriptName)_$logDate.log"
|
| |
|
| |
|
| | if (-not $Script:ScriptPath) {
|
| | $Script:ScriptPath = Split-Path -Parent $MyInvocation.MyCommand.Path
|
| | }
|
| |
|
| |
|
| | Load-Config -CategoriesFile $CategoriesFile
|
| |
|
| |
|
| | Load-LastUsed
|
| |
|
| |
|
| | 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 {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [string]$CategoriesFile
|
| | )
|
| |
|
| |
|
| | if ($CategoriesFile) {
|
| | $configPath = $CategoriesFile
|
| | }
|
| | else {
|
| | $configPath = Join-Path $Script:ScriptPath "config.json"
|
| | }
|
| |
|
| |
|
| | $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
|
| | }
|
| |
|
| |
|
| | if (Test-Path $configPath) {
|
| | try {
|
| | $configContent = Get-Content $configPath -Raw -ErrorAction Stop
|
| |
|
| | if ($Script:PSVersion -ge 6) {
|
| | $Script:Config = $configContent | ConvertFrom-Json -AsHashtable -ErrorAction Stop
|
| | }
|
| | else {
|
| | $loadedConfig = $configContent | ConvertFrom-Json -ErrorAction Stop
|
| |
|
| | $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 {
|
| |
|
| | $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"
|
| | }
|
| | }
|
| |
|
| |
|
| | 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 {
|
| | |
| | |
| | |
| |
|
| | [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 {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param()
|
| |
|
| | $lastUsedPath = Join-Path $Script:ScriptPath "lastused.json"
|
| |
|
| | if (Test-Path $lastUsedPath) {
|
| | try {
|
| | $content = Get-Content $lastUsedPath -Raw
|
| |
|
| | if ($Script:PSVersion -ge 6) {
|
| | $Script:LastUsed = $content | ConvertFrom-Json -AsHashtable -ErrorAction Stop
|
| | }
|
| | else {
|
| | $loaded = $content | ConvertFrom-Json -ErrorAction Stop
|
| |
|
| | $Script:LastUsed = @{}
|
| | $loaded.PSObject.Properties | ForEach-Object {
|
| | $Script:LastUsed[$_.Name] = $_.Value
|
| | }
|
| | }
|
| | }
|
| | catch {
|
| | $Script:LastUsed = @{ sourcePath = ""; destPath = "" }
|
| | }
|
| | }
|
| | else {
|
| | $Script:LastUsed = @{ sourcePath = ""; destPath = "" }
|
| | }
|
| | }
|
| |
|
| | function Save-LastUsed {
|
| | |
| | |
| | |
| |
|
| | [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 {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [Parameter(Mandatory)]
|
| | [string]$Level,
|
| |
|
| | [Parameter(Mandatory)]
|
| | [string]$Message
|
| | )
|
| |
|
| | $timestamp = Get-Date -Format "yyyy-MM-dd HH:mm:ss"
|
| | $logEntry = "[$timestamp] [$Level] $Message"
|
| |
|
| |
|
| | try {
|
| | Add-Content -Path $Script:LogFile -Value $logEntry -ErrorAction SilentlyContinue
|
| | }
|
| | catch {
|
| | Write-Verbose "Logging silently failed"
|
| | }
|
| |
|
| |
|
| | switch ($Level) {
|
| | "ERROR" { Write-Host $logEntry -ForegroundColor Red }
|
| | "WARN" { Write-Host $logEntry -ForegroundColor Yellow }
|
| | "INFO" { Write-Verbose $logEntry }
|
| | "DRYRUN" { Write-Host $logEntry -ForegroundColor Cyan }
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | function Test-OutGridViewAvailable {
|
| | |
| | |
| | |
| |
|
| | if ($IsWindows -or (-not $IsLinux -and -not $IsMacOS)) {
|
| | return $true
|
| | }
|
| | return $false
|
| | }
|
| |
|
| | function Select-SourceFolder {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [string]$InitialPath
|
| | )
|
| |
|
| |
|
| | if (-not $InitialPath -and $Script:LastUsed.sourcePath) {
|
| | if (Test-Path $Script:LastUsed.sourcePath) {
|
| | $InitialPath = $Script:LastUsed.sourcePath
|
| | }
|
| | }
|
| |
|
| |
|
| | 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 {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [string]$Description = "Select Destination Folder",
|
| | [string]$InitialPath
|
| | )
|
| |
|
| |
|
| | 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 {
|
| | |
| | |
| | |
| |
|
| | [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
|
| | }
|
| |
|
| | function Select-FileType {
|
| | |
| | |
| | |
| |
|
| | [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
|
| |
|
| |
|
| | $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
|
| |
|
| |
|
| | $selectedTypes = @()
|
| | while ($true) {
|
| | $result = $form.ShowDialog()
|
| |
|
| | if ($result -eq "OK") {
|
| | foreach ($item in $listBox.CheckedItems) {
|
| |
|
| | $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
|
| | }
|
| | }
|
| |
|
| | $form.Dispose()
|
| |
|
| | if ($selectedTypes.Count -eq 0) { return @() }
|
| | return $selectedTypes
|
| | }
|
| |
|
| | function Select-ActionMode {
|
| | |
| | |
| | |
| |
|
| | [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 {
|
| | |
| | |
| | |
| |
|
| | [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
|
| |
|
| |
|
| | $totalFiles = 0
|
| | foreach ($type in $SelectedTypes) {
|
| | $totalFiles += $TypeCounts[$type]
|
| | }
|
| |
|
| |
|
| | $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"
|
| |
|
| |
|
| | $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)
|
| |
|
| |
|
| | $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)
|
| |
|
| |
|
| | $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
|
| |
|
| |
|
| | $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)
|
| |
|
| |
|
| | $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"
|
| |
|
| |
|
| | $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 {
|
| | |
| | |
| | |
| |
|
| | [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"
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | function Get-FileType {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [Parameter(Mandatory)]
|
| | [string]$Extension
|
| | )
|
| |
|
| | $ext = $Extension.TrimStart('.').ToLower()
|
| |
|
| |
|
| | $categories = $Script:Config.categories
|
| | if ($categories -is [System.Collections.Hashtable]) {
|
| | $categoryList = $categories.Keys
|
| | }
|
| | else {
|
| |
|
| | $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 {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [Parameter(Mandatory)]
|
| | [string]$FileName
|
| | )
|
| |
|
| |
|
| | $patterns = $Script:Config.excludePatterns
|
| | if ($patterns -is [PSObject] -and $patterns -isnot [Array]) {
|
| | $patterns = @($patterns)
|
| | }
|
| | if (-not $patterns) { return $false }
|
| |
|
| | foreach ($pattern in $patterns) {
|
| |
|
| | if ($pattern.Contains("*")) {
|
| | if ($FileName -like $pattern) {
|
| | return $true
|
| | }
|
| | }
|
| |
|
| | elseif ($pattern.StartsWith("~")) {
|
| | if ($FileName.StartsWith($pattern)) {
|
| | return $true
|
| | }
|
| | }
|
| |
|
| | else {
|
| | if ($FileName -eq $pattern) {
|
| | return $true
|
| | }
|
| | }
|
| | }
|
| |
|
| | return $false
|
| | }
|
| |
|
| | function Scan-SourceFiles {
|
| | |
| | |
| | |
| |
|
| | [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 {
|
| |
|
| | if ($Script:PSVersion -ge 7) {
|
| | $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -Depth $Depth -ErrorAction SilentlyContinue)
|
| | }
|
| | else {
|
| |
|
| | if ($Depth -ge 100) {
|
| | $files = @(Get-ChildItem -Path $SourcePath -File -Recurse -ErrorAction SilentlyContinue)
|
| | }
|
| | else {
|
| |
|
| |
|
| | $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"
|
| | }
|
| |
|
| |
|
| | $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 {
|
| | |
| | |
| | |
| |
|
| | [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 {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [Parameter(Mandatory)]
|
| | [string]$DestinationPath,
|
| |
|
| | [Parameter(Mandatory)]
|
| | [string]$FileName
|
| | )
|
| |
|
| | $fullPath = Join-Path $DestinationPath $FileName
|
| |
|
| | if (-not (Test-Path $fullPath)) {
|
| | return $FileName
|
| | }
|
| |
|
| |
|
| | $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 {
|
| | |
| | |
| | |
| |
|
| | [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
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | function Process-Files {
|
| | |
| | |
| | |
| |
|
| | [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) {
|
| |
|
| | 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
|
| |
|
| |
|
| | $typeName = Get-FileType -Extension $file.Extension
|
| |
|
| |
|
| | $baseDest = $TypeDestinationMap[$typeName]
|
| |
|
| |
|
| | if ($CreateTypeSubfolder) {
|
| | $destPath = Join-Path $baseDest $typeName
|
| | }
|
| | else {
|
| | $destPath = $baseDest
|
| | }
|
| |
|
| |
|
| | 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
|
| | }
|
| | }
|
| |
|
| |
|
| | $newFileName = Get-UniqueFileName -DestinationPath $destPath -FileName $file.Name
|
| | $newFullPath = Join-Path $destPath $newFileName
|
| |
|
| |
|
| | if ($DryRun) {
|
| |
|
| | Write-Log "DRYRUN" "Would $Action '$($file.FullName)' -> '$newFullPath'"
|
| | $processedCount++
|
| | }
|
| | else {
|
| |
|
| | 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 @{
|
| | Processed = $processedCount
|
| | Errors = $errorCount
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | function Start-InteractiveMode {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param()
|
| |
|
| | Write-Host "`n=== FileOrganizer - Interactive Mode ===" -ForegroundColor Cyan
|
| | Write-Host "Press ESC anytime to cancel operation`n" -ForegroundColor Gray
|
| |
|
| |
|
| | Start-Sleep -Milliseconds 500
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| | Write-Host "`nStep 2: Select Scan Depth" -ForegroundColor Yellow
|
| | $depth = Select-DepthGUI
|
| | Write-Host "Using depth: $depth" -ForegroundColor Gray
|
| |
|
| |
|
| | $files = Scan-SourcePath -SourcePath $sourceFolder -Depth $depth
|
| |
|
| | if ($files.Count -eq 0) {
|
| | Write-Host "No files found in source folder." -ForegroundColor Yellow
|
| | return
|
| | }
|
| |
|
| |
|
| | $typeCounts = Get-TypeSummary -Files $files
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| | Write-Host "`nStep 4: Select Operation" -ForegroundColor Yellow
|
| | $action = Select-ActionMode
|
| | Write-Host "Operation: $action" -ForegroundColor Gray
|
| |
|
| |
|
| | Write-Host "`nStep 5: Select Destination Mode" -ForegroundColor Yellow
|
| | $destMode = Select-DestinationMode
|
| |
|
| | $typeDestinationMap = @{}
|
| |
|
| | if ($destMode -eq "Separate") {
|
| |
|
| | 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 {
|
| |
|
| | 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
|
| | }
|
| | }
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| | $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
|
| |
|
| |
|
| | 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)"
|
| |
|
| |
|
| | if ($result.Processed -gt 0 -and -not $dryRun) {
|
| | $firstDest = ($typeDestinationMap.Values | Select-Object -First 1)
|
| | Save-LastUsed -SourcePath $sourceFolder -DestPath $firstDest
|
| | }
|
| | }
|
| |
|
| | function Scan-SourcePath {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param(
|
| | [string]$SourcePath,
|
| | [int]$Depth
|
| | )
|
| |
|
| | return Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
|
| | }
|
| |
|
| | function Start-CLIMode {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param()
|
| |
|
| | Write-Host "`n=== FileOrganizer - CLI Mode ===" -ForegroundColor Cyan
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| | if (-not $DestPath) {
|
| | $DestPath = $SourcePath
|
| | Write-Host "No destination specified, using source path: $DestPath" -ForegroundColor Yellow
|
| | }
|
| |
|
| |
|
| | $files = Scan-SourceFiles -SourcePath $SourcePath -Depth $Depth
|
| |
|
| | if ($files.Count -eq 0) {
|
| | Write-Host "No files found." -ForegroundColor Yellow
|
| | return
|
| | }
|
| |
|
| |
|
| | $typeCounts = Get-TypeSummary -Files $files
|
| |
|
| |
|
| | Write-Host "`nFile Type Summary:" -ForegroundColor Green
|
| | foreach ($type in ($typeCounts.Keys | Sort-Object)) {
|
| | Write-Host " $type : $($typeCounts[$type]) files" -ForegroundColor White
|
| | }
|
| |
|
| |
|
| | $selectedTypes = $typeCounts.Keys
|
| |
|
| |
|
| | $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
|
| | }
|
| |
|
| |
|
| | if ($DryRun) {
|
| | Write-Host "`n*** DRY RUN MODE - No files will be modified ***" -ForegroundColor Cyan
|
| | }
|
| |
|
| |
|
| | $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
|
| |
|
| |
|
| | 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)"
|
| |
|
| |
|
| | if ($result.Processed -gt 0 -and -not $DryRun) {
|
| | Save-LastUsed -SourcePath $SourcePath -DestPath $DestPath
|
| | }
|
| | }
|
| |
|
| |
|
| |
|
| |
|
| |
|
| | function Main {
|
| | |
| | |
| | |
| |
|
| | [CmdletBinding()]
|
| | param()
|
| |
|
| |
|
| | Initialize-Environment
|
| |
|
| |
|
| | if ($Help) {
|
| | Get-Help $MyInvocation.MyCommand.Path -Detailed
|
| | return
|
| | }
|
| |
|
| |
|
| | $isCLIMode = $SourcePath -and $DestPath
|
| |
|
| | if ($isCLIMode) {
|
| |
|
| | Start-CLIMode
|
| | }
|
| | else {
|
| |
|
| | Start-InteractiveMode
|
| |
|
| |
|
| | 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"
|
| |
|
| |
|
| | Write-Host "`nPress Enter to exit..." -ForegroundColor Gray
|
| | try { [void](Read-Host) } catch { Start-Sleep -Seconds 5 }
|
| | }
|
| |
|
| |
|
| | 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
|
| | }
|
| |
|
| |
|
| |
|