Skip to content

Instantly share code, notes, and snippets.

@htlin222
Created September 21, 2025 03:15
Show Gist options
  • Save htlin222/84956dc3e4871b9d4867d9b1b8c96887 to your computer and use it in GitHub Desktop.
Save htlin222/84956dc3e4871b9d4867d9b1b8c96887 to your computer and use it in GitHub Desktop.
A Bash script that organizes files in the ~/Downloads folder by file age and file type.
#!/bin/bash
# Downloads Organizer Script
# Organizes files by age (3d, 7d, 14d, 30d, old) and then by file type
set -e
# Configuration
DOWNLOADS_DIR="/Users/htlin/Downloads"
DRY_RUN=${DRY_RUN:-false}
VERBOSE=${VERBOSE:-true}
# Colors for output
RED='\033[0;31m'
GREEN='\033[0;32m'
YELLOW='\033[1;33m'
BLUE='\033[0;34m'
NC='\033[0m' # No Color
# Logging function
log() {
if [[ $VERBOSE == true ]]; then
echo -e "${BLUE}[INFO]${NC} $1"
fi
}
log_success() {
echo -e "${GREEN}[SUCCESS]${NC} $1"
}
log_warning() {
echo -e "${YELLOW}[WARNING]${NC} $1"
}
log_error() {
echo -e "${RED}[ERROR]${NC} $1"
}
# Get file extension in lowercase
get_extension() {
local file="$1"
echo "${file##*.}" | tr '[:upper:]' '[:lower:]'
}
# Get file category based on extension
get_category() {
local ext="$1"
case "$ext" in
# Documents
pdf|doc|docx|ppt|pptx|txt|rtf|odt|pages)
echo "documents"
;;
# Images
jpg|jpeg|png|gif|bmp|tiff|svg|webp|ico|heic)
echo "images"
;;
# Data files
csv|xlsx|xls|json|xml|yaml|yml|db|sql)
echo "data"
;;
# Code and config
js|ts|py|go|java|c|cpp|h|css|html|sh|bash|zsh|fish|vim|conf|cfg|ini|toml|mod|sum)
echo "code"
;;
# Archives
zip|tar|gz|rar|7z|bz2|xz|dmg|pkg|deb|rpm)
echo "archives"
;;
# E-books
epub|mobi|azw|azw3|azw8|pdf)
# PDFs could be either documents or ebooks - check filename patterns
if [[ "$2" =~ (book|novel|guide|manual|tutorial) ]]; then
echo "ebooks"
else
echo "documents"
fi
;;
# Audio/Video
mp3|wav|flac|aac|mp4|avi|mkv|mov|wmv|webm)
echo "media"
;;
# Misc
*)
echo "misc"
;;
esac
}
# Get age bucket based on file modification time
get_age_bucket() {
local file="$1"
local mod_time
local now
local diff_days
# Get modification time in seconds since epoch
if [[ "$OSTYPE" == "darwin"* ]]; then
# macOS
mod_time=$(stat -f "%m" "$file" 2>/dev/null || echo 0)
else
# Linux
mod_time=$(stat -c "%Y" "$file" 2>/dev/null || echo 0)
fi
now=$(date +%s)
diff_days=$(( (now - mod_time) / 86400 ))
if [[ $diff_days -le 3 ]]; then
echo "3d"
elif [[ $diff_days -le 7 ]]; then
echo "7d"
elif [[ $diff_days -le 14 ]]; then
echo "14d"
elif [[ $diff_days -le 30 ]]; then
echo "30d"
else
echo "old"
fi
}
# Create directory structure
create_structure() {
local base_dirs=("3d" "7d" "14d" "30d" "old")
local sub_dirs=("documents" "images" "data" "code" "archives" "ebooks" "media" "misc")
log "Creating directory structure..."
for base in "${base_dirs[@]}"; do
for sub in "${sub_dirs[@]}"; do
local dir_path="$DOWNLOADS_DIR/$base/$sub"
if [[ $DRY_RUN == false ]]; then
mkdir -p "$dir_path"
fi
log "Created: $dir_path"
done
done
}
# Move file to appropriate directory
move_file() {
local file="$1"
local age_bucket="$2"
local category="$3"
local dest_dir="$DOWNLOADS_DIR/$age_bucket/$category"
local filename=$(basename "$file")
# Skip if file is already in organized structure (unless re-aging)
if [[ "$file" =~ ^$DOWNLOADS_DIR/(3d|7d|14d|30d|old)/ ]] && [[ "${4:-}" != "reage" ]]; then
log "Skipping already organized file: $filename"
return
fi
# Skip script files and hidden files
if [[ "$filename" == "organize.sh" ]] || [[ "$filename" == "Makefile" ]] || [[ "$filename" == .* ]]; then
log "Skipping system file: $filename"
return
fi
# Create destination directory if it doesn't exist
if [[ $DRY_RUN == false ]]; then
mkdir -p "$dest_dir"
fi
local dest_path="$dest_dir/$filename"
# Handle filename conflicts
local counter=1
local base_name="${filename%.*}"
local extension="${filename##*.}"
while [[ -e "$dest_path" ]]; do
if [[ "$filename" == "$extension" ]]; then
# No extension
dest_path="$dest_dir/${base_name}_${counter}"
else
dest_path="$dest_dir/${base_name}_${counter}.${extension}"
fi
((counter++))
done
if [[ $DRY_RUN == true ]]; then
echo "DRY RUN: Would move '$file' → '$dest_path'"
else
mv "$file" "$dest_path"
log_success "Moved: $filename → $age_bucket/$category/"
fi
}
# Organize files
organize_files() {
log "Starting file organization..."
local total_files=0
local moved_files=0
# Find all files in Downloads (excluding directories and already organized files)
while IFS= read -r -d '' file; do
# Skip directories
if [[ -d "$file" ]]; then
continue
fi
((total_files++))
local filename=$(basename "$file")
local extension=$(get_extension "$filename")
local age_bucket=$(get_age_bucket "$file")
local category=$(get_category "$extension" "$filename")
log "Processing: $filename (Age: $age_bucket, Category: $category)"
move_file "$file" "$age_bucket" "$category"
((moved_files++))
done < <(find "$DOWNLOADS_DIR" -maxdepth 1 -type f -print0)
log_success "Organization complete! Processed $total_files files, moved $moved_files files."
}
# Clean empty directories
clean_empty_dirs() {
log "Cleaning empty directories..."
if [[ $DRY_RUN == false ]]; then
find "$DOWNLOADS_DIR" -type d -empty -delete 2>/dev/null || true
fi
log_success "Empty directories cleaned."
}
# Re-age organized files based on current age
reage_files() {
log "Starting file re-aging based on current modification dates..."
local total_files=0
local moved_files=0
# Process all files in organized directories
for age_dir in "$DOWNLOADS_DIR"/{3d,7d,14d,30d,old}; do
if [[ -d "$age_dir" ]]; then
local current_age_bucket=$(basename "$age_dir")
while IFS= read -r -d '' file; do
# Skip directories
if [[ -d "$file" ]]; then
continue
fi
((total_files++))
local filename=$(basename "$file")
local extension=$(get_extension "$filename")
local correct_age_bucket=$(get_age_bucket "$file")
local category=$(get_category "$extension" "$filename")
# Only move if age bucket has changed
if [[ "$current_age_bucket" != "$correct_age_bucket" ]]; then
log "Re-aging: $filename ($current_age_bucket → $correct_age_bucket)"
move_file "$file" "$correct_age_bucket" "$category" "reage"
((moved_files++))
else
log "Age correct: $filename (staying in $current_age_bucket)"
fi
done < <(find "$age_dir" -type f -print0)
fi
done
log_success "Re-aging complete! Processed $total_files files, moved $moved_files files."
}
# Show statistics
show_stats() {
log "File organization statistics:"
for age_dir in "$DOWNLOADS_DIR"/{3d,7d,14d,30d,old}; do
if [[ -d "$age_dir" ]]; then
local age_name=$(basename "$age_dir")
echo -e "${YELLOW}$age_name:${NC}"
for cat_dir in "$age_dir"/*; do
if [[ -d "$cat_dir" ]]; then
local cat_name=$(basename "$cat_dir")
local file_count=$(find "$cat_dir" -type f | wc -l)
if [[ $file_count -gt 0 ]]; then
echo " $cat_name: $file_count files"
fi
fi
done
fi
done
}
# Main function
main() {
echo -e "${GREEN}Downloads Organizer${NC}"
echo "==================="
# Change to downloads directory
cd "$DOWNLOADS_DIR" || {
log_error "Cannot access Downloads directory: $DOWNLOADS_DIR"
exit 1
}
case "${1:-organize}" in
"organize")
create_structure
organize_files
clean_empty_dirs
show_stats
;;
"reage")
create_structure
reage_files
clean_empty_dirs
show_stats
;;
"reage-dry-run")
DRY_RUN=true
log_warning "Running REAGE in DRY RUN mode - no files will be moved"
create_structure
reage_files
;;
"stats")
show_stats
;;
"dry-run")
DRY_RUN=true
log_warning "Running in DRY RUN mode - no files will be moved"
create_structure
organize_files
;;
"help")
echo "Usage: $0 [organize|reage|stats|dry-run|reage-dry-run|help]"
echo ""
echo "Commands:"
echo " organize - Organize files (default)"
echo " reage - Move files to correct age folders based on current date"
echo " reage-dry-run - Preview re-aging without moving files"
echo " stats - Show organization statistics"
echo " dry-run - Show what would be moved without moving"
echo " help - Show this help"
echo ""
echo "Environment variables:"
echo " DRY_RUN=true - Run without moving files"
echo " VERBOSE=false - Reduce output"
;;
*)
log_error "Unknown command: $1"
echo "Use '$0 help' for usage information"
exit 1
;;
esac
}
# Run main function with all arguments
main "$@"
@htlin222
Copy link
Author

./organize.sh [organize|reage|stats|dry-run|reage-dry-run|help]

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment