Backups for the poor!

Backups for the poor!

You're new to Linux and don't have a backup strategy. You never know when you'll bork your fresh Linux install or loose data in the multitude of ways possible.

If this is an issue you'd like to mitigate, then continue reading.

A good backup strategy is important to safeguard your data against possible data loss, malware or theft.

An ideal backup tool does not exist. What works for me might very well not work for you. I assume that your setup doesn't have any fancy NAS server or an extra Linux box. This is a simple intentional backup process that is a step above copying files from A to B.

You should absolutely do more than this blog post, but this is a good place to start.

If you knew me, you'd know I'd start with rsync. Before you continue, have a quick look at its man-page. rsync is an easy tool to use, once you figure out its options. The problem is there is no way you are remembering it and using all its tedious options and so, we script.

Let's break this into steps:

  • Labelling your block device
  • Reliably mount and dismount disks attached to your system - it can be a flash drive, external HDD or SSD
  • A way to template the backup process, making it easier to add future directories to backup.

Labelling your block device

I'd rather not go over the details of changing a label in this post. There are much better sources for it. I personally prefer the examples given in this Arch Wiki page.

Persistent block device naming - ArchWiki

Sourcing bash functions

Bash is a command processor, we can run scripts and and interactive commands with bash. This tutorial will not speak on Bash, there's a bunch of tutorials and documentation available on the internet - Check here and here.

Knowing how to source bash functions is an important prerequisite for this article. In essence we need only to source a bash script that defines a set of bash functions, like so

source /path/to/script # or
. /path/to/another-script

You can source a script in your ~/.bash_profile ,~/.bashrc or save these scripts to $XDG_CONFIG_HOME/bash/functions.d and add the below command to dynamically source all scripts.

for f in "$XDG_CONFIG_HOME/bash/functions.d/"*; do source "$f"; done

Sourcing bash functions

As we get into the article you'll see me define bash functions, make sure you source them into your bash config!

Map partitions to a root directory

A precursor to the template __bac bash function is defining the mount paths of the possible disks that we intend to store our backups.

#!/usr/bin/env bash
# Author: Aakash Hemadri <[email protected]>
# Map a disk label to it's mounted root directory. 
# This script uses util-mount-disk/util-unmount-disk bash functions.

declare -A disk
# some-partition-label is mounted at /media/$USER/some-partition-label
disk[some-partition-label]=/media/$USER/some-partition-label/backups/$HOSTNAME
# example is mounted at /media/$USER/example
disk[example]=/media/$USER/example/backups/$HOSTNAME

`disk` is an associative bash array that maps a partition label to a directory

Templating with the __bac function

I started with the template backup __bac bash function. This is very helpful to quickly change the parameters of rsync and reuse this function for various targets and sources.

We use a few rsync options to preserve permissions and links and delete target files that no longer exist in SOURCE_DIR

# Template bac
__bac() {
	# !!Warning!! This command is designed for scripts and not to be used in shell
	# $1 --> LABEL: Map to TARGET_BASE with the disks bash variable
	# $2 --> TARGET_DIR: Subfolder from $TARGET_BASE
	# $3 --> SOURCE_DIR: Source directory/regex of source paths

	if [[ $# -eq 0 ]]; then
		echo -e "!! You've specified nothing to backup!\n!! Do not call this from shell!!!\n>> Exiting.."
	else
		TARGET_BASE="${disk[$1]}"
		TARGET_DIR="$TARGET_BASE/$2"
		SOURCE_DIR="${@:3}"

		echo -e "\t>> Backing up ${2}..."
		mkdir -p "$TARGET_DIR"
		rsync --append-verify \
			--recursive \
			--executability \
			--perms \
			--hard-links \
			--safe-links \
			--human-readable \
			--checksum \
			--delete \
			--info=progress2 \
			$SOURCE_DIR \
			$TARGET_DIR
	fi
}

__bac

Exploring the rsync man page should give you a better explanation of what these options do.

We now create a few more helper functions. I set the general scheme of other bash functions that call upon __bac to be __bac-app-*. For example,

__bac-app-doc() {
# $1 --> LABEL: Label of partition you'd like to backup.
	__bac $1 apps/home/Documents $HOME/Documents/
}

__bac-app-dwn() {
# $1 --> LABEL: Label of partition you'd like to backup.
	__bac $1 apps/home/Downloads $HOME/Downloads/
}

__bac-app-* functions

To make it even easier I added a bash function that dynamically runs all __bac-app-* bash functions.

__bac-apps() {
	# Store list of `__bac-app-*` bash functions
	apps=$(declare -F | awk '{print $NF}' | sort | grep "^__bac-app-[[:alpha:]]*")
    # Iterate and run through it
	for app in ${apps[@]}; do
		${app} $1
	done
}

Mounting and unmounting partitions

Before we expose bash functions to the user, we attempt at mounting a disk. I began by just using mount/umount to create bash functions which accept the block devices' label.

We use lsblk, mtab, mount and umount. These are preinstalled tools that come with any distro. If you don't find them on whatever bespoke Linux distribution you use, feel free to install them and make sure it's available in your PATH.

#!/usr/bin/env bash
# Author: Aakash Hemadri <[email protected]>
# Mount/Umount partition
# $1 is partition label

util-mount-disk() {
	# Block device by-label
	device="/dev/disk/by-label/$1"
	# Target mount point
	mount_point="/media/$USER/$1"
	# Set current_mount_point if block device with $1 label exists
	# If it DNE then exit
	if [[ $1 == $(lsblk --output LABEL | grep $1) ]]; then
		current_mount_point=$(lsblk -o MOUNTPOINT -nr $device)
	else
		echo -e "!! Please attach disk $1"
		return 1
	fi

	# Check if $1 is mounted
	if [[ -n $(grep $1 /etc/mtab) ]]; then
		# If $1 is mounted but target mount != current mount point then remount
		if [[ $mount_point != $current_mount_point ]]; then
			echo -e "!! Remounting disk $1 from $current_mount_point to $mount_point"
			sudo umount $device
			mkdir -p $mount_point
			sudo mount -t auto $device $mount_point
		fi
		return 0
	else
		mkdir -p $mount_point
		sudo mount -t auto $device $mount_point
		return 0
	fi
}

# Unmount disk 
# $1 is partition label
util-unmount-disk() {
	device="/dev/disk/by-label/$1"
	mount_point="/media/$USER/$1"

	if [[ $1 == $(lsblk --output LABEL | grep $1) ]]; then
		echo -e "!! Unmounting disk $1"
		sudo umount $device
	fi
}

util-mount-disk & util-unmount-disk

We now expose the user facing bash function that a user, script or cron job can call upon. Now we use our util-mount-disk & util-unmount-disk bash functions.

bac-disk() {
	if ! util-mount-disk $1; then
		return 1
	fi
	echo -e ">> Backing up to disk $1"
	__bac-apps $1
	echo -e ""
	util-unmount-disk $1
}

To make things even easier, we call upon another function to pass all labels at once.

bac-all() {
	bac-disk disk-1
	bac-disk example
	bac-disk some-partition-label
}

We're done!

Just run bac-all on your shell or run bac-disk <YOUR_DISK_LABEL>.

Show Comments