A script I wrote to sort my ~/Downloads/ directory content into different file extensions category directories.
It’s constantly a work in process and a way I use to test different technologies and features of new python versions.
I’ve taken inspiration from so many different scripts and projects so it’s hard to write them all.
#!/usr/bin/env python3
import argparse
import logging
import sys
from pathlib import Path
categories = {
"Images": {".jpg", ".jpeg", ".png", ".gif", ".bmp", ".tiff", ".svg", ".webp", ".heic"},
"3D Printing": {".stl", ".gcode", ".3mf", ".f3d", ".stp"},
"Documents": {
".pdf",
".doc",
".docx",
".xls",
".xlsx",
".ppt",
".pptx",
".txt",
".csv",
".rtf",
".odt",
".pages",
".key",
".md",
".opml",
".tex",
".bib",
".odp",
},
"Executables": {
".exe",
".msi",
".bat",
".sh",
".dmg",
".app",
".deb",
".rpm",
".apk",
".jar",
".pkg",
".xpi",
".appimage",
},
"Compressed": {".zip", ".rar", ".tar", ".gz", ".7z"},
"Videos": {".mp4", ".mov", ".wmv", ".flv", ".avi", ".mkv", ".webm", ".mpg", ".mpeg", ".3gp"},
"Audio": {".mp3", ".wav", ".aac", ".ogg", ".flac", ".m4a", ".wma"},
"Code": {
".py",
".ipynb",
".java",
".cpp",
".c",
".h",
".cs",
".xml",
".json",
".yaml",
".yml",
".sql",
".rb",
".pl",
".cmd",
".ps1",
".dockerfile",
".fig",
".js",
".html",
".css",
".php",
".sqlite",
".db",
},
"Others": {".kdbx"},
}
def get_config() -> argparse.Namespace:
"""Use argparse to get configuration."""
parser = argparse.ArgumentParser(
prog="directory-file-sorter.py",
formatter_class=argparse.ArgumentDefaultsHelpFormatter,
description="Sort directory files into category directories.",
)
parser.add_argument(
"-v",
"--verbose",
action="count",
dest="verbosity",
default=0,
help="verbose output (repeat for increased verbosity)",
)
parser.add_argument(
"-q",
"--quiet",
action="store_const",
const=-1,
default=0,
dest="verbosity",
help="quiet output (show errors only)",
)
parser.add_argument(
dest="directory",
type=lambda p: Path(p).absolute(),
)
return parser.parse_args()
def setup_logging(verbosity: int) -> None:
"""Setups logging with the correct verbosity level."""
base_loglevel = 20
verbosity = min(verbosity, 2)
loglevel = base_loglevel - (verbosity * 10)
logging.basicConfig(level=loglevel, format="%(message)s")
def create_destination_directories(base_path: Path) -> None:
"""Create all the sort destination directories."""
logger = logging.getLogger(__name__)
for category in categories:
category_directory = base_path / category
if not category_directory.exists():
category_directory.mkdir()
logger.info("Created category directory %s.", category_directory)
def get_file_category(file_extension: str) -> str:
"""Return category for specified extension."""
for category, extensions in categories.items():
if file_extension.lower() in extensions:
return category
return "Others"
def move_file(src_path: Path, dest_directory: Path) -> None:
"""Move file into destination directory."""
logger = logging.getLogger(__name__)
dest_path = dest_directory / src_path.name
counter = 1
while dest_path.exists():
dest_path = dest_directory / f"{src_path.stem}_{counter}{src_path.suffix}"
counter += 1
logger.debug("Moving to %s.", dest_path)
src_path.rename(dest_path)
def main() -> None:
"""System main loop."""
conf = get_config()
setup_logging(conf.verbosity)
logger = logging.getLogger(__name__)
if not conf.directory.exists():
logger.critical("Directory %s does not exist.", conf.directory)
sys.exit(1)
create_destination_directories(conf.directory)
for file in conf.directory.iterdir():
if file.is_file():
logger.debug("Handling file %s.", file)
category = get_file_category(file.suffix)
logger.debug("Categoried as %s.", category)
move_file(file, conf.directory / category)
if __name__ == "__main__":
main()