Skip to content

Instantly share code, notes, and snippets.

@readingdancer
Created November 27, 2025 14:59
Show Gist options
  • Select an option

  • Save readingdancer/703bdf1f330ab8239f25c0dc22bc127e to your computer and use it in GitHub Desktop.

Select an option

Save readingdancer/703bdf1f330ab8239f25c0dc22bc127e to your computer and use it in GitHub Desktop.
A PowerShell file organization script that sorts files into a clean 6-folder structure (Media, Documents, Development, Archives, Applications, Other) with automatic year-based subfolders for images/videos/audio/PDFs and format-based subfolders for images (JPG, PNG, PSD, Vector). Just place in any folder and run!
# ============================================================================
# TidyFolder.ps1
# ============================================================================
#
# Author: Chris Houston (chouston@vizioz.com)
# License: Open Source - MIT License
# Repository: https://github.com/user/TidyFolder (if applicable)
#
# Description: Organizes files into a structured folder hierarchy based on
# file type, with year-based organization for media files.
# The script processes files in the same directory where it is
# located and organizes them into the structure shown below.
#
# ============================================================================
# FOLDER STRUCTURE
# ============================================================================
#
# The script creates the following folder hierarchy:
#
# 1 - Media/
# Images/
# [Year]/ <- e.g., 2024, 2023
# JPG/ <- .jpg, .jpeg
# PNG/ <- .png
# PSD/ <- .psd
# Vector/ <- .ai, .eps, .svg
# Other/ <- .gif, .bmp, .tif, .webp, .avif, etc.
# Videos/
# [Year]/ <- .mov, .mp4, .avi, .mkv, .wmv, etc.
# Audio/
# [Year]/ <- .wav, .mp3, .flac, .aac, .ogg, etc.
#
# 2 - Documents/
# PDFs/
# [Year]/ <- .pdf
# Office/
# Word/ <- .doc, .docx, .docm, .dot, etc.
# Excel/ <- .xls, .xlsx, .csv, .xlsm, etc.
# PowerPoint/ <- .ppt, .pptx, .pps, .ppsx, etc.
# Text/ <- .txt
# Markdown/ <- .md, .mdc, .rst
# Other/ <- .odt, .rtf, .tex
#
# 3 - Development/
# Code/ <- .ts, .js, .py, .cs, .html, .css, etc.
# Projects/ <- .csproj, .sln, .vbproj, etc.
# Data/ <- .json, .xml, .yaml, .yml, .ini, etc.
# Patches/ <- .patch, .diff
# Certificates/ <- .crt, .cer, .pem, .pfx, .key
# Minecraft/ <- .mcstructure, .mcworld, .mcpack, etc.
#
# 4 - Archives/
# [Year]/ <- .zip, .7z, .gz, .rar, .tar, etc.
#
# 5 - Applications/
# Executables/ <- .exe, .msi, .vsix
# Fonts/ <- .woff, .woff2, .ttf, .otf, .eot
#
# 6 - Other/
# Folders/ <- Non-numbered folders (moved intact)
# Logs/ <- .log
#
# ============================================================================
# AUTOMATIC FILE DELETION
# ============================================================================
#
# The following file types are AUTOMATICALLY DELETED by this script:
#
# .lnk - Windows shortcut files
# .url - Internet shortcut files
# .uda - User-defined application files
# .ics - Calendar event files
#
# These files are typically temporary or easily recreated and are removed
# to reduce clutter. If you need to keep these file types, modify the
# $deleteExtensions variable in the script.
#
# ============================================================================
# DISCLAIMER
# ============================================================================
#
# THIS SOFTWARE IS PROVIDED "AS IS", WITHOUT WARRANTY OF ANY KIND, EXPRESS OR
# IMPLIED, INCLUDING BUT NOT LIMITED TO THE WARRANTIES OF MERCHANTABILITY,
# FITNESS FOR A PARTICULAR PURPOSE AND NONINFRINGEMENT.
#
# USE AT YOUR OWN RISK. The author shall not be held liable for any damages,
# data loss, or other issues arising from the use of this script.
#
# BEFORE RUNNING THIS SCRIPT:
# 1. Review the code to understand what it does
# 2. Back up any important files
# 3. Test on a small set of files first
# 4. Verify the folder structure meets your needs
#
# By running this script, you acknowledge that you have read and understood
# this disclaimer and accept full responsibility for any consequences.
#
# ============================================================================
# Force immediate output display
$host.UI.RawUI.WindowTitle = "TidyFolder - Organizing Files..."
[Console]::OutputEncoding = [System.Text.Encoding]::UTF8
Write-Host ""
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " TidyFolder - File Organization Script" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
# The script will organize files in the same folder where this script is located
$parentDirectory = $PSScriptRoot
Write-Host "Working in: $parentDirectory" -ForegroundColor Yellow
Write-Host ""
# ============================================================================
# FOLDER STRUCTURE DEFINITION
# ============================================================================
# Top-level directories
$mediaDirectory = Join-Path -Path $parentDirectory -ChildPath "1 - Media"
$documentsDirectory = Join-Path -Path $parentDirectory -ChildPath "2 - Documents"
$developmentDirectory = Join-Path -Path $parentDirectory -ChildPath "3 - Development"
$archivesDirectory = Join-Path -Path $parentDirectory -ChildPath "4 - Archives"
$applicationsDirectory = Join-Path -Path $parentDirectory -ChildPath "5 - Applications"
$otherDirectory = Join-Path -Path $parentDirectory -ChildPath "6 - Other"
# Media subdirectories
$imagesDirectory = Join-Path -Path $mediaDirectory -ChildPath "Images"
$videosDirectory = Join-Path -Path $mediaDirectory -ChildPath "Videos"
$audioDirectory = Join-Path -Path $mediaDirectory -ChildPath "Audio"
# Documents subdirectories
$pdfsDirectory = Join-Path -Path $documentsDirectory -ChildPath "PDFs"
$officeDirectory = Join-Path -Path $documentsDirectory -ChildPath "Office"
$wordDirectory = Join-Path -Path $officeDirectory -ChildPath "Word"
$excelDirectory = Join-Path -Path $officeDirectory -ChildPath "Excel"
$powerpointDirectory = Join-Path -Path $officeDirectory -ChildPath "PowerPoint"
$textDirectory = Join-Path -Path $documentsDirectory -ChildPath "Text"
$markdownDirectory = Join-Path -Path $documentsDirectory -ChildPath "Markdown"
$otherDocsDirectory = Join-Path -Path $documentsDirectory -ChildPath "Other"
# Development subdirectories
$codeDirectory = Join-Path -Path $developmentDirectory -ChildPath "Code"
$projectsDirectory = Join-Path -Path $developmentDirectory -ChildPath "Projects"
$dataDirectory = Join-Path -Path $developmentDirectory -ChildPath "Data"
$patchesDirectory = Join-Path -Path $developmentDirectory -ChildPath "Patches"
$certificatesDirectory = Join-Path -Path $developmentDirectory -ChildPath "Certificates"
$minecraftDirectory = Join-Path -Path $developmentDirectory -ChildPath "Minecraft"
# Applications subdirectories
$executablesDirectory = Join-Path -Path $applicationsDirectory -ChildPath "Executables"
$fontsDirectory = Join-Path -Path $applicationsDirectory -ChildPath "Fonts"
# Other subdirectories
$foldersDirectory = Join-Path -Path $otherDirectory -ChildPath "Folders"
$logsDirectory = Join-Path -Path $otherDirectory -ChildPath "Logs"
$miscDirectory = Join-Path -Path $otherDirectory -ChildPath "Misc"
# ============================================================================
# EXTENSION MAPPINGS (using hashtable for O(1) lookup)
# ============================================================================
Write-Host "Loading extension mappings..." -ForegroundColor Gray
# Image format groups for subfolder organization
$imageFormatMap = @{
".jpg" = "JPG"; ".jpeg" = "JPG"
".png" = "PNG"
".psd" = "PSD"
".ai" = "Vector"; ".eps" = "Vector"; ".svg" = "Vector"
".gif" = "Other"; ".bmp" = "Other"; ".tif" = "Other"; ".tiff" = "Other"
".webp" = "Other"; ".avif" = "Other"; ".jxl" = "Other"; ".ico" = "Other"
".heic" = "Other"; ".heif" = "Other"; ".raw" = "Other"
}
# Master extension to destination mapping
# Format: extension = @{ Destination = path; UseYear = $true/$false; UseImageFormat = $true/$false }
$extensionMap = @{}
# Images (with year and format)
@(".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tif", ".tiff", ".svg", ".webp", ".ai", ".psd", ".eps", ".avif", ".jxl", ".ico", ".heic", ".heif", ".raw") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $imagesDirectory; UseYear = $true; UseImageFormat = $true }
}
# Videos (with year)
@(".mov", ".mp4", ".avi", ".mkv", ".wmv", ".flv", ".webm", ".m4v") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $videosDirectory; UseYear = $true; UseImageFormat = $false }
}
# Audio (with year)
@(".wav", ".mp3", ".flac", ".aac", ".ogg", ".wma", ".m4a") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $audioDirectory; UseYear = $true; UseImageFormat = $false }
}
# PDFs (with year)
$extensionMap[".pdf"] = @{ Destination = $pdfsDirectory; UseYear = $true; UseImageFormat = $false }
# Archives (with year)
@(".zip", ".7zip", ".7z", ".gz", ".rar", ".tar", ".bz2") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $archivesDirectory; UseYear = $true; UseImageFormat = $false }
}
# Word
@(".doc", ".docx", ".docm", ".dot", ".dotm", ".dotx") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $wordDirectory; UseYear = $false; UseImageFormat = $false }
}
# Excel
@(".xlam", ".xls", ".xlsb", ".xlsm", ".xlsx", ".xlt", ".xltm", ".xltx", ".xlw", ".csv") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $excelDirectory; UseYear = $false; UseImageFormat = $false }
}
# PowerPoint
@(".pot", ".potm", ".potx", ".ppa", ".ppam", ".pps", ".ppsm", ".ppsx", ".ppt", ".pptm", ".pptx", ".thmx") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $powerpointDirectory; UseYear = $false; UseImageFormat = $false }
}
# Text
$extensionMap[".txt"] = @{ Destination = $textDirectory; UseYear = $false; UseImageFormat = $false }
# Markdown
@(".md", ".mdc", ".rst") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $markdownDirectory; UseYear = $false; UseImageFormat = $false }
}
# Other documents
@(".odt", ".rtf", ".tex") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $otherDocsDirectory; UseYear = $false; UseImageFormat = $false }
}
# Code (excluding .ps1 which needs special handling)
@(".ts", ".js", ".lua", ".bas", ".css", ".bat", ".cs", ".html", ".htm", ".py", ".rb", ".php", ".java", ".c", ".cpp", ".h", ".hpp", ".go", ".rs", ".swift", ".kt", ".scala", ".sh") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $codeDirectory; UseYear = $false; UseImageFormat = $false }
}
# Special handling for .ps1 - will check filename
$extensionMap[".ps1"] = @{ Destination = $codeDirectory; UseYear = $false; UseImageFormat = $false; ExcludeScript = $true }
# Projects
@(".csproj", ".sln", ".frm", ".frx", ".vbproj", ".fsproj", ".vcxproj", ".xcodeproj") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $projectsDirectory; UseYear = $false; UseImageFormat = $false }
}
# Data
@(".json", ".xml", ".yaml", ".yml", ".psv", ".toml", ".ini", ".cfg", ".conf") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $dataDirectory; UseYear = $false; UseImageFormat = $false }
}
# Patches
@(".patch", ".diff") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $patchesDirectory; UseYear = $false; UseImageFormat = $false }
}
# Certificates
@(".crt", ".cer", ".pem", ".pfx", ".p12", ".key") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $certificatesDirectory; UseYear = $false; UseImageFormat = $false }
}
# Minecraft
@(".mcstructure", ".mcworld", ".mcpack", ".mcaddon", ".mctemplate") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $minecraftDirectory; UseYear = $false; UseImageFormat = $false }
}
# Executables
@(".exe", ".msi", ".vsix") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $executablesDirectory; UseYear = $false; UseImageFormat = $false }
}
# Fonts
@(".woff", ".woff2", ".ttf", ".otf", ".eot") | ForEach-Object {
$extensionMap[$_] = @{ Destination = $fontsDirectory; UseYear = $false; UseImageFormat = $false }
}
# Logs
$extensionMap[".log"] = @{ Destination = $logsDirectory; UseYear = $false; UseImageFormat = $false }
# Extensions to delete
$deleteExtensions = @(".lnk", ".url", ".uda", ".ics")
# ============================================================================
# HELPER FUNCTIONS
# ============================================================================
function Ensure-Directory {
param ([string]$path)
if (-not (Test-Path -Path $path -PathType Container)) {
New-Item -Path $path -ItemType Directory | Out-Null
}
}
# ============================================================================
# MAIN PROCESSING - Single scan approach
# ============================================================================
Write-Host "Scanning root directory for files..." -ForegroundColor Gray
# Get all files from root directory ONCE
$allFiles = Get-ChildItem -Path $parentDirectory -File -ErrorAction SilentlyContinue
$totalFiles = $allFiles.Count
$processedCount = 0
$movedCount = 0
$deletedCount = 0
$skippedCount = 0
Write-Host "Found $totalFiles files to process" -ForegroundColor Green
Write-Host ""
Write-Host "Processing files..." -ForegroundColor Cyan
Write-Host "--------------------------------------------"
foreach ($file in $allFiles) {
$processedCount++
$ext = $file.Extension.ToLower()
# Check if this extension should be deleted
if ($deleteExtensions -contains $ext) {
try {
Remove-Item -Path $file.FullName -Force -ErrorAction Stop
Write-Host " Deleted: $($file.Name)" -ForegroundColor Red
$deletedCount++
} catch {
Write-Host " Failed to delete: $($file.Name) - $_" -ForegroundColor DarkRed
}
continue
}
# Check if we have a mapping for this extension
if ($extensionMap.ContainsKey($ext)) {
$mapping = $extensionMap[$ext]
# Skip TidyFolder.ps1
if ($mapping.ExcludeScript -and $file.Name -eq "TidyFolder.ps1") {
$skippedCount++
continue
}
# Build destination path
$destPath = $mapping.Destination
if ($mapping.UseYear) {
$year = $file.LastWriteTime.Year.ToString()
$destPath = Join-Path -Path $destPath -ChildPath $year
}
if ($mapping.UseImageFormat -and $imageFormatMap.ContainsKey($ext)) {
$formatFolder = $imageFormatMap[$ext]
$destPath = Join-Path -Path $destPath -ChildPath $formatFolder
}
# Ensure destination exists and move file
try {
Ensure-Directory -path $destPath
$destFile = Join-Path -Path $destPath -ChildPath $file.Name
Move-Item -Path $file.FullName -Destination $destFile -Force
Write-Host " Moved: $($file.Name) -> $destPath" -ForegroundColor Green
$movedCount++
} catch {
Write-Host " Failed to move: $($file.Name) - $_" -ForegroundColor DarkRed
}
} else {
# Unknown extension - skip or move to misc
$skippedCount++
}
}
Write-Host ""
Write-Host "--------------------------------------------"
Write-Host "Files processed: $processedCount" -ForegroundColor Cyan
Write-Host "Files moved: $movedCount" -ForegroundColor Green
Write-Host "Files deleted: $deletedCount" -ForegroundColor Red
Write-Host "Files skipped: $skippedCount" -ForegroundColor Yellow
Write-Host ""
# ============================================================================
# PROCESS FOLDERS
# ============================================================================
Write-Host "Processing folders..." -ForegroundColor Cyan
Write-Host "--------------------------------------------"
$foldersToMove = Get-ChildItem -Path $parentDirectory -Directory | Where-Object {
$_.Name -notmatch '^\d' -and
$_.Name -ne "1 - Media" -and
$_.Name -ne "2 - Documents" -and
$_.Name -ne "3 - Development" -and
$_.Name -ne "4 - Archives" -and
$_.Name -ne "5 - Applications" -and
$_.Name -ne "6 - Other"
}
$foldersMoved = 0
if ($foldersToMove.Count -gt 0) {
Ensure-Directory -path $foldersDirectory
foreach ($folder in $foldersToMove) {
try {
$destinationPath = Join-Path -Path $foldersDirectory -ChildPath $folder.Name
Move-Item -Path $folder.FullName -Destination $destinationPath -Force
Write-Host " Moved folder: $($folder.Name)" -ForegroundColor Green
$foldersMoved++
} catch {
Write-Host " Failed to move folder: $($folder.Name) - $_" -ForegroundColor DarkRed
}
}
}
Write-Host ""
Write-Host "Folders moved: $foldersMoved" -ForegroundColor Green
Write-Host ""
# ============================================================================
# CLEANUP - Delete unwanted files in subfolders
# ============================================================================
Write-Host "Cleaning up unwanted files in subfolders..." -ForegroundColor Cyan
Write-Host "--------------------------------------------"
$cleanupCount = 0
$filesToDelete = Get-ChildItem -Path $parentDirectory -File -Recurse -ErrorAction SilentlyContinue | Where-Object {
$deleteExtensions -contains $_.Extension.ToLower()
}
foreach ($file in $filesToDelete) {
try {
Remove-Item -Path $file.FullName -Force -ErrorAction Stop
Write-Host " Deleted: $($file.FullName)" -ForegroundColor Red
$cleanupCount++
} catch {
Write-Host " Failed to delete: $($file.FullName) - $_" -ForegroundColor DarkRed
}
}
Write-Host ""
Write-Host "Cleanup files deleted: $cleanupCount" -ForegroundColor Red
Write-Host ""
# ============================================================================
# COMPLETE
# ============================================================================
Write-Host "============================================" -ForegroundColor Cyan
Write-Host " File organization complete!" -ForegroundColor Cyan
Write-Host "============================================" -ForegroundColor Cyan
Write-Host ""
Read-Host -Prompt "Press Enter to continue..."
Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment