Backing up to a USB stick automatically via udev

There was some interesting discussion on the fediverse about tools for backing up to USB devices on Linux.

Here’s what I use, in case it helps anyone.

This is just one of my backups, of course. This is a backup to a USB device, which mounts to /media/neil/backup_disk, which has a LUKS-encrypted partition on it. The objective of this particular backup is so that I have an offline copy of my most important data, just in case something (ransomware?) affected my online systems.

The reason for making it run automatically, via udev, is to make it as easy to use as possible: I just need to plug it in and, a few minutes later, unmount it and remove it.

That way, I’ll actually use it. (And the key, from an anti-ransomware perspective, is to remove it once finished.)

udev rule

This is the bit of the config which triggers the backup automatically when the specific USB memory stick is plugged in.

In /etc/udev/rules.d/80-backupdrive.rules I have:

ACTION=="add", SUBSYSTEM=="usb", ENV{PRODUCT}=="789/5678/123", ENV{DEVTYPE}=="usb_device", RUN+="/home/neil/briefcase/Store/tools/backupdrive/udevbackuptimer.sh"

Multiple udev matching criteria

I match based on both the product serial number and the device type, both ascertained through watching the output of sudo udevadm monitor --kernel --property --subsystem-match=usb when I plugged in the device in question.

Originally I just used the product serial number, but because this appears in multiple udev messages, it meant that the script would be triggered multiple times. There was only one udev message with both the product serial number and the device type of “usb_device”, so, by matching on that, it means the script gets run only once.

The timer script which udev runs

The udev rule runs the script at /home/neil/briefcase/Store/tools/backupdrive/udevbackuptimer.sh.

It is a very short and simple bash script:

#!/bin/bash

alert() {

sudo -u neil DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "$1"  --icon=dialog-information -t 1000

}


sudo -u neil /usr/bin/at now +1 minutes -f /home/neil/briefcase/Store/tools/backupdrive/udevbackup.sh

alert "Backup to begin in 1 minute"

All it does is use the at command to run the actual backup script in one minute, and send a desktop alert about it.

Why bother with this?

The reason this script exists at all is that it seems - in my experimentation at least - that the script linked from the udev rule is run, and needs to complete, before the USB device is mounted.

If I use the udev rule to run the backup script itself, it attempts to do this before the USB device, onto which the backup is made, is available. So it fails.

One approach would be to include, within the script run by the udev rule, code to cover the mounting etc. of the USB device. Because I want this to be handed by the OS, which also takes care of the decryption of the LUKS partition using a key stored in the keychain, that is not a route I have explored.

So this is a bit of a workaround: a short script, which sets the backup script itself to run in one minutes time and then completes, after which the USB device is mounted and the LUKS partition is decrypted, automatically. Then the backup script runs.

Why the lengthy notify-send function?

Because of the way notify-send works - I will be logged in as user “neil”, and so I want to ensure that the alert is visible to me. Without this, notify-send would try to send the alert to the root user, which runs the script indicated by the udev rule, and so it would fail as there isn’t a login/desktop session for the root user.

The backup script itself

At /home/neil/briefcase/Store/tools/backupdrive/udevbackup.sh, I have the backup script itself.

It is not fancy and, obviously, is tailored to the directories that I want to back up.

It backs up:

I should probably set it to back up cronjobs too, just in case I had any.

To avoid it running multiple concurrent times, I’m using a very basis file-based mechanism: the script first checks if the file “/media/neil/backup_disk/.isbackingup” is present. If it is, it assumes that the script is already running, and so exits. It would also exit if, for some reason, the script was not running but had terminated before it got to the point of deleting that sort-of-lock-file. I’m sure there’s a better way of doing this.

Again, I use notify-send for desktop notifications. These are not perfect, as GNOME does not recognise the timer (-t 1000) switch, and so sometimes one alert hangs around meaning I don’t see others. It’s not the end of the world, as I can look in the notifications history in the taskbar to see what the latest alert was.

#! /bin/bash

