Auto-sort your downloads

2024 ended with a disappointing number of posts from me — one. This is a small sign of life. Let's see if I can still remember how to write gemtext.

        __
       |  |
     __|  |__
     \      /
     _\__  /
_____\   \/__
\         \  |
 \         \ |
  \_________\'

I'm a big fan of small programs that do a lot. In particular, ones that seem like magic but are effectively very customisable scripts adapting to different situations. One example of such tool is the plumbing mechanism in plan9, which lets you define an action to take depending on the nature of text under the cursor. The closest Linux equivalent is probably the following patch for the suckless terminal.

Right click to plumb (st patch)

Another such tool, and the topic of this post, is a lesser known program — inotifywait. It is an abstraction of a Linux kernel feature, inotify or “inode notify”, itself used by developers to watch a given directory for file changes without needing to constantly poll for timestamps. Among other applications, it's used by the Django framework to implement the autoreload function in its development server.

While inotify functions come packaged with the C standard library, it is also possible to install inotify-tools to bring them into your shell scripts. Most distributions have a package named inotify-tools. Alternatively, you can clone the repository and compile it manually.

inotify-tools (git repository)

By default, running “inotifywait .” in a terminal will print the next filesystem event and then promptly terminate. We want to use the tool to create an “auto-sorter” that runs continuously like a daemon. To achieve this we pass the --monitor flag.

inotifywait --monitor .

This tells the program to continue to “monitor” a directory of choosing without ever terminating, akin to dmesg --follow. In this case, the final argument is the current directory, indicated by a period, but we can change that to any dumping ground of files that need sorting; the trusty Downloads folder, for instance. Let's see what the output is like when we write a file using a text editor.

$ inotifywait --monitor ~/Downloads
Setting up watches.
Watches established.
/home/user/Downloads/ CREATE hello.txt
/home/user/Downloads/ OPEN hello.txt
/home/user/Downloads/ MODIFY hello.txt
/home/user/Downloads/ CLOSE_WRITE,CLOSE hello.txt

The default line format in inotifywait is [directory] [event] [filename], which we can tailor to our needs rather easily, but there is one problem: it seems that even a simple write produces multiple events. This becomes a bigger problem if we take into consideration modern web browsers which often use temporary .part files as part of the download process. For our purposes, we are only interested in the final event, the “close_write”. Let's specify that with the --event flag and use --format and --quiet to change the output to something that will facilitate moving the file later.

$ inotifywait --quiet --monitor --event close_write --format "%w%f" ~/Downloads
/home/user/Downloads/hello.txt

Much better. We can now use the standard “while read do” shell incantation to process the files one by one. While there are many tests to determine where to move a file, one of the simplest tests is to look at its file extension. Assuming we use the name “f” for incoming file paths, we can obtain the extension like so:

extension=$(echo "${f##*.}" | tr '[:upper:]' '[:lower:]')

The "${A##B}" expression cuts everything prior to pattern B from the variable A. The corresponding "${A%%B}" expression removes everything after the pattern. To save headaches during pattern matching, we also pass the extension through tr to convert everything to lowercase letters.

All we need now is a case statement to match against the extension. Here's an example of what the complete script may look like:

#!/bin/sh

inotifywait --quiet --monitor --event close_write --format "%w%f" ~/Downloads | while read f; do
	extension=$(echo "${f##*.}" | tr '[:upper:]' '[:lower:]')
	case $extension in
		png | gif | jpeg | jpg)
			mv "$f" ~/Images
			;;
		mp4 | mkv | webm)
			mv "$f" ~/Videos
			;;
		pdf | epub)
			zathura "$f"
			;;
	esac
done

This is a rather simple script but it achieves a lot. It will place all images and videos into their respective folders and automatically open PDFs and EPUBs in a document viewer.

It doesn't have to be limited to file extensions either. Below are some more advanced test that I didn't have time to include.

At this point, you can set your web browser, email client, torrent client etc. to always use ~/Downloads and never see a “Save as” dialog again. As long as the script is added to .xinitrc or whatever autostart list your environment uses, it will continue to diligently sort all your incoming junk.