FILE=/media/neil/backup_disk/.isbackingup

alert() {

sudo -u neil DISPLAY=wayland-0 DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus notify-send "$1"  --icon=dialog-information -t 1000

}

backup() {


	# alert "Disk is mounted"

	if test -f "$FILE"; then
		# alert "$FILE exists, so don't back up"
		exit

	else

		alert "Starting backup"

		touch "$FILE"

		# alert "$FILE does not exist, so backing up"

		# alert "Starting home backup"

		if [ -f "/home/neil/Clients/.isdecrypted" ]; then

			# alert "Backing up Clients"

			rsync -avz --ignore-errors --delete-before --exclude "neil/Desktop/provisioning_pi.img" /home/neil /media/neil/backup_disk

		else

			# alert "Not backing up Clients"

			rsync -avz --ignore-errors --delete-before --exclude "neil/Desktop/provisioning_pi.img" --exclude "neil/Clients" /home/neil /media/neil/backup_disk

		fi


		# alert "Starting /usr/local backup"

		rsync -avz --delete-before /usr/local /media/neil/backup_disk
		
		# alert "Starting /etc/apt backup"

		rsync -avz --delete-before /etc/apt /media/neil/backup_disk

		# alert "Starting package installation backup"

		apt list --installed > /media/neil/backup_disk/list_of_apt_installed_packages.txt

		dpkg-query -f '${binary:Package}n' -W > /media/neil/backup_disk/list_of_dpkg_installed_packages.txt

		snap list > /media/neil/backup_disk/list_of_snap_installed_packages.txt

		flatpak list > /media/neil/backup_disk/list_of_flatpak_installed_packages.txt
		
		alert "Backup finished"

		# alert "Removing $FILE"

		rm "$FILE"

		if [ -f "$FILE" ]; then
			
			alert "Removing $FILE failed"

	#	else

	#		alert "Removing $FILE succeeded"

		fi
	
		alert "Disk ready to unmount"
	fi

}





if [ -f "/media/neil/backup_disk/.ismounted" ]; then

	backup

else
	alert "No backup disk detected, sleeping"
	
	sleep 10


		if [ -f "/media/neil/backup_disk/.ismounted" ]; then

			backup

		else

			alert "Error backing up"

			exit

		fi

fi

I should probably add a function at the end to automatically close the LUKS partition and unmount the device when it is finished. For now, I just unmounted it using Nautilus.

Others’ suggestions

systemd

Fedi user ScaredyCat said:

I’d be inclined to use the udev rule to start a system service which you can run as a user

As an example, they said:

File is /etc/systemd/system/mq2my.service - change the mq2my to whatever you want to call it, and the user/group to your user account

Make WantedBy a blank string to not start at boot.

[Unit]
Description=MQTT to MySQL service
After=mysql.service

[Service]
Type=simple
User=andy
Group=andy
TimeoutStartSec=0
RemainAfterExit=no
ExecStart=/opt/mq2my/startMQ2MY.sh

[Install]
WantedBy=multi-user.target

when you’ve built this file,

systemctl enable mq2my.service

and

systemctl daemon-reload

Udev rule just starts the service which runs your script as you.

Thanks!

systemd, no udev

Another fedizen said:

why a udev rule at all? I can’t remember the details, but I have a systemd service file that has the following two lines and starts the service configured in it when the device is connected:

[Install]
WantedBy=dev-disk-byx2did-atax2dWDC_WD30EFRXx2dFOOx2dBARx2dpart1.device

and

I use "PrivateMounts=true" in the service section so that the mount is not even seen in the regular filesystem

Making it work like macOS’s Time Machine

Fedizen Frank added this useful tip:

You can tease time-machine-like time slices out of rsync with a pointer to a preceding backup. rsync will then set hard links to files that haven’t changed, in a fresh directory, so if you have backed up after bad stuff happens, you still have earlier pristine copies to rebuild from. Here’s the script I use for that: https://gist.github.com/fbennett/dcb066fc71e116d3e35d2ec9e144ab71